general: reformat using black (#37464)

black--target-version py35 --skip-string-normalization --line-length 110 .
This commit is contained in:
Frédéric Péters 2019-12-16 16:21:24 +01:00
parent 30cb3f672f
commit 30bbc8c90f
54 changed files with 1793 additions and 1103 deletions

View File

@ -6,20 +6,20 @@ from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
]
dependencies = []
operations = [
migrations.CreateModel(
name='Agenda',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('label', models.CharField(max_length=50, verbose_name='Label')),
('slug', models.SlugField(verbose_name='Identifier')),
],
options={
'ordering': ['label'],
},
options={'ordering': ['label'],},
bases=(models.Model,),
),
]

View File

@ -14,14 +14,15 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Event',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('start_datetime', models.DateTimeField(verbose_name='Date/time')),
('places', models.PositiveIntegerField(verbose_name='Places')),
('agenda', models.ForeignKey(to='agendas.Agenda', on_delete=models.CASCADE)),
],
options={
'ordering': ['agenda', 'start_datetime'],
},
options={'ordering': ['agenda', 'start_datetime'],},
bases=(models.Model,),
),
]

View File

@ -15,12 +15,14 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Booking',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('extra_data', jsonfield.fields.JSONField(null=True)),
('event', models.ForeignKey(to='agendas.Event', on_delete=models.CASCADE)),
],
options={
},
options={},
bases=(models.Model,),
),
]

View File

@ -14,6 +14,12 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='event',
name='label',
field=models.CharField(help_text='Optional label to identify this date.', max_length=50, null=True, blank=True, verbose_name='Label'),
field=models.CharField(
help_text='Optional label to identify this date.',
max_length=50,
null=True,
blank=True,
verbose_name='Label',
),
),
]

View File

@ -16,19 +16,15 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='booking',
name='creation_datetime',
field=models.DateTimeField(default=datetime.datetime(2016, 7, 7, 13, 57, 47, 975893, tzinfo=utc), auto_now_add=True),
field=models.DateTimeField(
default=datetime.datetime(2016, 7, 7, 13, 57, 47, 975893, tzinfo=utc), auto_now_add=True
),
preserve_default=False,
),
migrations.AddField(
model_name='booking',
name='in_waiting_list',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='event',
name='full',
field=models.BooleanField(default=False),
model_name='booking', name='in_waiting_list', field=models.BooleanField(default=False),
),
migrations.AddField(model_name='event', name='full', field=models.BooleanField(default=False),),
migrations.AddField(
model_name='event',
name='waiting_list_places',

View File

@ -12,8 +12,6 @@ class Migration(migrations.Migration):
operations = [
migrations.AlterField(
model_name='agenda',
name='label',
field=models.CharField(max_length=100, verbose_name='Label'),
model_name='agenda', name='label', field=models.CharField(max_length=100, verbose_name='Label'),
),
]

View File

@ -14,30 +14,51 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='MeetingType',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'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'],
},
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')])),
(
'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'],
},
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')]),
field=models.CharField(
default=b'events',
max_length=20,
verbose_name='Kind',
choices=[(b'events', 'Events'), (b'meetings', 'Meetings')],
),
),
migrations.AddField(
model_name='timeperiod',

View File

@ -13,17 +13,32 @@ class Migration(migrations.Migration):
operations = [
migrations.AlterModelOptions(
name='event',
options={'ordering': ['agenda', 'start_datetime', 'label']},
name='event', options={'ordering': ['agenda', 'start_datetime', 'label']},
),
migrations.AddField(
model_name='agenda',
name='edit_role',
field=models.ForeignKey(related_name='+', default=None, verbose_name='Edit Role', to='auth.Group', blank=True, null=True, on_delete=models.CASCADE),
field=models.ForeignKey(
related_name='+',
default=None,
verbose_name='Edit Role',
to='auth.Group',
blank=True,
null=True,
on_delete=models.CASCADE,
),
),
migrations.AddField(
model_name='agenda',
name='view_role',
field=models.ForeignKey(related_name='+', default=None, verbose_name='View Role', to='auth.Group', blank=True, null=True, on_delete=models.CASCADE),
field=models.ForeignKey(
related_name='+',
default=None,
verbose_name='View Role',
to='auth.Group',
blank=True,
null=True,
on_delete=models.CASCADE,
),
),
]

View File

@ -12,8 +12,6 @@ class Migration(migrations.Migration):
operations = [
migrations.AlterField(
model_name='meetingtype',
name='slug',
field=models.SlugField(verbose_name='Identifier'),
model_name='meetingtype', name='slug', field=models.SlugField(verbose_name='Identifier'),
),
]

View File

@ -14,6 +14,11 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='booking',
name='primary_booking',
field=models.ForeignKey(related_name='secondary_booking_set', to='agendas.Booking', null=True, on_delete=models.CASCADE),
field=models.ForeignKey(
related_name='secondary_booking_set',
to='agendas.Booking',
null=True,
on_delete=models.CASCADE,
),
),
]

View File

@ -12,14 +12,18 @@ class Migration(migrations.Migration):
operations = [
migrations.AlterField(
model_name='agenda',
name='label',
field=models.CharField(max_length=150, verbose_name='Label'),
model_name='agenda', name='label', field=models.CharField(max_length=150, verbose_name='Label'),
),
migrations.AlterField(
model_name='event',
name='label',
field=models.CharField(help_text='Optional label to identify this date.', max_length=150, null=True, verbose_name='Label', blank=True),
field=models.CharField(
help_text='Optional label to identify this date.',
max_length=150,
null=True,
verbose_name='Label',
blank=True,
),
),
migrations.AlterField(
model_name='meetingtype',

View File

@ -14,13 +14,14 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Desk',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('label', models.CharField(max_length=150, verbose_name='Label')),
('slug', models.SlugField(max_length=150, verbose_name='Identifier')),
('agenda', models.ForeignKey(to='agendas.Agenda', on_delete=models.CASCADE)),
],
options={
'ordering': ['label'],
},
options={'ordering': ['label'],},
),
]

View File

@ -9,7 +9,8 @@ def set_timeperiod_desk(apps, schema_editor):
Desk = apps.get_model('agendas', 'Desk')
for time_period in TimePeriod.objects.all():
desk, created = Desk.objects.get_or_create(
label='Guichet 1', slug='guichet-1', agenda=time_period.agenda)
label='Guichet 1', slug='guichet-1', agenda=time_period.agenda
)
time_period.desk = desk
time_period.save()
@ -36,8 +37,5 @@ class Migration(migrations.Migration):
name='desk',
field=models.ForeignKey(to='agendas.Desk', on_delete=models.CASCADE),
),
migrations.RemoveField(
model_name='timeperiod',
name='agenda',
),
migrations.RemoveField(model_name='timeperiod', name='agenda',),
]

View File

@ -11,8 +11,7 @@ def set_event_desk(apps, schema_editor):
if not event.agenda.kind == 'meetings':
continue
desk, created = Desk.objects.get_or_create(
label='Guichet 1', slug='guichet-1', agenda=event.agenda)
desk, created = Desk.objects.get_or_create(label='Guichet 1', slug='guichet-1', agenda=event.agenda)
event.desk = desk
event.save()
@ -33,5 +32,5 @@ class Migration(migrations.Migration):
name='desk',
field=models.ForeignKey(to='agendas.Desk', null=True, on_delete=models.CASCADE),
),
migrations.RunPython(set_event_desk, unset_event_desk)
migrations.RunPython(set_event_desk, unset_event_desk),
]

View File

@ -14,14 +14,18 @@ class Migration(migrations.Migration):
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)),
(
'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', on_delete=models.CASCADE)),
],
options={
'ordering': ['start_datetime'],
},
options={'ordering': ['start_datetime'],},
),
]

View File

@ -26,7 +26,9 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='timeperiodexception',
name='update_datetime',
field=models.DateTimeField(default=datetime.datetime(2017, 11, 2, 10, 21, 1, 826837, tzinfo=utc), auto_now=True),
field=models.DateTimeField(
default=datetime.datetime(2017, 11, 2, 10, 21, 1, 826837, tzinfo=utc), auto_now=True
),
preserve_default=False,
),
]

View File

@ -11,19 +11,11 @@ class Migration(migrations.Migration):
]
operations = [
migrations.AddField(model_name='booking', name='backoffice_url', field=models.URLField(blank=True),),
migrations.AddField(
model_name='booking',
name='backoffice_url',
field=models.URLField(blank=True),
model_name='booking', name='label', field=models.CharField(max_length=250, blank=True),
),
migrations.AddField(
model_name='booking',
name='label',
field=models.CharField(max_length=250, blank=True),
),
migrations.AddField(
model_name='booking',
name='user_name',
field=models.CharField(max_length=250, blank=True),
model_name='booking', name='user_name', field=models.CharField(max_length=250, blank=True),
),
]

View File

@ -17,9 +17,7 @@ class Migration(migrations.Migration):
field=models.SlugField(max_length=160, verbose_name='Identifier'),
),
migrations.AlterField(
model_name='desk',
name='slug',
field=models.SlugField(max_length=160, verbose_name='Identifier'),
model_name='desk', name='slug', field=models.SlugField(max_length=160, verbose_name='Identifier'),
),
migrations.AlterField(
model_name='meetingtype',

View File

@ -14,6 +14,8 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='desk',
name='timeperiod_exceptions_remote_url',
field=models.URLField(max_length=500, verbose_name='URL to fetch time period exceptions from', blank=True),
field=models.URLField(
max_length=500, verbose_name='URL to fetch time period exceptions from', blank=True
),
),
]

View File

@ -12,10 +12,7 @@ class Migration(migrations.Migration):
]
operations = [
migrations.AlterModelOptions(
name='meetingtype',
options={'ordering': ['duration', 'label']},
),
migrations.AlterModelOptions(name='meetingtype', options={'ordering': ['duration', 'label']},),
migrations.AddField(
model_name='timeperiodexception',
name='recurrence_id',

View File

@ -16,11 +16,27 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='agenda',
name='edit_role',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='auth.Group', verbose_name='Edit Role'),
field=models.ForeignKey(
blank=True,
default=None,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='+',
to='auth.Group',
verbose_name='Edit Role',
),
),
migrations.AlterField(
model_name='agenda',
name='view_role',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='auth.Group', verbose_name='View Role'),
field=models.ForeignKey(
blank=True,
default=None,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='+',
to='auth.Group',
verbose_name='View Role',
),
),
]

View File

@ -15,6 +15,8 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='event',
name='description',
field=models.TextField(blank=True, help_text='Optional event description.', null=True, verbose_name='Description'),
field=models.TextField(
blank=True, help_text='Optional event description.', null=True, verbose_name='Description'
),
),
]

View File

@ -15,7 +15,9 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='event',
name='slug',
field=models.SlugField(default=None, null=True, blank=True, max_length=160, verbose_name='Identifier'),
field=models.SlugField(
default=None, null=True, blank=True, max_length=160, verbose_name='Identifier'
),
preserve_default=False,
),
]

View File

@ -15,6 +15,8 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='event',
name='slug',
field=models.SlugField(default=None, null=True, blank=True, max_length=160, verbose_name='Identifier'),
field=models.SlugField(
default=None, null=True, blank=True, max_length=160, verbose_name='Identifier'
),
),
]

View File

@ -39,7 +39,11 @@ def set_slug_on_desks(apps, schema_editor):
def set_slug_on_meetingtypes(apps, schema_editor):
MeetingType = apps.get_model('agendas', 'MeetingType')
for meetingtype in MeetingType.objects.all().order_by('-pk'):
if not MeetingType.objects.filter(slug=meetingtype.slug, agenda=meetingtype.agenda).exclude(pk=meetingtype.pk).exists():
if (
not MeetingType.objects.filter(slug=meetingtype.slug, agenda=meetingtype.agenda)
.exclude(pk=meetingtype.pk)
.exists()
):
continue
meetingtype.slug = generate_slug(meetingtype, agenda=meetingtype.agenda)
meetingtype.save(update_fields=['slug'])

View File

@ -17,12 +17,6 @@ class Migration(migrations.Migration):
name='slug',
field=models.SlugField(max_length=160, unique=True, verbose_name='Identifier'),
),
migrations.AlterUniqueTogether(
name='desk',
unique_together=set([('agenda', 'slug')]),
),
migrations.AlterUniqueTogether(
name='meetingtype',
unique_together=set([('agenda', 'slug')]),
),
migrations.AlterUniqueTogether(name='desk', unique_together=set([('agenda', 'slug')]),),
migrations.AlterUniqueTogether(name='meetingtype', unique_together=set([('agenda', 'slug')]),),
]

View File

@ -15,10 +15,9 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='event',
name='slug',
field=models.SlugField(default=None, null=True, blank=True, max_length=160, verbose_name='Identifier')
),
migrations.AlterUniqueTogether(
name='event',
unique_together=set([('agenda', 'slug')]),
field=models.SlugField(
default=None, null=True, blank=True, max_length=160, verbose_name='Identifier'
),
),
migrations.AlterUniqueTogether(name='event', unique_together=set([('agenda', 'slug')]),),
]

View File

@ -71,16 +71,28 @@ class Agenda(models.Model):
label = models.CharField(_('Label'), max_length=150)
slug = models.SlugField(_('Identifier'), max_length=160, unique=True)
kind = models.CharField(_('Kind'), max_length=20, choices=AGENDA_KINDS, default='events')
minimal_booking_delay = models.PositiveIntegerField(
_('Minimal booking delay (in days)'), default=1)
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'),
on_delete=models.SET_NULL)
view_role = models.ForeignKey(Group, blank=True, null=True, default=None,
related_name='+', verbose_name=_('View Role'),
on_delete=models.SET_NULL)
_('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'),
on_delete=models.SET_NULL,
)
view_role = models.ForeignKey(
Group,
blank=True,
null=True,
default=None,
related_name='+',
verbose_name=_('View Role'),
on_delete=models.SET_NULL,
)
class Meta:
ordering = ['label']
@ -124,7 +136,7 @@ class Agenda(models.Model):
'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()]
@ -170,6 +182,7 @@ class Agenda(models.Model):
Desk.import_json(desk).save()
return created
WEEKDAYS_LIST = sorted(WEEKDAYS.items(), key=lambda x: x[0])
@ -198,9 +211,10 @@ class TimePeriod(models.Model):
def __str__(self):
return u'%s / %s%s' % (
force_text(WEEKDAYS[self.weekday]),
date_format(self.start_time, 'TIME_FORMAT'),
date_format(self.end_time, 'TIME_FORMAT'))
force_text(WEEKDAYS[self.weekday]),
date_format(self.start_time, 'TIME_FORMAT'),
date_format(self.end_time, 'TIME_FORMAT'),
)
@property
def weekday_str(self):
@ -212,9 +226,9 @@ class TimePeriod(models.Model):
def export_json(self):
return {
'weekday': self.weekday,
'start_time': self.start_time.strftime('%H:%M'),
'end_time': self.end_time.strftime('%H:%M'),
'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):
@ -223,19 +237,21 @@ class TimePeriod(models.Model):
min_datetime = make_naive(min_datetime)
max_datetime = make_naive(max_datetime)
real_min_datetime = min_datetime + datetime.timedelta(
days=self.weekday - min_datetime.weekday())
real_min_datetime = 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)
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 + meeting_duration
next_time = event_datetime + duration
if end_time.time() > self.end_time or event_datetime.date() != next_time.date():
# back to morning
event_datetime = event_datetime.replace(hour=self.start_time.hour, minute=self.start_time.minute)
event_datetime = event_datetime.replace(
hour=self.start_time.hour, minute=self.start_time.minute
)
# but next week
event_datetime += datetime.timedelta(days=7)
next_time = event_datetime + duration
@ -243,7 +259,9 @@ class TimePeriod(models.Model):
if event_datetime > max_datetime:
break
yield TimeSlot(start_datetime=make_aware(event_datetime), meeting_type=meeting_type, desk=self.desk)
yield TimeSlot(
start_datetime=make_aware(event_datetime), meeting_type=meeting_type, desk=self.desk
)
event_datetime = next_time
@ -265,7 +283,8 @@ class MeetingType(models.Model):
@classmethod
def import_json(cls, data):
meeting_type, created = cls.objects.get_or_create(
slug=data['slug'], agenda=data['agenda'], defaults=data)
slug=data['slug'], agenda=data['agenda'], defaults=data
)
if not created:
for k, v in data.items():
setattr(meeting_type, k, v)
@ -273,9 +292,9 @@ class MeetingType(models.Model):
def export_json(self):
return {
'label': self.label,
'slug': self.slug,
'duration': self.duration,
'label': self.label,
'slug': self.slug,
'duration': self.duration,
}
@ -284,13 +303,18 @@ class Event(models.Model):
agenda = models.ForeignKey(Agenda, on_delete=models.CASCADE)
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.'))
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.'),
)
slug = models.SlugField(_('Identifier'), max_length=160, null=True, blank=True, default=None)
description = models.TextField(_('Description'), null=True, blank=True,
help_text=_('Optional event description.'))
description = models.TextField(
_('Description'), null=True, blank=True, help_text=_('Optional event description.')
)
full = models.BooleanField(default=False)
meeting_type = models.ForeignKey(MeetingType, null=True, on_delete=models.CASCADE)
desk = models.ForeignKey('Desk', null=True, on_delete=models.CASCADE)
@ -310,14 +334,24 @@ class Event(models.Model):
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))
(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():
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()):
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
@ -327,13 +361,11 @@ class Event(models.Model):
@property
def booked_places(self):
return self.booking_set.filter(cancellation_datetime__isnull=True,
in_waiting_list=False).count()
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()
return self.booking_set.filter(cancellation_datetime__isnull=True, in_waiting_list=True).count()
@property
def end_datetime(self):
@ -344,8 +376,9 @@ class Event(models.Model):
@classmethod
def import_json(cls, data):
data['start_datetime'] = make_aware(datetime.datetime.strptime(
data['start_datetime'], '%Y-%m-%d %H:%M:%S'))
data['start_datetime'] = make_aware(
datetime.datetime.strptime(data['start_datetime'], '%Y-%m-%d %H:%M:%S')
)
if data.get('slug'):
event, created = cls.objects.get_or_create(slug=data['slug'], defaults=data)
if not created:
@ -372,11 +405,14 @@ class Booking(models.Model):
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')
primary_booking = models.ForeignKey(
'self', null=True, on_delete=models.CASCADE, related_name='secondary_booking_set'
)
label = models.CharField(max_length=250, blank=True)
user_display_label = models.CharField(verbose_name=_('Label displayed to user'), max_length=250, blank=True)
user_display_label = models.CharField(
verbose_name=_('Label displayed to user'), max_length=250, blank=True
)
user_name = models.CharField(max_length=250, blank=True)
backoffice_url = models.URLField(blank=True)
@ -405,14 +441,20 @@ class Booking(models.Model):
ics = vobject.iCalendar()
ics.add('prodid').value = '-//Entr\'ouvert//NON SGML Publik'
vevent = vobject.newFromBehavior('vevent')
vevent.add('uid').value = '%s-%s-%s' % (self.event.start_datetime.isoformat(), self.event.agenda.pk, self.pk)
vevent.add('uid').value = '%s-%s-%s' % (
self.event.start_datetime.isoformat(),
self.event.agenda.pk,
self.pk,
)
vevent.add('summary').value = self.user_display_label or self.label
vevent.add('dtstart').value = self.event.start_datetime
if self.user_name:
vevent.add('attendee').value = self.user_name
if self.event.meeting_type:
vevent.add('dtend').value = self.event.start_datetime + datetime.timedelta(minutes=self.event.meeting_type.duration)
vevent.add('dtend').value = self.event.start_datetime + datetime.timedelta(
minutes=self.event.meeting_type.duration
)
for field in ('description', 'location', 'comment', 'url'):
field_value = request and request.GET.get(field) or self.extra_data.get(field)
@ -422,15 +464,14 @@ class Booking(models.Model):
return ics.serialize()
@python_2_unicode_compatible
class Desk(models.Model):
agenda = models.ForeignKey(Agenda, on_delete=models.CASCADE)
label = models.CharField(_('Label'), max_length=150)
slug = models.SlugField(_('Identifier'), max_length=160)
timeperiod_exceptions_remote_url = models.URLField(
_('URL to fetch time period exceptions from'),
blank=True, max_length=500)
_('URL to fetch time period exceptions from'), blank=True, max_length=500
)
def __str__(self):
return self.label
@ -448,8 +489,7 @@ class Desk(models.Model):
def import_json(cls, data):
timeperiods = data.pop('timeperiods')
exceptions = data.pop('exceptions')
instance, created = cls.objects.get_or_create(
slug=data['slug'], agenda=data['agenda'], defaults=data)
instance, created = cls.objects.get_or_create(slug=data['slug'], agenda=data['agenda'], defaults=data)
if not created:
for k, v in data.items():
setattr(instance, k, v)
@ -462,21 +502,24 @@ class Desk(models.Model):
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()],
'exceptions': [exception.export_json() for exception in self.timeperiodexception_set.all()]
}
return {
'label': self.label,
'slug': self.slug,
'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 = make_aware(datetime.datetime.today() + datetime.timedelta(days=14))
exceptions = self.timeperiodexception_set.filter(end_datetime__gte=now()).filter(
Q(end_datetime__lte=in_two_weeks) | Q(start_datetime__lt=now()))
Q(end_datetime__lte=in_two_weeks) | Q(start_datetime__lt=now())
)
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()
next_exception = (
self.timeperiodexception_set.filter(start_datetime__gte=now()).order_by('start_datetime').first()
)
if next_exception:
return [next_exception]
return []
@ -491,12 +534,14 @@ class Desk(models.Model):
response.raise_for_status()
except requests.HTTPError as e:
raise ICSError(
_('Failed to retrieve remote calendar (%(url)s, HTTP error %(status_code)s).') %
{'url': url, 'status_code': e.response.status_code})
_('Failed to retrieve remote calendar (%(url)s, HTTP error %(status_code)s).')
% {'url': url, 'status_code': e.response.status_code}
)
except requests.RequestException as e:
raise ICSError(
_('Failed to retrieve remote calendar (%(url)s, %(exception)s).') %
{'url': url, 'exception': e})
_('Failed to retrieve remote calendar (%(url)s, %(exception)s).')
% {'url': url, 'exception': e}
)
return self.create_timeperiod_exceptions_from_ics(response.text, keep_synced_by_uid=True)
@ -582,7 +627,9 @@ class Desk(models.Model):
if end_dt < update_datetime:
TimePeriodException.objects.filter(**kwargs).update(**event)
else:
obj, created = TimePeriodException.objects.update_or_create(defaults=event, **kwargs)
obj, created = TimePeriodException.objects.update_or_create(
defaults=event, **kwargs
)
if created:
total_created += 1
# delete unseen occurrences
@ -591,8 +638,9 @@ class Desk(models.Model):
if keep_synced_by_uid:
# delete all outdated exceptions from remote calendar
TimePeriodException.objects.filter(update_datetime__lt=update_datetime,
desk=self).exclude(external_id='').delete()
TimePeriodException.objects.filter(update_datetime__lt=update_datetime, desk=self).exclude(
external_id=''
).delete()
return total_created
@ -606,8 +654,8 @@ class Desk(models.Model):
aware_date = make_aware(datetime.datetime(date.year, date.month, date.day))
aware_next_date = aware_date + datetime.timedelta(days=1)
for exception in self.timeperiodexception_set.filter(
start_datetime__lt=aware_next_date,
end_datetime__gt=aware_date):
start_datetime__lt=aware_next_date, end_datetime__gt=aware_date
):
openslots.remove(exception.start_datetime, exception.end_datetime)
return openslots.search(aware_date, aware_next_date)
@ -634,18 +682,21 @@ class TimePeriodException(models.Model):
exc_repr = u'%s' % date_format(localtime(self.start_datetime), 'SHORT_DATE_FORMAT')
else:
exc_repr = u'%s%s' % (
date_format(localtime(self.start_datetime), 'SHORT_DATE_FORMAT'),
date_format(localtime(self.end_datetime), 'SHORT_DATE_FORMAT'))
date_format(localtime(self.start_datetime), 'SHORT_DATE_FORMAT'),
date_format(localtime(self.end_datetime), 'SHORT_DATE_FORMAT'),
)
else:
if localtime(self.start_datetime).date() == localtime(self.end_datetime).date():
# same day
exc_repr = u'%s%s' % (
date_format(localtime(self.start_datetime), 'SHORT_DATETIME_FORMAT'),
date_format(localtime(self.end_datetime), 'TIME_FORMAT'))
date_format(localtime(self.start_datetime), 'SHORT_DATETIME_FORMAT'),
date_format(localtime(self.end_datetime), 'TIME_FORMAT'),
)
else:
exc_repr = u'%s%s' % (
date_format(localtime(self.start_datetime), 'SHORT_DATETIME_FORMAT'),
date_format(localtime(self.end_datetime), 'SHORT_DATETIME_FORMAT'))
date_format(localtime(self.start_datetime), 'SHORT_DATETIME_FORMAT'),
date_format(localtime(self.end_datetime), 'SHORT_DATETIME_FORMAT'),
)
if self.label:
exc_repr = u'%s (%s)' % (self.label, exc_repr)
@ -662,9 +713,8 @@ class TimePeriodException(models.Model):
# incomplete time period, can't tell
return False
for event in Event.objects.filter(
desk=self.desk,
booking__isnull=False,
booking__cancellation_datetime__isnull=True):
desk=self.desk, booking__isnull=False, booking__cancellation_datetime__isnull=True
):
if self.start_datetime <= event.start_datetime < self.end_datetime:
return True
return False

