add support for "meetings" agendas (#13139)
This commit is contained in:
parent
2efadcab9b
commit
b26f51e36c
|
@ -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),
|
||||
),
|
||||
]
|
|
@ -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})
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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 = []
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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),
|
||||
)
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue