schedule & diffusions check/update + cleanup Schedule methods
This commit is contained in:
		@ -1,7 +1,7 @@
 | 
			
		||||
from copy import copy
 | 
			
		||||
 | 
			
		||||
from django import forms
 | 
			
		||||
from django.contrib import admin
 | 
			
		||||
from django.forms import ModelForm
 | 
			
		||||
from django.utils.translation import gettext as _
 | 
			
		||||
 | 
			
		||||
from ..models import Episode, Diffusion
 | 
			
		||||
@ -11,7 +11,8 @@ from .sound import SoundInline, TrackInline
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DiffusionBaseAdmin:
 | 
			
		||||
    fields = ['type', 'start', 'end']
 | 
			
		||||
    fields = ('type', 'start', 'end', 'schedule')
 | 
			
		||||
    readonly_fields = ('schedule',)
 | 
			
		||||
 | 
			
		||||
    def get_readonly_fields(self, request, obj=None):
 | 
			
		||||
        fields = super().get_readonly_fields(request, obj)
 | 
			
		||||
@ -35,7 +36,8 @@ class DiffusionAdmin(DiffusionBaseAdmin, admin.ModelAdmin):
 | 
			
		||||
    list_editable = ('type',)
 | 
			
		||||
    ordering = ('-start', 'id')
 | 
			
		||||
 | 
			
		||||
    fields = ['type', 'start', 'end', 'initial', 'program']
 | 
			
		||||
    fields = ('type', 'start', 'end', 'initial', 'program', 'schedule')
 | 
			
		||||
    readonly_fields = ('schedule',)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DiffusionInline(DiffusionBaseAdmin, admin.TabularInline):
 | 
			
		||||
@ -47,7 +49,7 @@ class DiffusionInline(DiffusionBaseAdmin, admin.TabularInline):
 | 
			
		||||
        return request.user.has_perm('aircox_program.scheduling')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EpisodeAdminForm(forms.ModelForm):
 | 
			
		||||
class EpisodeAdminForm(ModelForm):
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
        self.fields['parent'].required = True
 | 
			
		||||
 | 
			
		||||
@ -1,20 +1,33 @@
 | 
			
		||||
from copy import copy
 | 
			
		||||
 | 
			
		||||
from django.contrib import admin
 | 
			
		||||
from django.forms import ModelForm
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
 | 
			
		||||
from ..models import Program, Schedule, Stream
 | 
			
		||||
from .page import PageAdmin
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# In order to simplify schedule_post_save algorithm, an existing schedule can't
 | 
			
		||||
# update the following fields: "frequency", "date"
 | 
			
		||||
class ScheduleInlineForm(ModelForm):
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
        if self.initial:
 | 
			
		||||
            self.fields['date'].disabled = True
 | 
			
		||||
            self.fields['frequency'].disabled = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ScheduleInline(admin.TabularInline):
 | 
			
		||||
    model = Schedule
 | 
			
		||||
    form = ScheduleInlineForm
 | 
			
		||||
    readonly_fields = ('timezone',)
 | 
			
		||||
    extra = 1
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StreamInline(admin.TabularInline):
 | 
			
		||||
    fields = ['delay', 'begin', 'end']
 | 
			
		||||
    model = Stream
 | 
			
		||||
    fields = ['delay', 'begin', 'end']
 | 
			
		||||
    extra = 1
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -71,4 +84,3 @@ class StreamAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ('id', 'program', 'delay', 'begin', 'end')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -203,7 +203,7 @@ msgstr "corbeille"
 | 
			
		||||
 | 
			
		||||
#: models/page.py:73
 | 
			
		||||
msgid "status"
 | 
			
		||||
msgstr "status"
 | 
			
		||||
msgstr "statut"
 | 
			
		||||
 | 
			
		||||
#: models/page.py:77
 | 
			
		||||
msgid "cover"
 | 
			
		||||
 | 
			
		||||
@ -52,23 +52,6 @@ class Actions:
 | 
			
		||||
        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,
 | 
			
		||||
                                      start__gt=self.date)
 | 
			
		||||
        items = []
 | 
			
		||||
        for diffusion in qs:
 | 
			
		||||
            schedules = Schedule.objects.filter(program=diffusion.program)
 | 
			
		||||
            for schedule in schedules:
 | 
			
		||||
                if schedule.match(diffusion.start):
 | 
			
		||||
                    break
 | 
			
		||||
            else:
 | 
			
		||||
                items.append(diffusion.id)
 | 
			
		||||
 | 
			
		||||
        logger.info('[check] %d diffusions will be removed', len(items))
 | 
			
		||||
        if items:
 | 
			
		||||
            Diffusion.objects.filter(id__in=items).delete()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = __doc__
 | 
			
		||||