View File

@ -21,29 +21,32 @@ from . import views
urlpatterns = [
url(r'agenda/$', views.agendas),
url(r'agenda/(?P<agenda_identifier>[\w-]+)/$', views.agenda_detail),
url(r'agenda/(?P<agenda_identifier>[\w-]+)/datetimes/$', views.datetimes, name='api-agenda-datetimes'),
url(r'agenda/(?P<agenda_identifier>[\w-]+)/fillslot/(?P<event_identifier>[\w:-]+)/$',
views.fillslot, name='api-fillslot'),
url(r'agenda/(?P<agenda_identifier>[\w-]+)/fillslots/$',
views.fillslots, name='api-agenda-fillslots'),
url(r'agenda/(?P<agenda_identifier>[\w-]+)/status/(?P<event_identifier>[\w-]+)/$', views.slot_status,
name='api-event-status'),
url(r'agenda/meetings/(?P<meeting_identifier>[\w-]+)/datetimes/$',
views.meeting_datetimes, name='api-agenda-meeting-datetimes-legacy'),
url(r'agenda/(?P<agenda_identifier>[\w-]+)/meetings/$',
views.meeting_list, name='api-agenda-meetings'),
url(r'agenda/(?P<agenda_identifier>[\w-]+)/desks/$',
views.agenda_desk_list, name='api-agenda-desks'),
url(r'agenda/(?P<agenda_identifier>[\w-]+)/meetings/(?P<meeting_identifier>[\w-]+)/datetimes/$',
views.meeting_datetimes, name='api-agenda-meeting-datetimes'),
url(
r'agenda/(?P<agenda_identifier>[\w-]+)/fillslot/(?P<event_identifier>[\w:-]+)/$',
views.fillslot,
name='api-fillslot',
),
url(r'agenda/(?P<agenda_identifier>[\w-]+)/fillslots/$', views.fillslots, name='api-agenda-fillslots'),
url(
r'agenda/(?P<agenda_identifier>[\w-]+)/status/(?P<event_identifier>[\w-]+)/$',
views.slot_status,
name='api-event-status',
),
url(
r'agenda/meetings/(?P<meeting_identifier>[\w-]+)/datetimes/$',
views.meeting_datetimes,
name='api-agenda-meeting-datetimes-legacy',
),
url(r'agenda/(?P<agenda_identifier>[\w-]+)/meetings/$', views.meeting_list, name='api-agenda-meetings'),
url(r'agenda/(?P<agenda_identifier>[\w-]+)/desks/$', views.agenda_desk_list, name='api-agenda-desks'),
url(
r'agenda/(?P<agenda_identifier>[\w-]+)/meetings/(?P<meeting_identifier>[\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,
name='api-cancel-booking'),
url(r'booking/(?P<booking_pk>\w+)/accept/$', views.accept_booking,
name='api-accept-booking'),
url(r'booking/(?P<booking_pk>\w+)/ics/$', views.booking_ics,
name='api-booking-ics'),
url(r'booking/(?P<booking_pk>\w+)/cancel/$', views.cancel_booking, name='api-cancel-booking'),
url(r'booking/(?P<booking_pk>\w+)/accept/$', views.accept_booking, name='api-accept-booking'),
url(r'booking/(?P<booking_pk>\w+)/ics/$', views.booking_ics, name='api-booking-ics'),
]

View File

@ -32,8 +32,7 @@ from rest_framework import permissions, serializers, status
from rest_framework.views import APIView
from chrono.api.utils import Response
from ..agendas.models import (Agenda, Event, Booking, MeetingType,
TimePeriod, Desk)
from ..agendas.models import Agenda, Event, Booking, MeetingType, TimePeriod, Desk
from ..interval import Intervals
@ -44,7 +43,9 @@ def format_response_datetime(dt):
def get_exceptions_by_desk(agenda):
exceptions_by_desk = {}
for desk in Desk.objects.filter(agenda=agenda).prefetch_related('timeperiodexception_set'):
exceptions_by_desk[desk.id] = [(exc.start_datetime, exc.end_datetime) for exc in desk.timeperiodexception_set.all()]
exceptions_by_desk[desk.id] = [
(exc.start_datetime, exc.end_datetime) for exc in desk.timeperiodexception_set.all()
]
return exceptions_by_desk
@ -58,14 +59,16 @@ def get_all_slots(agenda, meeting_type):
time_period_filters = {
'min_datetime': min_datetime,
'max_datetime': max_datetime,
'meeting_type': meeting_type
'meeting_type': meeting_type,
}
base_date = now().date()
open_slots_by_desk = defaultdict(lambda: Intervals())
for time_period in TimePeriod.objects.filter(desk__agenda=agenda):
duration = (datetime.datetime.combine(base_date, time_period.end_time) -
datetime.datetime.combine(base_date, time_period.start_time)).seconds / 60
duration = (
datetime.datetime.combine(base_date, time_period.end_time)
- datetime.datetime.combine(base_date, time_period.start_time)
).seconds / 60
if duration < meeting_type.duration:
# skip time period that can't even hold a single meeting
continue
@ -80,11 +83,15 @@ def get_all_slots(agenda, meeting_type):
begin, end = interval
open_slots_by_desk[desk].remove_overlap(localtime(begin), localtime(end))
for event in agenda.event_set.filter(
agenda=agenda, start_datetime__gte=min_datetime,
start_datetime__lte=max_datetime + datetime.timedelta(meeting_type.duration)).select_related(
'meeting_type').exclude(
booking__cancellation_datetime__isnull=False):
for event in (
agenda.event_set.filter(
agenda=agenda,
start_datetime__gte=min_datetime,
start_datetime__lte=max_datetime + datetime.timedelta(meeting_type.duration),
)
.select_related('meeting_type')
.exclude(booking__cancellation_datetime__isnull=False)
):
for slot in open_slots_by_desk[event.desk_id].search_data(event.start_datetime, event.end_datetime):
slot.full = True
@ -98,7 +105,7 @@ def get_all_slots(agenda, meeting_type):
def get_agenda_detail(request, agenda):
agenda_detail = {
'id': agenda.slug,
'slug': agenda.slug, # kept for compatibility
'slug': agenda.slug, # kept for compatibility
'text': agenda.label,
'kind': agenda.kind,
'minimal_booking_delay': agenda.minimal_booking_delay,
@ -108,21 +115,21 @@ def get_agenda_detail(request, agenda):
if agenda.kind == 'events':
agenda_detail['api'] = {
'datetimes_url': request.build_absolute_uri(
reverse('api-agenda-datetimes',
kwargs={'agenda_identifier': agenda.slug}))
reverse('api-agenda-datetimes', kwargs={'agenda_identifier': agenda.slug})
)
}
elif agenda.kind == 'meetings':
agenda_detail['api'] = {
'meetings_url': request.build_absolute_uri(
reverse('api-agenda-meetings',
kwargs={'agenda_identifier': agenda.slug})),
reverse('api-agenda-meetings', kwargs={'agenda_identifier': agenda.slug})
),
'desks_url': request.build_absolute_uri(
reverse('api-agenda-desks',
kwargs={'agenda_identifier': agenda.slug}))
reverse('api-agenda-desks', kwargs={'agenda_identifier': agenda.slug})
),
}
agenda_detail['api']['fillslots_url'] = request.build_absolute_uri(
reverse('api-agenda-fillslots',
kwargs={'agenda_identifier': agenda.slug}))
reverse('api-agenda-fillslots', kwargs={'agenda_identifier': agenda.slug})
)
return agenda_detail
@ -136,7 +143,7 @@ def get_event_places(event):
if event.waiting_list_places:
places['waiting_list_total'] = event.waiting_list_places
places['waiting_list_reserved'] = event.waiting_list
places['waiting_list_available'] = (event.waiting_list_places - event.waiting_list)
places['waiting_list_available'] = event.waiting_list_places - event.waiting_list
return places
@ -144,10 +151,10 @@ class Agendas(APIView):
permission_classes = ()
def get(self, request, format=None):
agendas = [get_agenda_detail(request, agenda)
for agenda in Agenda.objects.all().order_by('label')]
agendas = [get_agenda_detail(request, agenda) for agenda in Agenda.objects.all().order_by('label')]
return Response({'data': agendas})
agendas = Agendas.as_view()
@ -155,12 +162,14 @@ class AgendaDetail(APIView):
'''
Retrieve an agenda instance.
'''
permission_classes = ()
def get(self, request, agenda_identifier):
agenda = get_object_or_404(Agenda, slug=agenda_identifier)
return Response({'data': get_agenda_detail(request, agenda)})
agenda_detail = AgendaDetail.as_view()
@ -186,43 +195,68 @@ class Datetimes(APIView):
if agenda.minimal_booking_delay:
entries = entries.filter(
start_datetime__gte=localtime(now() + datetime.timedelta(days=agenda.minimal_booking_delay)).replace(hour=0, minute=0))
start_datetime__gte=localtime(
now() + datetime.timedelta(days=agenda.minimal_booking_delay)
).replace(hour=0, minute=0)
)
if agenda.maximal_booking_delay:
entries = entries.filter(
start_datetime__lt=localtime(now() + datetime.timedelta(days=agenda.maximal_booking_delay)).replace(hour=0, minute=0))
start_datetime__lt=localtime(
now() + datetime.timedelta(days=agenda.maximal_booking_delay)
).replace(hour=0, minute=0)
)
if 'date_start' in request.GET:
entries = entries.filter(start_datetime__gte=make_aware(
datetime.datetime.combine(parse_date(request.GET['date_start']), datetime.time(0, 0))))
entries = entries.filter(
start_datetime__gte=make_aware(
datetime.datetime.combine(parse_date(request.GET['date_start']), datetime.time(0, 0))
)
)
if 'date_end' in request.GET:
entries = entries.filter(start_datetime__lt=make_aware(
datetime.datetime.combine(parse_date(request.GET['date_end']), datetime.time(0, 0))))
entries = entries.filter(
start_datetime__lt=make_aware(
datetime.datetime.combine(parse_date(request.GET['date_end']), datetime.time(0, 0))
)
)
response = {'data': [{'id': x.id,
'slug': x.slug,
'text': force_text(x),
'datetime': format_response_datetime(x.start_datetime),
'description': x.description,
'disabled': bool(x.full),
'api': {
'fillslot_url': request.build_absolute_uri(
reverse('api-fillslot',
kwargs={
'agenda_identifier': agenda.slug,
'event_identifier': x.slug or x.id,
})),
'status_url': request.build_absolute_uri(
reverse('api-event-status',
kwargs={
'agenda_identifier': agenda.slug,
'event_identifier': x.slug or x.id,
}))
},
} for x in entries]}
response = {
'data': [
{
'id': x.id,
'slug': x.slug,
'text': force_text(x),
'datetime': format_response_datetime(x.start_datetime),
'description': x.description,
'disabled': bool(x.full),
'api': {
'fillslot_url': request.build_absolute_uri(
reverse(
'api-fillslot',
kwargs={
'agenda_identifier': agenda.slug,
'event_identifier': x.slug or x.id,
},
)
),
'status_url': request.build_absolute_uri(
reverse(
'api-event-status',
kwargs={
'agenda_identifier': agenda.slug,
'event_identifier': x.slug or x.id,
},
)
),
},
}
for x in entries
]
}
return Response(response)
datetimes = Datetimes.as_view()
@ -235,8 +269,9 @@ class MeetingDatetimes(APIView):
# legacy access by meeting id
meeting_type = MeetingType.objects.get(id=meeting_identifier)
else:
meeting_type = MeetingType.objects.get(slug=meeting_identifier,
agenda__slug=agenda_identifier)
meeting_type = MeetingType.objects.get(
slug=meeting_identifier, agenda__slug=agenda_identifier
)
except (ValueError, MeetingType.DoesNotExist):
raise Http404()
@ -259,22 +294,27 @@ class MeetingDatetimes(APIView):
# to request.build_absolute_uri()
fake_event_identifier = '__event_identifier__'
fillslot_url = request.build_absolute_uri(
reverse('api-fillslot',
kwargs={
'agenda_identifier': agenda.slug,
'event_identifier': fake_event_identifier,
}))
reverse(
'api-fillslot',
kwargs={'agenda_identifier': agenda.slug, 'event_identifier': fake_event_identifier,},
)
)
response = {'data': [{'id': x.id,
'datetime': format_response_datetime(x.start_datetime),
'text': force_text(x),
'disabled': bool(x.full),
'api': {
'fillslot_url': fillslot_url.replace(fake_event_identifier, str(x.id)),
},
} for x in slots]}
response = {
'data': [
{
'id': x.id,
'datetime': format_response_datetime(x.start_datetime),
'text': force_text(x),
'disabled': bool(x.full),
'api': {'fillslot_url': fillslot_url.replace(fake_event_identifier, str(x.id)),},
}
for x in slots
]
}
return Response(response)
meeting_datetimes = MeetingDatetimes.as_view()
@ -291,20 +331,28 @@ class MeetingList(APIView):
meeting_types = []
for meeting_type in agenda.meetingtype_set.all():
meeting_types.append({
'text': meeting_type.label,
'id': meeting_type.slug,
'duration': meeting_type.duration,
'api': {
'datetimes_url': request.build_absolute_uri(
reverse('api-agenda-meeting-datetimes',
kwargs={'agenda_identifier': agenda.slug,
'meeting_identifier': meeting_type.slug})),
}
})
meeting_types.append(
{
'text': meeting_type.label,
'id': meeting_type.slug,
'duration': meeting_type.duration,
'api': {
'datetimes_url': request.build_absolute_uri(
reverse(
'api-agenda-meeting-datetimes',
kwargs={
'agenda_identifier': agenda.slug,
'meeting_identifier': meeting_type.slug,
},
)
),
},
}
)
return Response({'data': meeting_types})
meeting_list = MeetingList.as_view()
@ -322,6 +370,7 @@ class AgendaDeskList(APIView):
desks = [{'id': x.slug, 'text': x.label} for x in agenda.desk_set.all()]
return Response({'data': desks})
agenda_desk_list = AgendaDeskList.as_view()
@ -329,6 +378,7 @@ class SlotSerializer(serializers.Serializer):
'''
payload to fill one slot. The slot (event id) is in the URL.
'''
label = serializers.CharField(max_length=250, allow_blank=True)
user_name = serializers.CharField(max_length=250, allow_blank=True)
user_display_label = serializers.CharField(max_length=250, allow_blank=True)
@ -342,8 +392,10 @@ class SlotsSerializer(SlotSerializer):
payload to fill multiple slots: same as SlotSerializer, but the
slots list is in the payload.
'''
slots = serializers.ListField(required=True,
child=serializers.CharField(max_length=64, allow_blank=False))
slots = serializers.ListField(
required=True, child=serializers.CharField(max_length=64, allow_blank=False)
)
class Fillslots(APIView):
@ -351,8 +403,7 @@ class Fillslots(APIView):
serializer_class = SlotsSerializer
def post(self, request, agenda_identifier=None, event_identifier=None, format=None):
return self.fillslot(request=request, agenda_identifier=agenda_identifier,
format=format)
return self.fillslot(request=request, agenda_identifier=agenda_identifier, format=format)
def fillslot(self, request, agenda_identifier=None, slots=[], format=None):
multiple_booking = bool(not slots)
@ -367,22 +418,28 @@ class Fillslots(APIView):
serializer = self.serializer_class(data=request.data, partial=True)
if not serializer.is_valid():
return Response({
'err': 1,
'err_class': 'invalid payload',
'err_desc': _('invalid payload'),
'errors': serializer.errors
}, status=status.HTTP_400_BAD_REQUEST)
return Response(
{
'err': 1,
'err_class': 'invalid payload',
'err_desc': _('invalid payload'),
'errors': serializer.errors,
},
status=status.HTTP_400_BAD_REQUEST,
)
payload = serializer.validated_data
if 'slots' in payload:
slots = payload['slots']
if not slots:
return Response({
'err': 1,
'err_class': 'slots list cannot be empty',
'err_desc': _('slots list cannot be empty'),
}, status=status.HTTP_400_BAD_REQUEST)
return Response(
{
'err': 1,
'err_class': 'slots list cannot be empty',
'err_desc': _('slots list cannot be empty'),
},
status=status.HTTP_400_BAD_REQUEST,
)
if 'count' in payload:
places_count = payload['count']
@ -391,20 +448,26 @@ class Fillslots(APIView):
try:
places_count = int(request.query_params['count'])
except ValueError:
return Response({
'err': 1,
'err_class': 'invalid value for count (%s)' % request.query_params['count'],
'err_desc': _('invalid value for count (%s)') % request.query_params['count'],
}, status=status.HTTP_400_BAD_REQUEST)
return Response(
{
'err': 1,
'err_class': 'invalid value for count (%s)' % request.query_params['count'],
'err_desc': _('invalid value for count (%s)') % request.query_params['count'],
},
status=status.HTTP_400_BAD_REQUEST,
)
else:
places_count = 1
if places_count <= 0:
return Response({
'err': 1,
'err_class': 'count cannot be less than or equal to zero',
'err_desc': _('count cannot be less than or equal to zero'),
}, status=status.HTTP_400_BAD_REQUEST)
return Response(
{
'err': 1,
'err_class': 'count cannot be less than or equal to zero',
'err_desc': _('count cannot be less than or equal to zero'),
},
status=status.HTTP_400_BAD_REQUEST,
)
to_cancel_booking = None
cancel_booking_id = None
@ -412,11 +475,14 @@ class Fillslots(APIView):
try:
cancel_booking_id = int(payload.get('cancel_booking_id'))
except (ValueError, TypeError):
return Response({
'err': 1,
'err_class': 'cancel_booking_id is not an integer',
'err_desc': _('cancel_booking_id is not an integer'),
}, status=status.HTTP_400_BAD_REQUEST)
return Response(
{
'err': 1,
'err_class': 'cancel_booking_id is not an integer',
'err_desc': _('cancel_booking_id is not an integer'),
},
status=status.HTTP_400_BAD_REQUEST,
)
if cancel_booking_id is not None:
cancel_error = None
@ -432,11 +498,7 @@ class Fillslots(APIView):
cancel_error = gettext_noop('cancel booking: booking does no exist')
if cancel_error:
return Response({
'err': 1,
'err_class': cancel_error,
'err_desc': _(cancel_error),
})
return Response({'err': 1, 'err_class': cancel_error, 'err_desc': _(cancel_error),})
extra_data = {}
for k, v in request.data.items():
@ -454,17 +516,25 @@ class Fillslots(APIView):
try:
meeting_type_id_, datetime_str = slot.split(':')
except ValueError:
return Response({
'err': 1,
'err_class': 'invalid slot: %s' % slot,
'err_desc': _('invalid slot: %s') % slot,
}, status=status.HTTP_400_BAD_REQUEST)
return Response(
{
'err': 1,
'err_class': 'invalid slot: %s' % slot,
'err_desc': _('invalid slot: %s') % slot,
},
status=status.HTTP_400_BAD_REQUEST,
)
if meeting_type_id_ != meeting_type_id:
return Response({
'err': 1,
'err_class': 'all slots must have the same meeting type id (%s)' % meeting_type_id,
'err_desc': _('all slots must have the same meeting type id (%s)') % meeting_type_id,
}, status=status.HTTP_400_BAD_REQUEST)
return Response(
{
'err': 1,
'err_class': 'all slots must have the same meeting type id (%s)'
% meeting_type_id,
'err_desc': _('all slots must have the same meeting type id (%s)')
% meeting_type_id,
},
status=status.HTTP_400_BAD_REQUEST,
)
datetimes.add(make_aware(datetime.datetime.strptime(datetime_str, '%Y-%m-%d-%H%M')))
# get all free slots and separate them by desk
@ -480,11 +550,13 @@ class Fillslots(APIView):
available_desk = Desk.objects.get(id=available_desk_id)
break
else:
return Response({
'err': 1,
'err_class': 'no more desk available',
'err_desc': _('no more desk available'),
})
return Response(
{
'err': 1,
'err_class': 'no more desk available',
'err_desc': _('no more desk available'),
}
)
# all datetimes are free, book them in order
datetimes = list(datetimes)
@ -494,11 +566,16 @@ class Fillslots(APIView):
# create them now, with data from the slots and the desk we found.
events = []
for start_datetime in datetimes:
events.append(Event.objects.create(agenda=agenda,
events.append(
Event.objects.create(
agenda=agenda,
meeting_type_id=meeting_type_id,
start_datetime=start_datetime,
full=False, places=1,
desk=available_desk))
full=False,
places=1,
desk=available_desk,
)
)
else:
try:
events = Event.objects.filter(id__in=[int(s) for s in slots]).order_by('start_datetime')
@ -514,18 +591,10 @@ class Fillslots(APIView):
# in the waiting list.
in_waiting_list = True
if (event.waiting_list + places_count) > event.waiting_list_places:
return Response({
'err': 1,
'err_class': 'sold out',
'err_desc': _('sold out'),
})
return Response({'err': 1, 'err_class': 'sold out', 'err_desc': _('sold out'),})
else:
if (event.booked_places + places_count) > event.places:
return Response({
'err': 1,
'err_class': 'sold out',
'err_desc': _('sold out')
})
return Response({'err': 1, 'err_class': 'sold out', 'err_desc': _('sold out')})
with transaction.atomic():
if to_cancel_booking:
@ -536,13 +605,15 @@ class Fillslots(APIView):
primary_booking = None
for event in events:
for i in range(places_count):
new_booking = Booking(event_id=event.id,
in_waiting_list=in_waiting_list,
label=payload.get('label', ''),
user_name=payload.get('user_name', ''),
backoffice_url=payload.get('backoffice_url', ''),
user_display_label=payload.get('user_display_label', ''),
extra_data=extra_data)
new_booking = Booking(
event_id=event.id,
in_waiting_list=in_waiting_list,
label=payload.get('label', ''),
user_name=payload.get('user_name', ''),
backoffice_url=payload.get('backoffice_url', ''),
user_display_label=payload.get('user_display_label', ''),
extra_data=extra_data,
)
if primary_booking is not None:
new_booking.primary_booking = primary_booking
new_booking.save()
@ -556,21 +627,22 @@ class Fillslots(APIView):
'datetime': format_response_datetime(events[0].start_datetime),
'api': {
'cancel_url': request.build_absolute_uri(
reverse('api-cancel-booking', kwargs={'booking_pk': primary_booking.id})),
reverse('api-cancel-booking', kwargs={'booking_pk': primary_booking.id})
),
'ics_url': request.build_absolute_uri(
reverse('api-booking-ics', kwargs={'booking_pk': primary_booking.id})),
}
reverse('api-booking-ics', kwargs={'booking_pk': primary_booking.id})
),
},
}
if in_waiting_list:
response['api']['accept_url'] = request.build_absolute_uri(
reverse('api-accept-booking', kwargs={'booking_pk': primary_booking.id}))
reverse('api-accept-booking', kwargs={'booking_pk': primary_booking.id})
)
if agenda.kind == 'meetings':
response['end_datetime'] = format_response_datetime(events[-1].end_datetime)
response['duration'] = (events[-1].end_datetime - events[-1].start_datetime).seconds // 60
if available_desk:
response['desk'] = {
'label': available_desk.label,
'slug': available_desk.slug}
response['desk'] = {'label': available_desk.label, 'slug': available_desk.slug}
if to_cancel_booking:
response['cancelled_booking_id'] = cancelled_booking_id
if agenda.kind == 'events' and not multiple_booking:
@ -579,6 +651,7 @@ class Fillslots(APIView):
return Response(response)
fillslots = Fillslots.as_view()
@ -586,10 +659,13 @@ class Fillslot(Fillslots):
serializer_class = SlotSerializer
def post(self, request, agenda_identifier=None, event_identifier=None, format=None):
return self.fillslot(request=request,
agenda_identifier=agenda_identifier,
slots=[event_identifier], # fill a "list on one slot"
format=format)
return self.fillslot(
request=request,
agenda_identifier=agenda_identifier,
slots=[event_identifier], # fill a "list on one slot"
format=format,
)
fillslot = Fillslot.as_view()
@ -599,14 +675,14 @@ class BookingAPI(APIView):
def initial(self, request, *args, **kwargs):
super(BookingAPI, self).initial(request, *args, **kwargs)
self.booking = Booking.objects.get(id=kwargs.get('booking_pk'),
cancellation_datetime__isnull=True)
self.booking = Booking.objects.get(id=kwargs.get('booking_pk'), cancellation_datetime__isnull=True)
def delete(self, request, *args, **kwargs):
self.booking.cancel()
response = {'err': 0, 'booking_id': self.booking.id}
return Response(response)
booking = BookingAPI.as_view()
@ -616,6 +692,7 @@ class CancelBooking(APIView):
It will return an error (code 1) if the booking was already cancelled.
'''
permission_classes = (permissions.IsAuthenticated,)
def post(self, request, booking_pk=None, format=None):
@ -631,6 +708,7 @@ class CancelBooking(APIView):
response = {'err': 0, 'booking_id': booking.id}
return Response(response)
cancel_booking = CancelBooking.as_view()
@ -641,6 +719,7 @@ class AcceptBooking(APIView):
It will return error codes if the booking was cancelled before (code 1) and
if the booking was not in waiting list (code 2).
'''
permission_classes = (permissions.IsAuthenticated,)
def post(self, request, booking_pk=None, format=None):
@ -663,6 +742,7 @@ class AcceptBooking(APIView):
response = {'err': 0, 'booking_id': booking.id}
return Response(response)
accept_booking = AcceptBooking.as_view()
@ -699,4 +779,5 @@ class BookingICS(APIView):
response = HttpResponse(booking.get_ics(request), content_type='text/calendar')
return response
booking_ics = BookingICS.as_view()

View File

@ -63,6 +63,7 @@ class Intervals(object):
10: [a],
}
'''
def __init__(self):
self.points = []
self.container = []

