Feat: packaging (#127)

- Add configuration files for packaging
- Precommit now uses ruff

Co-authored-by: bkfox <thomas bkfox net>
Reviewed-on: rc/aircox#127
This commit is contained in:
Thomas Kairos
2023-10-11 10:58:34 +02:00
parent 5ea092dba6
commit f7a61fe6c0
82 changed files with 332 additions and 935 deletions

View File

@ -19,11 +19,7 @@ __all__ = ("Diffusion", "DiffusionQuerySet")
class DiffusionQuerySet(RerunQuerySet):
def episode(self, episode=None, id=None):
"""Diffusions for this episode."""
return (
self.filter(episode=episode)
if id is None
else self.filter(episode__id=id)
)
return self.filter(episode=episode) if id is None else self.filter(episode__id=id)
def on_air(self):
"""On air diffusions."""
@ -40,9 +36,7 @@ class DiffusionQuerySet(RerunQuerySet):
"""Diffusions occuring date."""
date = date or datetime.date.today()
start = tz.make_aware(tz.datetime.combine(date, datetime.time()))
end = tz.make_aware(
tz.datetime.combine(date, datetime.time(23, 59, 59, 999))
)
end = tz.make_aware(tz.datetime.combine(date, datetime.time(23, 59, 59, 999)))
# start = tz.get_current_timezone().localize(start)
# end = tz.get_current_timezone().localize(end)
qs = self.filter(start__range=(start, end))
@ -50,11 +44,7 @@ class DiffusionQuerySet(RerunQuerySet):
def at(self, date, order=True):
"""Return diffusions at specified date or datetime."""
return (
self.now(date, order)
if isinstance(date, tz.datetime)
else self.date(date, order)
)
return self.now(date, order) if isinstance(date, tz.datetime) else self.date(date, order)
def after(self, date=None):
"""Return a queryset of diffusions that happen after the given date
@ -142,9 +132,7 @@ class Diffusion(Rerun):
class Meta:
verbose_name = _("Diffusion")
verbose_name_plural = _("Diffusions")
permissions = (
("programming", _("edit the diffusions' planification")),
)
permissions = (("programming", _("edit the diffusions' planification")),)
def __str__(self):
str_ = "{episode} - {date}".format(
@ -202,19 +190,12 @@ class Diffusion(Rerun):
def is_now(self):
"""True if diffusion is currently running."""
now = tz.now()
return (
self.type == self.TYPE_ON_AIR
and self.start <= now
and self.end >= now
)
return self.type == self.TYPE_ON_AIR and self.start <= now and self.end >= now
@property
def is_live(self):
"""True if Diffusion is live (False if there are sounds files)."""
return (
self.type == self.TYPE_ON_AIR
and not self.episode.sound_set.archive().count()
)
return self.type == self.TYPE_ON_AIR and not self.episode.sound_set.archive().count()
def get_playlist(self, **types):
"""Returns sounds as a playlist (list of *local* archive file path).
@ -224,9 +205,7 @@ class Diffusion(Rerun):
from .sound import Sound
return list(
self.get_sounds(**types)
.filter(path__isnull=False, type=Sound.TYPE_ARCHIVE)
.values_list("path", flat=True)
self.get_sounds(**types).filter(path__isnull=False, type=Sound.TYPE_ARCHIVE).values_list("path", flat=True)
)
def get_sounds(self, **types):
@ -238,9 +217,7 @@ class Diffusion(Rerun):
from .sound import Sound
sounds = (self.initial or self).sound_set.order_by("type", "path")
_in = [
getattr(Sound.Type, name) for name, value in types.items() if value
]
_in = [getattr(Sound.Type, name) for name, value in types.items() if value]
return sounds.filter(type__in=_in)
@ -262,8 +239,7 @@ class Diffusion(Rerun):
# .filter(conflict_with=True)
return (
Diffusion.objects.filter(
Q(start__lt=self.start, end__gt=self.start)
| Q(start__gt=self.start, start__lt=self.end)
Q(start__lt=self.start, end__gt=self.start) | Q(start__gt=self.start, start__lt=self.end)
)
.exclude(pk=self.pk)
.distinct()

View File

@ -24,10 +24,7 @@ class Episode(Page):
"""Return serialized data about podcasts."""
from ..serializers import PodcastSerializer
podcasts = [
PodcastSerializer(s).data
for s in self.sound_set.public().order_by("type")
]
podcasts = [PodcastSerializer(s).data for s in self.sound_set.public().order_by("type")]
if self.cover:
options = {"size": (128, 128), "crop": "scale"}
cover = get_thumbnailer(self.cover).get_thumbnail(options).url
@ -76,6 +73,4 @@ class Episode(Page):
if title is None
else title
)
return super().get_init_kwargs_from(
page, title=title, program=page, **kwargs
)
return super().get_init_kwargs_from(page, title=title, program=page, **kwargs)

View File

@ -18,11 +18,7 @@ __all__ = ("Log", "LogQuerySet")
class LogQuerySet(models.QuerySet):
def station(self, station=None, id=None):
return (
self.filter(station=station)
if id is None
else self.filter(station_id=id)
)
return self.filter(station=station) if id is None else self.filter(station_id=id)
def date(self, date):
start = tz.datetime.combine(date, datetime.time())
@ -32,11 +28,7 @@ class LogQuerySet(models.QuerySet):
# return self.filter(date__date=date)
def after(self, date):
return (
self.filter(date__gte=date)
if isinstance(date, tz.datetime)
else self.filter(date__date__gte=date)
)
return self.filter(date__gte=date) if isinstance(date, tz.datetime) else self.filter(date__date__gte=date)
def on_air(self):
return self.filter(type=Log.TYPE_ON_AIR)

View File

@ -25,9 +25,7 @@ __all__ = (
)
headline_re = re.compile(
r"(<p>)?" r"(?P<headline>[^\n]{1,140}(\n|[^\.]*?\.))" r"(</p>)?"
)
headline_re = re.compile(r"(<p>)?" r"(?P<headline>[^\n]{1,140}(\n|[^\.]*?\.))" r"(</p>)?")
class Category(models.Model):
@ -54,17 +52,11 @@ class BasePageQuerySet(InheritanceQuerySet):
def parent(self, parent=None, id=None):
"""Return pages having this parent."""
return (
self.filter(parent=parent)
if id is None
else self.filter(parent__id=id)
)
return self.filter(parent=parent) if id is None else self.filter(parent__id=id)
def search(self, q, search_content=True):
if search_content:
return self.filter(
models.Q(title__icontains=q) | models.Q(content__icontains=q)
)
return self.filter(models.Q(title__icontains=q) | models.Q(content__icontains=q))
return self.filter(title__icontains=q)
@ -89,9 +81,7 @@ class BasePage(models.Model):
related_name="child_set",
)
title = models.CharField(max_length=100)
slug = models.SlugField(
_("slug"), max_length=120, blank=True, unique=True, db_index=True
)
slug = models.SlugField(_("slug"), max_length=120, blank=True, unique=True, db_index=True)
status = models.PositiveSmallIntegerField(
_("status"),
default=STATUS_DRAFT,
@ -132,11 +122,7 @@ class BasePage(models.Model):
super().save(*args, **kwargs)
def get_absolute_url(self):
return (
reverse(self.detail_url_name, kwargs={"slug": self.slug})
if self.is_published
else "#"
)
return reverse(self.detail_url_name, kwargs={"slug": self.slug}) if self.is_published else "#"
@property
def is_draft(self):
@ -177,9 +163,7 @@ class BasePage(models.Model):
class PageQuerySet(BasePageQuerySet):
def published(self):
return self.filter(
status=Page.STATUS_PUBLISHED, pub_date__lte=tz.now()
)
return self.filter(status=Page.STATUS_PUBLISHED, pub_date__lte=tz.now())
class Page(BasePage):
@ -193,9 +177,7 @@ class Page(BasePage):
null=True,
db_index=True,
)
pub_date = models.DateTimeField(
_("publication date"), blank=True, null=True, db_index=True
)
pub_date = models.DateTimeField(_("publication date"), blank=True, null=True, db_index=True)
featured = models.BooleanField(
_("featured"),
default=False,
@ -296,9 +278,7 @@ class Comment(models.Model):
class NavItem(models.Model):
"""Navigation menu items."""
station = models.ForeignKey(
Station, models.CASCADE, verbose_name=_("station")
)
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)
@ -318,13 +298,7 @@ class NavItem(models.Model):
ordering = ("order", "pk")
def get_url(self):
return (
self.url
if self.url
else self.page.get_absolute_url()
if self.page
else None
)
return self.url if self.url else self.page.get_absolute_url() if self.page else None
def render(self, request, css_class="", active_class=""):
url = self.get_url()
@ -336,6 +310,4 @@ class NavItem(models.Model):
elif not css_class:
return format_html('<a href="{}">{}</a>', url, self.text)
else:
return format_html(
'<a href="{}" class="{}">{}</a>', url, css_class, self.text
)
return format_html('<a href="{}" class="{}">{}</a>', url, css_class, self.text)

