add time period exception management (#12550)

This commit is contained in:
Josue Kouka 2017-08-24 14:15:18 +02:00
parent bb043fb85e
commit 9d7738929c
14 changed files with 468 additions and 25 deletions

View File

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agendas', '0018_event_desk'),
]
operations = [
migrations.CreateModel(
name='TimePeriodException',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('label', models.CharField(max_length=150, null=True, verbose_name='Optional Label', blank=True)),
('start_datetime', models.DateTimeField(verbose_name='Exception start time')),
('end_datetime', models.DateTimeField(verbose_name='Exception end time')),
('desk', models.ForeignKey(to='agendas.Desk')),
],
options={
'ordering': ['start_datetime'],
},
),
]

View File

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
# chrono - agendas system
# Copyright (C) 2016 Entr'ouvert
#
@ -17,11 +18,13 @@
import datetime
from django.contrib.auth.models import Group
from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse
from django.db import models
from django.db import transaction
from django.utils.dateformat import DateFormat
from django.utils.dates import WEEKDAYS
from django.utils.formats import date_format
from django.utils.formats import date_format, get_format
from django.utils.text import slugify
from django.utils.timezone import localtime, now, make_aware
from django.utils.translation import ugettext_lazy as _
@ -34,6 +37,11 @@ AGENDA_KINDS = (
)
def is_midnight(dtime):
dtime = localtime(dtime)
return dtime.hour == 0 and dtime.minute == 0
class Agenda(models.Model):
label = models.CharField(_('Label'), max_length=150)
slug = models.SlugField(_('Slug'))
@ -361,14 +369,82 @@ class Desk(models.Model):
@classmethod
def import_json(cls, data):
timeperiods = data.pop('timeperiods')
exceptions = data.pop('exceptions')
instance, created = cls.objects.get_or_create(**data)
for timeperiod in timeperiods:
timeperiod['desk'] = instance
TimePeriod.import_json(timeperiod).save()
for exception in exceptions:
exception['desk'] = instance
TimePeriodException.import_json(exception).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()]
'timeperiods': [time_period.export_json() for time_period in self.timeperiod_set.all()],
'exceptions': [exception.export_json() for exception in self.timeperiodexception_set.all()]
}
def get_exceptions_within_two_weeks(self):
in_two_weeks = datetime.datetime.today() + datetime.timedelta(days=14)
exceptions = self.timeperiodexception_set.filter(
end_datetime__gte=now, end_datetime__lte=in_two_weeks)
if exceptions.exists():
return exceptions
# if none found within the 2 coming weeks, return the next one
next_exception = self.timeperiodexception_set.filter(
start_datetime__gte=now()).order_by('start_datetime').first()
if next_exception:
return [next_exception]
return []
def are_all_exceptions_displayed(self):
in_two_weeks = self.get_exceptions_within_two_weeks()
return self.timeperiodexception_set.count() == len(in_two_weeks)
class TimePeriodException(models.Model):
desk = models.ForeignKey(Desk)
label = models.CharField(_('Optional Label'), max_length=150, blank=True, null=True)
start_datetime = models.DateTimeField(_('Exception start time'))
end_datetime = models.DateTimeField(_('Exception end time'))
class Meta:
ordering = ['start_datetime']
def __unicode__(self):
date_format = get_format('SHORT_DATETIME_FORMAT')
if is_midnight(self.start_datetime) or is_midnight(self.end_datetime):
date_format = get_format('SHORT_DATE_FORMAT')
exc_repr = u'%s%s' % (DateFormat(localtime(self.start_datetime)).format(date_format),
DateFormat(localtime(self.end_datetime)).format(date_format))
if self.label:
exc_repr = u'%s (%s)' % (self.label, exc_repr)
return exc_repr
def clean(self):
super(TimePeriodException, self).clean()
if self.has_booking_within_time_slot():
raise ValidationError(_('One or several bookings exists within this time slot.'))
def has_booking_within_time_slot(self):
for event in Event.objects.filter(agenda=self.desk.agenda, booking__isnull=False):
if self.start_datetime <= event.start_datetime < self.end_datetime:
return True
return False
@classmethod
def import_json(cls, data):
data['start_datetime'] = make_aware(datetime.datetime.strptime(
data['start_datetime'], '%Y-%m-%d %H:%M:%S'))
data['end_datetime'] = make_aware(datetime.datetime.strptime(
data['end_datetime'], '%Y-%m-%d %H:%M:%S'))
return cls(**data)
def export_json(self):
return {
'label': self.label,
'start_datetime': self.start_datetime.strftime('%Y-%m-%d %H:%M:%S'),
'end_datetime': self.end_datetime.strftime('%Y-%m-%d %H:%M:%S'),
}