View File

@ -26,16 +26,23 @@ from django.utils.encoding import force_text
from django.utils.timezone import make_aware
from django.utils.translation import ugettext_lazy as _
from chrono.agendas.models import (Agenda, Event, MeetingType, TimePeriod, Desk,
TimePeriodException, WEEKDAYS_LIST)
from chrono.agendas.models import (
Agenda,
Event,
MeetingType,
TimePeriod,
Desk,
TimePeriodException,
WEEKDAYS_LIST,
)
from . import widgets
DATETIME_OPTIONS = {
'weekStart': 1,
'autoclose': True,
}
'weekStart': 1,
'autoclose': True,
}
class DateTimeWidget(widgets.DateTimeWidget):
@ -49,11 +56,11 @@ class AgendaAddForm(forms.ModelForm):
fields = ['label', 'kind', 'edit_role', 'view_role']
edit_role = forms.ModelChoiceField(
label=_('Edit Role'), required=False,
queryset=Group.objects.all().order_by('name'))
label=_('Edit Role'), required=False, queryset=Group.objects.all().order_by('name')
)
view_role = forms.ModelChoiceField(
label=_('View Role'), required=False,
queryset=Group.objects.all().order_by('name'))
label=_('View Role'), required=False, queryset=Group.objects.all().order_by('name')
)
class AgendaEditForm(AgendaAddForm):
@ -66,8 +73,8 @@ class NewEventForm(forms.ModelForm):
class Meta:
model = Event
widgets = {
'agenda': forms.HiddenInput(),
'start_datetime': DateTimeWidget(),
'agenda': forms.HiddenInput(),
'start_datetime': DateTimeWidget(),
}
exclude = ['full', 'meeting_type', 'desk', 'slug']
@ -76,8 +83,8 @@ class EventForm(forms.ModelForm):
class Meta:
model = Event
widgets = {
'agenda': forms.HiddenInput(),
'start_datetime': DateTimeWidget(),
'agenda': forms.HiddenInput(),
'start_datetime': DateTimeWidget(),
}
exclude = ['full', 'meeting_type', 'desk']
@ -86,7 +93,7 @@ class NewMeetingTypeForm(forms.ModelForm):
class Meta:
model = MeetingType
widgets = {
'agenda': forms.HiddenInput(),
'agenda': forms.HiddenInput(),
}
exclude = ['slug']
@ -95,16 +102,15 @@ class MeetingTypeForm(forms.ModelForm):
class Meta:
model = MeetingType
widgets = {
'agenda': forms.HiddenInput(),
'agenda': forms.HiddenInput(),
}
exclude = []
class TimePeriodAddForm(forms.Form):
weekdays = forms.MultipleChoiceField(
label=_('Days'),
widget=widgets.WeekdaysWidget(),
choices=WEEKDAYS_LIST)
label=_('Days'), widget=widgets.WeekdaysWidget(), choices=WEEKDAYS_LIST
)
start_time = forms.TimeField(label=_('Start Time'), widget=widgets.TimeWidget())
end_time = forms.TimeField(label=_('End Time'), widget=widgets.TimeWidget())
@ -118,9 +124,9 @@ class TimePeriodForm(forms.ModelForm):
class Meta:
model = TimePeriod
widgets = {
'start_time': widgets.TimeWidget(),
'end_time': widgets.TimeWidget(),
'desk': forms.HiddenInput(),
'start_time': widgets.TimeWidget(),
'end_time': widgets.TimeWidget(),
'desk': forms.HiddenInput(),
}
exclude = []
@ -166,11 +172,14 @@ class TimePeriodExceptionForm(forms.ModelForm):
class ImportEventsForm(forms.Form):
events_csv_file = forms.FileField(
label=_('Events File'),
required=True,
help_text=_('CSV file with date, time, number of places, '
'number of places in waiting list, and label '
'as columns.'))
label=_('Events File'),
required=True,
help_text=_(
'CSV file with date, time, number of places, '
'number of places in waiting list, and label '
'as columns.'
),
)
events = None
def __init__(self, agenda_pk, **kwargs):
@ -201,31 +210,37 @@ class ImportEventsForm(forms.Form):
if not csvline:
continue
if len(csvline) < 3:
raise ValidationError(_('Invalid file format. (line %d)') % (i+1))
raise ValidationError(_('Invalid file format. (line %d)') % (i + 1))
if i == 0 and csvline[0].strip('#') in ('date', 'Date', _('date'), _('Date')):
continue
event = Event()
event.agenda_id = self.agenda_pk
for datetime_fmt in ('%Y-%m-%d %H:%M', '%d/%m/%Y %H:%M',
'%d/%m/%Y %Hh%M', '%Y-%m-%d %H:%M:%S', '%d/%m/%Y %H:%M:%S'):
for datetime_fmt in (
'%Y-%m-%d %H:%M',
'%d/%m/%Y %H:%M',
'%d/%m/%Y %Hh%M',
'%Y-%m-%d %H:%M:%S',
'%d/%m/%Y %H:%M:%S',
):
try:
event_datetime = datetime.datetime.strptime(
'%s %s' % tuple(csvline[:2]), datetime_fmt)
event_datetime = datetime.datetime.strptime('%s %s' % tuple(csvline[:2]), datetime_fmt)
except ValueError:
continue
event.start_datetime = make_aware(event_datetime)
break
else:
raise ValidationError(_('Invalid file format. (date/time format, line %d)') % (i+1))
raise ValidationError(_('Invalid file format. (date/time format, line %d)') % (i + 1))
try:
event.places = int(csvline[2])
except ValueError:
raise ValidationError(_('Invalid file format. (number of places, line %d)') % (i+1))
raise ValidationError(_('Invalid file format. (number of places, line %d)') % (i + 1))
if len(csvline) >= 4:
try:
event.waiting_list_places = int(csvline[3])
except ValueError:
raise ValidationError(_('Invalid file format. (number of places in waiting list, line %d)') % (i+1))
raise ValidationError(
_('Invalid file format. (number of places in waiting list, line %d)') % (i + 1)
)
if len(csvline) >= 5:
event.label = force_text(csvline[4])
exclude = ['desk', 'meeting_type']
@ -237,11 +252,10 @@ class ImportEventsForm(forms.Form):
event.full_clean(exclude=exclude)
except ValidationError as e:
errors = [
_('Invalid file format. (%(label)s: %(errors)s, line %(line)d)') % {
'label': label,
'errors': u', '.join(field_errors),
'line': i + 1
} for label, field_errors in e.message_dict.items()]
_('Invalid file format. (%(label)s: %(errors)s, line %(line)d)')
% {'label': label, 'errors': u', '.join(field_errors), 'line': i + 1}
for label, field_errors in e.message_dict.items()
]
raise ValidationError(errors)
events.append(event)
self.events = events
@ -252,10 +266,16 @@ class ExceptionsImportForm(forms.ModelForm):
model = Desk
fields = []
ics_file = forms.FileField(label=_('ICS File'), required=False,
help_text=_('ICS file containing events which will be considered as exceptions.'))
ics_url = forms.URLField(label=_('URL'), required=False,
help_text=_('URL to remote calendar which will be synchronised hourly.'))
ics_file = forms.FileField(
label=_('ICS File'),
required=False,
help_text=_('ICS file containing events which will be considered as exceptions.'),
)
ics_url = forms.URLField(
label=_('URL'),
required=False,
help_text=_('URL to remote calendar which will be synchronised hourly.'),
)
class AgendasImportForm(forms.Form):

View File

@ -27,8 +27,8 @@ class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument(
'--output', metavar='FILE', default=None,
help='name of a file to write output to')
'--output', metavar='FILE', default=None, help='name of a file to write output to'
)
def handle(self, *args, **options):
if options['output']:

View File

@ -27,17 +27,12 @@ class Command(BaseCommand):
help = 'Import an exported site'
def add_arguments(self, parser):
parser.add_argument('filename', metavar='FILENAME', type=str,
help='name of file to import')
parser.add_argument('filename', metavar='FILENAME', type=str, help='name of file to import')
parser.add_argument('--clean', action='store_true', default=False, help='Clean site before importing')
parser.add_argument(
'--clean', action='store_true', default=False,
help='Clean site before importing')
parser.add_argument(
'--if-empty', action='store_true', default=False,
help='Import only if site is empty')
parser.add_argument(
'--overwrite', action='store_true', default=False,
help='Overwrite existing data')
'--if-empty', action='store_true', default=False, help='Import only if site is empty'
)
parser.add_argument('--overwrite', action='store_true', default=False, help='Overwrite existing data')
def handle(self, filename, **options):
if filename == '-':
@ -45,9 +40,11 @@ class Command(BaseCommand):
else:
fd = open(filename)
try:
import_site(json.load(fd),
if_empty=options['if_empty'],
clean=options['clean'],
overwrite=options['overwrite'])
import_site(
json.load(fd),
if_empty=options['if_empty'],
clean=options['clean'],
overwrite=options['overwrite'],
)
except AgendaImportError as exc:
raise CommandError(u'%s' % exc)

View File