View File

@ -47,9 +47,7 @@ class Program(Page):
"""
# explicit foreign key in order to avoid related name clashes
station = models.ForeignKey(
Station, models.CASCADE, verbose_name=_("station")
)
station = models.ForeignKey(Station, models.CASCADE, verbose_name=_("station"))
active = models.BooleanField(
_("active"),
default=True,
@ -126,12 +124,7 @@ class Program(Page):
# 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)
):
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,
@ -139,9 +132,7 @@ class Program(Page):
)
shutil.move(abspath, self.abspath)
Sound.objects.filter(path__startswith=path_).update(
file=Concat("file", Substr(F("file"), len(path_)))
)
Sound.objects.filter(path__startswith=path_).update(file=Concat("file", Substr(F("file"), len(path_))))
class ProgramChildQuerySet(PageQuerySet):

View File

@ -15,18 +15,10 @@ class RerunQuerySet(models.QuerySet):
"""Queryset for Rerun (sub)classes."""
def station(self, station=None, id=None):
return (
self.filter(program__station=station)
if id is None
else self.filter(program__station__id=id)
)
return self.filter(program__station=station) if id is None else self.filter(program__station__id=id)
def program(self, program=None, id=None):
return (
self.filter(program=program)
if id is None
else self.filter(program__id=id)
)
return self.filter(program=program) if id is None else self.filter(program__id=id)
def rerun(self):
return self.filter(initial__isnull=False)
@ -78,14 +70,8 @@ class Rerun(models.Model):
def clean(self):
super().clean()
if (
hasattr(self, "start")
and self.initial is not None
and self.initial.start >= self.start
):
raise ValidationError(
{"initial": _("rerun must happen after original")}
)
if hasattr(self, "start") and self.initial is not None and self.initial.start >= self.start:
raise ValidationError({"initial": _("rerun must happen after original")})
def save_rerun(self):
self.program = self.initial.program

View File

@ -102,11 +102,7 @@ class Schedule(Rerun):
"""Return frequency formated for display."""
from django.template.defaultfilters import date
return (
self._get_FIELD_display(self._meta.get_field("frequency"))
.format(day=date(self.date, "l"))
.capitalize()
)
return self._get_FIELD_display(self._meta.get_field("frequency")).format(day=date(self.date, "l")).capitalize()
def normalize(self, date):
"""Return a datetime set to schedule's time for the provided date,
@ -124,9 +120,7 @@ class Schedule(Rerun):
# last of the month
if freq == Schedule.Frequency.last:
date = date.replace(
day=calendar.monthrange(date.year, date.month)[1]
)
date = date.replace(day=calendar.monthrange(date.year, date.month)[1])
date_wday = date.weekday()
# end of month before the wanted weekday: move one week back
@ -138,9 +132,7 @@ class Schedule(Rerun):
# move to the first day of the month that matches the schedule's
# weekday. Check on SO#3284452 for the formula
date_wday, month = date.weekday(), date.month
date += tz.timedelta(
days=(7 if date_wday > sched_wday else 0) - date_wday + sched_wday
)
date += tz.timedelta(days=(7 if date_wday > sched_wday else 0) - date_wday + sched_wday)
if freq == Schedule.Frequency.one_on_two:
# - adjust date with modulo 14 (= 2 weeks in days)
@ -149,11 +141,7 @@ class Schedule(Rerun):
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 freq & (0b1 << week))
return [self.normalize(date) for date in dates if date.month == month]
@ -166,29 +154,22 @@ class Schedule(Rerun):
from .diffusion import Diffusion
from .episode import Episode
if (
self.initial is not None
or self.frequency == Schedule.Frequency.ponctual
):
if self.initial is not None or self.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 - self.date) for rerun in self.rerun_set.all()]
dates = {date: None for date in self.dates_of_month(date)}
dates.update(
(rerun.normalize(date.date() + delta), date)
for date in list(dates.keys())
for rerun, delta in reruns
(rerun.normalize(date.date() + delta), date) for date in list(dates.keys()) for rerun, delta in reruns
)
# remove dates corresponding to existing diffusions
saved = set(
Diffusion.objects.filter(
start__in=dates.keys(), program=self.program, schedule=self
).values_list("start", flat=True)
Diffusion.objects.filter(start__in=dates.keys(), program=self.program, schedule=self).values_list(
"start", flat=True
)
)
# make diffs