View File

@ -15,6 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from collections import defaultdict
from copy import deepcopy
import datetime
import operator
@ -35,7 +36,19 @@ from ..agendas.models import (Agenda, Event, Booking, MeetingType,
TimePeriod, Desk)
def get_open_slots(agenda, meeting_type):
def get_exceptions_by_desk(agenda):
exceptions_by_desk = {}
for desk in Desk.objects.filter(agenda=agenda).prefetch_related('timeperiodexception_set'):
exceptions = [exc for exc in desk.timeperiodexception_set.all()]
if not exceptions:
continue
exceptions_by_desk[desk.id] = (
min(exceptions, key=lambda x: x.start_datetime),
max(exceptions, key=lambda x: x.end_datetime))
return exceptions_by_desk
def get_all_slots(agenda, meeting_type):
min_datetime = now() + datetime.timedelta(days=agenda.minimal_booking_delay)
max_datetime = now() + datetime.timedelta(days=agenda.maximal_booking_delay)
min_datetime = min_datetime.replace(hour=0, minute=0, second=0, microsecond=0)
@ -51,7 +64,16 @@ def get_open_slots(agenda, 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)
open_slots_by_desk[time_period.desk_id].addi(slot.start_datetime, slot.end_datetime, slot)
# remove excluded slot
excluded_slot_by_desk = get_exceptions_by_desk(agenda)
for desk, excluded_interval in excluded_slot_by_desk.iteritems():
begin = localtime(excluded_interval[0].start_datetime)
end = localtime(excluded_interval[-1].end_datetime)
open_slots_by_desk[desk].remove_envelop(begin, end)
# keep a copy of all time slot before removing busy time slots
all_time_slots = reduce(operator.__or__, deepcopy(open_slots_by_desk).values())
for event in agenda.event_set.filter(
agenda=agenda, start_datetime__gte=min_datetime,
@ -66,7 +88,7 @@ def get_open_slots(agenda, meeting_type):
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
return open_slots, all_time_slots
def get_agenda_detail(request, agenda):
@ -186,18 +208,13 @@ class MeetingDatetimes(GenericAPIView):
agenda = meeting_type.agenda
now_datetime = now()
min_datetime = now() + datetime.timedelta(days=agenda.minimal_booking_delay)
max_datetime = now() + datetime.timedelta(days=agenda.maximal_booking_delay)
all_time_slots = []
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))
open_slots = get_open_slots(agenda, meeting_type)
open_slots, all_time_slots = get_all_slots(agenda, meeting_type)
open_entries = {}
closed_entries = {}
for time_slot in all_time_slots:
for interval in all_time_slots.all_intervals:
time_slot = interval.data
if time_slot.start_datetime < now_datetime:
continue
key = '%s-%s' % (time_slot.start_datetime, time_slot.end_datetime)
@ -297,10 +314,10 @@ class Fillslot(GenericAPIView):
available_desk = None
open_slots = get_open_slots(agenda, MeetingType.objects.get(id=meeting_type_id))
open_slots, _ = get_all_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
available_desk = slot.pop().data.desk
if not available_desk:
return Response({'err': 1, 'reason': 'no more desk available'})

