add support for "meetings" agendas (#13139)

This commit is contained in:
Frédéric Péters 2016-09-11 11:31:29 +02:00
parent 2efadcab9b
commit b26f51e36c
13 changed files with 692 additions and 17 deletions

View File

@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agendas', '0007_auto_20160722_1135'),
]
operations = [
migrations.CreateModel(
name='MeetingType',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('label', models.CharField(max_length=100, verbose_name='Label')),
('duration', models.IntegerField(default=30, verbose_name='Duration (in minutes)')),
],
options={
'ordering': ['label'],
},
),
migrations.CreateModel(
name='TimePeriod',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('weekday', models.IntegerField(verbose_name='Week day', choices=[(0, 'Monday'), (1, 'Tuesday'), (2, 'Wednesday'), (3, 'Thursday'), (4, 'Friday'), (5, 'Saturday'), (6, 'Sunday')])),
('start_time', models.TimeField(verbose_name='Start')),
('end_time', models.TimeField(verbose_name='End')),
],
options={
'ordering': ['weekday', 'start_time'],
},
),
migrations.AddField(
model_name='agenda',
name='kind',
field=models.CharField(default=b'events', max_length=20, verbose_name='Kind', choices=[(b'events', 'Events'), (b'meetings', 'Meetings')]),
),
migrations.AddField(
model_name='timeperiod',
name='agenda',
field=models.ForeignKey(to='agendas.Agenda'),
),
migrations.AddField(
model_name='meetingtype',
name='agenda',
field=models.ForeignKey(to='agendas.Agenda'),
),
migrations.AddField(
model_name='event',
name='meeting_type',
field=models.ForeignKey(to='agendas.MeetingType', null=True),
),
]

View File

@ -14,9 +14,12 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import datetime
from django.core.urlresolvers import reverse
from django.db import models
from django.db import transaction
from django.utils.dates import WEEKDAYS
from django.utils.formats import date_format
from django.utils.text import slugify
from django.utils.timezone import localtime, now
@ -24,10 +27,15 @@ from django.utils.translation import ugettext_lazy as _
from jsonfield import JSONField
AGENDA_KINDS = (
('events', _('Events')),
('meetings', _('Meetings')),
)
class Agenda(models.Model):
label = models.CharField(_('Label'), max_length=100)
slug = models.SlugField(_('Slug'))
kind = models.CharField(_('Kind'), max_length=20, choices=AGENDA_KINDS, default='events')
class Meta:
ordering = ['label']
@ -51,6 +59,75 @@ class Agenda(models.Model):
return reverse('chrono-manager-agenda-view', kwargs={'pk': self.id})
WEEKDAYS_LIST = sorted(WEEKDAYS.items(), key=lambda x: x[0])
class TimeSlot(object):
def __init__(self, start_datetime, meeting_type):
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
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'))
class Meta:
ordering = ['weekday', 'start_time']
@property
def weekday_str(self):
return WEEKDAYS[self.weekday]
def get_time_slots(self, min_datetime, max_datetime, meeting_type):
duration = datetime.timedelta(minutes=meeting_type.duration)
real_min_datetime = localtime(min_datetime) + datetime.timedelta(
days=self.weekday - min_datetime.weekday())
if real_min_datetime < min_datetime:
real_min_datetime += datetime.timedelta(days=7)
event_datetime = real_min_datetime.replace(hour=self.start_time.hour,
minute=self.start_time.minute, second=0, microsecond=0)
while event_datetime < max_datetime:
end_time = event_datetime + duration
if end_time.time() > self.end_time:
# back to morning
event_datetime = event_datetime.replace(hour=self.start_time.hour, minute=self.start_time.minute)
# but next week
event_datetime += datetime.timedelta(days=7)
end_time = event_datetime + duration
if event_datetime > max_datetime:
break
yield TimeSlot(start_datetime=event_datetime, meeting_type=meeting_type)
event_datetime = end_time
class MeetingType(models.Model):
agenda = models.ForeignKey(Agenda)
label = models.CharField(_('Label'), max_length=100)
duration = models.IntegerField(_('Duration (in minutes)'), default=30)
class Meta:
ordering = ['label']
class Event(models.Model):
agenda = models.ForeignKey(Agenda)
start_datetime = models.DateTimeField(_('Date/time'))
@ -60,6 +137,7 @@ class Event(models.Model):
label = models.CharField(_('Label'), max_length=50, null=True, blank=True,
help_text=_('Optional label to identify this date.'))
full = models.BooleanField(default=False)
meeting_type = models.ForeignKey(MeetingType, null=True)
class Meta:
ordering = ['agenda', 'start_datetime']
@ -88,6 +166,10 @@ class Event(models.Model):
return self.booking_set.filter(cancellation_datetime__isnull=True,
in_waiting_list=True).count()
@property
def end_datetime(self):
return self.start_datetime + datetime.timedelta(minutes=self.meeting_type.duration)
def get_absolute_url(self):
return reverse('chrono-manager-event-edit', kwargs={'pk': self.id})

