diff --git a/aircox/README.md b/aircox/README.md index 657dcdd..6aeecf5 100755 --- a/aircox/README.md +++ b/aircox/README.md @@ -1,24 +1,10 @@ -# Aircox Programs - -This application defines all base models and basic control of them. We have: -* **Nameable**: generic class used in any class needing to be named. Includes some utility functions; -* **Station**: a station -* **Program**: the program itself; -* **Diffusion**: occurrence of a program planified in the timetable. For rerun, informations are bound to the initial diffusion; -* **Schedule**: describes diffusions frequencies for each program; -* **Track**: track informations in a playlist of a diffusion; -* **Sound**: information about a sound that can be used for podcast or rerun; -* **Log**: logs - +# Aircox +Aircox application aims to provide basis of a radio management system. ## Architecture -A Station is basically an object that represent a radio station. On each station, we use the Program object, that is declined in two different types: -* **Scheduled**: the diffusion is based on a timetable and planified through one Schedule or more; Diffusion object represent the occurrence of these programs; -* **Streamed**: the diffusion is based on random playlist, used to fill gaps between the programs; +A Station contains programs that can be scheduled or streamed. A *Scheduled Program* is a regular show that has planified diffusions of its occurences (episodes). A *Streamed Program* is a program used to play randoms musics between the shows. -Each program has a directory in **AIRCOX_PROGRAMS_DIR**; For each, subdir: -* **archives**: complete episode record, can be used for diffusions or as a podcast -* **excerpts**: excerpt of an episode, or other elements, can be used as a podcast +Each program has a directory on the server where user puts its podcasts (in **AIRCOX_PROGRAM_DIR**). It contains the directories **archives** (complete show's podcasts) and **excerpts** (partial or whatever podcasts). ## manage.py's commands diff --git a/aircox/admin/__pycache__/__init__.cpython-37.pyc b/aircox/admin/__pycache__/__init__.cpython-37.pyc index 8a13be3..2e1c203 100644 Binary files a/aircox/admin/__pycache__/__init__.cpython-37.pyc and b/aircox/admin/__pycache__/__init__.cpython-37.pyc differ diff --git a/aircox/admin/__pycache__/episode.cpython-37.pyc b/aircox/admin/__pycache__/episode.cpython-37.pyc index efe1b5b..ec33e39 100644 Binary files a/aircox/admin/__pycache__/episode.cpython-37.pyc and b/aircox/admin/__pycache__/episode.cpython-37.pyc differ diff --git a/aircox/admin/__pycache__/page.cpython-37.pyc b/aircox/admin/__pycache__/page.cpython-37.pyc index 028ce77..1e5c3ea 100644 Binary files a/aircox/admin/__pycache__/page.cpython-37.pyc and b/aircox/admin/__pycache__/page.cpython-37.pyc differ diff --git a/aircox/admin/__pycache__/program.cpython-37.pyc b/aircox/admin/__pycache__/program.cpython-37.pyc index 4d67640..54a6e3b 100644 Binary files a/aircox/admin/__pycache__/program.cpython-37.pyc and b/aircox/admin/__pycache__/program.cpython-37.pyc differ diff --git a/aircox/admin/__pycache__/sound.cpython-37.pyc b/aircox/admin/__pycache__/sound.cpython-37.pyc index 9923ef2..9aea913 100644 Binary files a/aircox/admin/__pycache__/sound.cpython-37.pyc and b/aircox/admin/__pycache__/sound.cpython-37.pyc differ diff --git a/aircox/admin/page.py b/aircox/admin/page.py index 3370e3a..1e16eb1 100644 --- a/aircox/admin/page.py +++ b/aircox/admin/page.py @@ -25,6 +25,8 @@ class PageAdmin(admin.ModelAdmin): list_editable = ('status', 'category') prepopulated_fields = {"slug": ("title",)} + change_form_template = 'admin/aircox/page_change_form.html' + fieldsets = [ ('', { 'fields': ['title', 'slug', 'category', 'cover', 'content'], diff --git a/aircox/controllers.py b/aircox/controllers.py index 8977d91..11e04e8 100755 --- a/aircox/controllers.py +++ b/aircox/controllers.py @@ -78,18 +78,14 @@ class Streamer: @property def inputs(self): """ Return input ports of the station """ - return self.station.port_set.filter( - direction=Port.Direction.input, - active=True - ) + return self.station.port_set.filter(direction=Port.DIRECTION_INPUT, + active=True) @property def outputs(self): """ Return output ports of the station """ - return self.station.port_set.filter( - direction=Port.Direction.output, - active=True, - ) + return self.station.port_set.filter(direction=Port.DIRECTION_OUTPUT, + active=True) # Sources and config ############################################### def send(self, *args, **kwargs): diff --git a/aircox/management/commands/diffusions.py b/aircox/management/commands/diffusions.py index 0b8085c..cf21c9f 100755 --- a/aircox/management/commands/diffusions.py +++ b/aircox/management/commands/diffusions.py @@ -57,14 +57,14 @@ class Actions: diffusion.save() def clean(self): - qs = Diffusion.objects.filter(type=Diffusion.Type.unconfirmed, + qs = Diffusion.objects.filter(type=Diffusion.TYPE_UNCONFIRMED, start__lt=self.date) logger.info('[clean] %d diffusions will be removed', qs.count()) qs.delete() def check(self): # TODO: redo - qs = Diffusion.objects.filter(type=Diffusion.Type.unconfirmed, + qs = Diffusion.objects.filter(type=Diffusion.TYPE_UNCONFIRMED, start__gt=self.date) items = [] for diffusion in qs: diff --git a/aircox/management/commands/sounds_monitor.py b/aircox/management/commands/sounds_monitor.py index 2767eaa..afcaba4 100755 --- a/aircox/management/commands/sounds_monitor.py +++ b/aircox/management/commands/sounds_monitor.py @@ -184,9 +184,9 @@ class MonitorHandler(PatternMatchingEventHandler): """ self.subdir = subdir if self.subdir == settings.AIRCOX_SOUND_ARCHIVES_SUBDIR: - self.sound_kwargs = {'type': Sound.Type.archive} + self.sound_kwargs = {'type': Sound.TYPE_ARCHIVE} else: - self.sound_kwargs = {'type': Sound.Type.excerpt} + self.sound_kwargs = {'type': Sound.TYPE_EXCERPT} patterns = ['*/{}/*{}'.format(self.subdir, ext) for ext in settings.AIRCOX_SOUND_FILE_EXT] @@ -213,7 +213,7 @@ class MonitorHandler(PatternMatchingEventHandler): sound = Sound.objects.filter(path=event.src_path) if sound: sound = sound[0] - sound.type = sound.Type.removed + sound.type = sound.TYPE_REMOVED sound.save() def on_moved(self, event): @@ -259,11 +259,11 @@ class Command(BaseCommand): logger.info('#%d %s', program.id, program.title) self.scan_for_program( program, settings.AIRCOX_SOUND_ARCHIVES_SUBDIR, - type=Sound.Type.archive, + type=Sound.TYPE_ARCHIVE, ) self.scan_for_program( program, settings.AIRCOX_SOUND_EXCERPTS_SUBDIR, - type=Sound.Type.excerpt, + type=Sound.TYPE_EXCERPT, ) dirs.append(os.path.join(program.path)) @@ -317,7 +317,7 @@ class Command(BaseCommand): # get available sound files sounds = Sound.objects.filter(is_good_quality=False) \ - .exclude(type=Sound.Type.removed) + .exclude(type=Sound.TYPE_REMOVED) if check: self.check_sounds(sounds) diff --git a/aircox/management/commands/streamer.py b/aircox/management/commands/streamer.py index 49ca220..9b5f0ad 100755 --- a/aircox/management/commands/streamer.py +++ b/aircox/management/commands/streamer.py @@ -152,7 +152,7 @@ class Monitor: .now(air_time).first() # log sound on air - return self.log(type=Log.Type.on_air, date=source.air_time, + return self.log(type=Log.TYPE_ON_AIR, date=source.air_time, source=source.id, sound=sound, diffusion=diff, comment=air_uri) @@ -177,7 +177,7 @@ class Monitor: if pos > now: break # log track on air - self.log(type=Log.Type.on_air, date=pos, source=log.source, + self.log(type=Log.TYPE_ON_AIR, date=pos, source=log.source, track=track, comment=track) def handle_diffusions(self): @@ -208,7 +208,7 @@ class Monitor: # now = tz.now() diff = Diffusion.objects.station(self.station).on_air().now(now) \ - .filter(episode__sound__type=Sound.Type.archive) \ + .filter(episode__sound__type=Sound.TYPE_ARCHIVE) \ .first() # Can't use delay: diffusion may start later than its assigned start. log = None if not diff else self.logs.start().filter(diffusion=diff) @@ -228,13 +228,13 @@ class Monitor: def start_diff(self, source, diff): playlist = Sound.objects.episode(id=diff.episode_id).paths() source.append(*playlist) - self.log(type=Log.Type.start, source=source.id, diffusion=diff, + self.log(type=Log.TYPE_START, source=source.id, diffusion=diff, comment=str(diff)) def cancel_diff(self, source, diff): - diff.type = Diffusion.Type.cancel + diff.type = Diffusion.TYPE_CANCEL diff.save() - self.log(type=Log.Type.cancel, source=source.id, diffusion=diff, + self.log(type=Log.TYPE_CANCEL, source=source.id, diffusion=diff, comment=str(diff)) def sync(self): diff --git a/aircox/models/__pycache__/__init__.cpython-37.pyc b/aircox/models/__pycache__/__init__.cpython-37.pyc index 2b2f323..82b22fa 100644 Binary files a/aircox/models/__pycache__/__init__.cpython-37.pyc and b/aircox/models/__pycache__/__init__.cpython-37.pyc differ diff --git a/aircox/models/__pycache__/episode.cpython-37.pyc b/aircox/models/__pycache__/episode.cpython-37.pyc index c2f0469..5c71c93 100644 Binary files a/aircox/models/__pycache__/episode.cpython-37.pyc and b/aircox/models/__pycache__/episode.cpython-37.pyc differ diff --git a/aircox/models/__pycache__/log.cpython-37.pyc b/aircox/models/__pycache__/log.cpython-37.pyc index 1d10ae4..2b34c89 100644 Binary files a/aircox/models/__pycache__/log.cpython-37.pyc and b/aircox/models/__pycache__/log.cpython-37.pyc differ diff --git a/aircox/models/__pycache__/page.cpython-37.pyc b/aircox/models/__pycache__/page.cpython-37.pyc index 5bc0957..ce694a4 100644 Binary files a/aircox/models/__pycache__/page.cpython-37.pyc and b/aircox/models/__pycache__/page.cpython-37.pyc differ diff --git a/aircox/models/__pycache__/program.cpython-37.pyc b/aircox/models/__pycache__/program.cpython-37.pyc index 90e1f0b..d140015 100644 Binary files a/aircox/models/__pycache__/program.cpython-37.pyc and b/aircox/models/__pycache__/program.cpython-37.pyc differ diff --git a/aircox/models/__pycache__/sound.cpython-37.pyc b/aircox/models/__pycache__/sound.cpython-37.pyc index dc7b250..ccf4c73 100644 Binary files a/aircox/models/__pycache__/sound.cpython-37.pyc and b/aircox/models/__pycache__/sound.cpython-37.pyc differ diff --git a/aircox/models/__pycache__/station.cpython-37.pyc b/aircox/models/__pycache__/station.cpython-37.pyc index fb3904f..f240a6c 100644 Binary files a/aircox/models/__pycache__/station.cpython-37.pyc and b/aircox/models/__pycache__/station.cpython-37.pyc differ diff --git a/aircox/models/article.py b/aircox/models/article.py index e99f30a..c631632 100644 --- a/aircox/models/article.py +++ b/aircox/models/article.py @@ -1,11 +1,17 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ -from .page import Page +from .page import Page, PageQuerySet from .program import Program, InProgramQuerySet +class ArticleQuerySet(InProgramQuerySet, PageQuerySet): + pass + + class Article(Page): + detail_url_name = 'article-detail' + program = models.ForeignKey( Program, models.SET_NULL, verbose_name=_('program'), blank=True, null=True, @@ -17,7 +23,7 @@ class Article(Page): 'instead of a blog article'), ) - objects = InProgramQuerySet.as_manager() + objects = ArticleQuerySet.as_manager() class Meta: verbose_name = _('Article') diff --git a/aircox/models/episode.py b/aircox/models/episode.py index 0182282..8826611 100644 --- a/aircox/models/episode.py +++ b/aircox/models/episode.py @@ -1,9 +1,7 @@ import datetime -from enum import IntEnum from django.db import models -from django.db.models import F, Q -from django.db.models.functions import Concat, Substr +from django.db.models import Q from django.utils import timezone as tz from django.utils.translation import ugettext_lazy as _ from django.utils.functional import cached_property @@ -64,7 +62,7 @@ class DiffusionQuerySet(BaseRerunQuerySet): def on_air(self): """ On air diffusions """ - return self.filter(type=Diffusion.Type.on_air) + return self.filter(type=Diffusion.TYPE_ON_AIR) def now(self, now=None, order=True): """ Diffusions occuring now """ @@ -132,20 +130,20 @@ class Diffusion(BaseRerun): """ objects = DiffusionQuerySet.as_manager() - class Type(IntEnum): - on_air = 0x00 - unconfirmed = 0x01 - cancel = 0x02 + TYPE_ON_AIR = 0x00 + TYPE_UNCONFIRMED = 0x01 + TYPE_CANCEL = 0x02 + TYPE_CHOICES = ( + (TYPE_ON_AIR, _('on air')), + (TYPE_UNCONFIRMED, _('not confirmed')), + (TYPE_CANCEL, _('cancelled')), + ) episode = models.ForeignKey( - Episode, models.CASCADE, - verbose_name=_('episode'), + Episode, models.CASCADE, verbose_name=_('episode'), ) type = models.SmallIntegerField( - verbose_name=_('type'), - default=Type.on_air, - choices=[(int(y), _(x.replace('_', ' '))) - for x, y in Type.__members__.items()], + verbose_name=_('type'), default=TYPE_ON_AIR, choices=TYPE_CHOICES, ) start = models.DateTimeField(_('start')) end = models.DateTimeField(_('end')) @@ -222,7 +220,7 @@ class Diffusion(BaseRerun): # TODO: property? def is_live(self): """ True if Diffusion is live (False if there are sounds files). """ - return self.type == self.Type.on_air and \ + return self.type == self.TYPE_ON_AIR and \ not self.episode.sound_set.archive().count() def get_playlist(self, **types): @@ -232,7 +230,7 @@ class Diffusion(BaseRerun): """ from .sound import Sound return list(self.get_sounds(**types) - .filter(path__isnull=False, type=Sound.Type.archive) + .filter(path__isnull=False, type=Sound.TYPE_ARCHIVE) .values_list('path', flat=True)) def get_sounds(self, **types): diff --git a/aircox/models/log.py b/aircox/models/log.py index d4c8897..53097bf 100644 --- a/aircox/models/log.py +++ b/aircox/models/log.py @@ -1,6 +1,4 @@ from collections import deque -import datetime -from enum import IntEnum import logging import os @@ -9,7 +7,7 @@ from django.utils import timezone as tz from django.utils.translation import ugettext_lazy as _ -from aircox import settings, utils +from aircox import settings from .episode import Diffusion from .sound import Sound, Track from .station import Station @@ -35,10 +33,10 @@ class LogQuerySet(models.QuerySet): self.filter(date__date__gte=date) def on_air(self): - return self.filter(type=Log.Type.on_air) + return self.filter(type=Log.TYPE_ON_AIR) def start(self): - return self.filter(type=Log.Type.start) + return self.filter(type=Log.TYPE_START) def with_diff(self, with_it=True): return self.filter(diffusion__isnull=not with_it) @@ -163,43 +161,33 @@ class Log(models.Model): This only remember what has been played on the outputs, not on each source; Source designate here which source is responsible of that. """ - class Type(IntEnum): - stop = 0x00 - """ - Source has been stopped, e.g. manually - """ - # Rule: \/ diffusion != null \/ sound != null - start = 0x01 - """ Diffusion or sound has been request to be played. """ - cancel = 0x02 - """ Diffusion has been canceled. """ - # Rule: \/ sound != null /\ track == null - # \/ sound == null /\ track != null - # \/ sound == null /\ track == null /\ comment = sound_path - on_air = 0x03 - """ - The sound or diffusion has been detected occurring on air. Can - also designate live diffusion, although Liquidsoap did not play - them since they don't have an attached sound archive. - """ - other = 0x04 - """ Other log """ + + TYPE_STOP = 0x00 + """ Source has been stopped, e.g. manually """ + # Rule: \/ diffusion != null \/ sound != null + TYPE_START = 0x01 + """ Diffusion or sound has been request to be played. """ + TYPE_CANCEL = 0x02 + """ Diffusion has been canceled. """ + # Rule: \/ sound != null /\ track == null + # \/ sound == null /\ track != null + # \/ sound == null /\ track == null /\ comment = sound_path + TYPE_ON_AIR = 0x03 + """ Sound or diffusion occured on air """ + TYPE_OTHER = 0x04 + """ Other log """ + TYPE_CHOICES = ( + (TYPE_STOP, _('stop')), (TYPE_START, _('start')), + (TYPE_CANCEL, _('cancelled')), (TYPE_ON_AIR, _('on air')), + (TYPE_OTHER, _('other')) + ) station = models.ForeignKey( Station, models.CASCADE, - verbose_name=_('station'), - help_text=_('related station'), - ) - type = models.SmallIntegerField( - choices=[(int(y), _(x.replace('_', ' '))) - for x, y in Type.__members__.items()], - blank=True, null=True, - verbose_name=_('type'), - ) - date = models.DateTimeField( - default=tz.now, db_index=True, - verbose_name=_('date'), + verbose_name=_('station'), help_text=_('related station'), ) + type = models.SmallIntegerField(_('type'), choices=TYPE_CHOICES) + date = models.DateTimeField(_('date'), default=tz.now, db_index=True) source = models.CharField( # we use a CharField to avoid loosing logs information if the # source is removed diff --git a/aircox/models/page.py b/aircox/models/page.py index 8fc61e7..990fdeb 100644 --- a/aircox/models/page.py +++ b/aircox/models/page.py @@ -38,28 +38,30 @@ class Category(models.Model): class PageQuerySet(InheritanceQuerySet): def draft(self): - return self.filter(status=Page.STATUS.draft) + return self.filter(status=Page.STATUS_DRAFT) def published(self): - return self.filter(status=Page.STATUS.published) + return self.filter(status=Page.STATUS_PUBLISHED) def trash(self): - return self.filter(status=Page.STATUS.trash) + return self.filter(status=Page.STATUS_TRASH) class Page(models.Model): """ Base class for publishable content """ - class STATUS(IntEnum): - draft = 0x00 - published = 0x10 - trash = 0x20 + STATUS_DRAFT = 0x00 + STATUS_PUBLISHED = 0x10 + STATUS_TRASH = 0x20 + STATUS_CHOICES = ( + (STATUS_DRAFT, _('draft')), + (STATUS_PUBLISHED, _('published')), + (STATUS_TRASH, _('trash')), + ) title = models.CharField(max_length=128) slug = models.SlugField(_('slug'), blank=True, unique=True) status = models.PositiveSmallIntegerField( - _('status'), - default=STATUS.draft, - choices=[(int(y), _(x)) for x, y in STATUS.__members__.items()], + _('status'), default=STATUS_DRAFT, choices=STATUS_CHOICES, ) category = models.ForeignKey( Category, models.SET_NULL, @@ -84,8 +86,6 @@ class Page(models.Model): detail_url_name = None - class Meta: - abstract = True def __str__(self): return '{}: {}'.format(self._meta.verbose_name, @@ -104,15 +104,15 @@ class Page(models.Model): @property def is_draft(self): - return self.status == self.STATUS.draft + return self.status == self.STATUS_DRAFT @property def is_published(self): - return self.status == self.STATUS.published + return self.status == self.STATUS_PUBLISHED @property def is_trash(self): - return self.status == self.STATUS.trash + return self.status == self.STATUS_TRASH @cached_property def headline(self): @@ -132,6 +132,16 @@ class Page(models.Model): return cls(**cls.get_init_kwargs_from(page, **kwargs)) +class Comment(models.Model): + page = models.ForeignKey( + Page, models.CASCADE, verbose_name=_('related page'), + ) + nickname = models.CharField(_('nickname'), max_length=32) + email = models.EmailField(_('email'), max_length=32) + date = models.DateTimeField(auto_now_add=True) + content = models.TextField(_('content'), max_length=1024) + + class NavItem(models.Model): """ Navigation menu items """ station = models.ForeignKey( diff --git a/aircox/models/program.py b/aircox/models/program.py index dba587d..3e3b777 100644 --- a/aircox/models/program.py +++ b/aircox/models/program.py @@ -45,6 +45,11 @@ class Program(Page): Renaming a Program rename the corresponding directory to matches the new name if it does not exists. """ + # explicit foreign key in order to avoid related name clashes + page = models.OneToOneField( + Page, models.CASCADE, + parent_link=True, related_name='program_page' + ) station = models.ForeignKey( Station, verbose_name=_('station'), @@ -478,7 +483,7 @@ class Schedule(BaseRerun): initial = diffusions[initial] diffusions[date] = Diffusion( - episode=episode, type=Diffusion.Type.on_air, + episode=episode, type=Diffusion.TYPE_ON_AIR, initial=initial, start=date, end=date+duration ) return episodes.values(), diffusions.values() diff --git a/aircox/models/sound.py b/aircox/models/sound.py index f262dcb..a7c9178 100644 --- a/aircox/models/sound.py +++ b/aircox/models/sound.py @@ -36,7 +36,7 @@ class SoundQuerySet(models.QuerySet): def archive(self): """ Return sounds that are archives """ - return self.filter(type=Sound.Type.archive) + return self.filter(type=Sound.TYPE_ARCHIVE) def paths(self, archive=True, order_by=True): """ @@ -55,11 +55,14 @@ class Sound(models.Model): A Sound is the representation of a sound file that can be either an excerpt or a complete archive of the related diffusion. """ - class Type(IntEnum): - other = 0x00, - archive = 0x01, - excerpt = 0x02, - removed = 0x03, + TYPE_OTHER = 0x00 + TYPE_ARCHIVE = 0x01 + TYPE_EXCERPT = 0x02 + TYPE_REMOVED = 0x03 + TYPE_CHOICES = ( + (TYPE_OTHER, _('other')), (TYPE_ARCHIVE, _('archive')), + (TYPE_EXCERPT, _('excerpt')), (TYPE_REMOVED, _('removed')) + ) name = models.CharField(_('name'), max_length=64) program = models.ForeignKey( @@ -72,11 +75,7 @@ class Sound(models.Model): Episode, models.SET_NULL, blank=True, null=True, verbose_name=_('episode'), ) - type = models.SmallIntegerField( - verbose_name=_('type'), - choices=[(int(y), _(x)) for x, y in Type.__members__.items()], - blank=True, null=True - ) + type = models.SmallIntegerField(_('type'), choices=TYPE_CHOICES) # FIXME: url() does not use the same directory than here # should we use FileField for more reliability? path = models.FilePathField( @@ -196,21 +195,21 @@ class Sound(models.Model): """ if not self.file_exists(): - if self.type == self.Type.removed: + if self.type == self.TYPE_REMOVED: return logger.info('sound %s: has been removed', self.path) - self.type = self.Type.removed + self.type = self.TYPE_REMOVED return True # not anymore removed changed = False - if self.type == self.Type.removed and self.program: + if self.type == self.TYPE_REMOVED and self.program: changed = True - self.type = self.Type.archive \ + self.type = self.TYPE_ARCHIVE \ if self.path.startswith(self.program.archives_path) else \ - self.Type.excerpt + self.TYPE_EXCERPT # check mtime -> reset quality if changed (assume file changed) mtime = self.get_mtime() diff --git a/aircox/models/station.py b/aircox/models/station.py index 751cdf1..691a439 100644 --- a/aircox/models/station.py +++ b/aircox/models/station.py @@ -1,4 +1,3 @@ -from enum import IntEnum import os from django.db import models @@ -91,36 +90,32 @@ class Port(models.Model): Some port types may be not available depending on the direction of the port. """ - class Direction(IntEnum): - input = 0x00 - output = 0x01 + DIRECTION_INPUT = 0x00 + DIRECTION_OUTPUT = 0x01 + DIRECTION_CHOICES = ((DIRECTION_INPUT, _('input')), + (DIRECTION_OUTPUT, _('output'))) - class Type(IntEnum): - jack = 0x00 - alsa = 0x01 - pulseaudio = 0x02 - icecast = 0x03 - http = 0x04 - https = 0x05 - file = 0x06 + TYPE_JACK = 0x00 + TYPE_ALSA = 0x01 + TYPE_PULSEAUDIO = 0x02 + TYPE_ICECAST = 0x03 + TYPE_HTTP = 0x04 + TYPE_HTTPS = 0x05 + TYPE_FILE = 0x06 + TYPE_CHOICES = ( + (TYPE_JACK, 'jack'), (TYPE_ALSA, 'alsa'), + (TYPE_PULSEAUDIO, 'pulseaudio'), (TYPE_ICECAST, 'icecast'), + (TYPE_HTTP, 'http'), (TYPE_HTTPS, 'https'), + (TYPE_FILE, _('file')) + ) station = models.ForeignKey( - Station, - verbose_name=_('station'), - on_delete=models.CASCADE, - ) + Station, models.CASCADE, verbose_name=_('station')) direction = models.SmallIntegerField( - _('direction'), - choices=[(int(y), _(x)) for x, y in Direction.__members__.items()], - ) - type = models.SmallIntegerField( - _('type'), - # we don't translate the names since it is project names. - choices=[(int(y), x) for x, y in Type.__members__.items()], - ) + _('direction'), choices=DIRECTION_CHOICES) + type = models.SmallIntegerField(_('type'), choices=TYPE_CHOICES) active = models.BooleanField( - _('active'), - default=True, + _('active'), default=True, help_text=_('this port is active') ) settings = models.TextField( @@ -136,13 +131,13 @@ class Port(models.Model): Return True if the type is available for the given direction. """ - if self.direction == self.Direction.input: + if self.direction == self.DIRECTION_INPUT: return self.type not in ( - self.Type.icecast, self.Type.file + self.TYPE_ICECAST, self.TYPE_FILE ) return self.type not in ( - self.Type.http, self.Type.https + self.TYPE_HTTP, self.TYPE_HTTPS ) def save(self, *args, **kwargs): diff --git a/aircox/static/aircox/main.css b/aircox/static/aircox/main.css index 7d8416f..75b6975 100644 --- a/aircox/static/aircox/main.css +++ b/aircox/static/aircox/main.css @@ -7159,12 +7159,18 @@ label.panel-block { .is-borderless { border: none; } +.has-background-transparent { + background-color: transparent; } + .navbar + .container { margin-top: 1em; } .navbar.has-shadow, .navbar.is-fixed-bottom.has-shadow { box-shadow: 0em 0em 1em rgba(0, 0, 0, 0.1); } +a.navbar-item.is-active { + border-bottom: 1px grey solid; } + /* .navbar-brand img { min-height: 6em; diff --git a/aircox/static/aircox/main.js b/aircox/static/aircox/main.js index eef5775..f2ca7fc 100644 --- a/aircox/static/aircox/main.js +++ b/aircox/static/aircox/main.js @@ -419,7 +419,7 @@ eval("/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; -eval("/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"a\", function() { return render; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"b\", function() { return staticRenderFns; });\nvar render = function() {\n var _vm = this\n var _h = _vm.$createElement\n var _c = _vm._self._c || _h\n return _c(\n \"div\",\n [\n _c(\"div\", { staticClass: \"tabs is-centered\" }, [\n _c(\"ul\", [_vm._t(\"tabs\", null, { value: _vm.value })], 2)\n ]),\n _vm._v(\" \"),\n _vm._t(\"default\", null, { value: _vm.value })\n ],\n 2\n )\n}\nvar staticRenderFns = []\nrender._withStripped = true\n\n\n\n//# sourceURL=webpack:///./assets/vue/tabs.vue?./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib??vue-loader-options"); +eval("/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"a\", function() { return render; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"b\", function() { return staticRenderFns; });\nvar render = function() {\n var _vm = this\n var _h = _vm.$createElement\n var _c = _vm._self._c || _h\n return _c(\n \"div\",\n [\n _c(\"div\", { staticClass: \"tabs is-centered is-medium\" }, [\n _c(\"ul\", [_vm._t(\"tabs\", null, { value: _vm.value })], 2)\n ]),\n _vm._v(\" \"),\n _vm._t(\"default\", null, { value: _vm.value })\n ],\n 2\n )\n}\nvar staticRenderFns = []\nrender._withStripped = true\n\n\n\n//# sourceURL=webpack:///./assets/vue/tabs.vue?./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib??vue-loader-options"); /***/ }) diff --git a/aircox/templates/admin/aircox/page_change_form.html b/aircox/templates/admin/aircox/page_change_form.html new file mode 100644 index 0000000..9aac820 --- /dev/null +++ b/aircox/templates/admin/aircox/page_change_form.html @@ -0,0 +1,32 @@ +{% extends "admin/change_form.html" %} +{% load i18n static %} + +{% block extrahead %}{{ block.super }} + + +{% endblock %} + +{% block submit_buttons_bottom %} +{% if has_change_permission %} +
+
+ {% if original and not original.is_trash %} + + {% endif %} + {% if original and not original.is_draft %} + + {% endif %} +
+ +
+ + + {% if not original.is_published %} + + {% endif %} +
+ +{% endif %} +
+{% endblock %} + diff --git a/aircox/templates/aircox/article_detail.html b/aircox/templates/aircox/article_detail.html new file mode 100644 index 0000000..72a5630 --- /dev/null +++ b/aircox/templates/aircox/article_detail.html @@ -0,0 +1,29 @@ +{% extends "aircox/page.html" %} +{% load i18n %} + +{% block side_nav %} +{{ block.super }} + +{% if side_items %} +
+

{% trans "Latest news" %}

+ + {% for object in side_items %} + {% include "aircox/page_item.html" %} + {% endfor %} + +
+ +
+{% endif %} +{% endblock %} + diff --git a/aircox/templates/aircox/article_list.html b/aircox/templates/aircox/article_list.html new file mode 100644 index 0000000..cc4776e --- /dev/null +++ b/aircox/templates/aircox/article_list.html @@ -0,0 +1,12 @@ +{% extends "aircox/page_list.html" %} +{% load i18n aircox %} + +{% block title %} +{% if parent %} + {% with parent.title as parent %} + {% blocktrans %}Articles of {{ parent }}{% endblocktrans %} + {% endwith %} +{% else %}{{ block.super }}{% endif %} +{% endblock %} + + diff --git a/aircox/templates/aircox/base.html b/aircox/templates/aircox/base.html index 1812fdb..ea38221 100644 --- a/aircox/templates/aircox/base.html +++ b/aircox/templates/aircox/base.html @@ -67,6 +67,7 @@ Context: {% block main %}{% endblock main %} + {% if show_side_nav %}