@ -88,12 +71,6 @@ class Command(BaseCommand):
 | 
			
		||||
            '-l', '--clean', action='store_true',
 | 
			
		||||
            help='remove unconfirmed diffusions older than the given month'
 | 
			
		||||
        )
 | 
			
		||||
        group.add_argument(
 | 
			
		||||
            '-c', '--check', action='store_true',
 | 
			
		||||
            help='check unconfirmed later diffusions from the given '
 | 
			
		||||
                 'date agains\'t schedule. If no schedule is found, remove '
 | 
			
		||||
                 'it.'
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        group = parser.add_argument_group('date')
 | 
			
		||||
        group.add_argument(
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,7 @@ from django.utils.functional import cached_property
 | 
			
		||||
 | 
			
		||||
from aircox import settings, utils
 | 
			
		||||
from .program import Program, ProgramChildQuerySet, \
 | 
			
		||||
        BaseRerun, BaseRerunQuerySet
 | 
			
		||||
        BaseRerun, BaseRerunQuerySet, Schedule
 | 
			
		||||
from .page import Page, PageQuerySet
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -145,6 +145,10 @@ class Diffusion(BaseRerun):
 | 
			
		||||
    episode = models.ForeignKey(
 | 
			
		||||
        Episode, models.CASCADE, verbose_name=_('episode'),
 | 
			
		||||
    )
 | 
			
		||||
    schedule = models.ForeignKey(
 | 
			
		||||
        Schedule, models.CASCADE, verbose_name=_('schedule'),
 | 
			
		||||
        blank=True, null=True,
 | 
			
		||||
    )
 | 
			
		||||
    type = models.SmallIntegerField(
 | 
			
		||||
        verbose_name=_('type'), default=TYPE_ON_AIR, choices=TYPE_CHOICES,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
@ -229,7 +229,7 @@ class BaseRerun(models.Model):
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# BIG FIXME: self.date is still used as datetime
 | 
			
		||||
# ? BIG FIXME: self.date is still used as datetime
 | 
			
		||||
class Schedule(BaseRerun):
 | 
			
		||||
    """
 | 
			
		||||
    A Schedule defines time slots of programs' diffusions. It can be an initial
 | 
			
		||||
@ -284,7 +284,6 @@ class Schedule(BaseRerun):
 | 
			
		||||
        }[x]) for x, y in Frequency.__members__.items()],
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = _('Schedule')
 | 
			
		||||
        verbose_name_plural = _('Schedules')
 | 
			
		||||
@ -340,59 +339,10 @@ class Schedule(BaseRerun):
 | 
			
		||||
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    def match(self, date=None, check_time=True):
 | 
			
		||||
        """
 | 
			
		||||
        Return True if the given date(time) matches the schedule.
 | 
			
		||||
        """
 | 
			
		||||
        date = utils.date_or_default(
 | 
			
		||||
            date, tz.datetime if check_time else datetime.date)
 | 
			
		||||
 | 
			
		||||
        if self.date.weekday() != date.weekday() or \
 | 
			
		||||
                not self.match_week(date):
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        # we check against a normalized version (norm_date will have
 | 
			
		||||
        # schedule's date.
 | 
			
		||||
        return date == self.normalize(date) if check_time else True
 | 
			
		||||
 | 
			
		||||
    def match_week(self, date=None):
 | 
			
		||||
        """
 | 
			
		||||
        Return True if the given week number matches the schedule, False
 | 
			
		||||
        otherwise.
 | 
			
		||||
        If the schedule is ponctual, return None.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        if self.frequency == Schedule.Frequency.ponctual:
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        # since we care only about the week, go to the same day of the week
 | 
			
		||||
        date = utils.date_or_default(date, datetime.date)
 | 
			
		||||
        date += tz.timedelta(days=self.date.weekday() - date.weekday())
 | 
			
		||||
 | 
			
		||||
        # FIXME this case
 | 
			
		||||
 | 
			
		||||
        if self.frequency == Schedule.Frequency.one_on_two:
 | 
			
		||||
            # cf notes in date_of_month
 | 
			
		||||
            diff = date - utils.cast_date(self.date, datetime.date)
 | 
			
		||||
 | 
			
		||||
            return not (diff.days % 14)
 | 
			
		||||
 | 
			
		||||
        first_of_month = date.replace(day=1)
 | 
			
		||||
        week = date.isocalendar()[1] - first_of_month.isocalendar()[1]
 | 
			
		||||
 | 
			
		||||
        # weeks of month
 | 
			
		||||
 | 
			
		||||
        if week == 4:
 | 
			
		||||
            # fifth week: return if for every week
 | 
			
		||||
 | 
			
		||||
            return self.frequency == self.Frequency.every
 | 
			
		||||
 | 
			
		||||
        return (self.frequency & (0b0001 << week) > 0)
 | 
			
		||||
 | 
			
		||||
    def normalize(self, date):
 | 
			
		||||
        """
 | 
			
		||||
        Return a new datetime with schedule time. Timezone is handled
 | 
			
		||||
        using `schedule.timezone`.
 | 
			
		||||
        Return a datetime set to schedule's time for the provided date,
 | 
			
		||||
        handling timezone (based on schedule's timezone).
 | 
			
		||||
        """
 | 
			
		||||
        date = tz.datetime.combine(date, self.time)
 | 
			
		||||
        return self.tz.normalize(self.tz.localize(date))
 | 
			
		||||
@ -412,11 +362,9 @@ class Schedule(BaseRerun):
 | 
			
		||||
            date_wday = date.weekday()
 | 
			
		||||
 | 
			
		||||
            # end of month before the wanted weekday: move one week back
 | 
			
		||||
 | 
			
		||||
            if date_wday < sched_wday:
 | 
			
		||||
                date -= tz.timedelta(days=7)
 | 
			
		||||
            date += tz.timedelta(days=sched_wday - date_wday)
 | 
			
		||||
 | 
			
		||||
            return [self.normalize(date)]
 | 
			
		||||
 | 
			
		||||
        # move to the first day of the month that matches the schedule's weekday
 | 
			
		||||
@ -466,7 +414,8 @@ class Schedule(BaseRerun):
 | 
			
		||||
 | 
			
		||||
        # remove dates corresponding to existing diffusions
 | 
			
		||||
        saved = set(Diffusion.objects.filter(start__in=dates.keys(),
 | 
			
		||||
                                             program=self.program)
 | 
			
		||||
                                             program=self.program,
 | 
			
		||||
                                             schedule=self)
 | 
			
		||||
                             .values_list('start', flat=True))
 | 
			
		||||
 | 
			
		||||
        # make diffs
 | 
			
		||||
@ -487,7 +436,7 @@ class Schedule(BaseRerun):
 | 
			
		||||
                initial = diffusions[initial]
 | 
			
		||||
 | 
			
		||||
            diffusions[date] = Diffusion(
 | 
			
		||||
                episode=episode, type=Diffusion.TYPE_ON_AIR,
 | 
			
		||||
                episode=episode, schedule=self, type=Diffusion.TYPE_ON_AIR,
 | 
			
		||||
                initial=initial, start=date, end=date+duration
 | 
			
		||||
            )
 | 
			
		||||
        return episodes.values(), diffusions.values()
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
import pytz
 | 
			
		||||
 | 
			
		||||
from django.contrib.auth.models import User, Group, Permission
 | 
			
		||||
from django.db import transaction
 | 
			
		||||
from django.db.models import F, signals
 | 
			
		||||
from django.dispatch import receiver
 | 
			
		||||
from django.utils import timezone as tz
 | 
			
		||||
@ -62,7 +63,6 @@ def schedule_pre_save(sender, instance, *args, **kwargs):
 | 
			
		||||
        instance._initial = Schedule.objects.get(pk=instance.pk)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# TODO
 | 
			
		||||
@receiver(signals.post_save, sender=Schedule)
 | 
			
		||||
def schedule_post_save(sender, instance, created, *args, **kwargs):
 | 
			
		||||
    """
 | 
			
		||||
@ -76,27 +76,21 @@ def schedule_post_save(sender, instance, created, *args, **kwargs):
 | 
			
		||||
 | 
			
		||||
    today = tz.datetime.today()
 | 
			
		||||
    delta = instance.normalize(today) - initial.normalize(today)
 | 
			
		||||
 | 
			
		||||
    qs = Diffusion.objects.program(instance.program).after(tz.now())
 | 
			
		||||
    pks = [d.pk for d in qs if initial.match(d.date)]
 | 
			
		||||
    qs.filter(pk__in=pks).update(
 | 
			
		||||
        start=F('start') + delta,
 | 
			
		||||
        end=F('start') + delta + utils.to_timedelta(instance.duration)
 | 
			
		||||
    )
 | 
			
		||||
    duration = utils.to_timedelta(instance.duration)
 | 
			
		||||
    with transaction.atomic():
 | 
			
		||||
        qs = Diffusion.objects.filter(schedule=instance).after(tz.now())
 | 
			
		||||
        for diffusion in qs:
 | 
			
		||||
            diffusion.start = diffusion.start + delta
 | 
			
		||||
            diffusion.end = diffusion.start + duration
 | 
			
		||||
            diffusion.save()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@receiver(signals.pre_delete, sender=Schedule)
 | 
			
		||||
def schedule_pre_delete(sender, instance, *args, **kwargs):
 | 
			
		||||
    """
 | 
			
		||||
    Delete later corresponding diffusion to a changed schedule.
 | 
			
		||||
    """
 | 
			
		||||
    if not instance.program.sync:
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    qs = Diffusion.objects.program(instance.program).after(tz.now())
 | 
			
		||||
    pks = [d.pk for d in qs if instance.match(d.date)]
 | 
			
		||||
    qs.filter(pk__in=pks).delete()
 | 
			
		||||
 | 
			
		||||
    """ 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()
 | 
			
		||||
 | 
			
		||||
@receiver(signals.post_delete, sender=Diffusion)
 | 
			
		||||
def diffusion_post_delete(sender, instance, *args, **kwargs):
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user