diff --git a/aircox/admin/schedule.py b/aircox/admin/schedule.py index fbf6f6e..214e9d0 100644 --- a/aircox/admin/schedule.py +++ b/aircox/admin/schedule.py @@ -33,7 +33,7 @@ class ScheduleAdmin(admin.ModelAdmin): program_title.short_description = _("Program") def freq(self, obj): - return obj.get_frequency_verbose() + return obj.get_frequency_display() freq.short_description = _("Day") diff --git a/aircox/models/schedule.py b/aircox/models/schedule.py index ef1388d..e867682 100644 --- a/aircox/models/schedule.py +++ b/aircox/models/schedule.py @@ -1,5 +1,4 @@ import calendar -from collections import OrderedDict import pytz from django.db import models @@ -28,7 +27,7 @@ class Schedule(Rerun): # Important: the first week is always the first week where the weekday of # the schedule is present. # For ponctual programs, there is no need for a schedule, only a diffusion - class Frequency(models.IntegerChoice): + class Frequency(models.IntegerChoices): ponctual = 0b000000, _("ponctual") first = 0b000001, _("1st {day} of the month") second = 0b000010, _("2nd {day} of the month") @@ -50,7 +49,7 @@ class Schedule(Rerun): ) timezone = models.CharField( _("timezone"), - default=tz.get_current_timezone, + default=lambda: tz.get_current_timezone().zone, max_length=100, choices=[(x, x) for x in pytz.all_timezones], help_text=_("timezone used for the date"), @@ -71,7 +70,7 @@ class Schedule(Rerun): def __str__(self): return "{} - {}, {}".format( self.program.title, - self.get_frequency_verbose(), + self.get_frequency_display(), self.time.strftime("%H:%M"), ) @@ -97,12 +96,12 @@ class Schedule(Rerun): """Datetime of the end.""" return self.start + utils.to_timedelta(self.duration) - def get_frequency_verbose(self): + def get_frequency_display(self): """Return frequency formated for display.""" from django.template.defaultfilters import date return ( - self.get_frequency_display() + self._get_FIELD_display(self._meta.get_field("frequency")) .format(day=date(self.date, "l")) .capitalize() ) @@ -156,16 +155,6 @@ class Schedule(Rerun): return [self.normalize(date) for date in dates if date.month == month] - def _exclude_existing_date(self, dates): - from .diffusion import Diffusion - - saved = set( - Diffusion.objects.filter(start__in=dates).values_list( - "start", flat=True - ) - ) - return [date for date in dates if date not in saved] - def diffusions_of_month(self, date): """Get episodes and diffusions for month of provided date, including reruns. @@ -186,13 +175,11 @@ class Schedule(Rerun): (rerun, rerun.date - self.date) for rerun in self.rerun_set.all() ] - dates = OrderedDict((date, None) for date in self.dates_of_month(date)) + dates = {date: None for date in self.dates_of_month(date)} dates.update( - [ - (rerun.normalize(date.date() + delta), date) - for date in 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 diff --git a/aircox/templates/aircox/program_detail.html b/aircox/templates/aircox/program_detail.html index 67f7083..267067b 100644 --- a/aircox/templates/aircox/program_detail.html +++ b/aircox/templates/aircox/program_detail.html @@ -43,7 +43,7 @@

{% translate "Diffusions" %}