View File

@ -21,7 +21,8 @@ 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, Desk
from chrono.agendas.models import (Event, MeetingType, TimePeriod, Desk,
TimePeriodException)
from . import widgets
@ -94,6 +95,22 @@ class DeskForm(forms.ModelForm):
exclude = []
class TimePeriodExceptionForm(forms.ModelForm):
class Meta:
model = TimePeriodException
fields = ['desk', 'start_datetime', 'end_datetime', 'label']
widgets = {
'desk': forms.HiddenInput(),
'start_datetime': DateTimeWidget(),
'end_datetime': DateTimeWidget(),
}
def clean_end_datetime(self):
if self.cleaned_data['end_datetime'] < self.cleaned_data['start_datetime']:
raise ValidationError(_('End datetime must be greater than start datetime.'))
return self.cleaned_data['end_datetime']
class ImportEventsForm(forms.Form):
events_csv_file = forms.FileField(
label=_('Events File'),

View File

@ -65,3 +65,7 @@ h2 span.identifier {
font-family: FontAwesome;
padding-right: 1ex;
}
a.timeperiod-exception-all {
font-style: italic;
}

View File

@ -120,6 +120,19 @@
{% if user_can_manage %}
<li><a class="add" rel="popup" href="{{add_time_period_url}}">{% trans 'Add a time period' %}</a></li>
{% endif %}
{% if desk.timeperiod_set.count %}
{% url 'chrono-manager-agenda-add-time-period-exception' agenda_pk=object.pk pk=desk.pk as add_time_period_exception_url %}
<li><a href="#"><strong>{% trans 'Exceptions' %}</strong></a></li>
{% for exception in desk.get_exceptions_within_two_weeks %}
<li><a rel="popup" href="{% if user_can_manage %}{% url 'chrono-manager-time-period-exception-edit' pk=exception.pk %}{% else %}#{% endif %}">
{{ exception }}
{% if user_can_manage %}<a rel="popup" class="delete" href="{% url 'chrono-manager-time-period-exception-delete' pk=exception.id %}">{% trans "remove" %}</a>{% endif %}
{% endfor %}
{% if not desk.are_all_exceptions_displayed %}
<li><a class="timeperiod-exception-all" rel="popup" data-selector="div.timeperiod" href="{% url 'chrono-manager-time-period-exception-list' pk=desk.id %}">({% trans 'see all exceptions' %})</a></li>
{% endif %}
<li><a class="add" rel="popup" href="{{add_time_period_exception_url}}">{% trans 'Add a time period exception' %}</a></li>
{% endif %}
</ul>
</div>
{% endfor %}

View File

@ -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 %}
<a href="{% url 'chrono-manager-agenda-view' pk=desk.agenda.id %}">{{desk.agenda.label}}</a>
{% endif %}
{% endblock %}
{% block appbar %}
{% if object.id %}
<h2>{% trans "Edit time period exception" %}</h2>
<a rel="popup" href="{% url 'chrono-manager-time-period-exception-delete' pk=object.id %}">{% trans 'Delete' %}</a>
{% else %}
<h2>{% trans "New time period exception" %}</h2>
{% endif %}
{% endblock %}
{% block content %}
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<div class="buttons">
<button class="submit-button">{% trans "Save" %}</button>
<a class="cancel" href="{% url 'chrono-manager-agenda-view' pk=desk.agenda.id %}">{% trans 'Cancel' %}</a>
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,30 @@
{% extends "chrono/manager_desk_form.html" %}
{% load i18n %}
{% block extrascripts %}
{{ block.super }}
{{ form.media }}
{% endblock %}
{% block breadcrumb %}
{{ block.super }}
{% if desk %}
<a href="{% url 'chrono-manager-agenda-view' pk=desk.agenda.id %}">{{desk.label}}</a>
{% endif %}
{% endblock %}
{% block appbar %}
{% endblock %}
{% block content %}
<div class="timeperiod">
<ul class="objects-list single-links">
{% for exception in object_list %}
<li>
<a href="{% if user_can_manage %}{% url 'chrono-manager-time-period-exception-edit' pk=exception.id %}{% else %}#{% endif %}">{{ exception }}</a>
{% if user_can_manage %}<a rel="popup" class="delete" href="{% url 'chrono-manager-time-period-exception-delete' pk=exception.id %}">{% trans "remove" %}</a>{% endif %}
</li>
{% endfor %}
</ul>
</div>
{% endblock %}