@ -19,70 +19,91 @@ from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^$', views.homepage, name='chrono-manager-homepage'),
url(r'^agendas/add/$', views.agenda_add,
name='chrono-manager-agenda-add'),
url(r'^agendas/import/$', views.agendas_import,
name='chrono-manager-agendas-import'),
url(r'^agendas/(?P<pk>\d+)/$', views.agenda_view,
name='chrono-manager-agenda-view'),
url(r'^agendas/(?P<pk>\d+)/(?P<year>[0-9]{4})/(?P<month>[0-9]+)/$', views.agenda_monthly_view,
name='chrono-manager-agenda-month-view'),
url(r'^agendas/(?P<pk>\d+)/(?P<year>[0-9]{4})/(?P<month>[0-9]+)/(?P<day>[0-9]+)/$', views.agenda_day_view,
name='chrono-manager-agenda-day-view'),
url(r'^agendas/(?P<pk>\d+)/settings$', views.agenda_settings,
name='chrono-manager-agenda-settings'),
url(r'^agendas/(?P<pk>\d+)/edit$', views.agenda_edit,
name='chrono-manager-agenda-edit'),
url(r'^agendas/(?P<pk>\d+)/delete$', views.agenda_delete,
name='chrono-manager-agenda-delete'),
url(r'^agendas/(?P<pk>\d+)/export$', views.agenda_export,
name='chrono-manager-agenda-export'),
url(r'^agendas/(?P<pk>\d+)/add-event$', views.agenda_add_event,
name='chrono-manager-agenda-add-event'),
url(r'^agendas/(?P<pk>\d+)/import-events$', views.agenda_import_events,
name='chrono-manager-agenda-import-events'),
url(r'^events/(?P<pk>\d+)/$', views.event_edit,
name='chrono-manager-event-edit'),
url(r'^events/(?P<pk>\d+)/delete$', views.event_delete,
name='chrono-manager-event-delete'),
url(r'^agendas/(?P<pk>\d+)/add-meeting-type$', views.agenda_add_meeting_type,
name='chrono-manager-agenda-add-meeting-type'),
url(r'^meetingtypes/(?P<pk>\d+)/edit$', views.meeting_type_edit,
name='chrono-manager-meeting-type-edit'),
url(r'^meetingtypes/(?P<pk>\d+)/delete$', views.meeting_type_delete,
name='chrono-manager-meeting-type-delete'),
url(r'^agendas/(?P<agenda_pk>\d+)/desk/(?P<pk>\d+)/add-time-period$', views.agenda_add_time_period,
name='chrono-manager-agenda-add-time-period'),
url(r'^timeperiods/(?P<pk>\d+)/edit$', views.time_period_edit,
name='chrono-manager-time-period-edit'),
url(r'^timeperiods/(?P<pk>\d+)/delete$', views.time_period_delete,
name='chrono-manager-time-period-delete'),
url(r'^agendas/(?P<pk>\d+)/add-desk$', views.agenda_add_desk,
name='chrono-manager-agenda-add-desk'),
url(r'^desks/(?P<pk>\d+)/edit$', views.desk_edit,
name='chrono-manager-desk-edit'),
url(r'^desks/(?P<pk>\d+)/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'^agendas/desk/(?P<pk>\d+)/import-exceptions-from-ics/$', views.desk_import_time_period_exceptions,
name='chrono-manager-desk-add-import-time-period-exceptions'),
url(r'^time-period-exceptions/(?P<pk>\d+)/edit$', views.time_period_exception_edit,
name='chrono-manager-time-period-exception-edit'),
url(r'^time-period-exceptions/(?P<pk>\d+)/delete$', views.time_period_exception_delete,
name='chrono-manager-time-period-exception-delete'),
url(r'^time-period-exceptions/(?P<pk>\d+)/exception-extract-list$', views.time_period_exception_extract_list,
name='chrono-manager-time-period-exception-extract-list'),
url(r'^time-period-exceptions/(?P<pk>\d+)/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'),
url(r'^menu.json$', views.menu_json),
url(r'^$', views.homepage, name='chrono-manager-homepage'),
url(r'^agendas/add/$', views.agenda_add, name='chrono-manager-agenda-add'),
url(r'^agendas/import/$', views.agendas_import, name='chrono-manager-agendas-import'),
url(r'^agendas/(?P<pk>\d+)/$', views.agenda_view, name='chrono-manager-agenda-view'),
url(
r'^agendas/(?P<pk>\d+)/(?P<year>[0-9]{4})/(?P<month>[0-9]+)/$',
views.agenda_monthly_view,
name='chrono-manager-agenda-month-view',
),
url(
r'^agendas/(?P<pk>\d+)/(?P<year>[0-9]{4})/(?P<month>[0-9]+)/(?P<day>[0-9]+)/$',
views.agenda_day_view,
name='chrono-manager-agenda-day-view',
),
url(r'^agendas/(?P<pk>\d+)/settings$', views.agenda_settings, name='chrono-manager-agenda-settings'),
url(r'^agendas/(?P<pk>\d+)/edit$', views.agenda_edit, name='chrono-manager-agenda-edit'),
url(r'^agendas/(?P<pk>\d+)/delete$', views.agenda_delete, name='chrono-manager-agenda-delete'),
url(r'^agendas/(?P<pk>\d+)/export$', views.agenda_export, name='chrono-manager-agenda-export'),
url(r'^agendas/(?P<pk>\d+)/add-event$', views.agenda_add_event, name='chrono-manager-agenda-add-event'),
url(
r'^agendas/(?P<pk>\d+)/import-events$',
views.agenda_import_events,
name='chrono-manager-agenda-import-events',
),
url(r'^events/(?P<pk>\d+)/$', views.event_edit, name='chrono-manager-event-edit'),
url(r'^events/(?P<pk>\d+)/delete$', views.event_delete, name='chrono-manager-event-delete'),
url(
r'^agendas/(?P<pk>\d+)/add-meeting-type$',
views.agenda_add_meeting_type,
name='chrono-manager-agenda-add-meeting-type',
),
url(r'^meetingtypes/(?P<pk>\d+)/edit$', views.meeting_type_edit, name='chrono-manager-meeting-type-edit'),
url(
r'^meetingtypes/(?P<pk>\d+)/delete$',
views.meeting_type_delete,
name='chrono-manager-meeting-type-delete',
),
url(
r'^agendas/(?P<agenda_pk>\d+)/desk/(?P<pk>\d+)/add-time-period$',
views.agenda_add_time_period,
name='chrono-manager-agenda-add-time-period',
),
url(r'^timeperiods/(?P<pk>\d+)/edit$', views.time_period_edit, name='chrono-manager-time-period-edit'),
url(
r'^timeperiods/(?P<pk>\d+)/delete$',
views.time_period_delete,
name='chrono-manager-time-period-delete',
),
url(r'^agendas/(?P<pk>\d+)/add-desk$', views.agenda_add_desk, name='chrono-manager-agenda-add-desk'),
url(r'^desks/(?P<pk>\d+)/edit$', views.desk_edit, name='chrono-manager-desk-edit'),
url(r'^desks/(?P<pk>\d+)/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'^agendas/desk/(?P<pk>\d+)/import-exceptions-from-ics/$',
views.desk_import_time_period_exceptions,
name='chrono-manager-desk-add-import-time-period-exceptions',
),
url(
r'^time-period-exceptions/(?P<pk>\d+)/edit$',
views.time_period_exception_edit,
name='chrono-manager-time-period-exception-edit',
),
url(
r'^time-period-exceptions/(?P<pk>\d+)/delete$',
views.time_period_exception_delete,
name='chrono-manager-time-period-exception-delete',
),
url(
r'^time-period-exceptions/(?P<pk>\d+)/exception-extract-list$',
views.time_period_exception_extract_list,
name='chrono-manager-time-period-exception-extract-list',
),
url(
r'^time-period-exceptions/(?P<pk>\d+)/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',
),
url(r'^menu.json$', views.menu_json),
]

View File

