From f851de07df1288c46fa89fa02b2e16eb6dbf1f7e Mon Sep 17 00:00:00 2001 From: Josue Kouka Date: Fri, 1 Sep 2017 15:01:07 +0200 Subject: [PATCH] add multiple desk management (#15729) --- chrono/agendas/migrations/0016_desk.py | 26 +++++ .../migrations/0017_timeperiod_desk.py | 43 +++++++ chrono/agendas/migrations/0018_event_desk.py | 37 ++++++ chrono/agendas/models.py | 70 +++++++++--- chrono/api/views.py | 88 ++++++++++---- chrono/manager/forms.py | 24 +++- chrono/manager/static/css/style.css | 10 ++ .../templates/chrono/manager_agenda_view.html | 56 +++++---- .../templates/chrono/manager_desk_form.html | 35 ++++++ .../chrono/manager_time_period_form.html | 4 +- chrono/manager/urls.py | 9 +- chrono/manager/views.py | 90 ++++++++++++++- debian/control | 3 +- setup.py | 1 + tests/test_agendas.py | 23 ---- tests/test_api.py | 108 ++++++++++++++++-- tests/test_data_migrations.py | 77 +++++++++++++ tests/test_import_export.py | 8 +- tests/test_manager.py | 104 +++++++++++++++-- tests/test_time_periods.py | 15 +-- 20 files changed, 708 insertions(+), 123 deletions(-) create mode 100644 chrono/agendas/migrations/0016_desk.py create mode 100644 chrono/agendas/migrations/0017_timeperiod_desk.py create mode 100644 chrono/agendas/migrations/0018_event_desk.py create mode 100644 chrono/manager/templates/chrono/manager_desk_form.html create mode 100644 tests/test_data_migrations.py diff --git a/chrono/agendas/migrations/0016_desk.py b/chrono/agendas/migrations/0016_desk.py new file mode 100644 index 00000000..5fa87fb3 --- /dev/null +++ b/chrono/agendas/migrations/0016_desk.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('agendas', '0015_auto_20170628_1137'), + ] + + operations = [ + migrations.CreateModel( + name='Desk', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('label', models.CharField(max_length=150, verbose_name='Label')), + ('slug', models.SlugField(max_length=150, verbose_name='Slug')), + ('agenda', models.ForeignKey(to='agendas.Agenda')), + ], + options={ + 'ordering': ['label'], + }, + ), + ] diff --git a/chrono/agendas/migrations/0017_timeperiod_desk.py b/chrono/agendas/migrations/0017_timeperiod_desk.py new file mode 100644 index 00000000..eb96b4bf --- /dev/null +++ b/chrono/agendas/migrations/0017_timeperiod_desk.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +def set_timeperiod_desk(apps, schema_editor): + TimePeriod = apps.get_model('agendas', 'TimePeriod') + Desk = apps.get_model('agendas', 'Desk') + for time_period in TimePeriod.objects.all(): + desk, created = Desk.objects.get_or_create( + label='Guichet 1', slug='guichet-1', agenda=time_period.agenda) + time_period.desk = desk + time_period.save() + + +def unset_timeperiod_desk(apps, schema_editor): + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ('agendas', '0016_desk'), + ] + + operations = [ + migrations.AddField( + model_name='timeperiod', + name='desk', + field=models.ForeignKey(to='agendas.Desk', null=True), + ), + migrations.RunPython(set_timeperiod_desk, unset_timeperiod_desk), + migrations.AlterField( + model_name='timeperiod', + name='desk', + field=models.ForeignKey(to='agendas.Desk'), + ), + migrations.RemoveField( + model_name='timeperiod', + name='agenda', + ), + ] diff --git a/chrono/agendas/migrations/0018_event_desk.py b/chrono/agendas/migrations/0018_event_desk.py new file mode 100644 index 00000000..b298d093 --- /dev/null +++ b/chrono/agendas/migrations/0018_event_desk.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +def set_event_desk(apps, schema_editor): + Events = apps.get_model('agendas', 'Event') + Desk = apps.get_model('agendas', 'Desk') + for event in Events.objects.all(): + if not event.agenda.kind == 'meetings': + continue + + desk, created = Desk.objects.get_or_create( + label='Guichet 1', slug='guichet-1', agenda=event.agenda) + event.desk = desk + event.save() + + +def unset_event_desk(apps, schema_editor): + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ('agendas', '0017_timeperiod_desk'), + ] + + operations = [ + migrations.AddField( + model_name='event', + name='desk', + field=models.ForeignKey(to='agendas.Desk', null=True), + ), + migrations.RunPython(set_event_desk, unset_event_desk) + ] diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py index f52af113..1dbf7c97 100644 --- a/chrono/agendas/models.py +++ b/chrono/agendas/models.py @@ -33,6 +33,7 @@ AGENDA_KINDS = ( ('meetings', _('Meetings')), ) + class Agenda(models.Model): label = models.CharField(_('Label'), max_length=150) slug = models.SlugField(_('Slug')) @@ -95,7 +96,7 @@ class Agenda(models.Model): agenda['events'] = [x.export_json() for x in self.event_set.all()] elif self.kind == 'meetings': agenda['meetingtypes'] = [x.export_json() for x in self.meetingtype_set.all()] - agenda['timeperiods'] = [x.export_json() for x in self.timeperiod_set.all()] + agenda['desks'] = [desk.export_json() for desk in self.desk_set.all()] return agenda @classmethod @@ -106,7 +107,7 @@ class Agenda(models.Model): events = data.pop('events') elif data['kind'] == 'meetings': meetingtypes = data.pop('meetingtypes') - timeperiods = data.pop('timeperiods') + desks = data.pop('desks') agenda, created = cls.objects.get_or_create(slug=data['slug'], defaults=data) if data['kind'] == 'events': if overwrite: @@ -117,40 +118,34 @@ class Agenda(models.Model): elif data['kind'] == 'meetings': if overwrite: MeetingType.objects.filter(agenda=agenda).delete() - TimePeriod.objects.filter(agenda=agenda).delete() + Desk.objects.filter(agenda=agenda).delete() for type_data in meetingtypes: type_data['agenda'] = agenda MeetingType.import_json(type_data).save() - for period_data in timeperiods: - period_data['agenda'] = agenda - TimePeriod.import_json(period_data).save() + for desk in desks: + desk['agenda'] = agenda + Desk.import_json(desk).save() WEEKDAYS_LIST = sorted(WEEKDAYS.items(), key=lambda x: x[0]) class TimeSlot(object): - def __init__(self, start_datetime, meeting_type): + def __init__(self, start_datetime, meeting_type, desk): self.start_datetime = start_datetime self.end_datetime = start_datetime + datetime.timedelta(minutes=meeting_type.duration) self.meeting_type = meeting_type self.id = '%s:%s' % (self.meeting_type.id, start_datetime.strftime('%Y-%m-%d-%H%M')) - - def intersects(self, timeslot): - if self.start_datetime >= timeslot.end_datetime: - return False - if self.end_datetime <= timeslot.start_datetime: - return False - return True + self.desk = desk def __unicode__(self): return date_format(self.start_datetime, format='DATETIME_FORMAT') class TimePeriod(models.Model): - agenda = models.ForeignKey(Agenda) weekday = models.IntegerField(_('Week day'), choices=WEEKDAYS_LIST) start_time = models.TimeField(_('Start')) end_time = models.TimeField(_('End')) + desk = models.ForeignKey('Desk', on_delete=models.CASCADE) class Meta: ordering = ['weekday', 'start_time'] @@ -192,7 +187,7 @@ class TimePeriod(models.Model): if event_datetime > max_datetime: break - yield TimeSlot(start_datetime=event_datetime, meeting_type=meeting_type) + yield TimeSlot(start_datetime=event_datetime, meeting_type=meeting_type, desk=self.desk) event_datetime = end_time @@ -242,6 +237,7 @@ class Event(models.Model): help_text=_('Optional label to identify this date.')) full = models.BooleanField(default=False) meeting_type = models.ForeignKey(MeetingType, null=True) + desk = models.ForeignKey('Desk', null=True) class Meta: ordering = ['agenda', 'start_datetime', 'label'] @@ -334,3 +330,45 @@ class Booking(models.Model): with transaction.atomic(): self.secondary_booking_set.update(in_waiting_list=False) self.save() + + +class Desk(models.Model): + agenda = models.ForeignKey(Agenda) + label = models.CharField(_('Label'), max_length=150) + slug = models.SlugField(_('Slug'), max_length=150) + + def __unicode__(self): + return self.label + + class Meta: + ordering = ['label'] + + def save(self, *args, **kwargs): + if not self.slug: + base_slug = slugify(self.label) + slug = base_slug + i = 1 + while True: + try: + Desk.objects.get(slug=slug, agenda=self.agenda) + except self.DoesNotExist: + break + slug = '%s-%s' % (base_slug, i) + i += 1 + self.slug = slug + super(Desk, self).save(*args, **kwargs) + + @classmethod + def import_json(cls, data): + timeperiods = data.pop('timeperiods') + instance, created = cls.objects.get_or_create(**data) + for timeperiod in timeperiods: + timeperiod['desk'] = instance + TimePeriod.import_json(timeperiod).save() + return instance + + def export_json(self): + return {'label': self.label, + 'slug': self.slug, + 'timeperiods': [time_period.export_json() for time_period in self.timeperiod_set.all()] + } diff --git a/chrono/api/views.py b/chrono/api/views.py index f4e9a576..13b3ea57 100644 --- a/chrono/api/views.py +++ b/chrono/api/views.py @@ -14,7 +14,11 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from collections import defaultdict import datetime +import operator + +from intervaltree import IntervalTree from django.core.urlresolvers import reverse from django.http import Http404 @@ -27,7 +31,38 @@ from rest_framework.generics import GenericAPIView from rest_framework.response import Response from rest_framework.views import APIView -from ..agendas.models import Agenda, Event, Booking, MeetingType, TimePeriod +from ..agendas.models import (Agenda, Event, Booking, MeetingType, + TimePeriod, Desk) + + +def get_open_slots(agenda, meeting_type): + min_datetime = now() + datetime.timedelta(days=agenda.minimal_booking_delay) + max_datetime = now() + datetime.timedelta(days=agenda.maximal_booking_delay) + time_period_filters = { + 'min_datetime': min_datetime, + 'max_datetime': max_datetime, + 'meeting_type': meeting_type + } + + open_slots_by_desk = defaultdict(lambda: IntervalTree()) + for time_period in TimePeriod.objects.filter(desk__agenda=agenda): + for slot in time_period.get_time_slots(**time_period_filters): + open_slots_by_desk[time_period.desk_id].addi(slot.start_datetime, slot.end_datetime, slot.desk) + + for event in agenda.event_set.filter( + agenda=agenda, start_datetime__gte=min_datetime, + start_datetime__lte=max_datetime + datetime.timedelta(meeting_type.duration)).select_related( + 'meeting_type').extra( + select={ + 'booking_count': """SELECT COUNT(*) FROM agendas_booking + WHERE agendas_booking.event_id = agendas_event.id + AND agendas_booking.cancellation_datetime IS NOT NULL"""}): + if event.booking_count: + continue + open_slots_by_desk[event.desk_id].remove_overlap(event.start_datetime, event.end_datetime) + + open_slots = reduce(operator.__or__, open_slots_by_desk.values()) + return open_slots def get_agenda_detail(request, agenda): @@ -151,27 +186,26 @@ class MeetingDatetimes(GenericAPIView): max_datetime = now() + datetime.timedelta(days=agenda.maximal_booking_delay) all_time_slots = [] - for time_period in TimePeriod.objects.filter(agenda=agenda): - all_time_slots.extend(time_period.get_time_slots( - min_datetime=min_datetime, - max_datetime=max_datetime, - meeting_type=meeting_type)) + for time_period in TimePeriod.objects.filter(desk__agenda=agenda): + all_time_slots.extend(time_period.get_time_slots(min_datetime=min_datetime, max_datetime=max_datetime, + meeting_type=meeting_type)) - busy_time_slots = Event.objects.filter(agenda=agenda, - start_datetime__gte=min_datetime, - start_datetime__lte=max_datetime + datetime.timedelta(meeting_type.duration)) - busy_time_slots = list(busy_time_slots) - - entries = [] - # there's room for optimisations here, for a start both lists - # could be presorted and past busy time slots removed along the way. + open_slots = get_open_slots(agenda, meeting_type) + open_entries = {} + closed_entries = {} for time_slot in all_time_slots: if time_slot.start_datetime < now_datetime: continue - time_slot.full = bool(any((x for x in busy_time_slots if x.full and time_slot.intersects(x)))) - entries.append(time_slot) + key = '%s-%s' % (time_slot.start_datetime, time_slot.end_datetime) + if open_slots.search(time_slot.start_datetime, time_slot.end_datetime, strict=True): + time_slot.full = False + open_entries[key] = time_slot + else: + time_slot.full = True + closed_entries[key] = time_slot - entries.sort(key=lambda x: x.start_datetime) + closed_entries.update(open_entries) + entries = sorted(closed_entries.values(), key=lambda x: x.start_datetime) fake_event_pk = '__event_id__' fillslot_url = request.build_absolute_uri( @@ -251,12 +285,24 @@ class Fillslot(GenericAPIView): meeting_type_id, start_datetime_str = event_pk.split(':') start_datetime = make_aware(datetime.datetime.strptime( start_datetime_str, '%Y-%m-%d-%H%M')) - event, created = Event.objects.get_or_create(agenda=agenda, + + event = Event.objects.create(agenda=agenda, meeting_type_id=meeting_type_id, start_datetime=start_datetime, - defaults={'full': False, 'places': 1}) - if created: - event.save() + full=False, places=1) + + available_desk = None + + open_slots = get_open_slots(agenda, MeetingType.objects.get(id=meeting_type_id)) + slot = open_slots[event.start_datetime:event.end_datetime] + if slot: + available_desk = slot.pop().data + + if not available_desk: + return Response({'err': 1, 'reason': 'no more desk available'}) + + event.desk = available_desk + event.save() event_pk = event.id event = Event.objects.filter(id=event_pk)[0] diff --git a/chrono/manager/forms.py b/chrono/manager/forms.py index 5c73daa2..c60cb066 100644 --- a/chrono/manager/forms.py +++ b/chrono/manager/forms.py @@ -21,7 +21,7 @@ from django import forms from django.forms import ValidationError from django.utils.translation import ugettext_lazy as _ -from chrono.agendas.models import Event, MeetingType, TimePeriod +from chrono.agendas.models import Event, MeetingType, TimePeriod, Desk from . import widgets @@ -44,7 +44,7 @@ class EventForm(forms.ModelForm): 'agenda': forms.HiddenInput(), 'start_datetime': DateTimeWidget(), } - exclude = ['full', 'meeting_type'] + exclude = ['full', 'meeting_type', 'desk'] class NewMeetingTypeForm(forms.ModelForm): @@ -69,9 +69,27 @@ class TimePeriodForm(forms.ModelForm): class Meta: model = TimePeriod widgets = { - 'agenda': forms.HiddenInput(), 'start_time': widgets.TimeWidget(), 'end_time': widgets.TimeWidget(), + 'desk': forms.HiddenInput(), + } + exclude = [] + + +class NewDeskForm(forms.ModelForm): + class Meta: + model = Desk + widgets = { + 'agenda': forms.HiddenInput(), + } + exclude = ['slug'] + + +class DeskForm(forms.ModelForm): + class Meta: + model = Desk + widgets = { + 'agenda': forms.HiddenInput(), } exclude = [] diff --git a/chrono/manager/static/css/style.css b/chrono/manager/static/css/style.css index 0c888929..047ae198 100644 --- a/chrono/manager/static/css/style.css +++ b/chrono/manager/static/css/style.css @@ -38,3 +38,13 @@ h2 span.identifier { .time-only-picker tbody td { width: 200px; } + +.timeperiods { + position: relative; +} + +.timeperiod { + float: left; + display: block; + margin: 10px; +} diff --git a/chrono/manager/templates/chrono/manager_agenda_view.html b/chrono/manager/templates/chrono/manager_agenda_view.html index 5643aa02..3363a412 100644 --- a/chrono/manager/templates/chrono/manager_agenda_view.html +++ b/chrono/manager/templates/chrono/manager_agenda_view.html @@ -15,7 +15,7 @@ {% trans 'New Event' %} {% else %} {% trans 'New Meeting Type' %} - {% trans 'New Time Period' %} + {% trans 'New Desk' %} {% endif %} {% endif %} {% endblock %} @@ -105,25 +105,41 @@ {% endif %}

{% trans 'Time Periods' %}

-{% if object.timeperiod_set.count %} -
- -
-{% else %} -
- {% blocktrans %} - This agenda doesn't have any time period yet. Click on the "New Time Period" button in - the top right of the page to add a first one. - {% endblocktrans %} -
+ {% if object.desk_set.count %} +
+ {% for desk in object.desk_set.all %} +
+ {% if object.desk_set.count > 1 %} +

+ + {{ desk.label }} +

+ {% endif %} + {% url 'chrono-manager-agenda-add-time-period' agenda_pk=object.pk pk=desk.pk as add_time_period_url %} + {% if desk.timeperiod_set.count %} + + {% else %} +
+ {% trans "This desk doesn't have any time period yet." %} {% trans "New Time Period" %} +
+ {% endif %} +
+ {% endfor %} +
+ {% else %} +
+ {% blocktrans %} + This agenda doesn't have any desk yet. Click on the "New Desk" button in + the top right of the page to add a first one. + {% endblocktrans %} +
{% endif %} diff --git a/chrono/manager/templates/chrono/manager_desk_form.html b/chrono/manager/templates/chrono/manager_desk_form.html new file mode 100644 index 00000000..fadb2a9c --- /dev/null +++ b/chrono/manager/templates/chrono/manager_desk_form.html @@ -0,0 +1,35 @@ +{% extends "chrono/manager_agenda_view.html" %} +{% load i18n %} + +{% block extrascripts %} +{{ block.super }} +{{ form.media }} +{% endblock %} + +{% block breadcrumb %} +{{ block.super }} +{% if object.id %} +{{object.label}} +{% endif %} +{% endblock %} + +{% block appbar %} +{% if object.id %} +

{% trans "Edit Desk" %}

+{% trans 'Delete' %} +{% else %} +

{% trans "New Desk" %}

+{% endif %} +{% endblock %} + +{% block content %} + +
+ {% csrf_token %} + {{ form.as_p }} +
+ + {% trans 'Cancel' %} +
+
+{% endblock %} diff --git a/chrono/manager/templates/chrono/manager_time_period_form.html b/chrono/manager/templates/chrono/manager_time_period_form.html index 42c14697..b3659035 100644 --- a/chrono/manager/templates/chrono/manager_time_period_form.html +++ b/chrono/manager/templates/chrono/manager_time_period_form.html @@ -9,7 +9,7 @@ {% block breadcrumb %} {{ block.super }} {% if object.id %} -{{object.agenda.label}} +{{object.desk.agenda.label}} {% endif %} {% endblock %} @@ -29,7 +29,7 @@ {{ form.as_p }} {% endblock %} diff --git a/chrono/manager/urls.py b/chrono/manager/urls.py index 612b9f48..a5cf372d 100644 --- a/chrono/manager/urls.py +++ b/chrono/manager/urls.py @@ -44,13 +44,20 @@ urlpatterns = [ url(r'^meetingtypes/(?P\w+)/delete$', views.meeting_type_delete, name='chrono-manager-meeting-type-delete'), - url(r'^agendas/(?P\w+)/add-time-period$', views.agenda_add_time_period, + url(r'^agendas/(?P\d+)/desk/(?P\d+)/add-time-period$', views.agenda_add_time_period, name='chrono-manager-agenda-add-time-period'), url(r'^timeperiods/(?P\w+)/edit$', views.time_period_edit, name='chrono-manager-time-period-edit'), url(r'^timeperiods/(?P\w+)/delete$', views.time_period_delete, name='chrono-manager-time-period-delete'), + url(r'^agendas/(?P\w+)/add-desk$', views.agenda_add_desk, + name='chrono-manager-agenda-add-desk'), + url(r'^desks/(?P\w+)/edit$', views.desk_edit, + name='chrono-manager-desk-edit'), + url(r'^desks/(?P\w+)/delete$', views.desk_delete, + name='chrono-manager-desk-delete'), + url(r'^agendas/events.csv$', views.agenda_import_events_sample_csv, name='chrono-manager-sample-events-csv'), diff --git a/chrono/manager/views.py b/chrono/manager/views.py index 701c21e5..cc05e62e 100644 --- a/chrono/manager/views.py +++ b/chrono/manager/views.py @@ -27,10 +27,11 @@ from django.utils.encoding import force_text from django.views.generic import (DetailView, CreateView, UpdateView, ListView, DeleteView, FormView, TemplateView) -from chrono.agendas.models import Agenda, Event, MeetingType, TimePeriod, Booking +from chrono.agendas.models import (Agenda, Event, MeetingType, TimePeriod, + Booking, Desk) from .forms import (EventForm, NewMeetingTypeForm, MeetingTypeForm, - TimePeriodForm, ImportEventsForm) + TimePeriodForm, ImportEventsForm, NewDeskForm, DeskForm) class HomepageView(ListView): @@ -57,6 +58,13 @@ class AgendaAddView(CreateView): raise PermissionDenied() return super(AgendaAddView, self).dispatch(request, *args, **kwargs) + def form_valid(self, form): + model_form = super(AgendaAddView, self).form_valid(form) + if self.object.kind == 'meetings': + default_desk = Desk(agenda=self.object, label=_('Desk 1')) + default_desk.save() + return model_form + agenda_add = AgendaAddView.as_view() @@ -160,6 +168,50 @@ class ManagedAgendaSubobjectMixin(object): return self.agenda.get_absolute_url() +class ManagedDeskMixin(object): + desk = None + + def dispatch(self, request, *args, **kwargs): + try: + self.desk = Desk.objects.get(id=kwargs.get('pk')) + except Desk.DoesNotExist: + raise Http404() + if not self.desk.agenda.can_be_managed(request.user): + raise PermissionDenied() + return super(ManagedDeskMixin, self).dispatch(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + context = super(ManagedDeskMixin, self).get_context_data(**kwargs) + context['desk'] = self.desk + return context + + def get_initial(self): + initial = super(ManagedDeskMixin, self).get_initial() + initial['desk'] = self.desk + return initial + + def get_success_url(self): + return self.desk.agenda.get_absolute_url() + + +class ManagedDeskSubobjectMixin(object): + desk = None + + def dispatch(self, request, *args, **kwargs): + self.desk = self.get_object().desk + if not self.desk.agenda.can_be_managed(request.user): + raise PermissionDenied() + return super(ManagedDeskSubobjectMixin, self).dispatch(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + context = super(ManagedDeskSubobjectMixin, self).get_context_data(**kwargs) + context['desk'] = self.object.desk + return context + + def get_success_url(self): + return self.desk.agenda.get_absolute_url() + + class AgendaAddEventView(ManagedAgendaMixin, CreateView): template_name = 'chrono/manager_event_form.html' model = Event @@ -242,15 +294,15 @@ class MeetingTypeDeleteView(ManagedAgendaSubobjectMixin, DeleteView): meeting_type_delete = MeetingTypeDeleteView.as_view() -class AgendaAddTimePeriodView(ManagedAgendaMixin, CreateView): +class AgendaAddTimePeriodView(ManagedDeskMixin, CreateView): template_name = 'chrono/manager_time_period_form.html' - model = Event + model = TimePeriod form_class = TimePeriodForm agenda_add_time_period = AgendaAddTimePeriodView.as_view() -class TimePeriodEditView(ManagedAgendaSubobjectMixin, UpdateView): +class TimePeriodEditView(ManagedDeskSubobjectMixin, UpdateView): template_name = 'chrono/manager_time_period_form.html' model = TimePeriod form_class = TimePeriodForm @@ -258,13 +310,39 @@ class TimePeriodEditView(ManagedAgendaSubobjectMixin, UpdateView): time_period_edit = TimePeriodEditView.as_view() -class TimePeriodDeleteView(ManagedAgendaSubobjectMixin, DeleteView): +class TimePeriodDeleteView(ManagedDeskSubobjectMixin, DeleteView): template_name = 'chrono/manager_confirm_delete.html' model = TimePeriod time_period_delete = TimePeriodDeleteView.as_view() +class AgendaAddDesk(ManagedAgendaMixin, CreateView): + template_name = 'chrono/manager_desk_form.html' + model = Desk + form_class = NewDeskForm + + +agenda_add_desk = AgendaAddDesk.as_view() + + +class DeskEditView(ManagedAgendaSubobjectMixin, UpdateView): + template_name = 'chrono/manager_desk_form.html' + model = Desk + form_class = DeskForm + + +desk_edit = DeskEditView.as_view() + + +class DeskDeleteView(ManagedAgendaSubobjectMixin, DeleteView): + template_name = 'chrono/manager_confirm_delete.html' + model = Desk + + +desk_delete = DeskDeleteView.as_view() + + def menu_json(request): response = HttpResponse(content_type='application/json') label = _('Agendas') diff --git a/debian/control b/debian/control index ae205de2..87b73586 100644 --- a/debian/control +++ b/debian/control @@ -10,7 +10,8 @@ Package: python-chrono Architecture: all Depends: ${misc:Depends}, ${python:Depends}, python-django (>= 1.8), - python-gadjo + python-gadjo, + python-intervaltree Recommends: python-django-mellon Description: Agendas System (Python module) diff --git a/setup.py b/setup.py index e8b5a395..1455e0d5 100644 --- a/setup.py +++ b/setup.py @@ -106,6 +106,7 @@ setup( 'gadjo', 'djangorestframework>=3.1', 'django-jsonfield >= 0.9.3', + 'intervaltree', ], zip_safe=False, cmdclass={ diff --git a/tests/test_agendas.py b/tests/test_agendas.py index 93900f9d..50c1a34e 100644 --- a/tests/test_agendas.py +++ b/tests/test_agendas.py @@ -1,8 +1,6 @@ import pytest import datetime -from django.db import connection -from django.db.migrations.executor import MigrationExecutor from django.utils.timezone import now from chrono.agendas.models import Agenda, Event, Booking, MeetingType @@ -68,27 +66,6 @@ def test_event_bookable_period(): event.save() assert event.in_bookable_period() is False -def test_meeting_type_slug_migration(): - executor = MigrationExecutor(connection) - migrate_from = [('agendas', '0011_meetingtype_slug')] - migrate_to = [('agendas', '0013_auto_20161028_1603')] - executor.migrate(migrate_from) - executor.loader.build_graph() - - old_apps = executor.loader.project_state(migrate_from).apps - OldAgenda = old_apps.get_model('agendas', 'Agenda') - OldMeetingType = old_apps.get_model('agendas', 'MeetingType') - - agenda = OldAgenda(label=u'Foo bar') - agenda.save() - - meeting_type = OldMeetingType(agenda=agenda, label=u'Baz') - meeting_type.save() - - executor.migrate(migrate_to) - - assert MeetingType.objects.get(id=meeting_type.id).slug == 'baz' - def test_meeting_type_slugs(): agenda1 = Agenda(label=u'Foo bar') agenda1.save() diff --git a/tests/test_api.py b/tests/test_api.py index f92f4dfa..86a63619 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -5,10 +5,12 @@ import urlparse from webtest import TestApp from django.contrib.auth import get_user_model +from django.db import connection from django.test import override_settings +from django.test.utils import CaptureQueriesContext from django.utils.timezone import now, make_aware, localtime -from chrono.agendas.models import Agenda, Event, Booking, MeetingType, TimePeriod +from chrono.agendas.models import Agenda, Event, Booking, MeetingType, TimePeriod, Desk import chrono.api.views @@ -76,11 +78,13 @@ def meetings_agenda(time_zone, mock_now): test_1st_weekday = (localtime(now()).weekday() + 2) % 7 test_2nd_weekday = (localtime(now()).weekday() + 3) % 7 - time_period = TimePeriod(agenda=agenda, weekday=test_1st_weekday, - start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)) + default_desk, created = Desk.objects.get_or_create(agenda=agenda, label='Desk 1') + + time_period = TimePeriod(weekday=test_1st_weekday, + start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=default_desk) time_period.save() - time_period = TimePeriod(agenda=agenda, weekday=test_2nd_weekday, - start_time=datetime.time(10, 0), end_time=datetime.time(17, 0)) + time_period = TimePeriod(weekday=test_2nd_weekday, + start_time=datetime.time(10, 0), end_time=datetime.time(17, 0), desk=default_desk) time_period.save() return agenda @@ -204,7 +208,7 @@ def test_datetimes_api_meetings_agenda(app, meetings_agenda): resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) dt = datetime.datetime.strptime(resp.json['data'][2]['id'].split(':')[1], '%Y-%m-%d-%H%M') ev = Event(agenda=meetings_agenda, meeting_type=meeting_type, - places=1, full=False, start_datetime=make_aware(dt)) + places=1, full=False, start_datetime=make_aware(dt), desk=Desk.objects.first()) ev.save() booking = Booking(event=ev) booking.save() @@ -224,11 +228,12 @@ def test_datetimes_api_meetings_agenda(app, meetings_agenda): # skip this part of the test as it would require support for events # crossing midnight return - TimePeriod.objects.filter(agenda=meetings_agenda).delete() + default_desk, _ = Desk.objects.get_or_create(agenda=meetings_agenda, slug='desk-1') + TimePeriod.objects.filter(desk=default_desk).delete() start_time = localtime(now()) - datetime.timedelta(minutes=10) - time_period = TimePeriod(agenda=meetings_agenda, weekday=localtime(now()).weekday(), + time_period = TimePeriod(weekday=localtime(now()).weekday(), start_time=start_time, - end_time=start_time + datetime.timedelta(hours=1)) + end_time=start_time + datetime.timedelta(hours=1), desk=default_desk) time_period.save() meetings_agenda.minimal_booking_delay = 0 meetings_agenda.maximal_booking_delay = 10 @@ -284,7 +289,7 @@ def test_booking_api_meeting(app, meetings_agenda, user): # try booking the same timeslot resp2 = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id)) assert resp2.json['err'] == 1 - assert resp2.json['reason'] == 'sold out' + assert resp2.json['reason'] == 'no more desk available' # try booking another timeslot event_id = resp.json['data'][3]['id'] @@ -690,3 +695,86 @@ def test_agenda_api_date_range(app, some_data): assert len(resp.json['data']) == 3 assert resp.json['data'][0]['datetime'] == '2017-05-30 09:00:00' assert resp.json['data'][-1]['datetime'] == '2017-05-30 11:00:00' + +def test_agenda_meeting_api_multiple_desk(app, meetings_agenda, user): + app.authorization = ('Basic', ('john.doe', 'password')) + agenda_id = meetings_agenda.slug + meeting_type = MeetingType.objects.get(agenda=meetings_agenda) + + # add booking of another meeting type + meeting_type2 = MeetingType.objects.create(agenda=meetings_agenda, label='Tux kart', duration=60) + resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type2.id) + event_id = resp.json['data'][0]['id'] + resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id)) + cancel_url = resp.json['api']['cancel_url'] + + # add a second desk + time_period = meetings_agenda.desk_set.first().timeperiod_set.first() + desk2 = Desk.objects.create(label='Desk 2', agenda=meetings_agenda) + TimePeriod.objects.create( + start_time=time_period.start_time, end_time=time_period.end_time, + weekday=time_period.weekday, desk=desk2) + + resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) + event_id = resp.json['data'][1]['id'] + resp_booking = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id)) + assert Booking.objects.count() == 2 + assert resp_booking.json['datetime'][:16] == localtime(Booking.objects.last().event.start_datetime + ).isoformat()[:16] + + resp2 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) + assert len(resp.json['data']) == len([x for x in resp2.json['data'] if not x['disabled']]) + 1 + + # try booking the same timeslot and fail + resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id)) + assert Booking.objects.count() == 2 + assert resp.json['err'] == 1 + assert resp.json['reason'] == 'no more desk available' + + # cancel first booking and retry + resp = app.post(cancel_url) + # capture number of queries made for datetime endpoint with few bookings + with CaptureQueriesContext(connection) as ctx: + resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) + queries_count_datetime1 = len(ctx.captured_queries) + assert len(resp2.json['data']) == len([x for x in resp.json['data'] if not x['disabled']]) + + # capture number of queries made for fillslot endpoint with few bookings + with CaptureQueriesContext(connection) as ctx: + resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id)) + queries_count_fillslot1 = len(ctx.captured_queries) + + assert resp_booking.json['datetime'][:16] == localtime(Booking.objects.last().event.start_datetime + ).isoformat()[:16] + cancel_url = resp.json['api']['cancel_url'] + + resp3 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) + assert len(resp2.json['data']) == len([x for x in resp3.json['data'] if not x['disabled']]) + 1 + + # cancel a booking + resp = app.post(cancel_url) + resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) + assert len(resp.json['data']) == len(resp2.json['data']) + + # try booking the same slot to make sure that cancelled booking has freed the slot + resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id)) + assert Booking.objects.count() == 4 + assert Booking.objects.exclude(cancellation_datetime__isnull=True).count() == 2 + assert resp_booking.json['datetime'][:16] == localtime(Booking.objects.last().event.start_datetime + ).isoformat()[:16] + + # try booking the same timeslot again and fail + resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id)) + assert resp.json['err'] == 1 + assert resp.json['reason'] == 'no more desk available' + + # fill the agenda and make sure big O is O(1) + for idx, event_data in enumerate(resp2.json['data'][2:10]): + booking_url = event_data['api']['fillslot_url'] + with CaptureQueriesContext(connection) as ctx: + app.post(booking_url) + assert len(ctx.captured_queries) == queries_count_fillslot1 + + with CaptureQueriesContext(connection) as ctx: + app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) + assert queries_count_datetime1 == len(ctx.captured_queries) diff --git a/tests/test_data_migrations.py b/tests/test_data_migrations.py new file mode 100644 index 00000000..51eeb769 --- /dev/null +++ b/tests/test_data_migrations.py @@ -0,0 +1,77 @@ +import datetime +import pytest + +from django.db import connection +from django.db.migrations.executor import MigrationExecutor + +from chrono.agendas.models import MeetingType + +pytestmark = pytest.mark.django_db + + +def test_meeting_type_slug_migration(): + executor = MigrationExecutor(connection) + migrate_from = [('agendas', '0011_meetingtype_slug')] + migrate_to = [('agendas', '0013_auto_20161028_1603')] + executor.migrate(migrate_from) + executor.loader.build_graph() + + old_apps = executor.loader.project_state(migrate_from).apps + OldAgenda = old_apps.get_model('agendas', 'Agenda') + OldMeetingType = old_apps.get_model('agendas', 'MeetingType') + + agenda = OldAgenda(label=u'Foo bar') + agenda.save() + + meeting_type = OldMeetingType(agenda=agenda, label=u'Baz') + meeting_type.save() + + executor.migrate(migrate_to) + + assert MeetingType.objects.get(id=meeting_type.id).slug == 'baz' + + +def test_timepriod_data_migrations(transactional_db): + executor = MigrationExecutor(connection) + app = 'agendas' + migrate_from = [(app, '0016_desk')] + migrate_to = [(app, '0018_event_desk')] + executor.migrate(migrate_from) + old_apps = executor.loader.project_state(migrate_from).apps + Agenda = old_apps.get_model(app, 'Agenda') + MeetingType = old_apps.get_model(app, 'MeetingType') + TimePeriod = old_apps.get_model(app, 'TimePeriod') + Event = old_apps.get_model(app, 'Event') + agenda = Agenda.objects.create(label='foo', slug='foo', kind='meetings') + agenda2 = Agenda.objects.create(label='bar', slug='bar', kind='events') + TimePeriod.objects.create(agenda=agenda, weekday=1, + start_time=datetime.time(8, 0), + end_time=datetime.time(12, 0)) + TimePeriod.objects.create(agenda=agenda, weekday=2, + start_time=datetime.time(8, 0), + end_time=datetime.time(10, 0)) + TimePeriod.objects.create(agenda=agenda, weekday=3, + start_time=datetime.time(9, 0), + end_time=datetime.time(12, 0)) + meeting_type = MeetingType.objects.create(agenda=agenda, label='foo', + slug='foo', duration=60) + Event.objects.create(agenda=agenda, places=1, meeting_type=meeting_type, + start_datetime=datetime.datetime(2017, 5, 22, 9, 30)) + Event.objects.create(agenda=agenda, places=1, meeting_type=meeting_type, + start_datetime=datetime.datetime(2017, 5, 22, 10, 0)) + Event.objects.create(agenda=agenda2, places=5, + start_datetime=datetime.datetime(2017, 5, 22, 10, 0)) + executor.loader.build_graph() + executor.migrate(migrate_to) + new_apps = executor.loader.project_state(migrate_to).apps + Agenda = new_apps.get_model(app, 'Agenda') + agenda = Agenda.objects.get(slug='foo') + TimePeriod = new_apps.get_model(app, 'TimePeriod') + for time_period in TimePeriod.objects.all(): + assert time_period.desk.label == 'Guichet 1' + assert time_period.desk.slug == 'guichet-1' + + Event = new_apps.get_model(app, 'Event') + for event in Event.objects.filter(agenda=agenda): + assert event.desk.label == 'Guichet 1' + assert event.desk.slug == 'guichet-1' diff --git a/tests/test_import_export.py b/tests/test_import_export.py index 9870140a..19a4113a 100644 --- a/tests/test_import_export.py +++ b/tests/test_import_export.py @@ -9,7 +9,7 @@ import tempfile import pytest from django.core.management import call_command -from chrono.agendas.models import Agenda, Event, MeetingType, TimePeriod +from chrono.agendas.models import Agenda, Event, MeetingType, TimePeriod, Desk from chrono.manager.utils import export_site, import_site from test_api import some_data, meetings_agenda, time_zone, mock_now @@ -51,17 +51,19 @@ def test_import_export(app, some_data, meetings_agenda): agenda2 = Agenda.objects.get(label=u'Foo bar Meeting') event = Event(agenda=agenda1, start_datetime= datetime.datetime.now(), places=10) event.save() - timeperiod = TimePeriod(agenda=agenda2, weekday=2, + desk, _ = Desk.objects.get_or_create(agenda=agenda2, label='Desk A', slug='desk-a') + timeperiod = TimePeriod(weekday=2, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(11, 0)) timeperiod.save() import_site(json.loads(output), overwrite=True) assert Event.objects.filter(id=event.id).count() == 0 + assert Desk.objects.filter(slug='desk-a').count() == 0 assert TimePeriod.objects.filter(id=timeperiod.id).count() == 0 event = Event(agenda=agenda1, start_datetime= datetime.datetime.now(), places=10) event.save() - timeperiod = TimePeriod(agenda=agenda2, weekday=2, + timeperiod = TimePeriod(weekday=2, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(11, 0)) timeperiod.save() import_site(json.loads(output), overwrite=False) diff --git a/tests/test_manager.py b/tests/test_manager.py index 34518dd5..2b261c38 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -8,7 +8,8 @@ from webtest import TestApp, Upload from chrono.wsgi import application -from chrono.agendas.models import Agenda, Event, Booking, MeetingType, TimePeriod +from chrono.agendas.models import (Agenda, Event, Booking, MeetingType, + TimePeriod, Desk) pytestmark = pytest.mark.django_db @@ -569,23 +570,24 @@ def test_meetings_agenda_delete_meeting_type(app, admin_user): def test_meetings_agenda_add_time_period(app, admin_user): agenda = Agenda(label=u'Foo bar', kind='meetings') agenda.save() + desk = Desk.objects.create(agenda=agenda, label='Desk A') app = login(app) resp = app.get('/manage/agendas/%s/' % agenda.id, status=200) - assert "This agenda doesn't have any time period yet." in resp.body + assert "This desk doesn't have any time period yet." in resp.body resp = resp.click('New Time Period') resp.form['weekday'].select(text='Wednesday') resp.form['start_time'] = '10:00' resp.form['end_time'] = '17:00' resp = resp.form.submit() - assert TimePeriod.objects.get(agenda=agenda).weekday == 2 - assert TimePeriod.objects.get(agenda=agenda).start_time.hour == 10 - assert TimePeriod.objects.get(agenda=agenda).start_time.minute == 0 - assert TimePeriod.objects.get(agenda=agenda).end_time.hour == 17 - assert TimePeriod.objects.get(agenda=agenda).end_time.minute == 0 + assert TimePeriod.objects.get(desk=desk).weekday == 2 + assert TimePeriod.objects.get(desk=desk).start_time.hour == 10 + assert TimePeriod.objects.get(desk=desk).start_time.minute == 0 + assert TimePeriod.objects.get(desk=desk).end_time.hour == 17 + assert TimePeriod.objects.get(desk=desk).end_time.minute == 0 resp = resp.follow() # add a second time period - resp = resp.click('New Time Period') + resp = resp.click('Add') resp.form['weekday'].select(text='Monday') resp.form['start_time'] = '10:00' resp.form['end_time'] = '13:00' @@ -600,13 +602,15 @@ def test_meetings_agenda_add_time_period(app, admin_user): assert 'Edit Time Period' in resp.body resp.form['start_time'] = '9:00' resp = resp.form.submit() - assert TimePeriod.objects.get(agenda=agenda, weekday=2).start_time.hour == 9 + assert TimePeriod.objects.get(desk=desk, weekday=2).start_time.hour == 9 + def test_meetings_agenda_delete_time_period(app, admin_user): agenda = Agenda(label=u'Foo bar', kind='meetings') agenda.save() - time_period = TimePeriod(agenda=agenda, weekday=2, + desk = Desk.objects.create(agenda=agenda, label='Desk A') + time_period = TimePeriod(desk=desk, weekday=2, start_time=datetime.time(10, 0), end_time=datetime.time(18, 0)) time_period.save() @@ -618,3 +622,83 @@ def test_meetings_agenda_delete_time_period(app, admin_user): resp = resp.form.submit() assert resp.location == 'http://testserver/manage/agendas/%s/' % agenda.id assert TimePeriod.objects.count() == 0 + + +def test_meetings_agenda_add_time_period_on_missing_desk(app, admin_user): + app = login(app) + agenda = Agenda(label=u'Foo bar', kind='meetings') + agenda.save() + app.get('/manage/agendas/1/desk/777/add-time-period', status=404) + + +def test_meetings_agenda_add_time_period_as_manager(app, manager_user): + agenda = Agenda(label='Foo bar', kind='meetings') + agenda.view_role = manager_user.groups.all()[0] + agenda.save() + desk = Desk.objects.create(agenda=agenda, label='Desk A') + app = login(app, username='manager', password='manager') + resp = app.get('/manage/agendas/%d/' % agenda.id) + assert 'New Meeting Type' in resp.content + assert 'New Time Period' in resp.content + assert 'This desk doesn\'t have any time period yet.' in resp.content + app.get('/manage/agendas/%d/desk/%d/add-time-period' % (agenda.id, desk.id), status=403) + time_period = TimePeriod(desk=desk, weekday=0, start_time=datetime.time(9, 0), + end_time=datetime.time(12, 0)) + time_period.save() + app.get('/manage/timeperiods/%d/edit' % time_period.id, status=403) + # grant edit right to manager + agenda.edit_role = manager_user.groups.all()[0] + agenda.save() + app.get('/manage/agendas/%d/desk/%d/add-time-period' % (agenda.id, desk.id), status=200) + app.get('/manage/timeperiods/%d/edit' % time_period.id, status=200) + + +def test_meetings_agenda_add_desk(app, admin_user): + app = login(app) + resp = app.get('/manage/', status=200) + resp = resp.click('New') + resp.form['label'] = 'Foo bar' + resp.form['kind'] = 'meetings' + resp = resp.form.submit() + assert Desk.objects.count() == 1 + assert str(Desk.objects.first()) == 'Desk 1' + agenda = Agenda.objects.get(slug='foo-bar') + + resp = app.get('/manage/agendas/%s/' % agenda.id, status=200) + resp = resp.click('New Desk') + resp.form['label'] = 'Desk A' + resp = resp.form.submit().follow() + assert Desk.objects.count() == 2 + resp = resp.click('New Desk') + resp.form['label'] = 'Desk A' + resp = resp.form.submit().follow() + assert Desk.objects.count() == 3 + assert Desk.objects.filter(slug='desk-a-1').count() == 1 + assert 'Desk A' in resp.text + + resp = resp.click('Desk A', index=1) + resp.form['label'] = 'Desk B' + resp = resp.form.submit().follow() + assert 'Desk A' in resp.text + assert 'Desk B' in resp.text + + +def test_meetings_agenda_delete_desk(app, admin_user): + app = login(app) + resp = app.get('/manage/', status=200) + resp = resp.click('New') + resp.form['label'] = 'Foo bar' + resp.form['kind'] = 'meetings' + resp = resp.form.submit() + assert Desk.objects.count() == 1 + agenda = Agenda.objects.get(slug='foo-bar') + resp = app.get('/manage/agendas/%s/' % agenda.id, status=200) + resp = resp.click('New Desk') + resp.form['label'] = 'Desk A' + resp = resp.form.submit().follow() + assert Desk.objects.count() == 2 + resp = resp.click('Desk A', index=0) + resp = resp.click('Delete') + resp = resp.form.submit() + assert resp.location == 'http://testserver/manage/agendas/%s/' % agenda.id + assert Desk.objects.count() == 1 diff --git a/tests/test_time_periods.py b/tests/test_time_periods.py index f0dd9ef5..bfcb02dc 100644 --- a/tests/test_time_periods.py +++ b/tests/test_time_periods.py @@ -3,7 +3,7 @@ import pytest from django.utils.timezone import make_aware -from chrono.agendas.models import Agenda, TimePeriod, MeetingType +from chrono.agendas.models import Agenda, TimePeriod, MeetingType, Desk pytestmark = pytest.mark.django_db @@ -11,7 +11,8 @@ pytestmark = pytest.mark.django_db def test_timeperiod_time_slots(): agenda = Agenda(label=u'Foo bar', slug='bar') agenda.save() - timeperiod = TimePeriod(agenda=agenda, weekday=0, + desk = Desk.objects.create(label='Desk 1', agenda=agenda) + timeperiod = TimePeriod(desk=desk, weekday=0, start_time=datetime.time(9, 0), end_time=datetime.time(12, 0)) events = timeperiod.get_time_slots( @@ -28,7 +29,7 @@ def test_timeperiod_time_slots(): assert len(events) == 12 # another start before the timeperiod - timeperiod = TimePeriod(agenda=agenda, weekday=1, + timeperiod = TimePeriod(desk=desk, weekday=1, start_time=datetime.time(9, 0), end_time=datetime.time(12, 0)) events = timeperiod.get_time_slots( @@ -41,7 +42,7 @@ def test_timeperiod_time_slots(): assert len(events) == 12 # a start on the day of the timeperiod - timeperiod = TimePeriod(agenda=agenda, weekday=3, + timeperiod = TimePeriod(desk=desk, weekday=3, start_time=datetime.time(9, 0), end_time=datetime.time(12, 0)) events = timeperiod.get_time_slots( @@ -54,7 +55,7 @@ def test_timeperiod_time_slots(): assert len(events) == 15 # a start after the day of the timeperiod - timeperiod = TimePeriod(agenda=agenda, weekday=4, + timeperiod = TimePeriod(desk=desk, weekday=4, start_time=datetime.time(9, 0), end_time=datetime.time(12, 0)) events = timeperiod.get_time_slots( @@ -67,7 +68,7 @@ def test_timeperiod_time_slots(): assert len(events) == 15 # another start after the day of the timeperiod - timeperiod = TimePeriod(agenda=agenda, weekday=5, + timeperiod = TimePeriod(desk=desk, weekday=5, start_time=datetime.time(9, 0), end_time=datetime.time(12, 0)) events = timeperiod.get_time_slots( @@ -80,7 +81,7 @@ def test_timeperiod_time_slots(): assert len(events) == 12 # shorter duration -> double the events - timeperiod = TimePeriod(agenda=agenda, weekday=5, + timeperiod = TimePeriod(desk=desk, weekday=5, start_time=datetime.time(9, 0), end_time=datetime.time(12, 0)) events = timeperiod.get_time_slots(