View File

@ -32,9 +32,7 @@ def user_default_groups(sender, instance, created, *args, **kwargs):
group, created = Group.objects.get_or_create(name=group_name)
if created and permissions:
for codename in permissions:
permission = Permission.objects.filter(
codename=codename
).first()
permission = Permission.objects.filter(codename=codename).first()
if permission:
group.permissions.add(permission)
group.save()
@ -44,9 +42,7 @@ def user_default_groups(sender, instance, created, *args, **kwargs):
@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
)
Page.objects.filter(parent=instance, cover__isnull=True).update(cover=instance.cover)
@receiver(signals.post_save, sender=Program)
@ -54,15 +50,11 @@ def program_post_save(sender, instance, created, *args, **kwargs):
"""Clean-up later diffusions when a program becomes inactive."""
if not instance.active:
Diffusion.objects.program(instance).after(tz.now()).delete()
Episode.objects.parent(instance).filter(
diffusion__isnull=True
).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
)
Episode.objects.parent(instance).filter(cover__isnull=True).update(cover=instance.cover)
@receiver(signals.pre_save, sender=Schedule)
@ -77,8 +69,7 @@ def schedule_post_save(sender, instance, created, *args, **kwargs):
corresponding diffusions accordingly."""
initial = getattr(instance, "_initial", None)
if not initial or (
(instance.time, instance.duration, instance.timezone)
== (initial.time, initial.duration, initial.timezone)
(instance.time, instance.duration, instance.timezone) == (initial.time, initial.duration, initial.timezone)
):
return
@ -97,13 +88,9 @@ 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, sound__isnull=True).delete()
@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, sound__isnull=True).delete()

View File

@ -50,9 +50,7 @@ class SoundQuerySet(models.QuerySet):
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)
)
return self.filter(file__in=(p.replace(conf.MEDIA_ROOT + "/", "") for p in paths))
def playlist(self, archive=True, order_by=True):
"""Return files absolute paths as a flat list (exclude sound without
@ -66,9 +64,7 @@ class SoundQuerySet(models.QuerySet):
self = self.order_by("file")
return [
os.path.join(conf.MEDIA_ROOT, file)
for file in self.filter(file__isnull=False).values_list(
"file", flat=True
)
for file in self.filter(file__isnull=False).values_list("file", flat=True)
]
def search(self, query):
@ -122,11 +118,7 @@ class Sound(models.Model):
)
def _upload_to(self, filename):
subdir = (
settings.SOUND_ARCHIVES_SUBDIR
if self.type == self.TYPE_ARCHIVE
else settings.SOUND_EXCERPTS_SUBDIR
)
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(
@ -161,10 +153,7 @@ class Sound(models.Model):
)
is_downloadable = models.BooleanField(
_("downloadable"),
help_text=_(
"whether it can be publicly downloaded by visitors (sound must be "
"public)"
),
help_text=_("whether it can be publicly downloaded by visitors (sound must be " "public)"),
default=False,
)
@ -224,9 +213,7 @@ class Sound(models.Model):
if self.type == self.TYPE_REMOVED and self.program:
changed = True
self.type = (
self.TYPE_ARCHIVE
if self.file.name.startswith(self.program.archives_path)
else self.TYPE_EXCERPT
self.TYPE_ARCHIVE if self.file.name.startswith(self.program.archives_path) else self.TYPE_EXCERPT
)
# check mtime -> reset quality if changed (assume file changed)
@ -299,8 +286,7 @@ class Track(models.Model):
blank=True,
null=True,
help_text=_(
"additional informations about this track, such as "
"the version, if is it a remix, features, etc."
"additional informations about this track, such as " "the version, if is it a remix, features, etc."
),
)
@ -310,13 +296,9 @@ class Track(models.Model):
ordering = ("position",)
def __str__(self):
return "{self.artist} -- {self.title} -- {self.position}".format(
self=self
)
return "{self.artist} -- {self.title} -- {self.position}".format(self=self)
def save(self, *args, **kwargs):
if (self.sound is None and self.episode is None) or (
self.sound is not None and self.episode is not None
):
if (self.sound is None and self.episode is None) or (self.sound is not None and self.episode is not None):
raise ValueError("sound XOR episode is required")
super().save(*args, **kwargs)