View File

@ -58,6 +58,15 @@ urlpatterns = [
url(r'^desks/(?P<pk>\w+)/delete$', views.desk_delete,
name='chrono-manager-desk-delete'),
url(r'^agendas/(?P<agenda_pk>\d+)/desk/(?P<pk>\d+)/add-time-period-exception$', views.agenda_add_time_period_exception,
name='chrono-manager-agenda-add-time-period-exception'),
url(r'^time-period-exceptions/(?P<pk>\w+)/edit$', views.time_period_exception_edit,
name='chrono-manager-time-period-exception-edit'),
url(r'^time-period-exceptions/(?P<pk>\w+)/delete$', views.time_period_exception_delete,
name='chrono-manager-time-period-exception-delete'),
url(r'^time-period-exceptions/(?P<pk>\w+)/exception-list$', views.time_period_exception_list,
name='chrono-manager-time-period-exception-list'),
url(r'^agendas/events.csv$', views.agenda_import_events_sample_csv,
name='chrono-manager-sample-events-csv'),

View File

@ -28,10 +28,10 @@ from django.views.generic import (DetailView, CreateView, UpdateView,
ListView, DeleteView, FormView, TemplateView)
from chrono.agendas.models import (Agenda, Event, MeetingType, TimePeriod,
Booking, Desk)
Booking, Desk, TimePeriodException)
from .forms import (EventForm, NewMeetingTypeForm, MeetingTypeForm,
TimePeriodForm, ImportEventsForm, NewDeskForm, DeskForm)
TimePeriodForm, ImportEventsForm, NewDeskForm, DeskForm, TimePeriodExceptionForm)
class HomepageView(ListView):
@ -343,6 +343,48 @@ class DeskDeleteView(ManagedAgendaSubobjectMixin, DeleteView):
desk_delete = DeskDeleteView.as_view()
class AgendaAddTimePeriodExceptionView(ManagedDeskMixin, CreateView):
template_name = 'chrono/manager_time_period_exception_form.html'
model = TimePeriodException
form_class = TimePeriodExceptionForm
agenda_add_time_period_exception = AgendaAddTimePeriodExceptionView.as_view()
class TimePeriodExceptionEditView(ManagedDeskSubobjectMixin, UpdateView):
template_name = 'chrono/manager_time_period_exception_form.html'
model = TimePeriodException
form_class = TimePeriodExceptionForm
time_period_exception_edit = TimePeriodExceptionEditView.as_view()
class TimePeriodExceptionListView(ManagedDeskMixin, ListView):
template_name = 'chrono/manager_time_period_exception_list.html'
model = TimePeriodException
def get_queryset(self):
return self.model.objects.filter(desk=self.desk)
def get_context_data(self, **kwargs):
context = super(TimePeriodExceptionListView, self).get_context_data(**kwargs)
context['user_can_manage'] = self.desk.agenda.can_be_managed(self.request.user)
return context
time_period_exception_list = TimePeriodExceptionListView.as_view()
class TimePeriodExceptionDeleteView(ManagedDeskSubobjectMixin, DeleteView):
template_name = 'chrono/manager_confirm_delete.html'
model = TimePeriodException
time_period_exception_delete = TimePeriodExceptionDeleteView.as_view()
def menu_json(request):
response = HttpResponse(content_type='application/json')
label = _('Agendas')

View File

