chrono/chrono/agendas/models.py

375 lines
14 KiB
Python

# chrono - agendas system
# Copyright (C) 2016 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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.contrib.auth.models import Group
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, make_aware
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=150)
slug = models.SlugField(_('Slug'))
kind = models.CharField(_('Kind'), max_length=20, choices=AGENDA_KINDS, default='events')
minimal_booking_delay = models.PositiveIntegerField(
_('Minimal booking delay (in days)'), default=1)
maximal_booking_delay = models.PositiveIntegerField(
_('Maximal booking delay (in days)'), default=56) # eight weeks
edit_role = models.ForeignKey(Group, blank=True, null=True, default=None,
related_name='+', verbose_name=_('Edit Role'))
view_role = models.ForeignKey(Group, blank=True, null=True, default=None,
related_name='+', verbose_name=_('View Role'))
class Meta:
ordering = ['label']
def save(self, *args, **kwargs):
if not self.slug:
base_slug = slugify(self.label)
slug = base_slug
i = 1
while True:
try:
Agenda.objects.get(slug=slug)
except self.DoesNotExist:
break
slug = '%s-%s' % (base_slug, i)
i += 1
self.slug = slug
super(Agenda, self).save(*args, **kwargs)
def get_absolute_url(self):
return reverse('chrono-manager-agenda-view', kwargs={'pk': self.id})
def can_be_managed(self, user):
if user.is_staff:
return True
group_ids = [x.id for x in user.groups.all()]
return bool(self.edit_role_id in group_ids)
def can_be_viewed(self, user):
if self.can_be_managed(user):
return True
group_ids = [x.id for x in user.groups.all()]
return bool(self.view_role_id in group_ids)
def export_json(self):
agenda = {
'label': self.label,
'slug': self.slug,
'kind': self.kind,
'minimal_booking_delay': self.minimal_booking_delay,
'maximal_booking_delay': self.maximal_booking_delay,
'permissions': {
'view': self.view_role.name if self.view_role else None,
'edit': self.edit_role.name if self.edit_role else None,
}
}
if self.kind == 'events':
agenda['events'] = [x.export_json() for x in self.event_set.all()]
elif self.kind == 'meetings':
agenda['meetingtypes'] = [x.export_json() for x in self.meetingtype_set.all()]
agenda['desks'] = [desk.export_json() for desk in self.desk_set.all()]
return agenda
@classmethod
def import_json(cls, data, overwrite=False):
data = data.copy()
permissions = data.pop('permissions')
if data['kind'] == 'events':
events = data.pop('events')
elif data['kind'] == 'meetings':
meetingtypes = data.pop('meetingtypes')
desks = data.pop('desks')
agenda, created = cls.objects.get_or_create(slug=data['slug'], defaults=data)
if data['kind'] == 'events':
if overwrite:
Event.objects.filter(agenda=agenda).delete()
for event_data in events:
event_data['agenda'] = agenda
Event.import_json(event_data).save()
elif data['kind'] == 'meetings':
if overwrite:
MeetingType.objects.filter(agenda=agenda).delete()
Desk.objects.filter(agenda=agenda).delete()
for type_data in meetingtypes:
type_data['agenda'] = agenda
MeetingType.import_json(type_data).save()
for desk in desks:
desk['agenda'] = agenda
Desk.import_json(desk).save()
WEEKDAYS_LIST = sorted(WEEKDAYS.items(), key=lambda x: x[0])
class TimeSlot(object):
def __init__(self, start_datetime, meeting_type, desk):
self.start_datetime = start_datetime
self.end_datetime = start_datetime + datetime.timedelta(minutes=meeting_type.duration)
self.meeting_type = meeting_type
self.id = '%s:%s' % (self.meeting_type.id, start_datetime.strftime('%Y-%m-%d-%H%M'))
self.desk = desk
def __unicode__(self):
return date_format(self.start_datetime, format='DATETIME_FORMAT')
class TimePeriod(models.Model):
weekday = models.IntegerField(_('Week day'), choices=WEEKDAYS_LIST)
start_time = models.TimeField(_('Start'))
end_time = models.TimeField(_('End'))
desk = models.ForeignKey('Desk', on_delete=models.CASCADE)
class Meta:
ordering = ['weekday', 'start_time']
@property
def weekday_str(self):
return WEEKDAYS[self.weekday]
@classmethod
def import_json(cls, data):
return cls(**data)
def export_json(self):
return {
'weekday': self.weekday,
'start_time': self.start_time.strftime('%H:%M'),
'end_time': self.end_time.strftime('%H:%M'),
}
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, desk=self.desk)
event_datetime = end_time
class MeetingType(models.Model):
agenda = models.ForeignKey(Agenda)
label = models.CharField(_('Label'), max_length=150)
slug = models.SlugField(_('Slug'))
duration = models.IntegerField(_('Duration (in minutes)'), default=30)
class Meta:
ordering = ['label']
def save(self, *args, **kwargs):
if not self.slug:
base_slug = slugify(self.label)
slug = base_slug
i = 1
while True:
try:
MeetingType.objects.get(slug=slug, agenda=self.agenda)
except self.DoesNotExist:
break
slug = '%s-%s' % (base_slug, i)
i += 1
self.slug = slug
super(MeetingType, self).save(*args, **kwargs)
@classmethod
def import_json(cls, data):
return cls(**data)
def export_json(self):
return {
'label': self.label,
'slug': self.slug,
'duration': self.duration,
}
class Event(models.Model):
agenda = models.ForeignKey(Agenda)
start_datetime = models.DateTimeField(_('Date/time'))
places = models.PositiveIntegerField(_('Places'))
waiting_list_places = models.PositiveIntegerField(
_('Places in waiting list'), default=0)
label = models.CharField(_('Label'), max_length=150, null=True, blank=True,
help_text=_('Optional label to identify this date.'))
full = models.BooleanField(default=False)
meeting_type = models.ForeignKey(MeetingType, null=True)
desk = models.ForeignKey('Desk', null=True)
class Meta:
ordering = ['agenda', 'start_datetime', 'label']
def __unicode__(self):
if self.label:
return self.label
return date_format(localtime(self.start_datetime), format='DATETIME_FORMAT')
def save(self, *args, **kwargs):
self.check_full()
return super(Event, self).save(*args, **kwargs)
def check_full(self):
self.full = bool(
(self.booked_places >= self.places and self.waiting_list_places == 0) or
(self.waiting_list_places and self.waiting_list >= self.waiting_list_places))
def in_bookable_period(self):
if localtime(now()).date() > localtime(self.start_datetime - datetime.timedelta(days=self.agenda.minimal_booking_delay)).date():
return False
if self.agenda.maximal_booking_delay and (
localtime(now()).date() <= localtime(self.start_datetime - datetime.timedelta(days=self.agenda.maximal_booking_delay)).date()):
return False
if self.start_datetime < now():
# past the event date, we may want in the future to allow for some
# extra late booking but it's forbidden for now.
return False
return True
@property
def booked_places(self):
return self.booking_set.filter(cancellation_datetime__isnull=True,
in_waiting_list=False).count()
@property
def waiting_list(self):
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})
@classmethod
def import_json(cls, data):
data['start_datetime'] = make_aware(datetime.datetime.strptime(
data['start_datetime'], '%Y-%m-%d %H:%M:%S'))
return cls(**data)
def export_json(self):
return {
'start_datetime': self.start_datetime.strftime('%Y-%m-%d %H:%M:%S'),
'places': self.places,
'waiting_list_places': self.waiting_list_places,
'label': self.label
}
class Booking(models.Model):
event = models.ForeignKey(Event)
extra_data = JSONField(null=True)
cancellation_datetime = models.DateTimeField(null=True)
in_waiting_list = models.BooleanField(default=False)
creation_datetime = models.DateTimeField(auto_now_add=True)
# primary booking is used to group multiple bookings together
primary_booking = models.ForeignKey('self', null=True,
on_delete=models.CASCADE, related_name='secondary_booking_set')
def save(self, *args, **kwargs):
with transaction.atomic():
super(Booking, self).save(*args, **kwargs)
initial_value = self.event.full
self.event.check_full()
if self.event.full != initial_value:
self.event.save()
def cancel(self):
timestamp = now()
with transaction.atomic():
self.secondary_booking_set.update(cancellation_datetime=timestamp)
self.cancellation_datetime = timestamp
self.save()
def accept(self):
self.in_waiting_list = False
with transaction.atomic():
self.secondary_booking_set.update(in_waiting_list=False)
self.save()
class Desk(models.Model):
agenda = models.ForeignKey(Agenda)
label = models.CharField(_('Label'), max_length=150)
slug = models.SlugField(_('Slug'), max_length=150)
def __unicode__(self):
return self.label
class Meta:
ordering = ['label']
def save(self, *args, **kwargs):
if not self.slug:
base_slug = slugify(self.label)
slug = base_slug
i = 1
while True:
try:
Desk.objects.get(slug=slug, agenda=self.agenda)
except self.DoesNotExist:
break
slug = '%s-%s' % (base_slug, i)
i += 1
self.slug = slug
super(Desk, self).save(*args, **kwargs)
@classmethod
def import_json(cls, data):
timeperiods = data.pop('timeperiods')
instance, created = cls.objects.get_or_create(**data)
for timeperiod in timeperiods:
timeperiod['desk'] = instance
TimePeriod.import_json(timeperiod).save()
return instance
def export_json(self):
return {'label': self.label,
'slug': self.slug,
'timeperiods': [time_period.export_json() for time_period in self.timeperiod_set.all()]
}