View File

@ -20,9 +20,14 @@ from . import views
urlpatterns = patterns('',
url(r'agenda/$', views.agendas),
url(r'agenda/(?P<pk>\w+)/datetimes/$', views.datetimes, name='api-agenda-datetimes'),
url(r'agenda/(?P<agenda_pk>\w+)/fillslot/(?P<event_pk>\w+)/$', views.fillslot),
url(r'agenda/(?P<agenda_pk>\w+)/fillslot/(?P<event_pk>[\w:-]+)/$', views.fillslot),
url(r'agenda/(?P<agenda_pk>\w+)/status/(?P<event_pk>\w+)/$', views.slot_status),
url(r'agenda/meetings/(?P<pk>\w+)/datetimes/$', views.meeting_datetimes,
name='api-agenda-meeting-datetimes'),
url(r'booking/(?P<booking_pk>\w+)/$', views.booking),
url(r'booking/(?P<booking_pk>\w+)/cancel/$', views.cancel_booking),
url(r'booking/(?P<booking_pk>\w+)/accept/$', views.accept_booking),

View File

@ -14,16 +14,19 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import datetime
from django.core.urlresolvers import reverse
from django.shortcuts import get_object_or_404
from django.utils.timezone import now
from django.utils.timezone import now, make_aware
from rest_framework import permissions, serializers
from rest_framework.exceptions import APIException
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
from ..agendas.models import Agenda, Event, Booking, MeetingType, TimePeriod
class Agendas(GenericAPIView):
@ -45,17 +48,58 @@ agendas = Agendas.as_view()
class Datetimes(GenericAPIView):
def get(self, request, pk=None, format=None):
min_datetime = now()
response = {'data': [{
'id': x.id,
'text': unicode(x)}
for x in Event.objects.filter(agenda=pk).filter(
start_datetime__gte=min_datetime,
full=False)]}
agenda = Agenda.objects.get(id=pk)
if agenda.kind != 'events':
raise APIException('not an events agenda')
entries = Event.objects.filter(agenda=pk).filter(
start_datetime__gte=min_datetime,
full=False).order_by('start_datetime')
response = {'data': [{'id': x.id, 'text': unicode(x)} for x in entries]}
return Response(response)
datetimes = Datetimes.as_view()
class MeetingDatetimes(GenericAPIView):
def get(self, request, pk=None, format=None):
meeting_type = MeetingType.objects.get(id=pk)
agenda = meeting_type.agenda
if agenda.kind != 'meetings':
raise APIException('not a meetings agenda')
min_datetime = now()
max_datetime = now() + datetime.timedelta(days=60)
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))
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.
for time_slot in all_time_slots:
if any((x for x in busy_time_slots if x.full and time_slot.intersects(x))):
continue
entries.append(time_slot)
entries.sort(key=lambda x: x.start_datetime)
response = {'data': [{'id': x.id, 'text': unicode(x)} for x in entries]}
return Response(response)
meeting_datetimes = MeetingDatetimes.as_view()
class SlotSerializer(serializers.Serializer):
pass
@ -65,6 +109,20 @@ class Fillslot(GenericAPIView):
permission_classes = (permissions.IsAuthenticated,)
def post(self, request, agenda_pk=None, event_pk=None, format=None):
agenda = Agenda.objects.get(id=agenda_pk)
if agenda.kind == 'meetings':
# event is actually a timeslot, convert to a real event object
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,
meeting_type_id=meeting_type_id,
start_datetime=start_datetime,
defaults={'full': False, 'places': 1})
if created:
event.save()
event_pk = event.id
event = Event.objects.filter(id=event_pk)[0]
new_booking = Booking(event_id=event_pk, extra_data=request.data)