@ -10,7 +10,9 @@ 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, Desk
from chrono.agendas.models import (Agenda, Event, Booking,
MeetingType, TimePeriod, Desk,
TimePeriodException)
import chrono.api.views
@ -861,3 +863,51 @@ def test_agenda_meeting_next_day(app, meetings_agenda, mock_now, user):
assert not resp.json['data'][1]['disabled']
assert resp.json['data'][-1]['disabled']
assert not resp.json['data'][-2]['disabled']
def test_agenda_meeting_api_exception(app, meetings_agenda, user):
app.authorization = ('Basic', ('john.doe', 'password'))
meeting_type = MeetingType.objects.get(agenda=meetings_agenda)
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
desk = meetings_agenda.desk_set.first()
# test exception at the lowest limit
excp1 = TimePeriodException.objects.create(
desk=desk, start_datetime=datetime.datetime(2017, 5, 22, 10, 00),
end_datetime=datetime.datetime(2017, 5, 22, 12, 00))
resp2 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
assert len(resp.json['data']) == len(resp2.json['data']) + 4
# test exception at the highest limit
excp1.end_datetime = datetime.datetime(2017, 5, 22, 11, 00)
excp1.save()
resp2 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
assert len(resp.json['data']) == len(resp2.json['data']) + 2
# add an exception with an end datetime less than excp1 end datetime
# and make sure that excp1 end datetime preveil
excp1.end_datetime = datetime.datetime(2017, 5, 23, 11, 00)
excp1.save()
TimePeriodException.objects.create(
desk=excp1.desk, start_datetime=datetime.datetime(2017, 5, 22, 15, 00),
end_datetime=datetime.datetime(2017, 5, 23, 9, 00))
resp2 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
assert len(resp.json['data']) == len(resp2.json['data']) + 6
# with a second desk
desk2 = Desk.objects.create(label='Desk 2', agenda=meetings_agenda)
time_period = desk.timeperiod_set.first()
TimePeriod.objects.create(
desk=desk2, start_time=time_period.start_time, end_time=time_period.end_time,
weekday=time_period.weekday)
resp3 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
assert len(resp.json['data']) == len(resp3.json['data']) + 2 # +2 because excp1 changed
# try to booking just after an exception is set
TimePeriodException.objects.create(
desk=desk2, start_datetime=datetime.datetime(2017, 5, 22, 9, 00),
end_datetime=datetime.datetime(2017, 5, 22, 12, 00))
booking_url = resp3.json['data'][0]['api']['fillslot_url']
resp = app.post(booking_url)
assert resp.json['err'] == 1

View File

@ -31,7 +31,7 @@ def test_meeting_type_slug_migration():
assert MeetingType.objects.get(id=meeting_type.id).slug == 'baz'
def test_timepriod_data_migrations(transactional_db):
def test_timeperiod_data_migrations():
executor = MigrationExecutor(connection)
app = 'agendas'
migrate_from = [(app, '0016_desk')]

View File

@ -9,7 +9,8 @@ import tempfile
import pytest
from django.core.management import call_command
from chrono.agendas.models import Agenda, Event, MeetingType, TimePeriod, Desk
from chrono.agendas.models import (Agenda, Event, MeetingType, TimePeriod,
Desk, TimePeriodException)
from chrono.manager.utils import export_site, import_site
from test_api import some_data, meetings_agenda, time_zone, mock_now
@ -24,6 +25,14 @@ def get_output_of_command(command, *args, **kwargs):
return output.getvalue()
def test_import_export(app, some_data, meetings_agenda):
# add exception to meeting agenda
desk = meetings_agenda.desk_set.first()
TimePeriodException.objects.create(
desk=desk, start_datetime=datetime.datetime(2017, 5, 22, 8, 0),
end_datetime=datetime.datetime(2017, 5, 22, 12, 30))
TimePeriodException.objects.create(
desk=desk, start_datetime=datetime.datetime(2017, 5, 22, 8, 0),
end_datetime=datetime.datetime(2017, 5, 22, 12, 30))
output = get_output_of_command('export_site')
assert len(json.loads(output)['agendas']) == 3
import_site(data={}, clean=True)
@ -55,20 +64,30 @@ def test_import_export(app, some_data, meetings_agenda):
timeperiod = TimePeriod(weekday=2, desk=desk,
start_time=datetime.time(10, 0), end_time=datetime.time(11, 0))
timeperiod.save()
exception = TimePeriodException(
desk=desk, start_datetime=datetime.datetime(2017, 5, 22, 8, 0),
end_datetime=datetime.datetime(2017, 5, 22, 12, 30))
exception.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
assert TimePeriodException.objects.filter(id=exception.id).count() == 0
event = Event(agenda=agenda1, start_datetime= datetime.datetime.now(), places=10)
event.save()
timeperiod = TimePeriod(weekday=2, desk=desk,
start_time=datetime.time(10, 0), end_time=datetime.time(11, 0))
timeperiod.save()
exception = TimePeriodException(
desk=desk, start_datetime=datetime.datetime(2017, 5, 22, 8, 0),
end_datetime=datetime.datetime(2017, 5, 22, 12, 30))
exception.save()
import_site(json.loads(output), overwrite=False)
assert Event.objects.filter(id=event.id).count() == 1
assert TimePeriod.objects.filter(id=timeperiod.id).count() == 1
assert TimePeriodException.objects.filter(id=exception.id).count() == 1
import_site(data={}, if_empty=True)
assert Agenda.objects.count() == 3

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from django.contrib.auth.models import User, Group
from django.utils.timezone import make_aware, now
from django.utils.timezone import make_aware, now, localtime
import datetime
import pytest
from webtest import TestApp, Upload
@ -9,7 +9,7 @@ from webtest import TestApp, Upload
from chrono.wsgi import application
from chrono.agendas.models import (Agenda, Event, Booking, MeetingType,
TimePeriod, Desk)
TimePeriod, Desk, TimePeriodException)
pytestmark = pytest.mark.django_db
@ -589,7 +589,7 @@ def test_meetings_agenda_add_time_period(app, admin_user):
resp = resp.follow()
# add a second time period
resp = resp.click('Add')
resp = resp.click('Add a time period', index=0)
resp.form['weekday'].select(text='Monday')
resp.form['start_time'] = '10:00'
resp.form['end_time'] = '13:00'
@ -721,3 +721,107 @@ def test_meetings_agenda_delete_desk(app, admin_user):
resp = resp.form.submit()
assert resp.location == 'http://testserver/manage/agendas/%s/' % agenda.id
assert Desk.objects.count() == 1
def test_meetings_agenda_add_time_period_exception(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().follow()
agenda = Agenda.objects.first()
resp = resp.click('New Meeting Type')
resp.form['label'] = 'Blah'
resp.form['duration'] = '60'
resp = resp.form.submit().follow()
# adding a new time period
resp = resp.click('Add a time period')
resp.form['weekday'].select(text='Wednesday')
resp.form['start_time'] = '10:00'
resp.form['end_time'] = '17:00'
resp = resp.form.submit().follow()
resp = resp.click('Add a time period exception')
today = datetime.datetime.today().replace(hour=0, minute=0, second=0, microsecond=0)
tomorrow = make_aware(today + datetime.timedelta(days=1))
dt_format = '%Y-%m-%d %H:%M'
resp.form['label'] = 'Exception 1'
resp.form['start_datetime'] = tomorrow.replace(hour=8).strftime(dt_format)
resp.form['end_datetime'] = tomorrow.replace(hour=16).strftime(dt_format)
resp = resp.form.submit().follow()
assert TimePeriodException.objects.count() == 1
time_period_exception = TimePeriodException.objects.first()
assert localtime(time_period_exception.start_datetime).strftime(dt_format) == tomorrow.replace(hour=8).strftime(dt_format)
assert localtime(time_period_exception.end_datetime).strftime(dt_format) == tomorrow.replace(hour=16).strftime(dt_format)
# add an exception beyond 2 weeks and make sure it isn't listed
resp = resp.click('Add a time period exception', index=1)
future = tomorrow + datetime.timedelta(days=15)
resp.form['label'] = 'Exception 2'
resp.form['start_datetime'] = future.replace(hour=0, minute=0).strftime(dt_format)
resp.form['end_datetime'] = future.replace(hour=16).strftime(dt_format)
resp = resp.form.submit().follow()
assert TimePeriodException.objects.count() == 2
assert 'Exception 1' in resp.content
assert 'Exception 2' not in resp.content
resp = resp.click(href="/manage/time-period-exceptions/%d/exception-list" % agenda.desk_set.first().pk)
assert 'Exception 1' in resp.content
assert 'Exception 2' in resp.content
def test_meetings_agenda_add_time_period_exception_when_booking_exists(app, admin_user):
agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Desk A')
MeetingType(agenda=agenda, label='Blah').save()
TimePeriod.objects.create(weekday=1, desk=desk,
start_time=datetime.time(10, 0), end_time=datetime.time(12, 0))
event = Event.objects.create(agenda=agenda, places=1,
start_datetime=datetime.datetime(2017, 5, 22, 10, 30))
Booking.objects.create(event=event)
login(app)
resp = app.get('/manage/agendas/%d/' % agenda.pk)
resp = resp.click('Add a time period exception')
resp.form['start_datetime'] = '2017-05-22 08:00'
resp.form['end_datetime'] = '2017-05-26 17:30'
resp = resp.form.submit()
assert 'One or several bookings exists within this time slot.' in resp.content
def test_meetings_agenda_add_invalid_time_period_exception(app, admin_user):
agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Desk A')
MeetingType(agenda=agenda, label='Blah').save()
TimePeriod.objects.create(weekday=1, desk=desk,
start_time=datetime.time(10, 0), end_time=datetime.time(12, 0))
login(app)
resp = app.get('/manage/agendas/%d/' % agenda.pk)
resp = resp.click('Add a time period exception')
resp.form['start_datetime'] = '2017-05-26 17:30'
resp.form['end_datetime'] = '2017-05-22 08:00'
resp = resp.form.submit()
assert 'End datetime must be greater than start datetime.' in resp.content
def test_meetings_agenda_delete_time_period_exception(app, admin_user):
agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Desk A')
MeetingType(agenda=agenda, label='Blah').save()
TimePeriod.objects.create(weekday=1, desk=desk,
start_time=datetime.time(10, 0), end_time=datetime.time(12, 0))
login(app)
resp = app.get('/manage/agendas/%d/' % agenda.pk)
resp = resp.click('Add a time period exception')
today = datetime.datetime.today().replace(hour=0, minute=0, second=0, microsecond=0)
tomorrow = make_aware(today + datetime.timedelta(days=15))
dt_format = '%Y-%m-%d %H:%M'
resp.form['label'] = 'Exception 1'
resp.form['start_datetime'] = tomorrow.replace(hour=8).strftime(dt_format)
resp.form['end_datetime'] = tomorrow.replace(hour=16).strftime(dt_format)
resp = resp.form.submit().follow()
assert TimePeriodException.objects.count() == 1
time_period_exception = TimePeriodException.objects.first()
assert localtime(time_period_exception.start_datetime).strftime(dt_format) == tomorrow.replace(hour=8).strftime(dt_format)
assert localtime(time_period_exception.end_datetime).strftime(dt_format) == tomorrow.replace(hour=16).strftime(dt_format)
resp = resp.click(href='/manage/time-period-exceptions/%d/edit' % time_period_exception.id)
resp = resp.click('Delete')
resp = resp.form.submit()
assert TimePeriodException.objects.count() == 0