@ -28,17 +28,46 @@ from django.utils.timezone import now, make_aware, make_naive
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext
from django.utils.encoding import force_text
from django.views.generic import (DetailView, CreateView, UpdateView,
ListView, DeleteView, FormView, TemplateView, DayArchiveView,
MonthArchiveView)
from django.views.generic import (
DetailView,
CreateView,
UpdateView,
ListView,
DeleteView,
FormView,
TemplateView,
DayArchiveView,
MonthArchiveView,
)
from chrono.agendas.models import (Agenda, Event, MeetingType, TimePeriod,
Booking, Desk, TimePeriodException,
ICSError, AgendaImportError)
from chrono.agendas.models import (
Agenda,
Event,
MeetingType,
TimePeriod,
Booking,
Desk,
TimePeriodException,
ICSError,
AgendaImportError,
)
from .forms import (AgendaAddForm, AgendaEditForm, NewEventForm, EventForm, NewMeetingTypeForm, MeetingTypeForm,
TimePeriodForm, ImportEventsForm, NewDeskForm, DeskForm, TimePeriodExceptionForm,
ExceptionsImportForm, AgendasImportForm, TimePeriodAddForm)
from .forms import (
AgendaAddForm,
AgendaEditForm,
NewEventForm,
EventForm,
NewMeetingTypeForm,
MeetingTypeForm,
TimePeriodForm,
ImportEventsForm,
NewDeskForm,
DeskForm,
TimePeriodExceptionForm,
ExceptionsImportForm,
AgendasImportForm,
TimePeriodAddForm,
)
from .utils import import_site
@ -53,6 +82,7 @@ class HomepageView(ListView):
queryset = queryset.filter(Q(view_role_id__in=group_ids) | Q(edit_role_id__in=group_ids))
return queryset
homepage = HomepageView.as_view()
@ -76,6 +106,7 @@ class AgendaAddView(CreateView):
def get_success_url(self):
return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.object.id})
agenda_add = AgendaAddView.as_view()
@ -108,20 +139,21 @@ class AgendasImportView(FormView):
if results.get('created') == 0:
message1 = _('No agenda created.')
else:
message1 = ungettext('An agenda has been created.',
'%(count)d agendas have been created.', results['created']) % {
'count': results['created']}
message1 = ungettext(
'An agenda has been created.', '%(count)d agendas have been created.', results['created']
) % {'count': results['created']}
if results.get('updated') == 0:
message2 = _('No agenda updated.')
else:
message2 = ungettext('An agenda has been updated.',
'%(count)d agendas have been updated.', results['updated']) % {
'count': results['updated']}
message2 = ungettext(
'An agenda has been updated.', '%(count)d agendas have been updated.', results['updated']
) % {'count': results['updated']}
messages.info(self.request, u'%s %s' % (message1, message2))
return super(AgendasImportView, self).form_valid(form)
agendas_import = AgendasImportView.as_view()
@ -139,6 +171,7 @@ class AgendaEditView(UpdateView):
def get_success_url(self):
return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.object.id})
agenda_edit = AgendaEditView.as_view()
@ -155,9 +188,10 @@ class AgendaDeleteView(DeleteView):
def get_context_data(self, **kwargs):
context = super(AgendaDeleteView, self).get_context_data(**kwargs)
context['cannot_delete'] = Booking.objects.filter(
event__agenda=self.get_object(),
event__start_datetime__gt=now(),
cancellation_datetime__isnull=True).exists()
event__agenda=self.get_object(),
event__start_datetime__gt=now(),
cancellation_datetime__isnull=True,
).exists()
return context
def delete(self, request, *args, **kwargs):
@ -167,6 +201,7 @@ class AgendaDeleteView(DeleteView):
raise PermissionDenied()
return super(AgendaDeleteView, self).delete(request, *args, **kwargs)
agenda_delete = AgendaDeleteView.as_view()
@ -184,15 +219,16 @@ class AgendaView(DetailView):
if agenda.kind == 'meetings':
# redirect to today view
today = datetime.date.today()
return HttpResponseRedirect(reverse('chrono-manager-agenda-day-view',
kwargs={'pk': agenda.id,
'year': today.year,
'month': today.month,
'day': today.day}))
return HttpResponseRedirect(
reverse(
'chrono-manager-agenda-day-view',
kwargs={'pk': agenda.id, 'year': today.year, 'month': today.month, 'day': today.day},
)
)
# redirect to settings
return HttpResponseRedirect(
reverse('chrono-manager-agenda-settings', kwargs={'pk': agenda.id}))
return HttpResponseRedirect(reverse('chrono-manager-agenda-settings', kwargs={'pk': agenda.id}))
agenda_view = AgendaView.as_view()
@ -214,20 +250,23 @@ class AgendaDateView(object):
# specify 6am time to get the expected timezone on daylight saving time
# days.
try:
self.date = make_aware(datetime.datetime.strptime(
'%s-%s-%s 06:00' % (self.get_year(), self.get_month(), self.get_day()),
'%Y-%m-%d %H:%M'))
self.date = make_aware(
datetime.datetime.strptime(
'%s-%s-%s 06:00' % (self.get_year(), self.get_month(), self.get_day()), '%Y-%m-%d %H:%M'
)
)
except ValueError: # day is out of range for month
# redirect to last day of month
date = datetime.date(int(self.get_year()), int(self.get_month()), 1)
date += datetime.timedelta(days=40)
date = date.replace(day=1)
date -= datetime.timedelta(days=1)
return HttpResponseRedirect(reverse('chrono-manager-agenda-day-view',
kwargs={'pk': self.agenda.id,
'year': date.year,
'month': date.month,
'day': date.day}))
return HttpResponseRedirect(
reverse(
'chrono-manager-agenda-day-view',
kwargs={'pk': self.agenda.id, 'year': date.year, 'month': date.month, 'day': date.day},
)
)
return super(AgendaDateView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
@ -253,7 +292,7 @@ class AgendaDateView(object):
def get_years(self):
year = now().year
return [str(x) for x in range(year-1, year+5)]
return [str(x) for x in range(year - 1, year + 5)]
class AgendaDayView(AgendaDateView, DayArchiveView):
@ -261,25 +300,30 @@ class AgendaDayView(AgendaDateView, DayArchiveView):
def get_previous_day_url(self):
previous_day = self.date.date() - datetime.timedelta(days=1)
return reverse('chrono-manager-agenda-day-view',
kwargs={'pk': self.agenda.id,
'year': previous_day.year,
'month': previous_day.month,
'day': previous_day.day})
return reverse(
'chrono-manager-agenda-day-view',
kwargs={
'pk': self.agenda.id,
'year': previous_day.year,
'month': previous_day.month,
'day': previous_day.day,
},
)
def get_next_day_url(self):
next_day = self.date.date() + datetime.timedelta(days=1)
return reverse('chrono-manager-agenda-day-view',
kwargs={'pk': self.agenda.id,
'year': next_day.year,
'month': next_day.month,
'day': next_day.day})
return reverse(
'chrono-manager-agenda-day-view',
kwargs={
'pk': self.agenda.id,
'year': next_day.year,
'month': next_day.month,
'day': next_day.day,
},
)
def get_timetable_infos(self):
timeperiods = TimePeriod.objects.filter(
desk__agenda=self.agenda,
weekday=self.date.weekday(),
)
timeperiods = TimePeriod.objects.filter(desk__agenda=self.agenda, weekday=self.date.weekday(),)
if not timeperiods:
return
@ -307,15 +351,22 @@ class AgendaDayView(AgendaDateView, DayArchiveView):
# use first row to include opening hours
info['opening_hours'] = opening_hours = []
for opening_hour in desk.get_opening_hours(current_date.date()):
opening_hours.append({
'css_top': 100 * (opening_hour.begin - start_date).seconds // 3600,
'css_height': 100 * (opening_hour.end - opening_hour.begin).seconds // 3600,
})
opening_hours.append(
{
'css_top': 100 * (opening_hour.begin - start_date).seconds // 3600,
'css_height': 100 * (opening_hour.end - opening_hour.begin).seconds // 3600,
}
)
infos.append(info)
info['bookings'] = bookings = [] # bookings for this desk
finish_datetime = current_date + interval
for event in [x for x in self.object_list if x.desk_id == desk.id and
x.start_datetime >= current_date and x.start_datetime < finish_datetime]:
for event in [
x
for x in self.object_list
if x.desk_id == desk.id
and x.start_datetime >= current_date
and x.start_datetime < finish_datetime
]:
# don't consider cancelled bookings
for booking in [x for x in event.booking_set.all() if not x.cancellation_datetime]:
booking.css_top = int(100 * event.start_datetime.minute / 60)
@ -326,6 +377,7 @@ class AgendaDayView(AgendaDateView, DayArchiveView):
current_date += interval
first = False
agenda_day_view = AgendaDayView.as_view()
@ -334,17 +386,17 @@ class AgendaMonthView(AgendaDateView, MonthArchiveView):
def get_previous_month_url(self):
previous_month = self.get_previous_month(self.date.date())
return reverse('chrono-manager-agenda-month-view',
kwargs={'pk': self.agenda.id,
'year': previous_month.year,
'month': previous_month.month})
return reverse(
'chrono-manager-agenda-month-view',
kwargs={'pk': self.agenda.id, 'year': previous_month.year, 'month': previous_month.month},
)
def get_next_month_url(self):
next_month = self.get_next_month(self.date.date())
return reverse('chrono-manager-agenda-month-view',
kwargs={'pk': self.agenda.id,
'year': next_month.year,
'month': next_month.month})
return reverse(
'chrono-manager-agenda-month-view',
kwargs={'pk': self.agenda.id, 'year': next_month.year, 'month': next_month.month},
)
def get_day(self):
return '1'
@ -362,11 +414,11 @@ class AgendaMonthView(AgendaDateView, MonthArchiveView):
last_week_number = 53
for week_number in range(first_week_number, last_week_number + 1):
yield self.get_week_timetable_infos(week_number-first_week_number, timeperiods)
yield self.get_week_timetable_infos(week_number - first_week_number, timeperiods)
def get_week_timetable_infos(self, week_index, timeperiods):
date = self.date + datetime.timedelta(week_index*7)
date = self.date + datetime.timedelta(week_index * 7)
year, week_number, dow = date.isocalendar()
start_date = date - datetime.timedelta(dow)
@ -382,16 +434,23 @@ class AgendaMonthView(AgendaDateView, MonthArchiveView):
periods.append(period)
period = period + interval
return {'days': [self.get_day_timetable_infos(start_date + datetime.timedelta(i), interval) for i in range(1, 8)],
'periods': periods}
return {
'days': [
self.get_day_timetable_infos(start_date + datetime.timedelta(i), interval)
for i in range(1, 8)
],
'periods': periods,
}
def get_day_timetable_infos(self, day, interval):
day = make_aware(make_naive(day)) # give day correct timezone
period = current_date = day.replace(hour=self.min_timeperiod.hour, minute=0)
timetable = {'date': current_date,
'today': day.date() == datetime.date.today(),
'other_month': day.month != self.date.month,
'infos': {'opening_hours': [], 'booked_slots': []}}
timetable = {
'date': current_date,
'today': day.date() == datetime.date.today(),
'other_month': day.month != self.date.month,
'infos': {'opening_hours': [], 'booked_slots': []},
}
desks = self.agenda.desk_set.all()
desks_len = len(desks)
@ -406,35 +465,42 @@ class AgendaMonthView(AgendaDateView, MonthArchiveView):
period_end = period + interval
for desk_index, desk in enumerate(desks):
width = (98.0 / desks_len) - 1
for event in [x for x in self.object_list if x.desk_id == desk.id and
x.start_datetime >= period and x.start_datetime < period_end]:
for event in [
x
for x in self.object_list
if x.desk_id == desk.id and x.start_datetime >= period and x.start_datetime < period_end
]:
# don't consider cancelled bookings
bookings = [x for x in event.booking_set.all() if not x.cancellation_datetime]
if not bookings:
continue
booking = {'css_top': 100 * (event.start_datetime - current_date).seconds // 3600,
'css_height': 100 * event.meeting_type.duration // 60,
'css_width': width,
'css_left': left,
'desk': desk,
'booking': bookings[0]
booking = {
'css_top': 100 * (event.start_datetime - current_date).seconds // 3600,
'css_height': 100 * event.meeting_type.duration // 60,
'css_width': width,
'css_left': left,
'desk': desk,
'booking': bookings[0],
}
timetable['infos']['booked_slots'].append(booking)
# get desks opening hours on last period iteration
if period == max_date:
for hour in desk.get_opening_hours(current_date):
timetable['infos']['opening_hours'].append({
'css_top': 100 * (hour.begin - current_date).seconds // 3600,
'css_height': 100 * (hour.end - hour.begin).seconds // 3600,
'css_width': width,
'css_left': left,
})
timetable['infos']['opening_hours'].append(
{
'css_top': 100 * (hour.begin - current_date).seconds // 3600,
'css_height': 100 * (hour.end - hour.begin).seconds // 3600,
'css_width': width,
'css_left': left,
}
)
left += width + 1
period += interval
return timetable
agenda_monthly_view = AgendaMonthView.as_view()
@ -550,6 +616,7 @@ class AgendaSettings(ManagedAgendaMixin, DetailView):
context['user_can_manage'] = self.get_object().can_be_managed(self.request.user)
return context
agenda_settings = AgendaSettings.as_view()
@ -561,6 +628,7 @@ class AgendaExport(ManagedAgendaMixin, DetailView):
json.dump({'agendas': [self.get_object().export_json()]}, response, indent=2)
return response
agenda_export = AgendaExport.as_view()
@ -569,6 +637,7 @@ class AgendaAddEventView(ManagedAgendaMixin, CreateView):
model = Event
form_class = NewEventForm
agenda_add_event = AgendaAddEventView.as_view()
@ -583,6 +652,7 @@ class AgendaImportEventsSampleView(TemplateView):
context['some_future_date'] = some_future_date
return context
agenda_import_events_sample_csv = AgendaImportEventsSampleView.as_view()
@ -600,14 +670,13 @@ class AgendaImportEventsView(ManagedAgendaMixin, FormView):
if form.events:
for event in form.events:
event.agenda_id = self.kwargs['pk']
if event.slug and Event.objects.filter(
agenda_id=event.agenda_id,
slug=event.slug).exists():
if event.slug and Event.objects.filter(agenda_id=event.agenda_id, slug=event.slug).exists():
raise ValidationError(_('Duplicated event identifier'))
event.save()
messages.info(self.request, _('%d events have been imported.') % len(form.events))
return super(AgendaImportEventsView, self).form_valid(form)
agenda_import_events = AgendaImportEventsView.as_view()
@ -616,6 +685,7 @@ class EventEditView(ManagedAgendaSubobjectMixin, UpdateView):
model = Event
form_class = EventForm
event_edit = EventEditView.as_view()
@ -626,8 +696,9 @@ class EventDeleteView(ManagedAgendaSubobjectMixin, DeleteView):
def get_context_data(self, **kwargs):
context = super(EventDeleteView, self).get_context_data(**kwargs)
context['cannot_delete'] = bool(
self.object.booking_set.filter(cancellation_datetime__isnull=True).exists() and
self.object.start_datetime > now())
self.object.booking_set.filter(cancellation_datetime__isnull=True).exists()
and self.object.start_datetime > now()
)
return context
def delete(self, request, *args, **kwargs):
@ -637,6 +708,7 @@ class EventDeleteView(ManagedAgendaSubobjectMixin, DeleteView):
raise PermissionDenied()
return super(EventDeleteView, self).delete(request, *args, **kwargs)
event_delete = EventDeleteView.as_view()
@ -645,13 +717,16 @@ class AgendaAddMeetingTypeView(ManagedAgendaMixin, CreateView):
model = Event
form_class = NewMeetingTypeForm
agenda_add_meeting_type = AgendaAddMeetingTypeView.as_view()
class MeetingTypeEditView(ManagedAgendaSubobjectMixin, UpdateView):
template_name = 'chrono/manager_meeting_type_form.html'
model = MeetingType
form_class = MeetingTypeForm
meeting_type_edit = MeetingTypeEditView.as_view()
@ -659,6 +734,7 @@ class MeetingTypeDeleteView(ManagedAgendaSubobjectMixin, DeleteView):
template_name = 'chrono/manager_confirm_delete.html'
model = MeetingType
meeting_type_delete = MeetingTypeDeleteView.as_view()
@ -669,13 +745,15 @@ class AgendaAddTimePeriodView(ManagedDeskMixin, FormView):
def form_valid(self, form):
for weekday in form.cleaned_data.get('weekdays'):
period = TimePeriod(
weekday=weekday,
start_time=form.cleaned_data['start_time'],
end_time=form.cleaned_data['end_time'],
desk=self.desk)
weekday=weekday,
start_time=form.cleaned_data['start_time'],
end_time=form.cleaned_data['end_time'],
desk=self.desk,
)
period.save()
return super(AgendaAddTimePeriodView, self).form_valid(form)
agenda_add_time_period = AgendaAddTimePeriodView.as_view()
@ -684,6 +762,7 @@ class TimePeriodEditView(ManagedDeskSubobjectMixin, UpdateView):
model = TimePeriod
form_class = TimePeriodForm
time_period_edit = TimePeriodEditView.as_view()
@ -691,6 +770,7 @@ class TimePeriodDeleteView(ManagedDeskSubobjectMixin, DeleteView):
template_name = 'chrono/manager_confirm_delete.html'
model = TimePeriod
time_period_delete = TimePeriodDeleteView.as_view()
@ -719,9 +799,8 @@ class DeskDeleteView(ManagedAgendaSubobjectMixin, DeleteView):
def get_context_data(self, **kwargs):
context = super(DeskDeleteView, self).get_context_data(**kwargs)
context['cannot_delete'] = Booking.objects.filter(
event__desk=self.get_object(),
event__start_datetime__gt=now(),
cancellation_datetime__isnull=True).exists()
event__desk=self.get_object(), event__start_datetime__gt=now(), cancellation_datetime__isnull=True
).exists()
return context
def delete(self, request, *args, **kwargs):
@ -812,7 +891,9 @@ class DeskImportTimePeriodExceptionsView(ManagedAgendaSubobjectMixin, UpdateView
ics_file_content = force_text(form.cleaned_data['ics_file'].read())
exceptions = form.instance.create_timeperiod_exceptions_from_ics(ics_file_content)
elif form.cleaned_data['ics_url']:
exceptions = form.instance.create_timeperiod_exceptions_from_remote_ics(form.cleaned_data['ics_url'])
exceptions = form.instance.create_timeperiod_exceptions_from_remote_ics(
form.cleaned_data['ics_url']
)
else:
form.instance.remove_timeperiod_exceptions_from_remote_ics()
except ICSError as e:
@ -821,21 +902,28 @@ class DeskImportTimePeriodExceptionsView(ManagedAgendaSubobjectMixin, UpdateView
form.instance.timeperiod_exceptions_remote_url = form.cleaned_data['ics_url']
form.instance.save()
if exceptions is not None:
message = ungettext('An exception has been imported.',
'%(count)d exceptions have been imported.', exceptions)
message = ungettext(
'An exception has been imported.', '%(count)d exceptions have been imported.', exceptions
)
message = message % {'count': exceptions}
messages.info(self.request, message)
return super(DeskImportTimePeriodExceptionsView, self).form_valid(form)
desk_import_time_period_exceptions = DeskImportTimePeriodExceptionsView.as_view()
def menu_json(request):
label = _('Agendas')
json_str = json.dumps([{'label': force_text(label),
'slug': 'calendar',
'url': request.build_absolute_uri(reverse('chrono-manager-homepage'))
}])
json_str = json.dumps(
[
{
'label': force_text(label),
'slug': 'calendar',
'url': request.build_absolute_uri(reverse('chrono-manager-homepage')),
}
]
)
content_type = 'application/json'
for variable in ('jsonpCallback', 'callback'):
if variable in request.GET:

View File

@ -39,7 +39,7 @@ DATE_FORMAT_PY_JS_MAPPING = {
'%Y': 'yyyy',
'%y': 'yy',
'%p': 'P',
'%S': 'ss'
'%S': 'ss',
}
DATE_FORMAT_TO_JS_REGEX = re.compile(r'(?<!\w)(' + '|'.join(DATE_FORMAT_PY_JS_MAPPING.keys()) + r')\b')
@ -77,9 +77,8 @@ class PickerWidgetMixin(object):
# with a default, and convert it to a Python data format for later string parsing
date_format = self.options['format']
self.format = DATE_FORMAT_TO_PYTHON_REGEX.sub(
lambda x: DATE_FORMAT_JS_PY_MAPPING[x.group()],
date_format
)
lambda x: DATE_FORMAT_JS_PY_MAPPING[x.group()], date_format
)
super(PickerWidgetMixin, self).__init__(attrs, format=self.format)
@ -87,7 +86,7 @@ class PickerWidgetMixin(object):
final_attrs = self.build_attrs(attrs)
rendered_widget = super(PickerWidgetMixin, self).render(name, value, final_attrs, renderer=renderer)
#if not set, autoclose have to be true.
# if not set, autoclose have to be true.
self.options.setdefault('autoclose', True)
# Build javascript options out of python dictionary
@ -100,13 +99,15 @@ class PickerWidgetMixin(object):
# Use provided id or generate hex to avoid collisions in document
id = final_attrs.get('id', uuid.uuid4().hex)
return mark_safe(BOOTSTRAP_INPUT_TEMPLATE % dict(
id=id,
rendered_widget=rendered_widget,
clear_button=CLEAR_BTN_TEMPLATE if self.options.get('clearBtn') else '',
glyphicon=self.glyphicon,
options=js_options
)
return mark_safe(
BOOTSTRAP_INPUT_TEMPLATE
% dict(
id=id,
rendered_widget=rendered_widget,
clear_button=CLEAR_BTN_TEMPLATE if self.options.get('clearBtn') else '',
glyphicon=self.glyphicon,
options=js_options,
)
)
@ -136,6 +137,7 @@ class TimeWidget(TimeInput):
input type and has a bit of a fallback mechanism with the presence
of the pattern attribute in case a standard text input is used.
"""
input_type = 'time'
def __init__(self, **kwargs):
@ -149,13 +151,17 @@ class WeekdaysWidget(SelectMultiple):
s = []
value = value or []
for choice_id, choice_label in self.choices:
s.append('<li><label><input type="checkbox" '
' name="%(name)s-%(choice_id)s" %(checked)s>'
'<span>%(choice_label)s</span></label></li>' % {
'name': name,
'checked': 'checked' if choice_id in value else '',
'choice_id': choice_id,
'choice_label': choice_label})
s.append(
'<li><label><input type="checkbox" '
' name="%(name)s-%(choice_id)s" %(checked)s>'
'<span>%(choice_label)s</span></label></li>'
% {
'name': name,
'checked': 'checked' if choice_id in value else '',
'choice_id': choice_id,
'choice_label': choice_label,
}
)
return mark_safe('<ul id="%(id)s">' % attrs + '\n'.join(s) + '</ul>')
def value_from_datadict(self, data, files, name):

View File

@ -77,10 +77,7 @@ WSGI_APPLICATION = 'chrono.wsgi.application'
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
'default': {'ENGINE': 'django.db.backends.sqlite3', 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),}
}
# Internationalization
@ -96,7 +93,7 @@ USE_L10N = True
USE_TZ = True
LOCALE_PATHS = (os.path.join(BASE_DIR, 'chrono', 'locale'), )
LOCALE_PATHS = (os.path.join(BASE_DIR, 'chrono', 'locale'),)
FORMAT_MODULE_PATH = 'chrono.formats'
@ -104,8 +101,7 @@ FORMAT_MODULE_PATH = 'chrono.formats'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
],
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
@ -165,7 +161,8 @@ MELLON_IDENTITY_PROVIDERS = []
# (see http://docs.python-requests.org/en/master/user/advanced/#proxies)
REQUESTS_PROXIES = None
local_settings_file = os.environ.get('CHRONO_SETTINGS_FILE',
os.path.join(os.path.dirname(__file__), 'local_settings.py'))
local_settings_file = os.environ.get(
'CHRONO_SETTINGS_FILE', os.path.join(os.path.dirname(__file__), 'local_settings.py')
)
if os.path.exists(local_settings_file):
exec(open(local_settings_file).read())

View File

@ -28,8 +28,7 @@ from .manager.urls import urlpatterns as chrono_manager_urls
urlpatterns = [
url(r'^$', homepage, name='home'),
url(r'^manage/', decorated_includes(manager_required,
include(chrono_manager_urls))),
url(r'^manage/', decorated_includes(manager_required, include(chrono_manager_urls))),
url(r'^api/', include(chrono_api_urls)),
url(r'^logout/$', LogoutView.as_view(), name='auth_logout'),
url(r'^login/$', LoginView.as_view(), name='auth_login'),

View File

@ -37,6 +37,7 @@ class DecoratedURLPattern(URLPattern):
result.func = self._decorate_with(result.func)
return result
def decorated_includes(func, includes, *args, **kwargs):
urlconf_module, app_name, namespace = includes
@ -59,6 +60,7 @@ def manager_required(function=None, login_url=None):
raise PermissionDenied()
# As the last resort, show the login form
return False
actual_decorator = user_passes_test(check_manager, login_url=login_url)
if function:
return actual_decorator(function)

View File

@ -35,8 +35,9 @@ class LoginView(auth_views.LoginView):
if any(get_idps()):
if not 'next' in request.GET:
return HttpResponseRedirect(resolve_url('mellon_login'))
return HttpResponseRedirect(resolve_url('mellon_login') + '?next='
+ quote(request.GET.get('next')))
return HttpResponseRedirect(
resolve_url('mellon_login') + '?next=' + quote(request.GET.get('next'))
)
return super(LoginView, self).get(request, *args, **kwargs)

View File

@ -8,7 +8,9 @@ https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/
"""
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "chrono.settings")
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()

6
debian/settings.py vendored
View File

@ -14,15 +14,15 @@
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
#ADMINS = (
# ADMINS = (
# # ('User 1', 'watchdog@example.net'),
# # ('User 2', 'janitor@example.net'),
#)
# )
# ALLOWED_HOSTS must be correct in production!
# See https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
ALLOWED_HOSTS = [
'*',
'*',
]
# Databases

View File

@ -15,6 +15,7 @@ from distutils.errors import CompileError
from distutils.spawn import find_executable
from setuptools import setup, find_packages
class eo_sdist(sdist):
def run(self):
if os.path.exists('VERSION'):
@ -27,6 +28,7 @@ class eo_sdist(sdist):
if os.path.exists('VERSION'):
os.remove('VERSION')
def get_version():
'''Use the VERSION, if absent generates a version with git describe, if not
tag exists, take 0.0- and add the length of the commit log.
@ -35,23 +37,25 @@ def get_version():
with open('VERSION', 'r') as v:
return v.read()
if os.path.exists('.git'):
p = subprocess.Popen(['git','describe','--dirty=.dirty','--match=v*'],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
p = subprocess.Popen(
['git', 'describe', '--dirty=.dirty', '--match=v*'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
result = p.communicate()[0]
if p.returncode == 0:
result = result.decode('ascii').strip()[1:] # strip spaces/newlines and initial v
if '-' in result: # not a tagged version
result = result.decode('ascii').strip()[1:] # strip spaces/newlines and initial v
if '-' in result: # not a tagged version
real_number, commit_count, commit_hash = result.split('-', 2)
version = '%s.post%s+%s' % (real_number, commit_count, commit_hash)
else:
version = result
return version
else:
return '0.0.post%s' % len(
subprocess.check_output(
['git', 'rev-list', 'HEAD']).splitlines())
return '0.0.post%s' % len(subprocess.check_output(['git', 'rev-list', 'HEAD']).splitlines())
return '0.0'
def data_tree(destdir, sourcedir):
extensions = ['.css', '.png', '.jpeg', '.jpg', '.gif', '.xml', '.html', '.js']
r = []
@ -60,6 +64,7 @@ def data_tree(destdir, sourcedir):
r.append((root.replace(sourcedir, destdir, 1), l))
return r
class compile_translations(Command):
description = 'compile message catalogs to MO files via django compilemessages'
user_options = []
@ -74,6 +79,7 @@ class compile_translations(Command):
curdir = os.getcwd()
try:
from django.core.management import call_command
for path, dirs, files in os.walk('chrono'):
if 'locale' not in dirs:
continue
@ -102,7 +108,9 @@ class compile_scss(Command):
if sass_bin:
break
if not sass_bin:
raise CompileError('A sass compiler is required but none was found. See sass-lang.com for choices.')
raise CompileError(
'A sass compiler is required but none was found. See sass-lang.com for choices.'
)
for package in self.distribution.packages:
for package_path in __import__(package).__path__:
@ -112,13 +120,17 @@ class compile_scss(Command):
continue
if filename.startswith('_'):
continue
subprocess.check_call([sass_bin, '%s/%s' % (path, filename),
'%s/%s' % (path, filename.replace('.scss', '.css'))])
subprocess.check_call(
[
sass_bin,
'%s/%s' % (path, filename),
'%s/%s' % (path, filename.replace('.scss', '.css')),
]
)
class build(_build):
sub_commands = [('compile_translations', None),
('compile_scss', None) ] + _build.sub_commands
sub_commands = [('compile_translations', None), ('compile_scss', None)] + _build.sub_commands
class install_lib(_install_lib):
@ -148,14 +160,15 @@ setup(
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 3',
],
install_requires=['django>=1.11, <2.3',
install_requires=[
'django>=1.11, <2.3',
'gadjo',
'djangorestframework>=3.4',
'django-jsonfield >= 0.9.3',
'vobject',
'python-dateutil',
'requests'
],
'requests',
],
zip_safe=False,
cmdclass={
'build': build,

View File

@ -4,6 +4,7 @@ from django.contrib.auth.models import User
import django_webtest
@pytest.fixture
def app(request):
wtm = django_webtest.WebTestMixin()

View File

@ -10,8 +10,16 @@ from django.contrib.auth.models import Group
from django.core.management import call_command
from django.core.management.base import CommandError
from chrono.agendas.models import (Agenda, Event, Booking, MeetingType,
Desk, TimePeriod, TimePeriodException, ICSError)
from chrono.agendas.models import (
Agenda,
Event,
Booking,
MeetingType,
Desk,
TimePeriod,
TimePeriodException,
ICSError,
)
pytestmark = pytest.mark.django_db
@ -154,6 +162,7 @@ def test_event_manager():
booking.save()
assert Event.objects.all()[0].booked_places == 0
def test_event_bookable_period():
agenda = Agenda(label=u'Foo bar')
agenda.save()
@ -179,6 +188,7 @@ def test_event_bookable_period():
event.save()
assert event.in_bookable_period() is False
def test_meeting_type_slugs():
agenda1 = Agenda(label=u'Foo bar')
agenda1.save()
@ -197,6 +207,7 @@ def test_meeting_type_slugs():
meeting_type3.save()
assert meeting_type3.slug == 'baz'
def test_timeperiodexception_creation_from_ics():
agenda = Agenda(label=u'Test 1 agenda')
agenda.save()
@ -206,6 +217,7 @@ def test_timeperiodexception_creation_from_ics():
assert exceptions_count == 2
assert TimePeriodException.objects.filter(desk=desk).count() == 2
def test_timeperiodexception_creation_from_ics_without_startdt():
agenda = Agenda(label=u'Test 2 agenda')
agenda.save()
@ -222,6 +234,7 @@ def test_timeperiodexception_creation_from_ics_without_startdt():
exceptions_count = desk.create_timeperiod_exceptions_from_ics(ics_sample)
assert 'Event "Event 1" has no start date.' == str(e.value)
def test_timeperiodexception_creation_from_ics_without_enddt():
agenda = Agenda(label=u'Test 3 agenda')
agenda.save()
@ -248,14 +261,17 @@ def test_timeperiodexception_creation_from_ics_with_recurrences():
desk.save()
assert desk.create_timeperiod_exceptions_from_ics(ICS_SAMPLE_WITH_RECURRENT_EVENT) == 3
assert TimePeriodException.objects.filter(desk=desk).count() == 3
assert set(TimePeriodException.objects.values_list('start_datetime', flat=True)) == set([
make_aware(datetime.datetime(2018, 1, 1)), make_aware(datetime.datetime(2019, 1, 1))])
assert set(TimePeriodException.objects.values_list('start_datetime', flat=True)) == set(
[make_aware(datetime.datetime(2018, 1, 1)), make_aware(datetime.datetime(2019, 1, 1))]
)
assert desk.create_timeperiod_exceptions_from_ics(ICS_SAMPLE_WITH_RECURRENT_EVENT) == 0
# verify occurences are cleaned when count changed
assert desk.create_timeperiod_exceptions_from_ics(ICS_SAMPLE_WITH_RECURRENT_EVENT_2) == 0
assert TimePeriodException.objects.filter(desk=desk).count() == 2
assert set(TimePeriodException.objects.values_list('start_datetime', flat=True)) == set([
make_aware(datetime.datetime(2018, 1, 1)), make_aware(datetime.datetime(2018, 1, 2))])
assert set(TimePeriodException.objects.values_list('start_datetime', flat=True)) == set(
[make_aware(datetime.datetime(2018, 1, 1)), make_aware(datetime.datetime(2018, 1, 2))]
)
def test_timeexception_creation_from_ics_with_dates():
agenda = Agenda(label=u'Test 5 agenda')
@ -275,6 +291,7 @@ def test_timeexception_creation_from_ics_with_dates():
assert localtime(exception.start_datetime) == make_aware(datetime.datetime(2018, 1, 1, 0, 0))
assert localtime(exception.end_datetime) == make_aware(datetime.datetime(2018, 1, 1, 0, 0))
def test_timeexception_create_from_invalid_ics():
agenda = Agenda(label=u'Test 6 agenda')
agenda.save()
@ -284,6 +301,7 @@ def test_timeexception_create_from_invalid_ics():
exceptions_count = desk.create_timeperiod_exceptions_from_ics(INVALID_ICS_SAMPLE)
assert str(e.value) == 'File format is invalid.'
def test_timeexception_create_from_ics_with_no_events():
agenda = Agenda(label=u'Test 7 agenda')
agenda.save()
@ -293,6 +311,7 @@ def test_timeexception_create_from_ics_with_no_events():
exceptions_count = desk.create_timeperiod_exceptions_from_ics(ICS_SAMPLE_WITH_NO_EVENTS)
assert str(e.value) == "The file doesn't contain any events."
@mock.patch('chrono.agendas.models.requests.get')
def test_timeperiodexception_creation_from_remote_ics(mocked_get):
agenda = Agenda(label=u'Test 8 agenda')
@ -316,6 +335,7 @@ def test_timeperiodexception_creation_from_remote_ics(mocked_get):
assert exceptions_count == 0
TimePeriodException.objects.filter(external_id='desk-%s:' % desk.id).count() == 0
@mock.patch('chrono.agendas.models.requests.get')
def test_timeperiodexception_creation_from_unreachable_remote_ics(mocked_get):
agenda = Agenda(label=u'Test 9 agenda')
@ -325,13 +345,16 @@ def test_timeperiodexception_creation_from_unreachable_remote_ics(mocked_get):
mocked_response = mock.Mock()
mocked_response.text = ICS_SAMPLE
mocked_get.return_value = mocked_response
def mocked_requests_connection_error(*args, **kwargs):
raise requests.ConnectionError('unreachable')
mocked_get.side_effect = mocked_requests_connection_error
with pytest.raises(ICSError) as e:
exceptions_count = desk.create_timeperiod_exceptions_from_remote_ics('http://example.com/sample.ics')
assert str(e.value) == "Failed to retrieve remote calendar (http://example.com/sample.ics, unreachable)."
@mock.patch('chrono.agendas.models.requests.get')
def test_timeperiodexception_creation_from_forbidden_remote_ics(mocked_get):
agenda = Agenda(label=u'Test 10 agenda')
@ -341,35 +364,50 @@ def test_timeperiodexception_creation_from_forbidden_remote_ics(mocked_get):
mocked_response = mock.Mock()
mocked_response.status_code = 403
mocked_get.return_value = mocked_response
def mocked_requests_http_forbidden_error(*args, **kwargs):
raise requests.HTTPError(response=mocked_response)
mocked_get.side_effect = mocked_requests_http_forbidden_error
with pytest.raises(ICSError) as e:
exceptions_count = desk.create_timeperiod_exceptions_from_remote_ics('http://example.com/sample.ics')
assert str(e.value) == "Failed to retrieve remote calendar (http://example.com/sample.ics, HTTP error 403)."
assert (
str(e.value) == "Failed to retrieve remote calendar (http://example.com/sample.ics, HTTP error 403)."
)
@mock.patch('chrono.agendas.models.requests.get')
def test_sync_desks_timeperiod_exceptions_from_ics(mocked_get, capsys):
agenda = Agenda(label=u'Test 11 agenda')
agenda.save()
desk = Desk(label='Test 11 desk', agenda=agenda, timeperiod_exceptions_remote_url='http://example.com/sample.ics')
desk = Desk(
label='Test 11 desk', agenda=agenda, timeperiod_exceptions_remote_url='http://example.com/sample.ics'
)
desk.save()
mocked_response = mock.Mock()
mocked_response.status_code = 403
mocked_get.return_value = mocked_response
def mocked_requests_http_forbidden_error(*args, **kwargs):
raise requests.HTTPError(response=mocked_response)
mocked_get.side_effect = mocked_requests_http_forbidden_error
call_command('sync_desks_timeperiod_exceptions')
out, err = capsys.readouterr()
assert err == 'unable to create timeperiod exceptions for "Test 11 desk": Failed to retrieve remote calendar (http://example.com/sample.ics, HTTP error 403).\n'
assert (
err
== 'unable to create timeperiod exceptions for "Test 11 desk": Failed to retrieve remote calendar (http://example.com/sample.ics, HTTP error 403).\n'
)
@mock.patch('chrono.agendas.models.requests.get')
def test_sync_desks_timeperiod_exceptions_from_changing_ics(mocked_get, caplog):
agenda = Agenda(label=u'Test 11 agenda')
agenda.save()
desk = Desk(label='Test 11 desk', agenda=agenda, timeperiod_exceptions_remote_url='http:example.com/sample.ics')
desk = Desk(
label='Test 11 desk', agenda=agenda, timeperiod_exceptions_remote_url='http:example.com/sample.ics'
)
desk.save()
mocked_response = mock.Mock()
mocked_response.text = ICS_SAMPLE
@ -397,6 +435,7 @@ END:VCALENDAR"""
call_command('sync_desks_timeperiod_exceptions')
assert not TimePeriodException.objects.filter(desk=desk).exists()
def test_base_meeting_duration():
agenda = Agenda(label='Meeting', kind='meetings')
agenda.save()
@ -427,14 +466,18 @@ def test_timeperiodexception_creation_from_ics_with_duration():
exceptions_count = desk.create_timeperiod_exceptions_from_ics(ICS_SAMPLE_WITH_DURATION)
assert exceptions_count == 2
assert TimePeriodException.objects.filter(desk=desk).count() == 2
assert set(TimePeriodException.objects.values_list('start_datetime', flat=True)) == set([
make_aware(datetime.datetime(2017, 8, 31, 19, 8, 0)),
make_aware(datetime.datetime(2017, 8, 30, 20, 8, 0)),
])
assert set(TimePeriodException.objects.values_list('end_datetime', flat=True)) == set([
make_aware(datetime.datetime(2017, 8, 31, 22, 34, 0)),
make_aware(datetime.datetime(2017, 9, 1, 0, 34, 0)),
])
assert set(TimePeriodException.objects.values_list('start_datetime', flat=True)) == set(
[
make_aware(datetime.datetime(2017, 8, 31, 19, 8, 0)),
make_aware(datetime.datetime(2017, 8, 30, 20, 8, 0)),
]
)
assert set(TimePeriodException.objects.values_list('end_datetime', flat=True)) == set(
[
make_aware(datetime.datetime(2017, 8, 31, 22, 34, 0)),
make_aware(datetime.datetime(2017, 9, 1, 0, 34, 0)),
]
)
@pytest.mark.freeze_time('2017-12-01')
@ -447,8 +490,9 @@ def test_timeperiodexception_creation_from_ics_with_recurrences_in_the_past():
desk.save()
assert desk.create_timeperiod_exceptions_from_ics(ICS_SAMPLE_WITH_RECURRENT_EVENT_IN_THE_PAST) == 2
assert TimePeriodException.objects.filter(desk=desk).count() == 2
assert set(TimePeriodException.objects.values_list('start_datetime', flat=True)) == set([
make_aware(datetime.datetime(2018, 1, 1)), make_aware(datetime.datetime(2019, 1, 1))])
assert set(TimePeriodException.objects.values_list('start_datetime', flat=True)) == set(
[make_aware(datetime.datetime(2018, 1, 1)), make_aware(datetime.datetime(2019, 1, 1))]
)
assert desk.create_timeperiod_exceptions_from_ics(ICS_SAMPLE_WITH_RECURRENT_EVENT_IN_THE_PAST) == 0

File diff suppressed because it is too large Load Diff

View File

@ -3,13 +3,16 @@ import pytest
from chrono.api.utils import Response
@pytest.mark.parametrize('data, expected', [
(None, None),
({}, {}),
({'reason': 'foo'}, {'reason': 'foo'}),
({'err_class': 'foo'}, {'err_class': 'foo', 'reason': 'foo'}),
({'bar': 'foo'}, {'bar': 'foo'}),
])
@pytest.mark.parametrize(
'data, expected',
[
(None, None),
({}, {}),
({'reason': 'foo'}, {'reason': 'foo'}),
({'err_class': 'foo'}, {'err_class': 'foo', 'reason': 'foo'}),
({'bar': 'foo'}, {'bar': 'foo'}),
],
)
def test_response_data(data, expected):
resp = Response(data=data)
assert resp.data == expected

View File

@ -52,23 +52,31 @@ def test_timeperiod_data_migrations():
Event = old_apps.get_model(app, 'Event')
agenda = Agenda.objects.create(label='foo', slug='foo', kind='meetings')
agenda2 = Agenda.objects.create(label='bar', slug='bar', kind='events')
TimePeriod.objects.create(agenda=agenda, weekday=1,
start_time=datetime.time(8, 0),
end_time=datetime.time(12, 0))
TimePeriod.objects.create(agenda=agenda, weekday=2,
start_time=datetime.time(8, 0),
end_time=datetime.time(10, 0))
TimePeriod.objects.create(agenda=agenda, weekday=3,
start_time=datetime.time(9, 0),
end_time=datetime.time(12, 0))
meeting_type = MeetingType.objects.create(agenda=agenda, label='foo',
slug='foo', duration=60)
Event.objects.create(agenda=agenda, places=1, meeting_type=meeting_type,
start_datetime=make_aware(datetime.datetime(2017, 5, 22, 9, 30)))
Event.objects.create(agenda=agenda, places=1, meeting_type=meeting_type,
start_datetime=make_aware(datetime.datetime(2017, 5, 22, 10, 0)))
Event.objects.create(agenda=agenda2, places=5,
start_datetime=make_aware(datetime.datetime(2017, 5, 22, 10, 0)))
TimePeriod.objects.create(
agenda=agenda, weekday=1, start_time=datetime.time(8, 0), end_time=datetime.time(12, 0)
)
TimePeriod.objects.create(
agenda=agenda, weekday=2, start_time=datetime.time(8, 0), end_time=datetime.time(10, 0)
)
TimePeriod.objects.create(
agenda=agenda, weekday=3, start_time=datetime.time(9, 0), end_time=datetime.time(12, 0)
)
meeting_type = MeetingType.objects.create(agenda=agenda, label='foo', slug='foo', duration=60)
Event.objects.create(
agenda=agenda,
places=1,
meeting_type=meeting_type,
start_datetime=make_aware(datetime.datetime(2017, 5, 22, 9, 30)),
)
Event.objects.create(
agenda=agenda,
places=1,
meeting_type=meeting_type,
start_datetime=make_aware(datetime.datetime(2017, 5, 22, 10, 0)),
)
Event.objects.create(
agenda=agenda2, places=5, start_datetime=make_aware(datetime.datetime(2017, 5, 22, 10, 0))
)
executor.loader.build_graph()
executor.migrate(migrate_to)
new_apps = executor.loader.project_state(migrate_to).apps

View File

@ -16,8 +16,7 @@ from django.core.management import call_command, CommandError
from django.utils.encoding import force_bytes
from django.utils.timezone import make_aware
from chrono.agendas.models import (Agenda, Event, TimePeriod, Desk,
TimePeriodException, AgendaImportError)
from chrono.agendas.models import Agenda, Event, TimePeriod, Desk, TimePeriodException, AgendaImportError
from chrono.manager.utils import import_site
from test_api import some_data, meetings_agenda, time_zone, mock_now
@ -40,10 +39,7 @@ def test_import_export(app, some_data, meetings_agenda):
desk = meetings_agenda.desk_set.first()
tpx_start = make_aware(datetime.datetime(2017, 5, 22, 8, 0))
tpx_end = make_aware(datetime.datetime(2017, 5, 22, 12, 30))
TimePeriodException.objects.create(
desk=desk,
start_datetime=tpx_start,
end_datetime=tpx_end)
TimePeriodException.objects.create(desk=desk, start_datetime=tpx_start, end_datetime=tpx_end)
output = get_output_of_command('export_site')
assert len(json.loads(output)['agendas']) == 3
import_site(data={}, clean=True)
@ -77,15 +73,14 @@ def test_import_export(app, some_data, meetings_agenda):
event.save()
desk, _ = Desk.objects.get_or_create(agenda=agenda2, label='Desk A', slug='desk-a')
timeperiod = TimePeriod(
desk=desk,
weekday=2,
start_time=datetime.time(10, 0),
end_time=datetime.time(11, 0))
desk=desk, weekday=2, start_time=datetime.time(10, 0), end_time=datetime.time(11, 0)
)
timeperiod.save()
exception = TimePeriodException(
desk=desk,
start_datetime=make_aware(datetime.datetime(2017, 5, 22, 8, 0)),
end_datetime=make_aware(datetime.datetime(2017, 5, 22, 12, 30)))
end_datetime=make_aware(datetime.datetime(2017, 5, 22, 12, 30)),
)
exception.save()
import_site(json.loads(output), overwrite=True)
@ -97,15 +92,14 @@ def test_import_export(app, some_data, meetings_agenda):
event = Event(agenda=agenda1, start_datetime=make_aware(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))
weekday=2, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(11, 0)
)
timeperiod.save()
exception = TimePeriodException(
desk=desk,
start_datetime=make_aware(datetime.datetime(2017, 5, 22, 8, 0)),
end_datetime=make_aware(datetime.datetime(2017, 5, 22, 12, 30)))
end_datetime=make_aware(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

View File

@ -16,12 +16,12 @@ from webtest import Upload
from chrono.wsgi import application
from chrono.agendas.models import (Agenda, Event, Booking, MeetingType,
TimePeriod, Desk, TimePeriodException)
from chrono.agendas.models import Agenda, Event, Booking, MeetingType, TimePeriod, Desk, TimePeriodException
from chrono.manager.utils import export_site
pytestmark = pytest.mark.django_db
@pytest.fixture
def simple_user():
try:
@ -30,6 +30,7 @@ def simple_user():
user = User.objects.create_user('user', password='user')
return user
@pytest.fixture
def manager_user():
try:
@ -42,6 +43,7 @@ def manager_user():
user.groups.set([group])
return user
@pytest.fixture
def admin_user():
try:
@ -50,17 +52,20 @@ def admin_user():
user = User.objects.create_superuser('admin', email=None, password='admin')
return user
@pytest.fixture
def api_user():
try:
user = User.objects.get(username='api-user')
except User.DoesNotExist:
user = User.objects.create(username='john.doe',
first_name=u'John', last_name=u'Doe', email='john.doe@example.net')
user = User.objects.create(
username='john.doe', first_name=u'John', last_name=u'Doe', email='john.doe@example.net'
)
user.set_password('password')
user.save()
return user
def login(app, username='admin', password='admin'):
login_page = app.get('/login/')
login_form = login_page.forms[0]
@ -70,15 +75,18 @@ def login(app, username='admin', password='admin'):
assert resp.status_int == 302
return app
def test_unlogged_access(app):
# connect while not being logged in
assert app.get('/manage/', status=302).location.endswith('/login/?next=/manage/')
def test_simple_user_access(app, simple_user):
# connect while being logged as a simple user, access should be forbidden
app = login(app, username='user', password='user')
assert app.get('/manage/', status=403)
def test_manager_user_access(app, manager_user):
# connect while being logged as a manager user, access should be granted if
# there's at least an agenda that is viewable or editable.
@ -99,20 +107,24 @@ def test_manager_user_access(app, manager_user):
agenda.save()
assert app.get('/manage/', status=200)
def test_home_redirect(app):
assert app.get('/', status=302).location.endswith('/manage/')
def test_access(app, admin_user):
app = login(app)
resp = app.get('/manage/', status=200)
assert '<h2>Agendas</h2>' in resp.text
assert "This site doesn't have any agenda yet." in resp.text
def test_logout(app, admin_user):
app = login(app)
app.get('/logout/')
assert app.get('/manage/', status=302).location.endswith('/login/?next=/manage/')
def test_menu_json(app, admin_user):
app = login(app)
resp = app.get('/manage/menu.json', status=200)
@ -123,6 +135,7 @@ def test_menu_json(app, admin_user):
assert resp2.text == 'Q(%s);' % resp.text
assert resp2.content_type == 'application/javascript'
def test_view_agendas_as_manager(app, manager_user):
agenda = Agenda(label=u'Foo Bar')
agenda.view_role = manager_user.groups.all()[0]
@ -156,6 +169,7 @@ def test_view_agendas_as_manager(app, manager_user):
# check it gives a 404 on unknown agendas
resp = app.get('/manage/agendas/%s/settings' % '9999', status=404)
def test_add_agenda(app, admin_user):
app = login(app)
resp = app.get('/manage/', status=200)
@ -169,6 +183,7 @@ def test_add_agenda(app, admin_user):
assert 'Foo bar' in resp.text
assert '<h2>Settings' in resp.text
def test_add_agenda_as_manager(app, manager_user):
# open /manage/ access to manager_user, and check agenda creation is not
# allowed.
@ -179,6 +194,7 @@ def test_add_agenda_as_manager(app, manager_user):
resp = app.get('/manage/', status=200)
resp = app.get('/manage/agendas/add/', status=403)
def test_options_agenda(app, admin_user):
agenda = Agenda(label=u'Foo bar')
agenda.save()
@ -194,6 +210,7 @@ def test_options_agenda(app, admin_user):
assert 'Foo baz' in resp.text
assert '<h2>Settings' in resp.text
def test_options_agenda_as_manager(app, manager_user):
agenda = Agenda(label=u'Foo bar')
agenda.view_role = manager_user.groups.all()[0]
@ -227,6 +244,7 @@ def test_options_agenda_as_manager(app, manager_user):
assert 'Foo baz' in resp.text
assert '<h2>Settings' in resp.text
def test_delete_agenda(app, admin_user):
agenda = Agenda(label=u'Foo bar')
agenda.save()
@ -239,11 +257,11 @@ def test_delete_agenda(app, admin_user):
resp = resp.follow()
assert not 'Foo bar' in resp.text
def test_delete_busy_agenda(app, admin_user):
agenda = Agenda(label=u'Foo bar')
agenda.save()
event = Event(start_datetime=now() + datetime.timedelta(days=10),
places=10, agenda=agenda)
event = Event(start_datetime=now() + datetime.timedelta(days=10), places=10, agenda=agenda)
event.save()
app = login(app)
@ -272,6 +290,7 @@ def test_delete_busy_agenda(app, admin_user):
booking.save()
resp = resp.form.submit(status=403)
def test_delete_agenda_as_manager(app, manager_user):
agenda = Agenda(label=u'Foo bar')
agenda.edit_role = manager_user.groups.all()[0]
@ -290,8 +309,7 @@ def test_delete_busy_desk(app, admin_user):
desk_a = Desk.objects.create(agenda=agenda, label='Desk A')
desk_b = Desk.objects.create(agenda=agenda, label='Desk B')
event = Event(start_datetime=now() + datetime.timedelta(days=10),
places=10, agenda=agenda, desk=desk_a)
event = Event(start_datetime=now() + datetime.timedelta(days=10), places=10, agenda=agenda, desk=desk_a)
event.save()
app = login(app)
@ -354,6 +372,7 @@ def test_add_event_on_missing_agenda(app, admin_user):
app = login(app)
app.get('/manage/agendas/%s/add-event' % '999', status=404)
def test_add_event_as_manager(app, manager_user):
agenda = Agenda(label=u'Foo bar')
agenda.view_role = manager_user.groups.all()[0]
@ -378,12 +397,11 @@ def test_add_event_as_manager(app, manager_user):
assert 'Feb. 15, 2016, 5 p.m.' in resp.text
assert '10 places' in resp.text
def test_edit_event(app, admin_user):
agenda = Agenda(label=u'Foo bar')
agenda.save()
event = Event(
start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)),
places=20, agenda=agenda)
event = Event(start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)), places=20, agenda=agenda)
event.save()
app = login(app)
resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=200)
@ -397,16 +415,17 @@ def test_edit_event(app, admin_user):
assert 'Feb. 16, 2016, 5 p.m.' in resp.text
assert '20 places' in resp.text
def test_edit_missing_event(app, admin_user):
app = login(app)
app.get('/manage/agendas/999/', status=404)
def test_edit_event_as_manager(app, manager_user):
agenda = Agenda(label=u'Foo bar')
agenda.view_role = manager_user.groups.all()[0]
agenda.save()
event = Event(start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)),
places=20, agenda=agenda)
event = Event(start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)), places=20, agenda=agenda)
event.save()
app = login(app, username='manager', password='manager')
resp = app.get('/manage/events/%s/' % event.id, status=403)
@ -424,11 +443,11 @@ def test_edit_event_as_manager(app, manager_user):
assert 'Feb. 16, 2016, 5 p.m.' in resp.text
assert '20 places' in resp.text
def test_booked_places(app, admin_user):
agenda = Agenda(label=u'Foo bar')
agenda.save()
event = Event(start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)),
places=10, agenda=agenda)
event = Event(start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)), places=10, agenda=agenda)
event.save()
Booking(event=event).save()
Booking(event=event).save()
@ -437,11 +456,11 @@ def test_booked_places(app, admin_user):
assert '10 places' in resp.text
assert '2 booked places' in resp.text
def test_event_classes(app, admin_user):
agenda = Agenda(label=u'Foo bar')
agenda.save()
event = Event(start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)),
places=10, agenda=agenda)
event = Event(start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)), places=10, agenda=agenda)
event.save()
for i in range(2):
Booking(event=event).save()
@ -463,11 +482,11 @@ def test_event_classes(app, admin_user):
assert 'full' in resp.text
assert 'overbooking' in resp.text
def test_delete_event(app, admin_user):
agenda = Agenda(label=u'Foo bar')
agenda.save()
event = Event(start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)),
places=10, agenda=agenda)
event = Event(start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)), places=10, agenda=agenda)
event.save()
app = login(app)
@ -478,11 +497,11 @@ def test_delete_event(app, admin_user):
assert resp.location.endswith('/manage/agendas/%s/settings' % agenda.id)
assert Event.objects.count() == 0
def test_delete_busy_event(app, admin_user):
agenda = Agenda(label=u'Foo bar')
agenda.save()
event = Event(start_datetime=now() + datetime.timedelta(days=10),
places=10, agenda=agenda)
event = Event(start_datetime=now() + datetime.timedelta(days=10), places=10, agenda=agenda)
event.save()
app = login(app)
@ -511,12 +530,12 @@ def test_delete_busy_event(app, admin_user):
booking.save()
resp = resp.form.submit(status=403)
def test_delete_event_as_manager(app, manager_user):
agenda = Agenda(label=u'Foo bar')
agenda.edit_role = manager_user.groups.all()[0]
agenda.save()
event = Event(start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)),
places=10, agenda=agenda)
event = Event(start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)), places=10, agenda=agenda)
event.save()
app = login(app, username='manager', password='manager')
@ -527,6 +546,7 @@ def test_delete_event_as_manager(app, manager_user):
assert resp.location.endswith('/manage/agendas/%s/settings' % agenda.id)
assert Event.objects.count() == 0
def test_import_events(app, admin_user):
agenda = Agenda(label=u'Foo bar')
agenda.save()
@ -590,8 +610,9 @@ def test_import_events(app, admin_user):
Event.objects.all().delete()
resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200)
resp.form['events_csv_file'] = Upload('t.csv',
u'2016-09-16,18:00,10,5,éléphant'.encode('utf-8'), 'text/csv')
resp.form['events_csv_file'] = Upload(
't.csv', u'2016-09-16,18:00,10,5,éléphant'.encode('utf-8'), 'text/csv'
)
resp = resp.form.submit(status=302)
assert Event.objects.count() == 1
assert Event.objects.all()[0].start_datetime == make_aware(datetime.datetime(2016, 9, 16, 18, 0))
@ -601,8 +622,9 @@ def test_import_events(app, admin_user):
Event.objects.all().delete()
resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200)
resp.form['events_csv_file'] = Upload('t.csv',
u'2016-09-16,18:00,10,5,éléphant'.encode('iso-8859-15'), 'text/csv')
resp.form['events_csv_file'] = Upload(
't.csv', u'2016-09-16,18:00,10,5,éléphant'.encode('iso-8859-15'), 'text/csv'
)
resp = resp.form.submit(status=302)
assert Event.objects.count() == 1
assert Event.objects.all()[0].start_datetime == make_aware(datetime.datetime(2016, 9, 16, 18, 0))
@ -612,8 +634,9 @@ def test_import_events(app, admin_user):
Event.objects.all().delete()
resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200)
resp.form['events_csv_file'] = Upload('t.csv',
u'2016-09-16,18:00,10,5,éléphant'.encode('eucjp'), 'text/csv')
resp.form['events_csv_file'] = Upload(
't.csv', u'2016-09-16,18:00,10,5,éléphant'.encode('eucjp'), 'text/csv'
)
resp = resp.form.submit(status=302)
assert Event.objects.count() == 1
assert Event.objects.all()[0].start_datetime == make_aware(datetime.datetime(2016, 9, 16, 18, 0))
@ -623,18 +646,23 @@ def test_import_events(app, admin_user):
Event.objects.all().delete()
resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200)
resp.form['events_csv_file'] = Upload('t.csv', b'date,time,etc.\n'
b'2016-09-16,18:00,10,5,bla bla bla\n'
b'\n'
b'2016-09-19,18:00,10', 'text/csv')
resp.form['events_csv_file'] = Upload(
't.csv',
b'date,time,etc.\n' b'2016-09-16,18:00,10,5,bla bla bla\n' b'\n' b'2016-09-19,18:00,10',
'text/csv',
)
resp = resp.form.submit(status=302)
assert Event.objects.count() == 2
Event.objects.all().delete()
resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200)
resp.form['events_csv_file'] = Upload('t.csv', '"date"\t"time"\t"etc."\n'
'"2016-09-16"\t"18:00"\t"10"\t"5"\t"éléphant"\n'
'"2016-09-19"\t"18:00"\t"10"'.encode('iso-8859-15'), 'text/csv')
resp.form['events_csv_file'] = Upload(
't.csv',
'"date"\t"time"\t"etc."\n'
'"2016-09-16"\t"18:00"\t"10"\t"5"\t"éléphant"\n'
'"2016-09-19"\t"18:00"\t"10"'.encode('iso-8859-15'),
'text/csv',
)
resp = resp.form.submit(status=302)
assert Event.objects.count() == 2
Event.objects.all().delete()
@ -671,6 +699,7 @@ def test_add_meetings_agenda(app, admin_user):
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()
@ -693,6 +722,7 @@ def test_meetings_agenda_add_meeting_type(app, admin_user):
resp = resp.form.submit()
assert MeetingType.objects.get(agenda=agenda).duration == 30
def test_meetings_agenda_delete_meeting_type(app, admin_user):
agenda = Agenda(label=u'Foo bar', kind='meetings')
agenda.save()
@ -709,6 +739,7 @@ def test_meetings_agenda_delete_meeting_type(app, admin_user):
assert resp.location.endswith('/manage/agendas/%s/settings' % agenda.id)
assert MeetingType.objects.count() == 0
def test_meetings_agenda_add_time_period(app, admin_user):
agenda = Agenda(label=u'Foo bar', kind='meetings')
agenda.save()
@ -771,6 +802,7 @@ def test_meetings_agenda_add_time_period(app, admin_user):
resp = resp.form.submit()
assert TimePeriod.objects.filter(desk=desk).count() == 4
def test_meetings_agenda_delete_time_period(app, admin_user):
agenda = Agenda(label=u'Foo bar', kind='meetings')
agenda.save()
@ -778,9 +810,9 @@ def test_meetings_agenda_delete_time_period(app, admin_user):
MeetingType(agenda=agenda, label='Blah').save()
desk = Desk.objects.create(agenda=agenda, label='Desk A')
time_period = TimePeriod(desk=desk, weekday=2,
start_time=datetime.time(10, 0),
end_time=datetime.time(18, 0))
time_period = TimePeriod(
desk=desk, weekday=2, start_time=datetime.time(10, 0), end_time=datetime.time(18, 0)
)
time_period.save()
app = login(app)
@ -811,8 +843,9 @@ def test_meetings_agenda_add_time_period_as_manager(app, manager_user):
resp = app.get('/manage/agendas/%d/settings' % agenda.id, status=403)
MeetingType(agenda=agenda, label='Blah').save()
app.get('/manage/agendas/%d/desk/%d/add-time-period' % (agenda.id, desk.id), status=403)
time_period = TimePeriod(desk=desk, weekday=0, start_time=datetime.time(9, 0),
end_time=datetime.time(12, 0))
time_period = TimePeriod(
desk=desk, weekday=0, start_time=datetime.time(9, 0), end_time=datetime.time(12, 0)
)
time_period.save()
resp = app.get('/manage/agendas/%d/' % agenda.id)
app.get('/manage/timeperiods/%d/edit' % time_period.id, status=403)
@ -915,8 +948,12 @@ def test_meetings_agenda_add_time_period_exception(app, admin_user):
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)
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)
@ -927,7 +964,9 @@ def test_meetings_agenda_add_time_period_exception(app, admin_user):
assert TimePeriodException.objects.count() == 2
assert 'Exception 1' in resp.text
assert 'Exception 2' not in resp.text
resp = resp.click(href="/manage/time-period-exceptions/%d/exception-extract-list" % agenda.desk_set.first().pk)
resp = resp.click(
href="/manage/time-period-exceptions/%d/exception-extract-list" % agenda.desk_set.first().pk
)
assert 'Exception 1' in resp.text
assert 'Exception 2' in resp.text
@ -936,16 +975,18 @@ def test_meetings_agenda_add_time_period_exception_when_booking_exists(app, admi
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, desk=desk,
start_datetime=make_aware(datetime.datetime(2017, 5, 22, 10, 30)))
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, desk=desk, start_datetime=make_aware(datetime.datetime(2017, 5, 22, 10, 30))
)
Booking.objects.create(event=event)
login(app)
resp = app.get('/manage/agendas/%d/' % agenda.pk).follow()
resp = resp.click('Settings')
resp = resp.click('Add a time period exception')
resp = resp.form.submit() # submit empty form
resp = resp.form.submit() # submit empty form
# fields should be marked with errors
assert resp.text.count('This field is required.') == 2
# try again with data in fields
@ -957,8 +998,9 @@ def test_meetings_agenda_add_time_period_exception_when_booking_exists(app, admi
# check it's possible to add an exception on another desk
desk = Desk.objects.create(agenda=agenda, label='Desk B')
TimePeriod.objects.create(weekday=1, desk=desk,
start_time=datetime.time(10, 0), end_time=datetime.time(12, 0))
TimePeriod.objects.create(
weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)
)
resp = app.get('/manage/agendas/%d/' % agenda.pk).follow()
resp = resp.click('Settings')
resp = resp.click('Add a time period exception', href='desk/%s/' % desk.id)
@ -967,16 +1009,20 @@ def test_meetings_agenda_add_time_period_exception_when_booking_exists(app, admi
resp = resp.form.submit()
assert TimePeriodException.objects.count() == 1
def test_meetings_agenda_add_time_period_exception_when_cancelled_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=make_aware(datetime.datetime(2017, 5, 22, 10, 30)))
Booking.objects.create(event=event,
cancellation_datetime=make_aware(datetime.datetime(2017, 5, 20, 10, 30)))
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=make_aware(datetime.datetime(2017, 5, 22, 10, 30))
)
Booking.objects.create(
event=event, cancellation_datetime=make_aware(datetime.datetime(2017, 5, 20, 10, 30))
)
login(app)
resp = app.get('/manage/agendas/%d/' % agenda.pk).follow()
resp = resp.click('Settings')
@ -987,12 +1033,14 @@ def test_meetings_agenda_add_time_period_exception_when_cancelled_booking_exists
assert 'One or several bookings exists within this time slot.' not in resp.text
assert TimePeriodException.objects.count() == 1
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))
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).follow()
resp = resp.click('Settings')
@ -1007,8 +1055,9 @@ 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))
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).follow()
resp = resp.click('Settings')
@ -1022,8 +1071,12 @@ def test_meetings_agenda_delete_time_period_exception(app, admin_user):
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)
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().follow()
@ -1035,7 +1088,8 @@ def test_meetings_agenda_delete_time_period_exception(app, admin_user):
label='Future Exception',
desk=desk,
start_datetime=now() + datetime.timedelta(days=1),
end_datetime=now() + datetime.timedelta(days=2))
end_datetime=now() + datetime.timedelta(days=2),
)
resp = app.get('/manage/time-period-exceptions/%d/exception-list' % desk.pk)
resp = resp.click(href='/manage/time-period-exceptions/%d/delete' % time_period_exception.pk)
resp = resp.form.submit(
@ -1048,23 +1102,27 @@ def test_exception_list(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))
TimePeriod.objects.create(
weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)
)
past_exception = TimePeriodException.objects.create(
label='Past Exception',
desk=desk,
start_datetime=now() - datetime.timedelta(days=2),
end_datetime=now() - datetime.timedelta(days=1))
end_datetime=now() - datetime.timedelta(days=1),
)
current_exception = TimePeriodException.objects.create(
label='Current Exception',
desk=desk,
start_datetime=now() - datetime.timedelta(days=1),
end_datetime=now() + datetime.timedelta(days=1))
end_datetime=now() + datetime.timedelta(days=1),
)
future_exception = TimePeriodException.objects.create(
label='Future Exception',
desk=desk,
start_datetime=now() + datetime.timedelta(days=1),
end_datetime=now() + datetime.timedelta(days=2))
end_datetime=now() + datetime.timedelta(days=2),
)
login(app)
resp = app.get('/manage/agendas/%d/settings' % agenda.pk)
@ -1092,8 +1150,9 @@ def test_agenda_import_time_period_exception_from_ics(app, admin_user):
resp = resp.click('Settings')
assert 'Import exceptions from .ics' not in resp.text
TimePeriod.objects.create(weekday=1, desk=desk,
start_time=datetime.time(10, 0), end_time=datetime.time(12, 0))
TimePeriod.objects.create(
weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)
)
resp = app.get('/manage/agendas/%d/' % agenda.pk).follow()
resp = resp.click('Settings')
@ -1150,9 +1209,9 @@ def test_agenda_import_time_period_exception_from_ics_recurrent(app, admin_user)
agenda = Agenda.objects.create(label='Example', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Test Desk')
MeetingType(agenda=agenda, label='Foo').save()
TimePeriod.objects.create(weekday=1, desk=desk,
start_time=datetime.time(10, 0),
end_time=datetime.time(12, 0))
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).follow()
resp = resp.click('Settings')
@ -1182,8 +1241,9 @@ def test_agenda_import_time_period_exception_with_remote_ics(mocked_get, app, ad
resp = resp.click('Settings')
assert 'Import exceptions from .ics' not in resp.text
TimePeriod.objects.create(weekday=1, desk=desk,
start_time=datetime.time(10, 0), end_time=datetime.time(12, 0))
TimePeriod.objects.create(
weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)
)
resp = app.get('/manage/agendas/%d/' % agenda.pk).follow()
resp = resp.click('Settings')
@ -1193,7 +1253,7 @@ def test_agenda_import_time_period_exception_with_remote_ics(mocked_get, app, ad
assert 'ics_url' in resp.form.fields
resp.form['ics_url'] = 'http://example.com/foo.ics'
mocked_response = mock.Mock()
mocked_response.text = """BEGIN:VCALENDAR
mocked_response.text = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//foo.bar//EN
BEGIN:VEVENT
@ -1213,8 +1273,10 @@ END:VCALENDAR"""
resp = resp.click('upload')
resp.form['ics_url'] = ''
resp = resp.form.submit(status=302)
assert not TimePeriodException.objects.filter(desk=desk,
external_id='desk-%s:random-event-id' % desk.id).exists()
assert not TimePeriodException.objects.filter(
desk=desk, external_id='desk-%s:random-event-id' % desk.id
).exists()
@mock.patch('chrono.agendas.models.requests.get')
def test_agenda_import_time_period_exception_with_remote_ics_no_events(mocked_get, app, admin_user):
@ -1226,15 +1288,16 @@ def test_agenda_import_time_period_exception_with_remote_ics_no_events(mocked_ge
resp = resp.click('Settings')
assert 'Import exceptions from .ics' not in resp.text
TimePeriod.objects.create(weekday=1, desk=desk,
start_time=datetime.time(10, 0), end_time=datetime.time(12, 0))
TimePeriod.objects.create(
weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)
)
resp = app.get('/manage/agendas/%d/' % agenda.pk).follow()
resp = resp.click('Settings')
resp = resp.click('upload')
resp.form['ics_url'] = 'http://example.com/foo.ics'
mocked_response = mock.Mock()
mocked_response.text = """BEGIN:VCALENDAR
mocked_response.text = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//foo.bar//EN
BEGIN:VEVENT
@ -1257,8 +1320,7 @@ END:VCALENDAR"""
resp = resp.click('Settings')
resp = resp.click('upload')
resp = resp.form.submit(status=302)
assert not TimePeriodException.objects.filter(desk=desk,
external_id='random-event-id').exists()
assert not TimePeriodException.objects.filter(desk=desk, external_id='random-event-id').exists()
@mock.patch('chrono.agendas.models.requests.get')
@ -1271,15 +1333,16 @@ def test_agenda_update_time_period_exception_from_remote_ics(mocked_get, app, ad
resp = resp.click('Settings')
assert 'Import exceptions from .ics' not in resp.text
TimePeriod.objects.create(weekday=1, desk=desk,
start_time=datetime.time(10, 0), end_time=datetime.time(12, 0))
TimePeriod.objects.create(
weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)
)
resp = app.get('/manage/agendas/%d/' % agenda.pk).follow()
resp = resp.click('Settings')
resp = resp.click('upload')
resp.form['ics_url'] = 'http://example.com/foo.ics'
mocked_response = mock.Mock()
mocked_response.text = """BEGIN:VCALENDAR
mocked_response.text = """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//foo.bar//EN
BEGIN:VEVENT
@ -1316,8 +1379,11 @@ END:VCALENDAR"""
resp = resp.form.submit(status=302)
assert TimePeriodException.objects.filter(desk=desk).count() == 1
@mock.patch('chrono.agendas.models.requests.get')
def test_agenda_import_time_period_exception_from_remote_ics_with_connection_error(mocked_get, app, admin_user):
def test_agenda_import_time_period_exception_from_remote_ics_with_connection_error(
mocked_get, app, admin_user
):
agenda = Agenda.objects.create(label='New Example', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='New Desk')
MeetingType(agenda=agenda, label='Bar').save()
@ -1326,8 +1392,9 @@ def test_agenda_import_time_period_exception_from_remote_ics_with_connection_err
resp = resp.click('Settings')
assert 'Import exceptions from .ics' not in resp.text
TimePeriod.objects.create(weekday=1, desk=desk,
start_time=datetime.time(10, 0), end_time=datetime.time(12, 0))
TimePeriod.objects.create(
weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)
)
resp = app.get('/manage/agendas/%d/' % agenda.pk).follow()
resp = resp.click('Settings')
@ -1338,12 +1405,15 @@ def test_agenda_import_time_period_exception_from_remote_ics_with_connection_err
resp.form['ics_url'] = 'http://example.com/foo.ics'
mocked_response = mock.Mock()
mocked_get.return_value = mocked_response
def mocked_requests_connection_error(*args, **kwargs):
raise requests.exceptions.ConnectionError('unreachable')
mocked_get.side_effect = mocked_requests_connection_error
resp = resp.form.submit(status=200)
assert 'Failed to retrieve remote calendar (http://example.com/foo.ics, unreachable).' in resp.text
@mock.patch('chrono.agendas.models.requests.get')
def test_agenda_import_time_period_exception_from_forbidden_remote_ics(mocked_get, app, admin_user):
agenda = Agenda.objects.create(label='New Example', kind='meetings')
@ -1354,8 +1424,9 @@ def test_agenda_import_time_period_exception_from_forbidden_remote_ics(mocked_ge
resp = resp.click('Settings')
assert 'Import exceptions from .ics' not in resp.text
TimePeriod.objects.create(weekday=1, desk=desk,
start_time=datetime.time(10, 0), end_time=datetime.time(12, 0))
TimePeriod.objects.create(
weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)
)
resp = app.get('/manage/agendas/%d/' % agenda.pk).follow()
resp = resp.click('Settings')
@ -1364,12 +1435,15 @@ def test_agenda_import_time_period_exception_from_forbidden_remote_ics(mocked_ge
mocked_response = mock.Mock()
mocked_response.status_code = 403
mocked_get.return_value = mocked_response
def mocked_requests_http_forbidden_error(*args, **kwargs):
raise requests.exceptions.HTTPError(response=mocked_response)
mocked_get.side_effect = mocked_requests_http_forbidden_error
resp = resp.form.submit(status=200)
assert 'Failed to retrieve remote calendar (http://example.com/foo.ics, HTTP error 403).' in resp.text
@mock.patch('chrono.agendas.models.requests.get')
def test_agenda_import_time_period_exception_from_remote_ics_with_ssl_error(mocked_get, app, admin_user):
agenda = Agenda.objects.create(label='New Example', kind='meetings')
@ -1379,8 +1453,9 @@ def test_agenda_import_time_period_exception_from_remote_ics_with_ssl_error(mock
resp = app.get('/manage/agendas/%d/' % agenda.pk).follow()
resp = resp.click('Settings')
assert 'Import exceptions from .ics' not in resp.text
TimePeriod.objects.create(weekday=1, desk=desk,
start_time=datetime.time(10, 0), end_time=datetime.time(12, 0))
TimePeriod.objects.create(
weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)
)
resp = app.get('/manage/agendas/%d/' % agenda.pk).follow()
resp = resp.click('Settings')
@ -1388,12 +1463,15 @@ def test_agenda_import_time_period_exception_from_remote_ics_with_ssl_error(mock
resp.form['ics_url'] = 'https://example.com/foo.ics'
mocked_response = mock.Mock()
mocked_get.return_value = mocked_response
def mocked_requests_http_ssl_error(*args, **kwargs):
raise requests.exceptions.SSLError('SSL error')
mocked_get.side_effect = mocked_requests_http_ssl_error
resp = resp.form.submit(status=200)
assert 'Failed to retrieve remote calendar (https://example.com/foo.ics, SSL error).' in resp.text
def test_agenda_day_view(app, admin_user, manager_user, api_user):
agenda = Agenda.objects.create(label='New Example', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='New Desk')
@ -1409,9 +1487,9 @@ def test_agenda_day_view(app, admin_user, manager_user, api_user):
resp = resp.follow()
assert 'No opening hours this day.' in resp.text # no time pediod
timeperiod = TimePeriod(desk=desk, weekday=today.weekday(),
start_time=datetime.time(10, 0),
end_time=datetime.time(18, 0))
timeperiod = TimePeriod(
desk=desk, weekday=today.weekday(), start_time=datetime.time(10, 0), end_time=datetime.time(18, 0)
)
timeperiod.save()
resp = app.get('/manage/agendas/%s/' % agenda.id, status=302).follow()
assert not 'No opening hours this day.' in resp.text
@ -1434,14 +1512,12 @@ def test_agenda_day_view(app, admin_user, manager_user, api_user):
booking_url = resp.json['data'][0]['api']['fillslot_url']
booking_url2 = resp.json['data'][2]['api']['fillslot_url']
resp = app.post(booking_url)
resp = app.post_json(booking_url2,
params={'label': 'foo', 'user': 'bar', 'url': 'http://baz/'})
resp = app.post_json(booking_url2, params={'label': 'foo', 'user': 'bar', 'url': 'http://baz/'})
app.reset()
login(app)
date = Booking.objects.all()[0].event.start_datetime
resp = app.get('/manage/agendas/%s/%d/%d/%d/' % (
agenda.id, date.year, date.month, date.day))
resp = app.get('/manage/agendas/%s/%d/%d/%d/' % (agenda.id, date.year, date.month, date.day))
assert resp.text.count('div class="booking') == 2
assert 'hourspan-2' in resp.text # table CSS class
assert 'height: 50%; top: 0%;' in resp.text # booking cells
@ -1450,8 +1526,7 @@ def test_agenda_day_view(app, admin_user, manager_user, api_user):
# (and visually this will give more room for events)
meetingtype = MeetingType(agenda=agenda, label='Baz', duration=15)
meetingtype.save()
resp = app.get('/manage/agendas/%s/%d/%d/%d/' % (
agenda.id, date.year, date.month, date.day))
resp = app.get('/manage/agendas/%s/%d/%d/%d/' % (agenda.id, date.year, date.month, date.day))
assert resp.text.count('div class="booking') == 2
assert 'hourspan-4' in resp.text # table CSS class
@ -1464,29 +1539,26 @@ def test_agenda_day_view(app, admin_user, manager_user, api_user):
app.reset()
login(app)
resp = app.get('/manage/agendas/%s/%d/%d/%d/' % (
agenda.id, date.year, date.month, date.day))
resp = app.get('/manage/agendas/%s/%d/%d/%d/' % (agenda.id, date.year, date.month, date.day))
assert resp.text.count('div class="booking') == 1
# wrong type
agenda2 = Agenda(label=u'Foo bar')
agenda2.save()
resp = app.get('/manage/agendas/%s/%d/%d/%d/' % (
agenda2.id, date.year, date.month, date.day), status=404)
resp = app.get('/manage/agendas/%s/%d/%d/%d/' % (agenda2.id, date.year, date.month, date.day), status=404)
# not enough permissions
agenda2.view_role = manager_user.groups.all()[0]
agenda2.save()
app.reset()
app = login(app, username='manager', password='manager')
resp = app.get('/manage/agendas/%s/%d/%d/%d/' % (
agenda.id, date.year, date.month, date.day), status=403)
resp = app.get('/manage/agendas/%s/%d/%d/%d/' % (agenda.id, date.year, date.month, date.day), status=403)
# just enough permissions
agenda.view_role = manager_user.groups.all()[0]
agenda.save()
resp = app.get('/manage/agendas/%s/%d/%d/%d/' % (
agenda.id, date.year, date.month, date.day), status=200)
resp = app.get('/manage/agendas/%s/%d/%d/%d/' % (agenda.id, date.year, date.month, date.day), status=200)
def test_agenda_day_view_late_meeting(app, admin_user, manager_user, api_user):
agenda = Agenda.objects.create(label='New Example', kind='meetings')
@ -1498,9 +1570,9 @@ def test_agenda_day_view_late_meeting(app, admin_user, manager_user, api_user):
today = datetime.date.today()
timeperiod = TimePeriod(desk=desk, weekday=today.weekday(),
start_time=datetime.time(10, 0),
end_time=datetime.time(23, 30))
timeperiod = TimePeriod(
desk=desk, weekday=today.weekday(), start_time=datetime.time(10, 0), end_time=datetime.time(23, 30)
)
timeperiod.save()
login(app)
@ -1508,6 +1580,7 @@ def test_agenda_day_view_late_meeting(app, admin_user, manager_user, api_user):
assert resp.text.count('<tr') == 15
assert '<th class="hour">11 p.m.</th>' in resp.text
def test_agenda_invalid_day_view(app, admin_user, manager_user, api_user):
agenda = Agenda.objects.create(label='New Example', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='New Desk')
@ -1520,6 +1593,7 @@ def test_agenda_invalid_day_view(app, admin_user, manager_user, api_user):
resp = app.get('/manage/agendas/%s/%d/%d/%d/' % (agenda.id, 2018, 11, 31), status=302)
assert resp.location.endswith('2018/11/30/')
def test_agenda_month_view(app, admin_user, manager_user, api_user):
agenda = Agenda.objects.create(label='Passeports', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Desk A')
@ -1535,20 +1609,20 @@ def test_agenda_month_view(app, admin_user, manager_user, api_user):
today = datetime.date.today()
assert resp.request.url.endswith('%s/%s/' % (today.year, today.month))
assert 'Day view' in resp.text # date view link should be present
assert 'Day view' in resp.text # date view link should be present
assert 'No opening hours this month.' in resp.text
today = datetime.date(2018, 11, 10) # fixed day
timeperiod_weekday = today.weekday()
timeperiod = TimePeriod(desk=desk, weekday=timeperiod_weekday,
start_time=datetime.time(10, 0),
end_time=datetime.time(18, 0))
timeperiod = TimePeriod(
desk=desk, weekday=timeperiod_weekday, start_time=datetime.time(10, 0), end_time=datetime.time(18, 0)
)
timeperiod.save()
resp = app.get('/manage/agendas/%s/%s/%s/' % (agenda.id, today.year, today.month))
assert not 'No opening hours this month.' in resp.text
assert not '<div class="booking' in resp.text
first_month_day = today.replace(day=1)
last_month_day = today.replace(day=1, month=today.month+1) - datetime.timedelta(days=1)
last_month_day = today.replace(day=1, month=today.month + 1) - datetime.timedelta(days=1)
start_week_number = first_month_day.isocalendar()[1]
end_week_number = last_month_day.isocalendar()[1]
weeks_number = end_week_number - start_week_number + 1
@ -1564,19 +1638,17 @@ def test_agenda_month_view(app, admin_user, manager_user, api_user):
booking_url = resp.json['data'][0]['api']['fillslot_url']
booking_url2 = resp.json['data'][2]['api']['fillslot_url']
booking = app.post(booking_url)
booking_2 = app.post_json(booking_url2,
params={'label': 'foo', 'user': 'bar', 'url': 'http://baz/'})
booking_2 = app.post_json(booking_url2, params={'label': 'foo', 'user': 'bar', 'url': 'http://baz/'})
app.reset()
login(app)
date = Booking.objects.all()[0].event.start_datetime
resp = app.get('/manage/agendas/%s/%d/%d/' % (
agenda.id, date.year, date.month))
assert resp.text.count('<div class="booking" style="left:1.0%;height:33.0%;') == 2 # booking cells
resp = app.get('/manage/agendas/%s/%d/%d/' % (agenda.id, date.year, date.month))
assert resp.text.count('<div class="booking" style="left:1.0%;height:33.0%;') == 2 # booking cells
desk = Desk.objects.create(agenda=agenda, label='Desk B')
timeperiod = TimePeriod(desk=desk, weekday=timeperiod_weekday,
start_time=datetime.time(10, 0),
end_time=datetime.time(18, 0))
timeperiod = TimePeriod(
desk=desk, weekday=timeperiod_weekday, start_time=datetime.time(10, 0), end_time=datetime.time(18, 0)
)
timeperiod.save()
app.reset()
@ -1613,6 +1685,7 @@ def test_agenda_month_view(app, admin_user, manager_user, api_user):
resp = app.get('/manage/agendas/%s/%s/%s/' % (agenda.id, today.year, today.month))
assert not 'No opening hours this month.' in resp.text
def test_agenda_month_view_dst_change(app, admin_user, manager_user, api_user):
agenda = Agenda.objects.create(label='Passeports', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Desk A')
@ -1621,9 +1694,9 @@ def test_agenda_month_view_dst_change(app, admin_user, manager_user, api_user):
meetingtype.save()
for weekday in range(0, 7): # open all mornings
TimePeriod(desk=desk, weekday=weekday,
start_time=datetime.time(9, 0),
end_time=datetime.time(12, 0)).save()
TimePeriod(
desk=desk, weekday=weekday, start_time=datetime.time(9, 0), end_time=datetime.time(12, 0)
).save()
login(app)
for date in ('2019-10-01', '2019-10-31'):
@ -1658,6 +1731,7 @@ def test_import_agenda_as_manager(app, manager_user):
resp = app.get('/manage/', status=200)
resp = app.get('/manage/agendas/import/', status=403)
def test_import_agenda(app, admin_user):
agenda = Agenda(label=u'Foo bar')
agenda.save()

View File

@ -1,5 +1,6 @@
from chrono.manager.widgets import DateTimeWidget, TimeWidget
def test_widgets_init():
DateTimeWidget()
TimeWidget()

View File

@ -6,6 +6,7 @@ from chrono.wsgi import application
pytestmark = pytest.mark.django_db
def test_sso(app):
with override_settings(MELLON_IDENTITY_PROVIDERS=[{'METADATA': 'x', 'ENTITY_ID': 'x'}]):
resp = app.get('/login/')
@ -14,6 +15,7 @@ def test_sso(app):
resp = app.get('/login/?next=/manage/')
assert resp.location.endswith('/accounts/mellon/login/?next=/manage/')
def test_slo(app):
with override_settings(MELLON_IDENTITY_PROVIDERS=[{'METADATA': 'x', 'ENTITY_ID': 'x'}]):
resp = app.get('/logout/')

View File

@ -18,13 +18,14 @@ def test_timeperiod_time_slots():
desk = Desk.objects.create(label='Desk 1', agenda=agenda)
meeting_type = MeetingType(duration=60, agenda=agenda)
meeting_type.save()
timeperiod = TimePeriod(desk=desk, weekday=0,
start_time=datetime.time(9, 0),
end_time=datetime.time(12, 0))
timeperiod = TimePeriod(
desk=desk, 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=meeting_type)
min_datetime=make_aware(datetime.datetime(2016, 9, 1)),
max_datetime=make_aware(datetime.datetime(2016, 10, 1)),
meeting_type=meeting_type,
)
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)
@ -35,52 +36,56 @@ def test_timeperiod_time_slots():
assert len(events) == 12
# another start before the timeperiod
timeperiod = TimePeriod(desk=desk, weekday=1,
start_time=datetime.time(9, 0),
end_time=datetime.time(12, 0))
timeperiod = TimePeriod(
desk=desk, 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=meeting_type)
min_datetime=make_aware(datetime.datetime(2016, 9, 1)),
max_datetime=make_aware(datetime.datetime(2016, 10, 1)),
meeting_type=meeting_type,
)
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(desk=desk, weekday=3,
start_time=datetime.time(9, 0),
end_time=datetime.time(12, 0))
timeperiod = TimePeriod(
desk=desk, 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=meeting_type)
min_datetime=make_aware(datetime.datetime(2016, 9, 1)),
max_datetime=make_aware(datetime.datetime(2016, 10, 1)),
meeting_type=meeting_type,
)
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(desk=desk, weekday=4,
start_time=datetime.time(9, 0),
end_time=datetime.time(12, 0))
timeperiod = TimePeriod(
desk=desk, 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=meeting_type)
min_datetime=make_aware(datetime.datetime(2016, 9, 1)),
max_datetime=make_aware(datetime.datetime(2016, 10, 1)),
meeting_type=meeting_type,
)
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(desk=desk, weekday=5,
start_time=datetime.time(9, 0),
end_time=datetime.time(12, 0))
timeperiod = TimePeriod(
desk=desk, 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=meeting_type)
min_datetime=make_aware(datetime.datetime(2016, 9, 1)),
max_datetime=make_aware(datetime.datetime(2016, 10, 1)),
meeting_type=meeting_type,
)
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)
@ -89,13 +94,14 @@ def test_timeperiod_time_slots():
# shorter duration -> double the events
meeting_type.duration = 30
meeting_type.save()
timeperiod = TimePeriod(desk=desk, weekday=5,
start_time=datetime.time(9, 0),
end_time=datetime.time(12, 0))
timeperiod = TimePeriod(
desk=desk, 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=meeting_type)
min_datetime=make_aware(datetime.datetime(2016, 9, 1)),
max_datetime=make_aware(datetime.datetime(2016, 10, 1)),
meeting_type=meeting_type,
)
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)
@ -105,28 +111,48 @@ def test_timeperiod_time_slots():
@override_settings(LANGUAGE_CODE='fr-fr')
def test_time_period_exception_as_string():
# single day
assert force_text(TimePeriodException(
start_datetime=make_aware(datetime.datetime(2018, 1, 18)),
end_datetime=make_aware(datetime.datetime(2018, 1, 19)))
) == u'18 jan. 2018'
assert (
force_text(
TimePeriodException(
start_datetime=make_aware(datetime.datetime(2018, 1, 18)),
end_datetime=make_aware(datetime.datetime(2018, 1, 19)),
)
)
== u'18 jan. 2018'
)
# multiple full days
assert force_text(TimePeriodException(
start_datetime=make_aware(datetime.datetime(2018, 1, 18)),
end_datetime=make_aware(datetime.datetime(2018, 1, 20)))
) == u'18 jan. 2018 → 20 jan. 2018'
assert (
force_text(
TimePeriodException(
start_datetime=make_aware(datetime.datetime(2018, 1, 18)),
end_datetime=make_aware(datetime.datetime(2018, 1, 20)),
)
)
== u'18 jan. 2018 → 20 jan. 2018'
)
# a few hours in a day
assert force_text(TimePeriodException(
start_datetime=make_aware(datetime.datetime(2018, 1, 18, 10, 0)),
end_datetime=make_aware(datetime.datetime(2018, 1, 18, 12, 0)))
) == u'18 jan. 2018 10:00 → 12:00'
assert (
force_text(
TimePeriodException(
start_datetime=make_aware(datetime.datetime(2018, 1, 18, 10, 0)),
end_datetime=make_aware(datetime.datetime(2018, 1, 18, 12, 0)),
)
)
== u'18 jan. 2018 10:00 → 12:00'
)
# multiple days and different times
assert force_text(TimePeriodException(
start_datetime=make_aware(datetime.datetime(2018, 1, 18, 10, 0)),
end_datetime=make_aware(datetime.datetime(2018, 1, 20, 12, 0)))
) == u'18 jan. 2018 10:00 → 20 jan. 2018 12:00'
assert (
force_text(
TimePeriodException(
start_datetime=make_aware(datetime.datetime(2018, 1, 18, 10, 0)),
end_datetime=make_aware(datetime.datetime(2018, 1, 20, 12, 0)),
)
)
== u'18 jan. 2018 10:00 → 20 jan. 2018 12:00'
)
def test_desk_opening_hours():
@ -139,18 +165,14 @@ def test_desk_opening_hours():
assert len(hours) == 0
# morning
TimePeriod(desk=desk, weekday=0,
start_time=datetime.time(9, 0),
end_time=datetime.time(12, 0)).save()
TimePeriod(desk=desk, weekday=0, start_time=datetime.time(9, 0), end_time=datetime.time(12, 0)).save()
hours = list(desk.get_opening_hours(datetime.date(2018, 1, 22)))
assert len(hours) == 1
assert hours[0].begin.time() == datetime.time(9, 0)
assert hours[0].end.time() == datetime.time(12, 0)
# and afternoon
TimePeriod(desk=desk, weekday=0,
start_time=datetime.time(14, 0),
end_time=datetime.time(17, 0)).save()
TimePeriod(desk=desk, weekday=0, start_time=datetime.time(14, 0), end_time=datetime.time(17, 0)).save()
hours = list(desk.get_opening_hours(datetime.date(2018, 1, 22)))
assert len(hours) == 2
assert hours[0].begin.time() == datetime.time(9, 0)
@ -161,9 +183,10 @@ def test_desk_opening_hours():
# full day exception
exception = TimePeriodException(
desk=desk,
start_datetime=make_aware(datetime.datetime(2018, 1, 22)),
end_datetime=make_aware(datetime.datetime(2018, 1, 23)))
desk=desk,
start_datetime=make_aware(datetime.datetime(2018, 1, 22)),
end_datetime=make_aware(datetime.datetime(2018, 1, 23)),
)
exception.save()
hours = list(desk.get_opening_hours(datetime.date(2018, 1, 22)))
@ -200,13 +223,14 @@ def test_timeperiod_midnight_overlap_time_slots():
desk = Desk.objects.create(label='Desk 1', agenda=agenda)
meeting_type = MeetingType(duration=120, agenda=agenda)
meeting_type.save()
timeperiod = TimePeriod(desk=desk, weekday=0,
start_time=datetime.time(21, 0),
end_time=datetime.time(23, 0))
timeperiod = TimePeriod(
desk=desk, weekday=0, start_time=datetime.time(21, 0), end_time=datetime.time(23, 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=meeting_type)
min_datetime=make_aware(datetime.datetime(2016, 9, 1)),
max_datetime=make_aware(datetime.datetime(2016, 10, 1)),
meeting_type=meeting_type,
)
events = list(sorted(events, key=lambda x: x.start_datetime))
assert events[0].start_datetime.timetuple()[:5] == (2016, 9, 5, 21, 0)
assert events[1].start_datetime.timetuple()[:5] == (2016, 9, 12, 21, 0)