View File

@ -67,9 +67,7 @@ class Station(models.Model):
max_length=2048,
null=True,
blank=True,
help_text=_(
"Audio streams urls used by station's player. One url " "a line."
),
help_text=_("Audio streams urls used by station's player. One url " "a line."),
)
default_cover = FilerImageField(
on_delete=models.SET_NULL,
@ -153,16 +151,10 @@ class Port(models.Model):
(TYPE_FILE, _("file")),
)
station = models.ForeignKey(
Station, models.CASCADE, verbose_name=_("station")
)
direction = models.SmallIntegerField(
_("direction"), choices=DIRECTION_CHOICES
)
station = models.ForeignKey(Station, models.CASCADE, verbose_name=_("station"))
direction = models.SmallIntegerField(_("direction"), choices=DIRECTION_CHOICES)
type = models.SmallIntegerField(_("type"), choices=TYPE_CHOICES)
active = models.BooleanField(
_("active"), default=True, help_text=_("this port is active")
)
active = models.BooleanField(_("active"), default=True, help_text=_("this port is active"))
settings = models.TextField(
_("port settings"),
help_text=_(
@ -193,8 +185,6 @@ class Port(models.Model):
def save(self, *args, **kwargs):
if not self.is_valid_type():
raise ValueError(
"port type is not allowed with the given port direction"
)
raise ValueError("port type is not allowed with the given port direction")
return super().save(*args, **kwargs)

View File

@ -15,6 +15,4 @@ class UserSettings(models.Model):
related_name="aircox_settings",
)
playlist_editor_columns = models.JSONField(_("Playlist Editor Columns"))
playlist_editor_sep = models.CharField(
_("Playlist Editor Separator"), max_length=16
)
playlist_editor_sep = models.CharField(_("Playlist Editor Separator"), max_length=16)