{% for schedule in program.schedule_set.all %} - {{ schedule.get_frequency_verbose }} + {{ schedule.get_frequency_display }} {% with schedule.start|date:"H:i" as start %} {% with schedule.end|date:"H:i" as end %} diff --git a/aircox/tests/conftest.py b/aircox/tests/conftest.py index ed26e75..2a29393 100644 --- a/aircox/tests/conftest.py +++ b/aircox/tests/conftest.py @@ -4,47 +4,60 @@ import itertools import pytest from model_bakery import baker -from aircox.models import Diffusion +from aircox import models @pytest.fixture def stations(): - return baker.make("aircox.station", quantity=2) + return baker.make("aircox.station", _quantity=2) @pytest.fixture def programs(stations): - return list( + items = list( itertools.chain( *( - baker.make("aircox.program", quantity=3, station=station) + baker.make("aircox.program", station=station, _quantity=3) for station in stations ) ) ) + for item in items: + item.save() + return items @pytest.fixture def sched_initials(programs): - # use concrete class - return [ - baker.make("aircox.schedule", program=program, time=time(16, 00)) + # use concrete class; timezone is provided in order to ensure DST + items = [ + baker.prepare( + "aircox.schedule", + program=program, + time=time(16, 00), + timezone="Europe/Brussels", + ) for program in programs ] + models.Schedule.objects.bulk_create(items) + return items @pytest.fixture -def sched_reruns(initials): +def sched_reruns(sched_initials): # use concrete class - return [ - baker.make( + items = [ + baker.prepare( "aircox.schedule", initial=initial, + program=initial.program, date=initial.date, time=(initial.start + timedelta(hours=1)).time(), ) - for initial in initials + for initial in sched_initials ] + models.Schedule.objects.bulk_create(items) + return items @pytest.fixture @@ -54,10 +67,6 @@ def schedules(sched_initials, sched_reruns): @pytest.fixture def episodes(programs): - items = [] - for program in programs: - items += [ - baker.make("aircox.episode", parent=program, type=type) - for type, _ in Diffusion.TYPE_CHOICES - ] - return items + return [ + baker.make("aircox.episode", parent=program) for program in programs + ] diff --git a/aircox/tests/models/test_rerun.py b/aircox/tests/models/test_rerun.py index 7f6529e..5cfb895 100644 --- a/aircox/tests/models/test_rerun.py +++ b/aircox/tests/models/test_rerun.py @@ -10,60 +10,60 @@ from aircox.models import Schedule class TestRerunQuerySet: @pytest.mark.django_db - def test_station_by_obj(self, stations): + def test_station_by_obj(self, stations, schedules): for station in stations: queryset = ( Schedule.objects.station(station) .distinct() - .value_list("station", flat=True) + .values_list("program__station", flat=True) ) assert queryset.count() == 1 - assert queryset.first() == station + assert queryset.first() == station.pk @pytest.mark.django_db - def test_station_by_id(self, stations): + def test_station_by_id(self, stations, schedules): for station in stations: queryset = ( Schedule.objects.station(id=station.pk) .distinct() - .value_list("station", flat=True) + .values_list("program__station", flat=True) ) assert queryset.count() == 1 - assert queryset.first() == station + assert queryset.first() == station.pk @pytest.mark.django_db - def test_program_by_obj(self, programs): + def test_program_by_obj(self, programs, schedules): for program in programs: queryset = ( Schedule.objects.program(program) .distinct() - .value_list("program", flat=True) + .values_list("program", flat=True) ) assert queryset.count() == 1 - assert queryset.first() == program + assert queryset.first() == program.pk @pytest.mark.django_db - def test_program_by_id(self, programs): + def test_program_by_id(self, programs, schedules): for program in programs: queryset = ( Schedule.objects.program(id=program.pk) .distinct() - .value_list("program", flat=True) + .values_list("program", flat=True) ) assert queryset.count() == 1 - assert queryset.first() == program + assert queryset.first() == program.pk @pytest.mark.django_db - def test_rerun(self, sched_reruns): - queryset = Schedule.objects.rerun().value_list("initial", flat=True) + def test_rerun(self, schedules): + queryset = Schedule.objects.rerun().values_list("initial", flat=True) assert None not in queryset @pytest.mark.django_db - def test_initial(self, sched_initials): + def test_initial(self, schedules): queryset = ( - Schedule.objects.rerun() + Schedule.objects.initial() .distinct() - .value_list("initial", flat=True) + .values_list("initial", flat=True) ) assert queryset.count() == 1 assert queryset.first() is None diff --git a/aircox/tests/models/test_schedule.py b/aircox/tests/models/test_schedule.py index bde431e..737185a 100644 --- a/aircox/tests/models/test_schedule.py +++ b/aircox/tests/models/test_schedule.py @@ -1,7 +1,13 @@ +from datetime import date, datetime, time, timedelta + import pytest -from datetime import datetime +from model_bakery import baker + +import calendar +from dateutil.relativedelta import relativedelta from aircox import utils +from aircox.models import Diffusion, Schedule class TestSchedule: @@ -32,43 +38,98 @@ class TestSchedule: delta = utils.to_timedelta(schedule.duration) assert schedule.end - schedule.start == delta - # def test_get_frequency_verbose(self): + # def test_get_frequency_display(self): # pass @pytest.mark.django_db def test_normalize(self, schedules): for schedule in schedules: dt = datetime.combine(schedule.date, schedule.time) - assert schedule.normalize(dt).tzinfo.zone == schedule.timezone.zone + assert schedule.normalize(dt).tzinfo.zone == schedule.timezone @pytest.mark.django_db def test_dates_of_month_ponctual(self): - pass + schedule = baker.prepare( + Schedule, frequency=Schedule.Frequency.ponctual + ) + at = schedule.date + relativedelta(months=4) + assert schedule.dates_of_month(at) == [] @pytest.mark.django_db - def test_dates_of_month_n_day_of_month(self): - pass + @pytest.mark.parametrize("months", range(0, 25, 2)) + @pytest.mark.parametrize("hour", range(0, 24, 3)) + def test_dates_of_month_last(self, months, hour): + schedule = baker.prepare( + Schedule, time=time(hour, 00), frequency=Schedule.Frequency.last + ) + at = schedule.date + relativedelta(months=months) + datetimes = schedule.dates_of_month(at) + assert len(datetimes) == 1 + + dt = datetimes[0] + self._assert_date(schedule, at, dt) + + month_info = calendar.monthrange(at.year, at.month) + at = date(at.year, at.month, month_info[1]) + if at.weekday() < schedule.date.weekday(): + at -= timedelta(days=7) + at += timedelta(days=schedule.date.weekday()) - timedelta( + days=at.weekday() + ) + assert dt.date() == at + + # since the same method is used for first, second, etc. frequencies + # we assume testing every is sufficient + @pytest.mark.django_db + @pytest.mark.parametrize("months", range(0, 25, 2)) + @pytest.mark.parametrize("hour", range(0, 24, 3)) + def test_dates_of_month_every(self, months, hour): + schedule = baker.prepare( + Schedule, time=time(hour, 00), frequency=Schedule.Frequency.every + ) + at = schedule.date + relativedelta(months=months) + datetimes = schedule.dates_of_month(at) + last = None + for dt in datetimes: + self._assert_date(schedule, at, dt) + if last: + assert (dt - last).days == 7 + last = dt @pytest.mark.django_db - def test_dates_of_month_first_and_third(self): - pass + @pytest.mark.parametrize("months", range(0, 25, 2)) + @pytest.mark.parametrize("hour", range(0, 24, 3)) + def test_dates_of_month_one_on_two(self, months, hour): + schedule = baker.prepare( + Schedule, + time=time(hour, 00), + frequency=Schedule.Frequency.one_on_two, + ) + at = schedule.date + relativedelta(months=months) + datetimes = schedule.dates_of_month(at) + for dt in datetimes: + self._assert_date(schedule, at, dt) + delta = dt.date() - schedule.date + assert delta.days % 14 == 0 + + def _assert_date(self, schedule, at, dt): + assert dt.year == at.year + assert dt.month == at.month + assert dt.weekday() == schedule.date.weekday() + assert dt.time() == schedule.time + assert dt.tzinfo.zone == schedule.timezone @pytest.mark.django_db - def test_dates_of_month_second_and_fourth(self): - pass + def test_diffusions_of_month(self, sched_initials): + # TODO: test values of initial, rerun + for schedule in sched_initials: + at = schedule.start + timedelta(days=30) + dates = set(schedule.dates_of_month(at)) + episodes, diffusions = schedule.diffusions_of_month(at) - @pytest.mark.django_db - def test_dates_of_month_every(self): - pass - - @pytest.mark.django_db - def test_dates_of_month_one_on_two(self): - pass - - @pytest.mark.django_db - def test__exclude_existing_date(self): - pass - - @pytest.mark.django_db - def test_diffusions_of_month(self): - pass + assert all(r.date in dates for r in episodes) + assert all( + (not r.initial or r.date in dates) + and r.type == Diffusion.TYPE_ON_AIR + for r in diffusions + )