View File

@ -16,7 +16,7 @@
from django import forms
from chrono.agendas.models import Event
from chrono.agendas.models import Event, MeetingType, TimePeriod
from . import widgets
@ -39,4 +39,24 @@ class EventForm(forms.ModelForm):
'agenda': forms.HiddenInput(),
'start_datetime': DateTimeWidget(),
}
exclude = ['full']
exclude = ['full', 'meeting_type']
class MeetingTypeForm(forms.ModelForm):
class Meta:
model = MeetingType
widgets = {
'agenda': forms.HiddenInput(),
}
exclude = []
class TimePeriodForm(forms.ModelForm):
class Meta:
model = TimePeriod
widgets = {
'agenda': forms.HiddenInput(),
'start_time': widgets.TimeWidget(),
'end_time': widgets.TimeWidget(),
}
exclude = []

View File

@ -5,7 +5,12 @@
<h2>{{ object.label }}</h2>
<a rel="popup" href="{% url 'chrono-manager-agenda-delete' pk=object.id %}">{% trans 'Delete' %}</a>
<a rel="popup" href="{% url 'chrono-manager-agenda-edit' pk=object.id %}">{% trans 'Rename' %}</a>
{% if object.kind == "events" %}
<a rel="popup" href="{% url 'chrono-manager-agenda-add-event' pk=object.id %}">{% trans 'New Event' %}</a>
{% else %}
<a rel="popup" href="{% url 'chrono-manager-agenda-add-meeting-type' pk=object.id %}">{% trans 'New Meeting Type' %}</a>
<a rel="popup" href="{% url 'chrono-manager-agenda-add-time-period' pk=object.id %}">{% trans 'New Time Period' %}</a>
{% endif %}
{% endblock %}
{% block breadcrumb %}
@ -19,6 +24,7 @@
{% block content %}
{% if object.kind == "events" %}
{% if object.event_set.count %}
<div>
<ul class="objects-list single-links">
@ -58,6 +64,54 @@
the top right of the page to add a first one.
{% endblocktrans %}
</div>
{% endif %}
{% endif %}
{% if object.kind == "meetings" %}
<h3>{% trans 'Meeting Types' %}</h3>
{% if object.meetingtype_set.count %}
<div>
<ul class="objects-list single-links">
{% for meeting_type in object.meetingtype_set.all %}
<li><a href="{% url 'chrono-manager-meeting-type-edit' pk=meeting_type.id %}">
{{meeting_type.label}}
</a>
</li>
{% endfor %}
</ul>
</div>
{% else %}
<div class="big-msg-info">
{% blocktrans %}
This agenda doesn't have any meeting type yet. Click on the "New Meeting Type" button in
the top right of the page to add a first one.
{% endblocktrans %}
</div>
{% endif %}
<h3>{% trans 'Time Periods' %}</h3>
{% if object.timeperiod_set.count %}
<div>
<ul class="objects-list single-links">
{% for time_period in object.timeperiod_set.all %}
<li><a href="{% url 'chrono-manager-time-period-edit' pk=time_period.id %}">
{{time_period.weekday_str}} / {{time_period.start_time}} → {{time_period.end_time}}
</a>
</li>
{% endfor %}
</ul>
</div>
{% else %}
<div class="big-msg-info">
{% 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 %}
</div>
{% endif %}
{% endif %}
{% endblock %}

View File

@ -0,0 +1,34 @@
{% 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=object.agenda.id %}">{{object.agenda.label}}</a>
{% endif %}
{% endblock %}
{% block appbar %}
{% if object.id %}
<h2>{% trans "Edit Meeting Type" %}</h2>
{% else %}
<h2>{% trans "New Meeting Type" %}</h2>
{% endif %}
{% endblock %}
{% block content %}
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<div class="buttons">
<button>{% trans "Save" %}</button>
<a class="cancel" href="{% url 'chrono-manager-agenda-view' pk=agenda.id %}">{% trans 'Cancel' %}</a>
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,34 @@
{% 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=object.agenda.id %}">{{object.agenda.label}}</a>
{% endif %}
{% endblock %}
{% block appbar %}
{% if object.id %}
<h2>{% trans "Edit Time Period" %}</h2>
{% else %}
<h2>{% trans "New Time Period" %}</h2>
{% endif %}
{% endblock %}
{% block content %}
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<div class="buttons">
<button>{% trans "Save" %}</button>
<a class="cancel" href="{% url 'chrono-manager-agenda-view' pk=agenda.id %}">{% trans 'Cancel' %}</a>
</div>
</form>
{% endblock %}

