add time period exception management (#12550)
This commit is contained in:
parent
bb043fb85e
commit
9d7738929c
|
@ -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'],
|
||||
},
|
||||
),
|
||||
]
|
|
@ -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'),
|
||||
}
|
||||
|
|
|
@ -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'})
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -65,3 +65,7 @@ h2 span.identifier {
|
|||
font-family: FontAwesome;
|
||||
padding-right: 1ex;
|
||||
}
|
||||
|
||||
a.timeperiod-exception-all {
|
||||
font-style: italic;
|
||||
}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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'),
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue