diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py index 44c4ba0b..ae80e4aa 100644 --- a/chrono/agendas/models.py +++ b/chrono/agendas/models.py @@ -795,7 +795,7 @@ class Agenda(models.Model): end_datetime__gt=min_start, ) - def prefetch_desks_and_exceptions(self, with_sources=False, min_date=None): + def prefetch_desks_and_exceptions(self, min_date, max_date=None, with_sources=False): if self.kind == 'meetings': desks = self.desk_set.all() elif self.kind == 'virtual': @@ -807,11 +807,12 @@ class Agenda(models.Model): else: raise ValueError('does not work with kind %r' % self.kind) - if min_date: - past_date_time_periods = TimePeriod.objects.filter(desk=OuterRef('pk'), date__lt=min_date) - desks = desks.annotate(has_past_date_time_periods=Exists(past_date_time_periods)) + past_date_time_periods = TimePeriod.objects.filter(desk=OuterRef('pk'), date__lt=min_date) + desks = desks.annotate(has_past_date_time_periods=Exists(past_date_time_periods)) - time_period_queryset = TimePeriod.objects.filter(Q(date__isnull=True) | Q(date__gte=min_date)) + time_period_queryset = TimePeriod.objects.filter(Q(date__isnull=True) | Q(date__gte=min_date)) + if max_date: + time_period_queryset = time_period_queryset.filter(Q(date__isnull=True) | Q(date__lte=max_date)) self.prefetched_desks = desks.prefetch_related( 'unavailability_calendars', Prefetch('timeperiod_set', queryset=time_period_queryset) @@ -2276,11 +2277,12 @@ class Desk(models.Model): def get_opening_hours(self, date): openslots = IntervalSet() weekday_index = get_weekday_index(date) + real_date = date.date() if isinstance(date, datetime.datetime) else date for timeperiod in self.timeperiod_set.all(): if timeperiod.weekday_indexes and weekday_index not in timeperiod.weekday_indexes: continue # timeperiod_set.all() are prefetched, do not filter in queryset - if timeperiod.weekday != date.weekday(): + if timeperiod.date != real_date and timeperiod.weekday != date.weekday(): continue start_datetime = make_aware(datetime.datetime.combine(date, timeperiod.start_time)) end_datetime = make_aware(datetime.datetime.combine(date, timeperiod.end_time)) diff --git a/chrono/manager/views.py b/chrono/manager/views.py index a3d437d5..e4546e99 100644 --- a/chrono/manager/views.py +++ b/chrono/manager/views.py @@ -1264,7 +1264,7 @@ class AgendaDateView(DateMixin, ViewableAgendaMixin): if self.agenda.kind == 'events': queryset = self.agenda.event_set.filter(recurrence_days__isnull=True) else: - self.agenda.prefetch_desks_and_exceptions() + self.agenda.prefetch_desks_and_exceptions(min_date=self.date, max_date=self.get_max_date()) if self.agenda.kind == 'meetings': queryset = self.agenda.event_set.select_related('meeting_type').prefetch_related( 'booking_set' @@ -1324,12 +1324,16 @@ class AgendaDayView(AgendaDateView, DayArchiveView): }, ) + def get_max_date(self): + return self.date.date() + datetime.timedelta(days=1) + def get_timetable_infos(self): timeperiods = itertools.chain(*(d.timeperiod_set.all() for d in self.agenda.prefetched_desks)) timeperiods = [ t for t in timeperiods - if t.weekday == self.date.weekday() + if t.date == self.date.date() + or t.weekday == self.date.weekday() and (not t.weekday_indexes or get_weekday_index(self.date) in t.weekday_indexes) ] @@ -1457,9 +1461,11 @@ class AgendaWeekMonthMixin: if timeperiods: min_timeperiod = min(x.start_time for x in timeperiods) max_timeperiod = max(x.end_time for x in timeperiods) - hide_sunday_timeperiod = not any([e.weekday == 6 for e in timeperiods]) + hide_sunday_timeperiod = not any( + [e.weekday == 6 or (e.date and e.date.weekday() == 6) for e in timeperiods] + ) hide_weekend_timeperiod = hide_sunday_timeperiod and not any( - [e.weekday == 5 for e in timeperiods] + [e.weekday == 5 or (e.date and e.date.weekday() == 5) for e in timeperiods] ) active_events = [ x for x in self.object_list if any([y.cancellation_datetime is None for y in x.booking_set.all()]) @@ -1633,6 +1639,9 @@ class AgendaWeekView(AgendaWeekMonthMixin, AgendaDateView, WeekArchiveView): date = datetime.datetime.strptime('%s-W%s-1' % (self.get_year(), self.get_week()), "%Y-W%W-%w") return date.day + def get_max_date(self): + return self.get_next_week(self.date.date()) + agenda_weekly_view = AgendaWeekView.as_view() @@ -1665,6 +1674,9 @@ class AgendaMonthView(AgendaWeekMonthMixin, AgendaDateView, MonthArchiveView): def get_day(self): return '1' + def get_max_date(self): + return self.get_next_month(self.date.date()) + agenda_monthly_view = AgendaMonthView.as_view() diff --git a/tests/manager/test_all.py b/tests/manager/test_all.py index 181de4a5..23d2bbd0 100644 --- a/tests/manager/test_all.py +++ b/tests/manager/test_all.py @@ -3716,3 +3716,152 @@ def test_agenda_day_and_month_views_weekday_indexes(app, admin_user): assert resp.text.count('height:400.0%') == 1 assert resp.text.count('height:700.0%') == 1 assert resp.text.count('height:300.0%') == 1 + + +@freezegun.freeze_time('2022-11-15 14:00') +def test_agenda_calendar_views_date_time_period(app, admin_user): + agenda = Agenda.objects.create(label='New Example', kind='meetings') + desk = Desk.objects.create(agenda=agenda, label='New Desk') + MeetingType.objects.create(agenda=agenda, label='Bar', duration=30) + today = datetime.date.today() + TimePeriod.objects.create( + desk=desk, + date=today, + start_time=datetime.time(10, 0), + end_time=datetime.time(14, 0), + ) + login(app) + + # check day view + resp = app.get('/manage/agendas/%s/%s/%s/%s/' % (agenda.pk, today.year, today.month, today.day)) + assert resp.text.count('14 + assert 'style="height: 400%; top: 0%;"' in resp.text + + resp = app.get('/manage/agendas/%s/%s/%s/%s/' % (agenda.pk, today.year, today.month, today.day + 7)) + assert 'No opening hours this day.' in resp.text + + # check week view + resp = app.get('/manage/agendas/%s/%s/week/%s/' % (agenda.pk, today.year, today.isocalendar().week)) + assert resp.text.count('height:400.0%') == 1 + + # check month view + resp = app.get('/manage/agendas/%s/%s/%s/' % (agenda.pk, today.year, today.month)) + assert resp.text.count('height:400.0%') == 1 + + # check month boundaries + TimePeriod.objects.create( + desk=desk, + date=today.replace(day=1), + start_time=datetime.time(10, 0), + end_time=datetime.time(14, 0), + ) + TimePeriod.objects.create( + desk=desk, + date=today.replace(day=30), + start_time=datetime.time(10, 0), + end_time=datetime.time(14, 0), + ) + resp = app.get('/manage/agendas/%s/%s/%s/' % (agenda.pk, today.year, today.month)) + assert resp.text.count('height:400.0%') == 3 + + TimePeriod.objects.create( + desk=desk, + date=today.replace(day=31, month=10), + start_time=datetime.time(10, 0), + end_time=datetime.time(14, 0), + ) + TimePeriod.objects.create( + desk=desk, + date=today.replace(day=1, month=12), + start_time=datetime.time(10, 0), + end_time=datetime.time(14, 0), + ) + resp = app.get('/manage/agendas/%s/%s/%s/' % (agenda.pk, today.year, today.month)) + assert resp.text.count('height:400.0%') == 3 + + # check week boundaries + TimePeriod.objects.create( + desk=desk, + date=today.replace(day=14), + start_time=datetime.time(10, 0), + end_time=datetime.time(14, 0), + ) + TimePeriod.objects.create( + desk=desk, + date=today.replace(day=20), + start_time=datetime.time(10, 0), + end_time=datetime.time(14, 0), + ) + resp = app.get('/manage/agendas/%s/%s/week/%s/' % (agenda.pk, today.year, today.isocalendar().week)) + assert resp.text.count('height:400.0%') == 3 + + TimePeriod.objects.create( + desk=desk, + date=today.replace(day=13), + start_time=datetime.time(10, 0), + end_time=datetime.time(14, 0), + ) + TimePeriod.objects.create( + desk=desk, + date=today.replace(day=21), + start_time=datetime.time(10, 0), + end_time=datetime.time(14, 0), + ) + resp = app.get('/manage/agendas/%s/%s/week/%s/' % (agenda.pk, today.year, today.isocalendar().week)) + assert resp.text.count('height:400.0%') == 3 + + +@freezegun.freeze_time('2022-11-15 14:00') +@pytest.mark.parametrize('kind', ['meetings', 'virtual']) +def test_agenda_date_time_period_hide_weekend(app, admin_user, kind): + today = datetime.date.today() # Tuesday + if kind == 'meetings': + agenda = Agenda.objects.create(label='Passeports', kind='meetings') + desk = Desk.objects.create(agenda=agenda, label='Desk A') + else: + agenda = Agenda.objects.create(label='Virtual', kind='virtual') + real_agenda = Agenda.objects.create(label='Real 1', kind='meetings') + VirtualMember.objects.create(virtual_agenda=agenda, real_agenda=real_agenda) + desk = Desk.objects.create(agenda=real_agenda, label='New Desk') + TimePeriod.objects.create( + desk=desk, date=today, start_time=datetime.time(10, 0), end_time=datetime.time(18, 0) + ) + + login(app) + # check month view + resp = app.get('/manage/agendas/%s/%s/%s/' % (agenda.pk, today.year, today.month)) + assert 'Sunday' not in resp.text + assert 'Saturday' not in resp.text + + # check week view + resp = app.get('/manage/agendas/%s/%s/week/%s/' % (agenda.pk, today.year, today.isocalendar().week)) + assert 'Sunday' not in resp.text + assert 'Saturday' not in resp.text + + TimePeriod.objects.create( + desk=desk, date=today.replace(day=19), start_time=datetime.time(10, 0), end_time=datetime.time(18, 0) + ) # Saturday + + # check month view + resp = app.get('/manage/agendas/%s/%s/%s/' % (agenda.pk, today.year, today.month)) + assert 'Sunday' not in resp.text + assert 'Saturday' in resp.text + + # check week view + resp = app.get('/manage/agendas/%s/%s/week/%s/' % (agenda.pk, today.year, today.isocalendar().week)) + assert 'Sunday' not in resp.text + assert 'Saturday' in resp.text + + TimePeriod.objects.create( + desk=desk, date=today.replace(day=20), start_time=datetime.time(10, 0), end_time=datetime.time(18, 0) + ) # Sunday + + # check month view + resp = app.get('/manage/agendas/%s/%s/%s/' % (agenda.pk, today.year, today.month)) + assert 'Sunday' in resp.text + assert 'Saturday' in resp.text + + # check week view + resp = app.get('/manage/agendas/%s/%s/week/%s/' % (agenda.pk, today.year, today.isocalendar().week)) + assert 'Sunday' in resp.text + assert 'Saturday' in resp.text diff --git a/tests/test_agendas.py b/tests/test_agendas.py index bb72dd85..2b4d2094 100644 --- a/tests/test_agendas.py +++ b/tests/test_agendas.py @@ -276,7 +276,7 @@ def test_agenda_is_available_for_simple_management(settings, with_prefetch): def check_is_available(result, use_prefetch=True): agenda = Agenda.objects.get() if with_prefetch and use_prefetch: - agenda.prefetch_desks_and_exceptions(with_sources=True) + agenda.prefetch_desks_and_exceptions(with_sources=True, min_date=now()) assert agenda.is_available_for_simple_management() == result agenda = Agenda.objects.create(label='Agenda', kind='meetings') diff --git a/tests/test_time_periods.py b/tests/test_time_periods.py index 0cf542d0..b414ab99 100644 --- a/tests/test_time_periods.py +++ b/tests/test_time_periods.py @@ -299,6 +299,77 @@ def test_desk_opening_hours_weekday_indexes(): assert len(hours) == 0 +def test_desk_opening_hours_date_time_period(): + def set_prefetched_exceptions(desk): + desk.prefetched_exceptions = TimePeriodException.objects.filter( + Q(desk=desk) | Q(unavailability_calendar__desks=desk) + ) + + agenda = Agenda.objects.create(label='Foo bar', slug='bar') + desk = Desk.objects.create(label='Desk 1', agenda=agenda) + + # morning + TimePeriod.objects.create( + desk=desk, + date=datetime.date(2022, 10, 24), + start_time=datetime.time(9, 0), + end_time=datetime.time(12, 0), + ) + set_prefetched_exceptions(desk) + hours = desk.get_opening_hours(datetime.date(2022, 10, 24)) + assert len(hours) == 1 + assert hours[0].begin.time() == datetime.time(9, 0) + assert hours[0].end.time() == datetime.time(12, 0) + + # and afternoon + TimePeriod.objects.create( + desk=desk, + date=datetime.date(2022, 10, 24), + start_time=datetime.time(14, 0), + end_time=datetime.time(17, 0), + ) + set_prefetched_exceptions(desk) + previous_hours = hours + hours = desk.get_opening_hours(datetime.date(2022, 10, 24)) + assert len(hours) == 2 + assert hours[0] == previous_hours[0] + + assert hours[1].begin.time() == datetime.time(14, 0) + assert hours[1].end.time() == datetime.time(17, 0) + + # mix with repeating period + TimePeriod.objects.create( + desk=desk, + weekday=0, + start_time=datetime.time(19, 0), + end_time=datetime.time(20, 0), + ) + previous_hours = hours + hours = desk.get_opening_hours(datetime.date(2022, 10, 24)) + assert len(hours) == 3 + assert hours[:2] == previous_hours[:2] + + assert hours[2].begin.time() == datetime.time(19, 0) + assert hours[2].end.time() == datetime.time(20, 0) + + # full day exception + TimePeriodException.objects.create( + desk=desk, + start_datetime=make_aware(datetime.datetime(2022, 10, 24)), + end_datetime=make_aware(datetime.datetime(2022, 10, 25)), + ) + + set_prefetched_exceptions(desk) + hours = desk.get_opening_hours(datetime.date(2022, 10, 24)) + assert len(hours) == 0 + + # next week + hours = desk.get_opening_hours(datetime.date(2022, 10, 31)) + assert len(hours) == 1 + assert hours[0].begin.time() == datetime.time(19, 0) + assert hours[0].end.time() == datetime.time(20, 0) + + def test_timeperiod_midnight_overlap_time_slots(): # https://dev.entrouvert.org/issues/29142 agenda = Agenda(label='Foo bar', slug='bar')