View File

@ -34,5 +34,16 @@ urlpatterns = patterns('chrono.views',
name='chrono-manager-event-edit'),
url(r'^events/(?P<pk>\w+)/delete$', views.event_delete,
name='chrono-manager-event-delete'),
url(r'^agendas/(?P<pk>\w+)/add-meeting-type$', views.agenda_add_meeting_type,
name='chrono-manager-agenda-add-meeting-type'),
url(r'^meetingtypes/(?P<pk>\w+)/edit$', views.meeting_type_edit,
name='chrono-manager-meeting-type-edit'),
url(r'^agendas/(?P<pk>\w+)/add-time-period$', views.agenda_add_time_period,
name='chrono-manager-agenda-add-time-period'),
url(r'^timeperiods/(?P<pk>\w+)/edit$', views.time_period_edit,
name='chrono-manager-time-period-edit'),
url(r'^menu.json$', views.menu_json),
)

View File

@ -23,9 +23,9 @@ from django.utils.encoding import force_text
from django.views.generic import (DetailView, CreateView, UpdateView,
ListView, DeleteView)
from chrono.agendas.models import Agenda, Event
from chrono.agendas.models import Agenda, Event, MeetingType, TimePeriod
from .forms import EventForm
from .forms import EventForm, MeetingTypeForm, TimePeriodForm
class HomepageView(ListView):
@ -38,7 +38,7 @@ homepage = HomepageView.as_view()
class AgendaAddView(CreateView):
template_name = 'chrono/manager_agenda_form.html'
model = Agenda
fields = ['label']
fields = ['label', 'kind']
agenda_add = AgendaAddView.as_view()
@ -113,6 +113,79 @@ class EventDeleteView(DeleteView):
event_delete = EventDeleteView.as_view()
class AgendaAddMeetingTypeView(CreateView):
template_name = 'chrono/manager_meeting_type_form.html'
model = Event
form_class = MeetingTypeForm
def get_success_url(self):
return reverse('chrono-manager-agenda-view', kwargs={'pk': self.kwargs.get('pk')})
def get_initial(self):
initial = super(AgendaAddMeetingTypeView, self).get_initial()
initial['agenda'] = self.kwargs.get('pk')
return initial
def get_context_data(self, **kwargs):
context = super(AgendaAddMeetingTypeView, self).get_context_data(**kwargs)
context['agenda'] = Agenda.objects.get(id=self.kwargs.get('pk'))
return context
agenda_add_meeting_type = AgendaAddMeetingTypeView.as_view()
class MeetingTypeEditView(UpdateView):
template_name = 'chrono/manager_meeting_type_form.html'
model = MeetingType
form_class = MeetingTypeForm
def get_context_data(self, **kwargs):
context = super(MeetingTypeEditView, self).get_context_data(**kwargs)
context['agenda'] = self.object.agenda
return context
def get_success_url(self):
return reverse('chrono-manager-agenda-view', kwargs={'pk': self.object.agenda_id})
meeting_type_edit = MeetingTypeEditView.as_view()
class AgendaAddTimePeriodView(CreateView):
template_name = 'chrono/manager_time_period_form.html'
model = Event
form_class = TimePeriodForm
def get_success_url(self):
return reverse('chrono-manager-agenda-view', kwargs={'pk': self.kwargs.get('pk')})
def get_initial(self):
initial = super(AgendaAddTimePeriodView, self).get_initial()
initial['agenda'] = self.kwargs.get('pk')
return initial
def get_context_data(self, **kwargs):
context = super(AgendaAddTimePeriodView, self).get_context_data(**kwargs)
context['agenda'] = Agenda.objects.get(id=self.kwargs.get('pk'))
return context
agenda_add_time_period = AgendaAddTimePeriodView.as_view()
class TimePeriodEditView(UpdateView):
template_name = 'chrono/manager_time_period_form.html'
model = TimePeriod
form_class = TimePeriodForm
def get_context_data(self, **kwargs):
context = super(TimePeriodEditView, self).get_context_data(**kwargs)
context['agenda'] = self.object.agenda
return context
def get_success_url(self):
return reverse('chrono-manager-agenda-view', kwargs={'pk': self.object.agenda_id})
time_period_edit = TimePeriodEditView.as_view()
def menu_json(request):
response = HttpResponse(content_type='application/json')
label = _('Agendas')

