)?" r"(?P
" in self.content:
+ return self.content
+ content = self._url_re.sub(r'\1', self.content)
+ return content.replace("\n\n", "\n").replace("\n", " ' + func(text) + ' fred, barney, & pebbles \n {{ error }}\n \n \n \n \n \n \n
")
@classmethod
def get_init_kwargs_from(cls, page, **kwargs):
@@ -161,6 +187,7 @@ class BasePage(models.Model):
return cls(**cls.get_init_kwargs_from(page, **kwargs))
+# FIXME: rename
class PageQuerySet(BasePageQuerySet):
def published(self):
return self.filter(status=Page.STATUS_PUBLISHED, pub_date__lte=tz.now())
@@ -189,18 +216,67 @@ class Page(BasePage):
objects = PageQuerySet.as_manager()
+ detail_url_name = ""
+ list_url_name = "page-list"
+ edit_url_name = ""
+
+ @classmethod
+ def get_list_url(cls, kwargs={}):
+ return reverse(cls.list_url_name, kwargs=kwargs)
+
class Meta:
verbose_name = _("Publication")
verbose_name_plural = _("Publications")
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.__initial_cover = self.cover
+
def save(self, *args, **kwargs):
if self.is_published and self.pub_date is None:
self.pub_date = tz.now()
elif not self.is_published:
self.pub_date = None
+ super().save(*args, **kwargs)
- if self.parent and not self.category:
- self.category = self.parent.category
+
+class ChildPage(Page):
+ parent = models.ForeignKey(Page, models.CASCADE, blank=True, null=True, db_index=True, related_name="%(class)s_set")
+
+ class Meta:
+ abstract = True
+
+ @property
+ def display_title(self):
+ if self.is_published:
+ return self.title
+ return self.parent and self.parent.title or ""
+
+ @property
+ def display_headline(self):
+ if not self.content or not self.is_published:
+ return self.parent and self.parent.display_headline or ""
+ return super().display_headline
+
+ @cached_property
+ def parent_subclass(self):
+ if self.parent_id:
+ return Page.objects.get_subclass(id=self.parent_id)
+ return None
+
+ def get_absolute_url(self):
+ if not self.is_published and self.parent_subclass:
+ return self.parent_subclass.get_absolute_url()
+ return super().get_absolute_url()
+
+ def save(self, *args, **kwargs):
+ if self.parent:
+ if self.parent == self:
+ self.parent = None
+ if not self.cover:
+ self.cover = self.parent.cover
+ if not self.category:
+ self.category = self.parent.category
super().save(*args, **kwargs)
@@ -209,45 +285,37 @@ class StaticPage(BasePage):
detail_url_name = "static-page-detail"
- ATTACH_TO_HOME = 0x00
- ATTACH_TO_DIFFUSIONS = 0x01
- ATTACH_TO_LOGS = 0x02
- ATTACH_TO_PROGRAMS = 0x03
- ATTACH_TO_EPISODES = 0x04
- ATTACH_TO_ARTICLES = 0x05
+ class Target(models.TextChoices):
+ NONE = "", _("None")
+ HOME = "home", _("Home Page")
+ TIMETABLE = "timetable-list", _("Timetable")
+ PROGRAMS = "program-list", _("Programs list")
+ EPISODES = "episode-list", _("Episodes list")
+ ARTICLES = "article-list", _("Articles list")
+ PAGES = "page-list", _("Publications list")
+ PODCASTS = "podcast-list", _("Podcasts list")
- ATTACH_TO_CHOICES = (
- (ATTACH_TO_HOME, _("Home page")),
- (ATTACH_TO_DIFFUSIONS, _("Diffusions page")),
- (ATTACH_TO_LOGS, _("Logs page")),
- (ATTACH_TO_PROGRAMS, _("Programs list")),
- (ATTACH_TO_EPISODES, _("Episodes list")),
- (ATTACH_TO_ARTICLES, _("Articles list")),
- )
- VIEWS = {
- ATTACH_TO_HOME: "home",
- ATTACH_TO_DIFFUSIONS: "diffusion-list",
- ATTACH_TO_LOGS: "log-list",
- ATTACH_TO_PROGRAMS: "program-list",
- ATTACH_TO_EPISODES: "episode-list",
- ATTACH_TO_ARTICLES: "article-list",
- }
-
- attach_to = models.SmallIntegerField(
+ attach_to = models.CharField(
_("attach to"),
- choices=ATTACH_TO_CHOICES,
+ choices=Target.choices,
+ max_length=32,
blank=True,
null=True,
help_text=_("display this page content to related element"),
)
+ def get_related_view(self):
+ from ..views.page import attached_views
+
+ return self.attach_to and attached_views.get(self.attach_to) or None
+
def get_absolute_url(self):
if self.attach_to:
- return reverse(self.VIEWS[self.attach_to])
+ return reverse(self.attach_to)
return super().get_absolute_url()
-class Comment(models.Model):
+class Comment(Renderable, models.Model):
page = models.ForeignKey(
Page,
models.CASCADE,
@@ -260,7 +328,7 @@ class Comment(models.Model):
date = models.DateTimeField(auto_now_add=True)
content = models.TextField(_("content"), max_length=1024)
- item_template_name = "aircox/widgets/comment_item.html"
+ template_prefix = "comment"
@cached_property
def parent(self):
@@ -268,7 +336,7 @@ class Comment(models.Model):
return Page.objects.select_subclasses().filter(id=self.page_id).first()
def get_absolute_url(self):
- return self.parent.get_absolute_url()
+ return self.parent.get_absolute_url() + f"#{self._meta.label_lower}-{self.pk}"
class Meta:
verbose_name = _("Comment")
@@ -281,7 +349,7 @@ class NavItem(models.Model):
station = models.ForeignKey(Station, models.CASCADE, verbose_name=_("station"))
menu = models.SlugField(_("menu"), max_length=24)
order = models.PositiveSmallIntegerField(_("order"))
- text = models.CharField(_("title"), max_length=64)
+ text = models.CharField(_("title"), max_length=64, blank=True, null=True)
url = models.CharField(_("url"), max_length=256, blank=True, null=True)
page = models.ForeignKey(
StaticPage,
@@ -300,14 +368,21 @@ class NavItem(models.Model):
def get_url(self):
return self.url if self.url else self.page.get_absolute_url() if self.page else None
+ def get_label(self):
+ if self.text:
+ return self.text
+ elif self.page:
+ return self.page.title
+
def render(self, request, css_class="", active_class=""):
url = self.get_url()
+ label = self.get_label()
if active_class and request.path.startswith(url):
css_class += " " + active_class
if not url:
- return self.text
+ return label
elif not css_class:
- return format_html('{}', url, self.text)
+ return format_html('{}', url, label)
else:
- return format_html('{}', url, css_class, self.text)
+ return format_html('{}', url, css_class, label)
diff --git a/aircox/models/program.py b/aircox/models/program.py
index 7a4fd16..68ca556 100644
--- a/aircox/models/program.py
+++ b/aircox/models/program.py
@@ -1,11 +1,8 @@
-import logging
import os
-import shutil
from django.conf import settings as conf
+from django.contrib.auth.models import Group
from django.db import models
-from django.db.models import F
-from django.db.models.functions import Concat, Substr
from django.utils.translation import gettext_lazy as _
from aircox.conf import settings
@@ -13,13 +10,11 @@ from aircox.conf import settings
from .page import Page, PageQuerySet
from .station import Station
-logger = logging.getLogger("aircox")
-
__all__ = (
+ "ProgramQuerySet",
"Program",
"ProgramChildQuerySet",
- "ProgramQuerySet",
"Stream",
)
@@ -32,6 +27,16 @@ class ProgramQuerySet(PageQuerySet):
def active(self):
return self.filter(active=True)
+ def editor(self, user):
+ """Return programs for which user is an editor.
+
+ Superuser is considered as editor of all groups.
+ """
+ if user.is_superuser:
+ return self
+ groups = self.request.user.groups.all()
+ return self.filter(editors_group__in=groups)
+
class Program(Page):
"""A Program can either be a Streamed or a Scheduled program.
@@ -58,9 +63,12 @@ class Program(Page):
default=True,
help_text=_("update later diffusions according to schedule changes"),
)
+ editors_group = models.ForeignKey(Group, models.CASCADE, verbose_name=_("editors"))
objects = ProgramQuerySet.as_manager()
detail_url_name = "program-detail"
+ list_url_name = "program-list"
+ edit_url_name = "program-edit"
@property
def path(self):
@@ -80,11 +88,10 @@ class Program(Page):
def excerpts_path(self):
return os.path.join(self.path, settings.SOUND_ARCHIVES_SUBDIR)
- def __init__(self, *kargs, **kwargs):
- super().__init__(*kargs, **kwargs)
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
if self.slug:
self.__initial_path = self.path
- self.__initial_cover = self.cover
@classmethod
def get_from_path(cl, path):
@@ -116,27 +123,21 @@ class Program(Page):
def __str__(self):
return self.title
- def save(self, *kargs, **kwargs):
- from .sound import Sound
+ def save(self, *args, **kwargs):
+ if not self.editors_group_id:
+ from aircox import permissions
- super().save(*kargs, **kwargs)
+ saved = permissions.program.init(self)
+ if saved:
+ return
- # TODO: move in signals
- path_ = getattr(self, "__initial_path", None)
- abspath = path_ and os.path.join(conf.MEDIA_ROOT, path_)
- if path_ is not None and path_ != self.path and os.path.exists(abspath) and not os.path.exists(self.abspath):
- logger.info(
- "program #%s's dir changed to %s - update it.",
- self.id,
- self.title,
- )
-
- shutil.move(abspath, self.abspath)
- Sound.objects.filter(path__startswith=path_).update(file=Concat("file", Substr(F("file"), len(path_))))
+ super().save()
class ProgramChildQuerySet(PageQuerySet):
def station(self, station=None, id=None):
+ # lookup `__program` is due to parent being a page subclass (page is
+ # concrete).
return (
self.filter(parent__program__station=station)
if id is None
@@ -146,6 +147,10 @@ class ProgramChildQuerySet(PageQuerySet):
def program(self, program=None, id=None):
return self.parent(program, id)
+ def editor(self, user):
+ programs = Program.objects.editor(user)
+ return self.filter(parent__program__in=programs)
+
class Stream(models.Model):
"""When there are no program scheduled, it is possible to play sounds in
diff --git a/aircox/models/rerun.py b/aircox/models/rerun.py
index 068b22c..f641117 100644
--- a/aircox/models/rerun.py
+++ b/aircox/models/rerun.py
@@ -1,5 +1,6 @@
from django.core.exceptions import ValidationError
from django.db import models
+from django.db.models import F, Q
from django.utils.translation import gettext_lazy as _
from .program import Program
@@ -45,7 +46,7 @@ class Rerun(models.Model):
models.SET_NULL,
related_name="rerun_set",
verbose_name=_("rerun of"),
- limit_choices_to={"initial__isnull": True},
+ limit_choices_to=Q(initial__isnull=True) & Q(program=F("program")),
blank=True,
null=True,
db_index=True,
@@ -74,7 +75,10 @@ class Rerun(models.Model):
raise ValidationError({"initial": _("rerun must happen after original")})
def save_rerun(self):
- self.program = self.initial.program
+ if not self.program_id:
+ self.program = self.initial.program
+ if self.program != self.initial.program:
+ raise ValidationError("Program for the rerun should be the same")
def save_initial(self):
pass
diff --git a/aircox/models/schedule.py b/aircox/models/schedule.py
index 7513d70..8e67001 100644
--- a/aircox/models/schedule.py
+++ b/aircox/models/schedule.py
@@ -42,6 +42,7 @@ class Schedule(Rerun):
second_and_fourth = 0b001010, _("2nd and 4th {day} of the month")
every = 0b011111, _("{day}")
one_on_two = 0b100000, _("one {day} on two")
+ # every_weekday = 0b10000000 _("from Monday to Friday")
date = models.DateField(
_("date"),
@@ -71,6 +72,10 @@ class Schedule(Rerun):
verbose_name = _("Schedule")
verbose_name_plural = _("Schedules")
+ def __init__(self, *args, **kwargs):
+ self._initial = kwargs
+ super().__init__(*args, **kwargs)
+
def __str__(self):
return "{} - {}, {}".format(
self.program.title,
@@ -110,16 +115,28 @@ class Schedule(Rerun):
date = tz.datetime.combine(date, self.time)
return date.replace(tzinfo=self.tz)
- def dates_of_month(self, date):
- """Return normalized diffusion dates of provided date's month."""
- if self.frequency == Schedule.Frequency.ponctual:
+ def dates_of_month(self, date, frequency=None, sched_date=None):
+ """Return normalized diffusion dates of provided date's month.
+
+ :param Date date: date of the month to get dates from;
+ :param Schedule.Frequency frequency: frequency (defaults to ``self.frequency``)
+ :param Date sched_date: schedule start date (defaults to ``self.date``)
+ :return list of diffusion dates
+ """
+ if frequency is None:
+ frequency = self.frequency
+
+ if sched_date is None:
+ sched_date = self.date
+
+ if frequency == Schedule.Frequency.ponctual:
return []
- sched_wday, freq = self.date.weekday(), self.frequency
+ sched_wday = sched_date.weekday()
date = date.replace(day=1)
# last of the month
- if freq == Schedule.Frequency.last:
+ if frequency == Schedule.Frequency.last:
date = date.replace(day=calendar.monthrange(date.year, date.month)[1])
date_wday = date.weekday()
@@ -134,33 +151,42 @@ class Schedule(Rerun):
date_wday, month = date.weekday(), date.month
date += tz.timedelta(days=(7 if date_wday > sched_wday else 0) - date_wday + sched_wday)
- if freq == Schedule.Frequency.one_on_two:
+ if frequency == Schedule.Frequency.one_on_two:
# - adjust date with modulo 14 (= 2 weeks in days)
# - there are max 3 "weeks on two" per month
- if (date - self.date).days % 14:
+ if (date - sched_date).days % 14:
date += tz.timedelta(days=7)
dates = (date + tz.timedelta(days=14 * i) for i in range(0, 3))
else:
- dates = (date + tz.timedelta(days=7 * week) for week in range(0, 5) if freq & (0b1 << week))
+ dates = (date + tz.timedelta(days=7 * week) for week in range(0, 5) if frequency & (0b1 << week))
return [self.normalize(date) for date in dates if date.month == month]
- def diffusions_of_month(self, date):
+ def diffusions_of_month(self, date, frequency=None, sched_date=None):
"""Get episodes and diffusions for month of provided date, including
reruns.
+ :param Date date: date of the month to get diffusions from;
+ :param Schedule.Frequency frequency: frequency (defaults to ``self.frequency``)
+ :param Date sched_date: schedule start date (defaults to ``self.date``)
:returns: tuple([Episode], [Diffusion])
"""
from .diffusion import Diffusion
from .episode import Episode
- if self.initial is not None or self.frequency == Schedule.Frequency.ponctual:
+ if frequency is None:
+ frequency = self.frequency
+
+ if sched_date is None:
+ sched_date = self.date
+
+ if self.initial is not None or frequency == Schedule.Frequency.ponctual:
return [], []
# dates for self and reruns as (date, initial)
- reruns = [(rerun, rerun.date - self.date) for rerun in self.rerun_set.all()]
+ reruns = [(rerun, rerun.date - sched_date) for rerun in self.rerun_set.all()]
- dates = {date: None for date in self.dates_of_month(date)}
+ dates = {date: None for date in self.dates_of_month(date, frequency, sched_date)}
dates.update(
(rerun.normalize(date.date() + delta), date) for date in list(dates.keys()) for rerun, delta in reruns
)
diff --git a/aircox/models/signals.py b/aircox/models/signals.py
index a30353a..b465310 100755
--- a/aircox/models/signals.py
+++ b/aircox/models/signals.py
@@ -1,16 +1,27 @@
+import logging
+import os
+import shutil
+
+from django.conf import settings as conf
from django.contrib.auth.models import Group, Permission, User
from django.db import transaction
-from django.db.models import signals
+from django.db.models import signals, F
+from django.db.models.functions import Concat, Substr
from django.dispatch import receiver
from django.utils import timezone as tz
from aircox import utils
from aircox.conf import settings
+from .article import Article
from .diffusion import Diffusion
from .episode import Episode
from .page import Page
from .program import Program
from .schedule import Schedule
+from .sound import Sound
+
+
+logger = logging.getLogger("aircox")
# Add a default group to a user when it is created. It also assigns a list
@@ -39,27 +50,43 @@ def user_default_groups(sender, instance, created, *args, **kwargs):
instance.groups.add(group)
+# ---- page
@receiver(signals.post_save, sender=Page)
-def page_post_save(sender, instance, created, *args, **kwargs):
- if not created and instance.cover:
- Page.objects.filter(parent=instance, cover__isnull=True).update(cover=instance.cover)
+def page_post_save__child_page_defaults(sender, instance, created, *args, **kwargs):
+ initial_cover = getattr(instance, "__initial_cover", None)
+ if initial_cover is None and instance.cover is not None:
+ Episode.objects.filter(parent=instance, cover__isnull=True).update(cover=instance.cover)
+ Article.objects.filter(parent=instance, cover__isnull=True).update(cover=instance.cover)
+# ---- program
@receiver(signals.post_save, sender=Program)
-def program_post_save(sender, instance, created, *args, **kwargs):
- """Clean-up later diffusions when a program becomes inactive."""
+def program_post_save__clean_later_episodes(sender, instance, created, *args, **kwargs):
if not instance.active:
Diffusion.objects.program(instance).after(tz.now()).delete()
Episode.objects.parent(instance).filter(diffusion__isnull=True).delete()
- cover = getattr(instance, "__initial_cover", None)
- if cover is None and instance.cover is not None:
- Episode.objects.parent(instance).filter(cover__isnull=True).update(cover=instance.cover)
+
+@receiver(signals.post_save, sender=Program)
+def program_post_save__mv_sounds(sender, instance, created, *args, **kwargs):
+ path_ = getattr(instance, "__initial_path", None)
+ if path_ in (None, instance.path):
+ return
+
+ abspath = path_ and os.path.join(conf.MEDIA_ROOT, path_)
+ if os.path.exists(abspath) and not os.path.exists(instance.abspath):
+ logger.info(
+ f"program #{instance.pk}'s dir changed to {instance.title} - update it.", instance.id, instance.title
+ )
+
+ shutil.move(abspath, instance.abspath)
+ Sound.objects.filter(path__startswith=path_).update(file=Concat("file", Substr(F("file"), len(path_))))
+# ---- schedule
@receiver(signals.pre_save, sender=Schedule)
def schedule_pre_save(sender, instance, *args, **kwargs):
- if getattr(instance, "pk") is not None:
+ if getattr(instance, "pk") is not None and "raw" not in kwargs:
instance._initial = Schedule.objects.get(pk=instance.pk)
@@ -88,9 +115,23 @@ def schedule_post_save(sender, instance, created, *args, **kwargs):
def schedule_pre_delete(sender, instance, *args, **kwargs):
"""Delete later corresponding diffusion to a changed schedule."""
Diffusion.objects.filter(schedule=instance).after(tz.now()).delete()
- Episode.objects.filter(diffusion__isnull=True, content__isnull=True, sound__isnull=True).delete()
+ Episode.objects.filter(diffusion__isnull=True, content__isnull=True, episodesound__isnull=True).delete()
+# ---- diffusion
@receiver(signals.post_delete, sender=Diffusion)
def diffusion_post_delete(sender, instance, *args, **kwargs):
- Episode.objects.filter(diffusion__isnull=True, content__isnull=True, sound__isnull=True).delete()
+ Episode.objects.filter(diffusion__isnull=True, content__isnull=True, episodesound__isnull=True).delete()
+
+
+# ---- files
+@receiver(signals.post_delete, sender=Sound)
+def delete_file(sender, instance, *args, **kwargs):
+ """Deletes file on `post_delete`"""
+ if not instance.file:
+ return
+
+ path = instance.file.path
+ qs = sender.objects.filter(file=path)
+ if not qs.exists() and os.path.exists(path):
+ os.remove(path)
diff --git a/aircox/models/sound.py b/aircox/models/sound.py
index 18133cc..83e6261 100644
--- a/aircox/models/sound.py
+++ b/aircox/models/sound.py
@@ -1,304 +1,195 @@
-import logging
+from datetime import date
import os
+import re
from django.conf import settings as conf
from django.db import models
-from django.db.models import Q
from django.utils import timezone as tz
from django.utils.translation import gettext_lazy as _
-from taggit.managers import TaggableManager
+from aircox import utils
from aircox.conf import settings
-from .episode import Episode
from .program import Program
-
-logger = logging.getLogger("aircox")
+from .file import File, FileQuerySet
-__all__ = ("Sound", "SoundQuerySet", "Track")
+__all__ = ("Sound", "SoundQuerySet")
-class SoundQuerySet(models.QuerySet):
- def station(self, station=None, id=None):
- id = station.pk if id is None else id
- return self.filter(program__station__id=id)
-
- def episode(self, episode=None, id=None):
- id = episode.pk if id is None else id
- return self.filter(episode__id=id)
-
- def diffusion(self, diffusion=None, id=None):
- id = diffusion.pk if id is None else id
- return self.filter(episode__diffusion__id=id)
-
- def available(self):
- return self.exclude(type=Sound.TYPE_REMOVED)
-
- def public(self):
- """Return sounds available as podcasts."""
- return self.filter(is_public=True)
-
+class SoundQuerySet(FileQuerySet):
def downloadable(self):
"""Return sounds available as podcasts."""
return self.filter(is_downloadable=True)
- def archive(self):
+ def broadcast(self):
"""Return sounds that are archives."""
- return self.filter(type=Sound.TYPE_ARCHIVE)
+ return self.filter(broadcast=True, is_removed=False)
- def path(self, paths):
- if isinstance(paths, str):
- return self.filter(file=paths.replace(conf.MEDIA_ROOT + "/", ""))
- return self.filter(file__in=(p.replace(conf.MEDIA_ROOT + "/", "") for p in paths))
-
- def playlist(self, archive=True, order_by=True):
+ def playlist(self, order_by="file"):
"""Return files absolute paths as a flat list (exclude sound without
- path).
-
- If `order_by` is True, order by path.
- """
- if archive:
- self = self.archive()
+ path)."""
if order_by:
- self = self.order_by("file")
+ self = self.order_by(order_by)
return [
os.path.join(conf.MEDIA_ROOT, file)
for file in self.filter(file__isnull=False).values_list("file", flat=True)
]
- def search(self, query):
- return self.filter(
- Q(name__icontains=query)
- | Q(file__icontains=query)
- | Q(program__title__icontains=query)
- | Q(episode__title__icontains=query)
- )
-
-# TODO:
-# - provide a default name based on program and episode
-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."""
-
- 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(
- Program,
- models.CASCADE,
- blank=True, # NOT NULL
- verbose_name=_("program"),
- help_text=_("program related to it"),
- db_index=True,
- )
- episode = models.ForeignKey(
- Episode,
- models.SET_NULL,
- blank=True,
- null=True,
- verbose_name=_("episode"),
- db_index=True,
- )
- type = models.SmallIntegerField(_("type"), choices=TYPE_CHOICES)
- position = models.PositiveSmallIntegerField(
- _("order"),
- default=0,
- help_text=_("position in the playlist"),
- )
-
- def _upload_to(self, filename):
- subdir = settings.SOUND_ARCHIVES_SUBDIR if self.type == self.TYPE_ARCHIVE else settings.SOUND_EXCERPTS_SUBDIR
- return os.path.join(self.program.path, subdir, filename)
-
- file = models.FileField(
- _("file"),
- upload_to=_upload_to,
- max_length=256,
- db_index=True,
- unique=True,
- )
+class Sound(File):
duration = models.TimeField(
_("duration"),
blank=True,
null=True,
help_text=_("duration of the sound"),
)
- mtime = models.DateTimeField(
- _("modification time"),
- blank=True,
- null=True,
- help_text=_("last modification date and time"),
- )
is_good_quality = models.BooleanField(
_("good quality"),
help_text=_("sound meets quality requirements"),
blank=True,
null=True,
)
- is_public = models.BooleanField(
- _("public"),
- help_text=_("whether it is publicly available as podcast"),
- default=False,
- )
is_downloadable = models.BooleanField(
_("downloadable"),
- help_text=_("whether it can be publicly downloaded by visitors (sound must be " "public)"),
+ help_text=_("Sound can be downloaded by website visitors."),
default=False,
)
+ broadcast = models.BooleanField(
+ _("Broadcast"),
+ default=False,
+ help_text=_("The sound is broadcasted on air"),
+ )
objects = SoundQuerySet.as_manager()
class Meta:
- verbose_name = _("Sound")
- verbose_name_plural = _("Sounds")
+ verbose_name = _("Sound file")
+ verbose_name_plural = _("Sound files")
- @property
- def url(self):
- return self.file and self.file.url
+ _path_re = re.compile(
+ "^(?P-1}function aa(e,t){var n=this.__data__,r=ir(n,e);return r<0?(++this.size,n.push([e,t])):n[r][1]=t,this}et.prototype.clear=ua,et.prototype.delete=la,et.prototype.get=oa,et.prototype.has=fa,et.prototype.set=aa;function tt(e){var t=-1,n=e==null?0:e.length;for(this.clear();++t\n \n
\n\n\n","\n \n \n \n \n \n {{ data.row+1 }} \n \n \n \n \n
\n \n \n \n \n \n \n \n
\n \n \n \n
\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n