View File

@ -4,9 +4,9 @@ from webtest import TestApp
from django.contrib.auth import get_user_model
from django.test import override_settings
from django.utils.timezone import now
from django.utils.timezone import now, make_aware
from chrono.agendas.models import Agenda, Event, Booking
from chrono.agendas.models import Agenda, Event, Booking, MeetingType, TimePeriod
pytestmark = pytest.mark.django_db
@ -45,6 +45,19 @@ def some_data():
places=10, agenda=agenda)
event.save()
@pytest.fixture
def meetings_agenda():
agenda = Agenda(label=u'Foo bar', kind='meetings')
agenda.save()
meeting_type = MeetingType(agenda=agenda, label='Blah', duration=30)
meeting_type.save()
time_period = TimePeriod(agenda=agenda, weekday=0,
start_time=datetime.time(10, 0), end_time=datetime.time(12, 0))
time_period.save()
time_period = TimePeriod(agenda=agenda, weekday=1,
start_time=datetime.time(10, 0), end_time=datetime.time(17, 0))
time_period.save()
return agenda
def test_agendas_api(app, some_data):
agenda_id = Agenda.objects.filter(label=u'Foo bar')[0].id
@ -63,6 +76,12 @@ def test_datetimes_api(app, some_data):
assert 'data' in resp.json
assert len(resp.json['data']) == 3
def test_datetimes_api_wrong_kind(app, some_data):
agenda = Agenda.objects.filter(label=u'Foo bar')[0]
agenda.kind = 'meetings'
agenda.save()
resp = app.get('/api/agenda/%s/datetimes/' % agenda.id, status=500)
def test_datetime_api_fr(app, some_data):
agenda_id = Agenda.objects.filter(label=u'Foo bar')[0].id
with override_settings(LANGUAGE_CODE='fr-fr'):
@ -79,6 +98,24 @@ def test_datetime_api_label(app, some_data):
resp = app.get('/api/agenda/%s/datetimes/' % agenda_id)
assert 'Hello world' in [x['text'] for x in resp.json['data']]
def test_datetimes_api_meetings_agenda(app, meetings_agenda):
meeting_type = MeetingType.objects.get(agenda=meetings_agenda)
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
assert len(resp.json['data']) == 162
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))
ev.save()
booking = Booking(event=ev)
booking.save()
resp2 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
assert len(resp2.json['data']) == 161
assert resp.json['data'][0] == resp2.json['data'][0]
assert resp.json['data'][1] == resp2.json['data'][1]
assert resp.json['data'][3] == resp2.json['data'][2]
def test_booking_api(app, some_data, user):
agenda_id = Agenda.objects.filter(label=u'Foo bar')[0].id
event = Event.objects.filter(agenda_id=agenda_id)[0]
@ -91,6 +128,24 @@ def test_booking_api(app, some_data, user):
Booking.objects.get(id=resp.json['booking_id'])
assert Booking.objects.count() == 1
def test_booking_api_meeting(app, meetings_agenda, user):
agenda_id = meetings_agenda.id
meeting_type = MeetingType.objects.get(agenda=meetings_agenda)
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
event_id = resp.json['data'][2]['id']
app.authorization = ('Basic', ('john.doe', 'password'))
app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id))
assert Booking.objects.count() == 1
resp2 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
assert len(resp.json['data']) == len(resp2.json['data']) + 1
# try booking the same timeslot
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id))
assert resp.json['err'] == 1
assert resp.json['reason'] == 'sold out'
def test_booking_api_with_data(app, some_data, user):
agenda_id = Agenda.objects.filter(label=u'Foo bar')[0].id
event = Event.objects.filter(agenda_id=agenda_id)[0]
@ -132,6 +187,30 @@ def test_booking_cancellation_post_api(app, some_data, user):
resp = app.post('/api/booking/%s/cancel/' % booking_id, status=200)
assert resp.json['err'] == 1
def test_booking_cancellation_post_meeting_api(app, meetings_agenda, user):
agenda_id = Agenda.objects.filter(label=u'Foo bar')[0].id
meeting_type = MeetingType.objects.get(agenda=meetings_agenda)
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
nb_events = len(resp.json['data'])
event_id = resp.json['data'][2]['id']
app.authorization = ('Basic', ('john.doe', 'password'))
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id))
assert Booking.objects.count() == 1
booking_id = resp.json['booking_id']
assert Booking.objects.count() == 1
resp = app.post('/api/booking/%s/cancel/' % booking_id)
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
assert len(resp.json['data']) == nb_events
# book the same time slot
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id))
assert resp.json['err'] == 0
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
assert len(resp.json['data']) == nb_events - 1
def test_soldout(app, some_data, user):
agenda_id = Agenda.objects.filter(label=u'Foo bar')[0].id
event = Event.objects.filter(agenda_id=agenda_id).exclude(start_datetime__lt=now())[0]

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
from django.contrib.auth.models import User
import datetime
import pytest
@ -5,7 +7,7 @@ from webtest import TestApp
from chrono.wsgi import application
from chrono.agendas.models import Agenda, Event, Booking
from chrono.agendas.models import Agenda, Event, Booking, MeetingType, TimePeriod
pytestmark = pytest.mark.django_db
@ -195,3 +197,76 @@ def test_delete_event(app, admin_user):
resp = resp.form.submit()
assert resp.location == 'http://localhost:80/manage/agendas/%s/' % agenda.id
assert Event.objects.count() == 0
def test_add_meetings_agenda(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()
agenda = Agenda.objects.get(label='Foo bar')
assert resp.location == 'http://localhost:80/manage/agendas/%s/' % agenda.id
resp = resp.follow()
assert '<h2>Foo bar</h2>' in resp.body
assert 'Meeting Types' in resp.body
agenda = Agenda.objects.get(label='Foo bar')
assert agenda.kind == 'meetings'
def test_meetings_agenda_add_meeting_type(app, admin_user):
agenda = Agenda(label=u'Foo bar', kind='meetings')
agenda.save()
app = login(app)
resp = app.get('/manage/agendas/%s/' % agenda.id, status=200)
assert "This agenda doesn't have any meeting type yet." in resp.body
resp = resp.click('New Meeting Type')
resp.form['label'] = 'Blah'
resp.form['duration'] = '60'
resp = resp.form.submit()
assert MeetingType.objects.get(agenda=agenda).label == 'Blah'
assert MeetingType.objects.get(agenda=agenda).duration == 60
resp = resp.follow()
assert 'Blah' in resp.body
# and edit
resp = resp.click('Blah')
resp.form['duration'] = '30'
resp = resp.form.submit()
assert MeetingType.objects.get(agenda=agenda).duration == 30
def test_meetings_agenda_add_time_period(app, admin_user):
agenda = Agenda(label=u'Foo bar', kind='meetings')
agenda.save()
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
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
resp = resp.follow()
# add a second time period
resp = resp.click('New Time Period')
resp.form['weekday'].select(text='Monday')
resp.form['start_time'] = '10:00'
resp.form['end_time'] = '13:00'
resp = resp.form.submit()
resp = resp.follow()
assert u'Monday / 10 a.m. → 1 p.m.' in resp.text
assert u'Wednesday / 10 a.m. → 5 p.m.' in resp.text
assert resp.body.index('Monday') < resp.body.index('Wednesday')
# and edit
resp = resp.click(u'Wednesday / 10 a.m. → 5 p.m.')
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

View File

@ -0,0 +1,93 @@
import datetime
import pytest
from django.utils.timezone import make_aware
from chrono.agendas.models import Agenda, TimePeriod, MeetingType
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,
start_time=datetime.time(9, 0),
end_time=datetime.time(12, 0))
events = timeperiod.get_time_slots(
min_datetime=make_aware(datetime.datetime(2016, 9, 1)),
max_datetime=make_aware(datetime.datetime(2016, 10, 1)),
meeting_type=MeetingType(duration=60))
events = list(sorted(events, key=lambda x: x.start_datetime))
assert events[0].start_datetime.timetuple()[:5] == (2016, 9, 5, 9, 0)
assert events[1].start_datetime.timetuple()[:5] == (2016, 9, 5, 10, 0)
assert events[2].start_datetime.timetuple()[:5] == (2016, 9, 5, 11, 0)
assert events[3].start_datetime.timetuple()[:5] == (2016, 9, 12, 9, 0)
assert events[4].start_datetime.timetuple()[:5] == (2016, 9, 12, 10, 0)
assert events[-1].start_datetime.timetuple()[:5] == (2016, 9, 26, 11, 0)
assert len(events) == 12
# another start before the timeperiod
timeperiod = TimePeriod(agenda=agenda, weekday=1,
start_time=datetime.time(9, 0),
end_time=datetime.time(12, 0))
events = timeperiod.get_time_slots(
min_datetime=make_aware(datetime.datetime(2016, 9, 1)),
max_datetime=make_aware(datetime.datetime(2016, 10, 1)),
meeting_type=MeetingType(duration=60))
events = list(sorted(events, key=lambda x: x.start_datetime))
assert events[0].start_datetime.timetuple()[:5] == (2016, 9, 6, 9, 0)
assert events[-1].start_datetime.timetuple()[:5] == (2016, 9, 27, 11, 0)
assert len(events) == 12
# a start on the day of the timeperiod
timeperiod = TimePeriod(agenda=agenda, weekday=3,
start_time=datetime.time(9, 0),
end_time=datetime.time(12, 0))
events = timeperiod.get_time_slots(
min_datetime=make_aware(datetime.datetime(2016, 9, 1)),
max_datetime=make_aware(datetime.datetime(2016, 10, 1)),
meeting_type=MeetingType(duration=60))
events = list(sorted(events, key=lambda x: x.start_datetime))
assert events[0].start_datetime.timetuple()[:5] == (2016, 9, 1, 9, 0)
assert events[-1].start_datetime.timetuple()[:5] == (2016, 9, 29, 11, 0)
assert len(events) == 15
# a start after the day of the timeperiod
timeperiod = TimePeriod(agenda=agenda, weekday=4,
start_time=datetime.time(9, 0),
end_time=datetime.time(12, 0))
events = timeperiod.get_time_slots(
min_datetime=make_aware(datetime.datetime(2016, 9, 1)),
max_datetime=make_aware(datetime.datetime(2016, 10, 1)),
meeting_type=MeetingType(duration=60))
events = list(sorted(events, key=lambda x: x.start_datetime))
assert events[0].start_datetime.timetuple()[:5] == (2016, 9, 2, 9, 0)
assert events[-1].start_datetime.timetuple()[:5] == (2016, 9, 30, 11, 0)
assert len(events) == 15
# another start after the day of the timeperiod
timeperiod = TimePeriod(agenda=agenda, weekday=5,
start_time=datetime.time(9, 0),
end_time=datetime.time(12, 0))
events = timeperiod.get_time_slots(
min_datetime=make_aware(datetime.datetime(2016, 9, 1)),
max_datetime=make_aware(datetime.datetime(2016, 10, 1)),
meeting_type=MeetingType(duration=60))
events = list(sorted(events, key=lambda x: x.start_datetime))
assert events[0].start_datetime.timetuple()[:5] == (2016, 9, 3, 9, 0)
assert events[-1].start_datetime.timetuple()[:5] == (2016, 9, 24, 11, 0)
assert len(events) == 12
# shorter duration -> double the events
timeperiod = TimePeriod(agenda=agenda, weekday=5,
start_time=datetime.time(9, 0),
end_time=datetime.time(12, 0))
events = timeperiod.get_time_slots(
min_datetime=make_aware(datetime.datetime(2016, 9, 1)),
max_datetime=make_aware(datetime.datetime(2016, 10, 1)),
meeting_type=MeetingType(duration=30))
events = list(sorted(events, key=lambda x: x.start_datetime))
assert events[0].start_datetime.timetuple()[:5] == (2016, 9, 3, 9, 0)
assert events[-1].start_datetime.timetuple()[:5] == (2016, 9, 24, 11, 30)
assert len(events) == 24