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): class Migration(migrations.Migration):
dependencies = [ dependencies = []
]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Agenda', name='Agenda',
fields=[ 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')), ('label', models.CharField(max_length=50, verbose_name='Label')),
('slug', models.SlugField(verbose_name='Identifier')), ('slug', models.SlugField(verbose_name='Identifier')),
], ],
options={ options={'ordering': ['label'],},
'ordering': ['label'],
},
bases=(models.Model,), bases=(models.Model,),
), ),
] ]

View File

@ -14,14 +14,15 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='Event', name='Event',
fields=[ 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')), ('start_datetime', models.DateTimeField(verbose_name='Date/time')),
('places', models.PositiveIntegerField(verbose_name='Places')), ('places', models.PositiveIntegerField(verbose_name='Places')),
('agenda', models.ForeignKey(to='agendas.Agenda', on_delete=models.CASCADE)), ('agenda', models.ForeignKey(to='agendas.Agenda', on_delete=models.CASCADE)),
], ],
options={ options={'ordering': ['agenda', 'start_datetime'],},
'ordering': ['agenda', 'start_datetime'],
},
bases=(models.Model,), bases=(models.Model,),
), ),
] ]

View File

@ -15,12 +15,14 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='Booking', name='Booking',
fields=[ 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)), ('extra_data', jsonfield.fields.JSONField(null=True)),
('event', models.ForeignKey(to='agendas.Event', on_delete=models.CASCADE)), ('event', models.ForeignKey(to='agendas.Event', on_delete=models.CASCADE)),
], ],
options={ options={},
},
bases=(models.Model,), bases=(models.Model,),
), ),
] ]

View File

@ -14,6 +14,12 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='event', model_name='event',
name='label', 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( migrations.AddField(
model_name='booking', model_name='booking',
name='creation_datetime', 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, preserve_default=False,
), ),
migrations.AddField( migrations.AddField(
model_name='booking', model_name='booking', name='in_waiting_list', field=models.BooleanField(default=False),
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='full', field=models.BooleanField(default=False),),
migrations.AddField( migrations.AddField(
model_name='event', model_name='event',
name='waiting_list_places', name='waiting_list_places',

View File

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

View File

@ -14,30 +14,51 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='MeetingType', name='MeetingType',
fields=[ 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')), ('label', models.CharField(max_length=100, verbose_name='Label')),
('duration', models.IntegerField(default=30, verbose_name='Duration (in minutes)')), ('duration', models.IntegerField(default=30, verbose_name='Duration (in minutes)')),
], ],
options={ options={'ordering': ['label'],},
'ordering': ['label'],
},
), ),
migrations.CreateModel( migrations.CreateModel(
name='TimePeriod', name='TimePeriod',
fields=[ 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')), ('start_time', models.TimeField(verbose_name='Start')),
('end_time', models.TimeField(verbose_name='End')), ('end_time', models.TimeField(verbose_name='End')),
], ],
options={ options={'ordering': ['weekday', 'start_time'],},
'ordering': ['weekday', 'start_time'],
},
), ),
migrations.AddField( migrations.AddField(
model_name='agenda', model_name='agenda',
name='kind', 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( migrations.AddField(
model_name='timeperiod', model_name='timeperiod',

View File

@ -13,17 +13,32 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.AlterModelOptions( migrations.AlterModelOptions(
name='event', name='event', options={'ordering': ['agenda', 'start_datetime', 'label']},
options={'ordering': ['agenda', 'start_datetime', 'label']},
), ),
migrations.AddField( migrations.AddField(
model_name='agenda', model_name='agenda',
name='edit_role', 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( migrations.AddField(
model_name='agenda', model_name='agenda',
name='view_role', 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 = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='meetingtype', model_name='meetingtype', name='slug', field=models.SlugField(verbose_name='Identifier'),
name='slug',
field=models.SlugField(verbose_name='Identifier'),
), ),
] ]

View File

@ -14,6 +14,11 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='booking', model_name='booking',
name='primary_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 = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='agenda', model_name='agenda', name='label', field=models.CharField(max_length=150, verbose_name='Label'),
name='label',
field=models.CharField(max_length=150, verbose_name='Label'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='event', model_name='event',
name='label', 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( migrations.AlterField(
model_name='meetingtype', model_name='meetingtype',

View File

@ -14,13 +14,14 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='Desk', name='Desk',
fields=[ 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')), ('label', models.CharField(max_length=150, verbose_name='Label')),
('slug', models.SlugField(max_length=150, verbose_name='Identifier')), ('slug', models.SlugField(max_length=150, verbose_name='Identifier')),
('agenda', models.ForeignKey(to='agendas.Agenda', on_delete=models.CASCADE)), ('agenda', models.ForeignKey(to='agendas.Agenda', on_delete=models.CASCADE)),
], ],
options={ options={'ordering': ['label'],},
'ordering': ['label'],
},
), ),
] ]

View File

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

View File

@ -11,8 +11,7 @@ def set_event_desk(apps, schema_editor):
if not event.agenda.kind == 'meetings': if not event.agenda.kind == 'meetings':
continue continue
desk, created = Desk.objects.get_or_create( desk, created = Desk.objects.get_or_create(label='Guichet 1', slug='guichet-1', agenda=event.agenda)
label='Guichet 1', slug='guichet-1', agenda=event.agenda)
event.desk = desk event.desk = desk
event.save() event.save()
@ -33,5 +32,5 @@ class Migration(migrations.Migration):
name='desk', name='desk',
field=models.ForeignKey(to='agendas.Desk', null=True, on_delete=models.CASCADE), 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( migrations.CreateModel(
name='TimePeriodException', name='TimePeriodException',
fields=[ 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')), ('start_datetime', models.DateTimeField(verbose_name='Exception start time')),
('end_datetime', models.DateTimeField(verbose_name='Exception end time')), ('end_datetime', models.DateTimeField(verbose_name='Exception end time')),
('desk', models.ForeignKey(to='agendas.Desk', on_delete=models.CASCADE)), ('desk', models.ForeignKey(to='agendas.Desk', on_delete=models.CASCADE)),
], ],
options={ options={'ordering': ['start_datetime'],},
'ordering': ['start_datetime'],
},
), ),
] ]

View File

@ -26,7 +26,9 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='timeperiodexception', model_name='timeperiodexception',
name='update_datetime', 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, preserve_default=False,
), ),
] ]

View File

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

View File

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

View File

@ -14,6 +14,8 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='desk', model_name='desk',
name='timeperiod_exceptions_remote_url', 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 = [ operations = [
migrations.AlterModelOptions( migrations.AlterModelOptions(name='meetingtype', options={'ordering': ['duration', 'label']},),
name='meetingtype',
options={'ordering': ['duration', 'label']},
),
migrations.AddField( migrations.AddField(
model_name='timeperiodexception', model_name='timeperiodexception',
name='recurrence_id', name='recurrence_id',

View File

@ -16,11 +16,27 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='agenda', model_name='agenda',
name='edit_role', 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( migrations.AlterField(
model_name='agenda', model_name='agenda',
name='view_role', 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( migrations.AddField(
model_name='event', model_name='event',
name='description', 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( migrations.AddField(
model_name='event', model_name='event',
name='slug', 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, preserve_default=False,
), ),
] ]

View File

@ -15,6 +15,8 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='event', model_name='event',
name='slug', 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): def set_slug_on_meetingtypes(apps, schema_editor):
MeetingType = apps.get_model('agendas', 'MeetingType') MeetingType = apps.get_model('agendas', 'MeetingType')
for meetingtype in MeetingType.objects.all().order_by('-pk'): 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 continue
meetingtype.slug = generate_slug(meetingtype, agenda=meetingtype.agenda) meetingtype.slug = generate_slug(meetingtype, agenda=meetingtype.agenda)
meetingtype.save(update_fields=['slug']) meetingtype.save(update_fields=['slug'])

View File

@ -17,12 +17,6 @@ class Migration(migrations.Migration):
name='slug', name='slug',
field=models.SlugField(max_length=160, unique=True, verbose_name='Identifier'), field=models.SlugField(max_length=160, unique=True, verbose_name='Identifier'),
), ),
migrations.AlterUniqueTogether( migrations.AlterUniqueTogether(name='desk', unique_together=set([('agenda', 'slug')]),),
name='desk', migrations.AlterUniqueTogether(name='meetingtype', unique_together=set([('agenda', 'slug')]),),
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( migrations.AlterField(
model_name='event', model_name='event',
name='slug', 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'
migrations.AlterUniqueTogether( ),
name='event',
unique_together=set([('agenda', 'slug')]),
), ),
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) label = models.CharField(_('Label'), max_length=150)
slug = models.SlugField(_('Identifier'), max_length=160, unique=True) slug = models.SlugField(_('Identifier'), max_length=160, unique=True)
kind = models.CharField(_('Kind'), max_length=20, choices=AGENDA_KINDS, default='events') kind = models.CharField(_('Kind'), max_length=20, choices=AGENDA_KINDS, default='events')
minimal_booking_delay = models.PositiveIntegerField( minimal_booking_delay = models.PositiveIntegerField(_('Minimal booking delay (in days)'), default=1)
_('Minimal booking delay (in days)'), default=1)
maximal_booking_delay = models.PositiveIntegerField( maximal_booking_delay = models.PositiveIntegerField(
_('Maximal booking delay (in days)'), default=56) # eight weeks _('Maximal booking delay (in days)'), default=56
edit_role = models.ForeignKey(Group, blank=True, null=True, default=None, ) # eight weeks
related_name='+', verbose_name=_('Edit Role'), edit_role = models.ForeignKey(
on_delete=models.SET_NULL) Group,
view_role = models.ForeignKey(Group, blank=True, null=True, default=None, blank=True,
related_name='+', verbose_name=_('View Role'), null=True,
on_delete=models.SET_NULL) 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: class Meta:
ordering = ['label'] ordering = ['label']
@ -124,7 +136,7 @@ class Agenda(models.Model):
'permissions': { 'permissions': {
'view': self.view_role.name if self.view_role else None, 'view': self.view_role.name if self.view_role else None,
'edit': self.edit_role.name if self.edit_role else None, 'edit': self.edit_role.name if self.edit_role else None,
} },
} }
if self.kind == 'events': if self.kind == 'events':
agenda['events'] = [x.export_json() for x in self.event_set.all()] 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() Desk.import_json(desk).save()
return created return created
WEEKDAYS_LIST = sorted(WEEKDAYS.items(), key=lambda x: x[0]) WEEKDAYS_LIST = sorted(WEEKDAYS.items(), key=lambda x: x[0])
@ -198,9 +211,10 @@ class TimePeriod(models.Model):
def __str__(self): def __str__(self):
return u'%s / %s%s' % ( return u'%s / %s%s' % (
force_text(WEEKDAYS[self.weekday]), force_text(WEEKDAYS[self.weekday]),
date_format(self.start_time, 'TIME_FORMAT'), date_format(self.start_time, 'TIME_FORMAT'),
date_format(self.end_time, 'TIME_FORMAT')) date_format(self.end_time, 'TIME_FORMAT'),
)
@property @property
def weekday_str(self): def weekday_str(self):
@ -212,9 +226,9 @@ class TimePeriod(models.Model):
def export_json(self): def export_json(self):
return { return {
'weekday': self.weekday, 'weekday': self.weekday,
'start_time': self.start_time.strftime('%H:%M'), 'start_time': self.start_time.strftime('%H:%M'),
'end_time': self.end_time.strftime('%H:%M'), 'end_time': self.end_time.strftime('%H:%M'),
} }
def get_time_slots(self, min_datetime, max_datetime, meeting_type): 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) min_datetime = make_naive(min_datetime)
max_datetime = make_naive(max_datetime) max_datetime = make_naive(max_datetime)
real_min_datetime = min_datetime + datetime.timedelta( real_min_datetime = min_datetime + datetime.timedelta(days=self.weekday - min_datetime.weekday())
days=self.weekday - min_datetime.weekday())
if real_min_datetime < min_datetime: if real_min_datetime < min_datetime:
real_min_datetime += datetime.timedelta(days=7) real_min_datetime += datetime.timedelta(days=7)
event_datetime = real_min_datetime.replace(hour=self.start_time.hour, event_datetime = real_min_datetime.replace(
minute=self.start_time.minute, second=0, microsecond=0) hour=self.start_time.hour, minute=self.start_time.minute, second=0, microsecond=0
)
while event_datetime < max_datetime: while event_datetime < max_datetime:
end_time = event_datetime + meeting_duration end_time = event_datetime + meeting_duration
next_time = event_datetime + duration next_time = event_datetime + duration
if end_time.time() > self.end_time or event_datetime.date() != next_time.date(): if end_time.time() > self.end_time or event_datetime.date() != next_time.date():
# back to morning # 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 # but next week
event_datetime += datetime.timedelta(days=7) event_datetime += datetime.timedelta(days=7)
next_time = event_datetime + duration next_time = event_datetime + duration
@ -243,7 +259,9 @@ class TimePeriod(models.Model):
if event_datetime > max_datetime: if event_datetime > max_datetime:
break 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 event_datetime = next_time
@ -265,7 +283,8 @@ class MeetingType(models.Model):
@classmethod @classmethod
def import_json(cls, data): def import_json(cls, data):
meeting_type, created = cls.objects.get_or_create( 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: if not created:
for k, v in data.items(): for k, v in data.items():
setattr(meeting_type, k, v) setattr(meeting_type, k, v)
@ -273,9 +292,9 @@ class MeetingType(models.Model):
def export_json(self): def export_json(self):
return { return {
'label': self.label, 'label': self.label,
'slug': self.slug, 'slug': self.slug,
'duration': self.duration, 'duration': self.duration,
} }
@ -284,13 +303,18 @@ class Event(models.Model):
agenda = models.ForeignKey(Agenda, on_delete=models.CASCADE) agenda = models.ForeignKey(Agenda, on_delete=models.CASCADE)
start_datetime = models.DateTimeField(_('Date/time')) start_datetime = models.DateTimeField(_('Date/time'))
places = models.PositiveIntegerField(_('Places')) places = models.PositiveIntegerField(_('Places'))
waiting_list_places = models.PositiveIntegerField( waiting_list_places = models.PositiveIntegerField(_('Places in waiting list'), default=0)
_('Places in waiting list'), default=0) label = models.CharField(
label = models.CharField(_('Label'), max_length=150, null=True, blank=True, _('Label'),
help_text=_('Optional label to identify this date.')) 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) slug = models.SlugField(_('Identifier'), max_length=160, null=True, blank=True, default=None)
description = models.TextField(_('Description'), null=True, blank=True, description = models.TextField(
help_text=_('Optional event description.')) _('Description'), null=True, blank=True, help_text=_('Optional event description.')
)
full = models.BooleanField(default=False) full = models.BooleanField(default=False)
meeting_type = models.ForeignKey(MeetingType, null=True, on_delete=models.CASCADE) meeting_type = models.ForeignKey(MeetingType, null=True, on_delete=models.CASCADE)
desk = models.ForeignKey('Desk', 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): def check_full(self):
self.full = bool( self.full = bool(
(self.booked_places >= self.places and self.waiting_list_places == 0) or (self.booked_places >= self.places and self.waiting_list_places == 0)
(self.waiting_list_places and self.waiting_list >= self.waiting_list_places)) or (self.waiting_list_places and self.waiting_list >= self.waiting_list_places)
)
def in_bookable_period(self): 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 return False
if self.agenda.maximal_booking_delay and ( 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 return False
if self.start_datetime < now(): if self.start_datetime < now():
# past the event date, we may want in the future to allow for some # past the event date, we may want in the future to allow for some
@ -327,13 +361,11 @@ class Event(models.Model):
@property @property
def booked_places(self): def booked_places(self):
return self.booking_set.filter(cancellation_datetime__isnull=True, return self.booking_set.filter(cancellation_datetime__isnull=True, in_waiting_list=False).count()
in_waiting_list=False).count()
@property @property
def waiting_list(self): def waiting_list(self):
return self.booking_set.filter(cancellation_datetime__isnull=True, return self.booking_set.filter(cancellation_datetime__isnull=True, in_waiting_list=True).count()
in_waiting_list=True).count()
@property @property
def end_datetime(self): def end_datetime(self):
@ -344,8 +376,9 @@ class Event(models.Model):
@classmethod @classmethod
def import_json(cls, data): def import_json(cls, data):
data['start_datetime'] = make_aware(datetime.datetime.strptime( data['start_datetime'] = make_aware(
data['start_datetime'], '%Y-%m-%d %H:%M:%S')) datetime.datetime.strptime(data['start_datetime'], '%Y-%m-%d %H:%M:%S')
)
if data.get('slug'): if data.get('slug'):
event, created = cls.objects.get_or_create(slug=data['slug'], defaults=data) event, created = cls.objects.get_or_create(slug=data['slug'], defaults=data)
if not created: if not created:
@ -372,11 +405,14 @@ class Booking(models.Model):
in_waiting_list = models.BooleanField(default=False) in_waiting_list = models.BooleanField(default=False)
creation_datetime = models.DateTimeField(auto_now_add=True) creation_datetime = models.DateTimeField(auto_now_add=True)
# primary booking is used to group multiple bookings together # primary booking is used to group multiple bookings together
primary_booking = models.ForeignKey('self', null=True, primary_booking = models.ForeignKey(
on_delete=models.CASCADE, related_name='secondary_booking_set') 'self', null=True, on_delete=models.CASCADE, related_name='secondary_booking_set'
)
label = models.CharField(max_length=250, blank=True) 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) user_name = models.CharField(max_length=250, blank=True)
backoffice_url = models.URLField(blank=True) backoffice_url = models.URLField(blank=True)
@ -405,14 +441,20 @@ class Booking(models.Model):
ics = vobject.iCalendar() ics = vobject.iCalendar()
ics.add('prodid').value = '-//Entr\'ouvert//NON SGML Publik' ics.add('prodid').value = '-//Entr\'ouvert//NON SGML Publik'
vevent = vobject.newFromBehavior('vevent') 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('summary').value = self.user_display_label or self.label
vevent.add('dtstart').value = self.event.start_datetime vevent.add('dtstart').value = self.event.start_datetime
if self.user_name: if self.user_name:
vevent.add('attendee').value = self.user_name vevent.add('attendee').value = self.user_name
if self.event.meeting_type: 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'): for field in ('description', 'location', 'comment', 'url'):
field_value = request and request.GET.get(field) or self.extra_data.get(field) 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() return ics.serialize()
@python_2_unicode_compatible @python_2_unicode_compatible
class Desk(models.Model): class Desk(models.Model):
agenda = models.ForeignKey(Agenda, on_delete=models.CASCADE) agenda = models.ForeignKey(Agenda, on_delete=models.CASCADE)
label = models.CharField(_('Label'), max_length=150) label = models.CharField(_('Label'), max_length=150)
slug = models.SlugField(_('Identifier'), max_length=160) slug = models.SlugField(_('Identifier'), max_length=160)
timeperiod_exceptions_remote_url = models.URLField( timeperiod_exceptions_remote_url = models.URLField(
_('URL to fetch time period exceptions from'), _('URL to fetch time period exceptions from'), blank=True, max_length=500
blank=True, max_length=500) )
def __str__(self): def __str__(self):
return self.label return self.label
@ -448,8 +489,7 @@ class Desk(models.Model):
def import_json(cls, data): def import_json(cls, data):
timeperiods = data.pop('timeperiods') timeperiods = data.pop('timeperiods')
exceptions = data.pop('exceptions') exceptions = data.pop('exceptions')
instance, created = cls.objects.get_or_create( instance, 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: if not created:
for k, v in data.items(): for k, v in data.items():
setattr(instance, k, v) setattr(instance, k, v)
@ -462,21 +502,24 @@ class Desk(models.Model):
return instance return instance
def export_json(self): def export_json(self):
return {'label': self.label, return {
'slug': self.slug, 'label': self.label,
'timeperiods': [time_period.export_json() for time_period in self.timeperiod_set.all()], 'slug': self.slug,
'exceptions': [exception.export_json() for exception in self.timeperiodexception_set.all()] 'timeperiods': [time_period.export_json() for time_period in self.timeperiod_set.all()],
} 'exceptions': [exception.export_json() for exception in self.timeperiodexception_set.all()],
}
def get_exceptions_within_two_weeks(self): def get_exceptions_within_two_weeks(self):
in_two_weeks = make_aware(datetime.datetime.today() + datetime.timedelta(days=14)) in_two_weeks = make_aware(datetime.datetime.today() + datetime.timedelta(days=14))
exceptions = self.timeperiodexception_set.filter(end_datetime__gte=now()).filter( 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(): if exceptions.exists():
return exceptions return exceptions
# if none found within the 2 coming weeks, return the next one # if none found within the 2 coming weeks, return the next one
next_exception = self.timeperiodexception_set.filter( next_exception = (
start_datetime__gte=now()).order_by('start_datetime').first() self.timeperiodexception_set.filter(start_datetime__gte=now()).order_by('start_datetime').first()
)
if next_exception: if next_exception:
return [next_exception] return [next_exception]
return [] return []
@ -491,12 +534,14 @@ class Desk(models.Model):
response.raise_for_status() response.raise_for_status()
except requests.HTTPError as e: except requests.HTTPError as e:
raise ICSError( raise ICSError(
_('Failed to retrieve remote calendar (%(url)s, HTTP error %(status_code)s).') % _('Failed to retrieve remote calendar (%(url)s, HTTP error %(status_code)s).')
{'url': url, 'status_code': e.response.status_code}) % {'url': url, 'status_code': e.response.status_code}
)
except requests.RequestException as e: except requests.RequestException as e:
raise ICSError( raise ICSError(
_('Failed to retrieve remote calendar (%(url)s, %(exception)s).') % _('Failed to retrieve remote calendar (%(url)s, %(exception)s).')
{'url': url, 'exception': e}) % {'url': url, 'exception': e}
)
return self.create_timeperiod_exceptions_from_ics(response.text, keep_synced_by_uid=True) 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: if end_dt < update_datetime:
TimePeriodException.objects.filter(**kwargs).update(**event) TimePeriodException.objects.filter(**kwargs).update(**event)
else: else:
obj, created = TimePeriodException.objects.update_or_create(defaults=event, **kwargs) obj, created = TimePeriodException.objects.update_or_create(
defaults=event, **kwargs
)
if created: if created:
total_created += 1 total_created += 1
# delete unseen occurrences # delete unseen occurrences
@ -591,8 +638,9 @@ class Desk(models.Model):
if keep_synced_by_uid: if keep_synced_by_uid:
# delete all outdated exceptions from remote calendar # delete all outdated exceptions from remote calendar
TimePeriodException.objects.filter(update_datetime__lt=update_datetime, TimePeriodException.objects.filter(update_datetime__lt=update_datetime, desk=self).exclude(
desk=self).exclude(external_id='').delete() external_id=''
).delete()
return total_created return total_created
@ -606,8 +654,8 @@ class Desk(models.Model):
aware_date = make_aware(datetime.datetime(date.year, date.month, date.day)) aware_date = make_aware(datetime.datetime(date.year, date.month, date.day))
aware_next_date = aware_date + datetime.timedelta(days=1) aware_next_date = aware_date + datetime.timedelta(days=1)
for exception in self.timeperiodexception_set.filter( for exception in self.timeperiodexception_set.filter(
start_datetime__lt=aware_next_date, start_datetime__lt=aware_next_date, end_datetime__gt=aware_date
end_datetime__gt=aware_date): ):
openslots.remove(exception.start_datetime, exception.end_datetime) openslots.remove(exception.start_datetime, exception.end_datetime)
return openslots.search(aware_date, aware_next_date) 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') exc_repr = u'%s' % date_format(localtime(self.start_datetime), 'SHORT_DATE_FORMAT')
else: else:
exc_repr = u'%s%s' % ( exc_repr = u'%s%s' % (
date_format(localtime(self.start_datetime), 'SHORT_DATE_FORMAT'), date_format(localtime(self.start_datetime), 'SHORT_DATE_FORMAT'),
date_format(localtime(self.end_datetime), 'SHORT_DATE_FORMAT')) date_format(localtime(self.end_datetime), 'SHORT_DATE_FORMAT'),
)
else: else:
if localtime(self.start_datetime).date() == localtime(self.end_datetime).date(): if localtime(self.start_datetime).date() == localtime(self.end_datetime).date():
# same day # same day
exc_repr = u'%s%s' % ( exc_repr = u'%s%s' % (
date_format(localtime(self.start_datetime), 'SHORT_DATETIME_FORMAT'), date_format(localtime(self.start_datetime), 'SHORT_DATETIME_FORMAT'),
date_format(localtime(self.end_datetime), 'TIME_FORMAT')) date_format(localtime(self.end_datetime), 'TIME_FORMAT'),
)
else: else:
exc_repr = u'%s%s' % ( exc_repr = u'%s%s' % (
date_format(localtime(self.start_datetime), 'SHORT_DATETIME_FORMAT'), date_format(localtime(self.start_datetime), 'SHORT_DATETIME_FORMAT'),
date_format(localtime(self.end_datetime), 'SHORT_DATETIME_FORMAT')) date_format(localtime(self.end_datetime), 'SHORT_DATETIME_FORMAT'),
)
if self.label: if self.label:
exc_repr = u'%s (%s)' % (self.label, exc_repr) exc_repr = u'%s (%s)' % (self.label, exc_repr)
@ -662,9 +713,8 @@ class TimePeriodException(models.Model):
# incomplete time period, can't tell # incomplete time period, can't tell
return False return False
for event in Event.objects.filter( for event in Event.objects.filter(
desk=self.desk, desk=self.desk, booking__isnull=False, booking__cancellation_datetime__isnull=True
booking__isnull=False, ):
booking__cancellation_datetime__isnull=True):
if self.start_datetime <= event.start_datetime < self.end_datetime: if self.start_datetime <= event.start_datetime < self.end_datetime:
return True return True
return False return False

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -39,7 +39,7 @@ DATE_FORMAT_PY_JS_MAPPING = {
'%Y': 'yyyy', '%Y': 'yyyy',
'%y': 'yy', '%y': 'yy',
'%p': 'P', '%p': 'P',
'%S': 'ss' '%S': 'ss',
} }
DATE_FORMAT_TO_JS_REGEX = re.compile(r'(?<!\w)(' + '|'.join(DATE_FORMAT_PY_JS_MAPPING.keys()) + r')\b') 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 # with a default, and convert it to a Python data format for later string parsing
date_format = self.options['format'] date_format = self.options['format']
self.format = DATE_FORMAT_TO_PYTHON_REGEX.sub( self.format = DATE_FORMAT_TO_PYTHON_REGEX.sub(
lambda x: DATE_FORMAT_JS_PY_MAPPING[x.group()], lambda x: DATE_FORMAT_JS_PY_MAPPING[x.group()], date_format
date_format )
)
super(PickerWidgetMixin, self).__init__(attrs, format=self.format) super(PickerWidgetMixin, self).__init__(attrs, format=self.format)
@ -87,7 +86,7 @@ class PickerWidgetMixin(object):
final_attrs = self.build_attrs(attrs) final_attrs = self.build_attrs(attrs)
rendered_widget = super(PickerWidgetMixin, self).render(name, value, final_attrs, renderer=renderer) 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) self.options.setdefault('autoclose', True)
# Build javascript options out of python dictionary # 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 # Use provided id or generate hex to avoid collisions in document
id = final_attrs.get('id', uuid.uuid4().hex) id = final_attrs.get('id', uuid.uuid4().hex)
return mark_safe(BOOTSTRAP_INPUT_TEMPLATE % dict( return mark_safe(
id=id, BOOTSTRAP_INPUT_TEMPLATE
rendered_widget=rendered_widget, % dict(
clear_button=CLEAR_BTN_TEMPLATE if self.options.get('clearBtn') else '', id=id,
glyphicon=self.glyphicon, rendered_widget=rendered_widget,
options=js_options 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 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. of the pattern attribute in case a standard text input is used.
""" """
input_type = 'time' input_type = 'time'
def __init__(self, **kwargs): def __init__(self, **kwargs):
@ -149,13 +151,17 @@ class WeekdaysWidget(SelectMultiple):
s = [] s = []
value = value or [] value = value or []
for choice_id, choice_label in self.choices: for choice_id, choice_label in self.choices:
s.append('<li><label><input type="checkbox" ' s.append(
' name="%(name)s-%(choice_id)s" %(checked)s>' '<li><label><input type="checkbox" '
'<span>%(choice_label)s</span></label></li>' % { ' name="%(name)s-%(choice_id)s" %(checked)s>'
'name': name, '<span>%(choice_label)s</span></label></li>'
'checked': 'checked' if choice_id in value else '', % {
'choice_id': choice_id, 'name': name,
'choice_label': choice_label}) '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>') return mark_safe('<ul id="%(id)s">' % attrs + '\n'.join(s) + '</ul>')
def value_from_datadict(self, data, files, name): 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 # https://docs.djangoproject.com/en/1.7/ref/settings/#databases
DATABASES = { DATABASES = {
'default': { 'default': {'ENGINE': 'django.db.backends.sqlite3', 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),}
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
} }
# Internationalization # Internationalization
@ -96,7 +93,7 @@ USE_L10N = True
USE_TZ = 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' FORMAT_MODULE_PATH = 'chrono.formats'
@ -104,8 +101,7 @@ FORMAT_MODULE_PATH = 'chrono.formats'
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [ 'DIRS': [],
],
'APP_DIRS': True, 'APP_DIRS': True,
'OPTIONS': { 'OPTIONS': {
'context_processors': [ 'context_processors': [
@ -165,7 +161,8 @@ MELLON_IDENTITY_PROVIDERS = []
# (see http://docs.python-requests.org/en/master/user/advanced/#proxies) # (see http://docs.python-requests.org/en/master/user/advanced/#proxies)
REQUESTS_PROXIES = None REQUESTS_PROXIES = None
local_settings_file = os.environ.get('CHRONO_SETTINGS_FILE', local_settings_file = os.environ.get(
os.path.join(os.path.dirname(__file__), 'local_settings.py')) 'CHRONO_SETTINGS_FILE', os.path.join(os.path.dirname(__file__), 'local_settings.py')
)
if os.path.exists(local_settings_file): if os.path.exists(local_settings_file):
exec(open(local_settings_file).read()) exec(open(local_settings_file).read())

View File

@ -28,8 +28,7 @@ from .manager.urls import urlpatterns as chrono_manager_urls
urlpatterns = [ urlpatterns = [
url(r'^$', homepage, name='home'), url(r'^$', homepage, name='home'),
url(r'^manage/', decorated_includes(manager_required, url(r'^manage/', decorated_includes(manager_required, include(chrono_manager_urls))),
include(chrono_manager_urls))),
url(r'^api/', include(chrono_api_urls)), url(r'^api/', include(chrono_api_urls)),
url(r'^logout/$', LogoutView.as_view(), name='auth_logout'), url(r'^logout/$', LogoutView.as_view(), name='auth_logout'),
url(r'^login/$', LoginView.as_view(), name='auth_login'), 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) result.func = self._decorate_with(result.func)
return result return result
def decorated_includes(func, includes, *args, **kwargs): def decorated_includes(func, includes, *args, **kwargs):
urlconf_module, app_name, namespace = includes urlconf_module, app_name, namespace = includes
@ -59,6 +60,7 @@ def manager_required(function=None, login_url=None):
raise PermissionDenied() raise PermissionDenied()
# As the last resort, show the login form # As the last resort, show the login form
return False return False
actual_decorator = user_passes_test(check_manager, login_url=login_url) actual_decorator = user_passes_test(check_manager, login_url=login_url)
if function: if function:
return actual_decorator(function) return actual_decorator(function)

View File

@ -35,8 +35,9 @@ class LoginView(auth_views.LoginView):
if any(get_idps()): if any(get_idps()):
if not 'next' in request.GET: if not 'next' in request.GET:
return HttpResponseRedirect(resolve_url('mellon_login')) return HttpResponseRedirect(resolve_url('mellon_login'))
return HttpResponseRedirect(resolve_url('mellon_login') + '?next=' return HttpResponseRedirect(
+ quote(request.GET.get('next'))) resolve_url('mellon_login') + '?next=' + quote(request.GET.get('next'))
)
return super(LoginView, self).get(request, *args, **kwargs) 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 import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "chrono.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "chrono.settings")
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
application = 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! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False DEBUG = False
#ADMINS = ( # ADMINS = (
# # ('User 1', 'watchdog@example.net'), # # ('User 1', 'watchdog@example.net'),
# # ('User 2', 'janitor@example.net'), # # ('User 2', 'janitor@example.net'),
#) # )
# ALLOWED_HOSTS must be correct in production! # ALLOWED_HOSTS must be correct in production!
# See https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts # See https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
ALLOWED_HOSTS = [ ALLOWED_HOSTS = [
'*', '*',
] ]
# Databases # Databases

View File

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

View File

@ -4,6 +4,7 @@ from django.contrib.auth.models import User
import django_webtest import django_webtest
@pytest.fixture @pytest.fixture
def app(request): def app(request):
wtm = django_webtest.WebTestMixin() 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 import call_command
from django.core.management.base import CommandError from django.core.management.base import CommandError
from chrono.agendas.models import (Agenda, Event, Booking, MeetingType, from chrono.agendas.models import (
Desk, TimePeriod, TimePeriodException, ICSError) Agenda,
Event,
Booking,
MeetingType,
Desk,
TimePeriod,
TimePeriodException,
ICSError,
)
pytestmark = pytest.mark.django_db pytestmark = pytest.mark.django_db
@ -154,6 +162,7 @@ def test_event_manager():
booking.save() booking.save()
assert Event.objects.all()[0].booked_places == 0 assert Event.objects.all()[0].booked_places == 0
def test_event_bookable_period(): def test_event_bookable_period():
agenda = Agenda(label=u'Foo bar') agenda = Agenda(label=u'Foo bar')
agenda.save() agenda.save()
@ -179,6 +188,7 @@ def test_event_bookable_period():
event.save() event.save()
assert event.in_bookable_period() is False assert event.in_bookable_period() is False
def test_meeting_type_slugs(): def test_meeting_type_slugs():
agenda1 = Agenda(label=u'Foo bar') agenda1 = Agenda(label=u'Foo bar')
agenda1.save() agenda1.save()
@ -197,6 +207,7 @@ def test_meeting_type_slugs():
meeting_type3.save() meeting_type3.save()
assert meeting_type3.slug == 'baz' assert meeting_type3.slug == 'baz'
def test_timeperiodexception_creation_from_ics(): def test_timeperiodexception_creation_from_ics():
agenda = Agenda(label=u'Test 1 agenda') agenda = Agenda(label=u'Test 1 agenda')
agenda.save() agenda.save()
@ -206,6 +217,7 @@ def test_timeperiodexception_creation_from_ics():
assert exceptions_count == 2 assert exceptions_count == 2
assert TimePeriodException.objects.filter(desk=desk).count() == 2 assert TimePeriodException.objects.filter(desk=desk).count() == 2
def test_timeperiodexception_creation_from_ics_without_startdt(): def test_timeperiodexception_creation_from_ics_without_startdt():
agenda = Agenda(label=u'Test 2 agenda') agenda = Agenda(label=u'Test 2 agenda')
agenda.save() agenda.save()
@ -222,6 +234,7 @@ def test_timeperiodexception_creation_from_ics_without_startdt():
exceptions_count = desk.create_timeperiod_exceptions_from_ics(ics_sample) exceptions_count = desk.create_timeperiod_exceptions_from_ics(ics_sample)
assert 'Event "Event 1" has no start date.' == str(e.value) assert 'Event "Event 1" has no start date.' == str(e.value)
def test_timeperiodexception_creation_from_ics_without_enddt(): def test_timeperiodexception_creation_from_ics_without_enddt():
agenda = Agenda(label=u'Test 3 agenda') agenda = Agenda(label=u'Test 3 agenda')
agenda.save() agenda.save()
@ -248,14 +261,17 @@ def test_timeperiodexception_creation_from_ics_with_recurrences():
desk.save() desk.save()
assert desk.create_timeperiod_exceptions_from_ics(ICS_SAMPLE_WITH_RECURRENT_EVENT) == 3 assert desk.create_timeperiod_exceptions_from_ics(ICS_SAMPLE_WITH_RECURRENT_EVENT) == 3
assert TimePeriodException.objects.filter(desk=desk).count() == 3 assert TimePeriodException.objects.filter(desk=desk).count() == 3
assert set(TimePeriodException.objects.values_list('start_datetime', flat=True)) == set([ 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))]) [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 assert desk.create_timeperiod_exceptions_from_ics(ICS_SAMPLE_WITH_RECURRENT_EVENT) == 0
# verify occurences are cleaned when count changed # verify occurences are cleaned when count changed
assert desk.create_timeperiod_exceptions_from_ics(ICS_SAMPLE_WITH_RECURRENT_EVENT_2) == 0 assert desk.create_timeperiod_exceptions_from_ics(ICS_SAMPLE_WITH_RECURRENT_EVENT_2) == 0
assert TimePeriodException.objects.filter(desk=desk).count() == 2 assert TimePeriodException.objects.filter(desk=desk).count() == 2
assert set(TimePeriodException.objects.values_list('start_datetime', flat=True)) == set([ 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))]) [make_aware(datetime.datetime(2018, 1, 1)), make_aware(datetime.datetime(2018, 1, 2))]
)
def test_timeexception_creation_from_ics_with_dates(): def test_timeexception_creation_from_ics_with_dates():
agenda = Agenda(label=u'Test 5 agenda') 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.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)) assert localtime(exception.end_datetime) == make_aware(datetime.datetime(2018, 1, 1, 0, 0))
def test_timeexception_create_from_invalid_ics(): def test_timeexception_create_from_invalid_ics():
agenda = Agenda(label=u'Test 6 agenda') agenda = Agenda(label=u'Test 6 agenda')
agenda.save() agenda.save()
@ -284,6 +301,7 @@ def test_timeexception_create_from_invalid_ics():
exceptions_count = desk.create_timeperiod_exceptions_from_ics(INVALID_ICS_SAMPLE) exceptions_count = desk.create_timeperiod_exceptions_from_ics(INVALID_ICS_SAMPLE)
assert str(e.value) == 'File format is invalid.' assert str(e.value) == 'File format is invalid.'
def test_timeexception_create_from_ics_with_no_events(): def test_timeexception_create_from_ics_with_no_events():
agenda = Agenda(label=u'Test 7 agenda') agenda = Agenda(label=u'Test 7 agenda')
agenda.save() 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) exceptions_count = desk.create_timeperiod_exceptions_from_ics(ICS_SAMPLE_WITH_NO_EVENTS)
assert str(e.value) == "The file doesn't contain any events." assert str(e.value) == "The file doesn't contain any events."
@mock.patch('chrono.agendas.models.requests.get') @mock.patch('chrono.agendas.models.requests.get')
def test_timeperiodexception_creation_from_remote_ics(mocked_get): def test_timeperiodexception_creation_from_remote_ics(mocked_get):
agenda = Agenda(label=u'Test 8 agenda') agenda = Agenda(label=u'Test 8 agenda')
@ -316,6 +335,7 @@ def test_timeperiodexception_creation_from_remote_ics(mocked_get):
assert exceptions_count == 0 assert exceptions_count == 0
TimePeriodException.objects.filter(external_id='desk-%s:' % desk.id).count() == 0 TimePeriodException.objects.filter(external_id='desk-%s:' % desk.id).count() == 0
@mock.patch('chrono.agendas.models.requests.get') @mock.patch('chrono.agendas.models.requests.get')
def test_timeperiodexception_creation_from_unreachable_remote_ics(mocked_get): def test_timeperiodexception_creation_from_unreachable_remote_ics(mocked_get):
agenda = Agenda(label=u'Test 9 agenda') 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 = mock.Mock()
mocked_response.text = ICS_SAMPLE mocked_response.text = ICS_SAMPLE
mocked_get.return_value = mocked_response mocked_get.return_value = mocked_response
def mocked_requests_connection_error(*args, **kwargs): def mocked_requests_connection_error(*args, **kwargs):
raise requests.ConnectionError('unreachable') raise requests.ConnectionError('unreachable')
mocked_get.side_effect = mocked_requests_connection_error mocked_get.side_effect = mocked_requests_connection_error
with pytest.raises(ICSError) as e: with pytest.raises(ICSError) as e:
exceptions_count = desk.create_timeperiod_exceptions_from_remote_ics('http://example.com/sample.ics') 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)." assert str(e.value) == "Failed to retrieve remote calendar (http://example.com/sample.ics, unreachable)."
@mock.patch('chrono.agendas.models.requests.get') @mock.patch('chrono.agendas.models.requests.get')
def test_timeperiodexception_creation_from_forbidden_remote_ics(mocked_get): def test_timeperiodexception_creation_from_forbidden_remote_ics(mocked_get):
agenda = Agenda(label=u'Test 10 agenda') 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 = mock.Mock()
mocked_response.status_code = 403 mocked_response.status_code = 403
mocked_get.return_value = mocked_response mocked_get.return_value = mocked_response
def mocked_requests_http_forbidden_error(*args, **kwargs): def mocked_requests_http_forbidden_error(*args, **kwargs):
raise requests.HTTPError(response=mocked_response) raise requests.HTTPError(response=mocked_response)
mocked_get.side_effect = mocked_requests_http_forbidden_error mocked_get.side_effect = mocked_requests_http_forbidden_error
with pytest.raises(ICSError) as e: with pytest.raises(ICSError) as e:
exceptions_count = desk.create_timeperiod_exceptions_from_remote_ics('http://example.com/sample.ics') 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') @mock.patch('chrono.agendas.models.requests.get')
def test_sync_desks_timeperiod_exceptions_from_ics(mocked_get, capsys): def test_sync_desks_timeperiod_exceptions_from_ics(mocked_get, capsys):
agenda = Agenda(label=u'Test 11 agenda') agenda = Agenda(label=u'Test 11 agenda')
agenda.save() 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() desk.save()
mocked_response = mock.Mock() mocked_response = mock.Mock()
mocked_response.status_code = 403 mocked_response.status_code = 403
mocked_get.return_value = mocked_response mocked_get.return_value = mocked_response
def mocked_requests_http_forbidden_error(*args, **kwargs): def mocked_requests_http_forbidden_error(*args, **kwargs):
raise requests.HTTPError(response=mocked_response) raise requests.HTTPError(response=mocked_response)
mocked_get.side_effect = mocked_requests_http_forbidden_error mocked_get.side_effect = mocked_requests_http_forbidden_error
call_command('sync_desks_timeperiod_exceptions') call_command('sync_desks_timeperiod_exceptions')
out, err = capsys.readouterr() 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') @mock.patch('chrono.agendas.models.requests.get')
def test_sync_desks_timeperiod_exceptions_from_changing_ics(mocked_get, caplog): def test_sync_desks_timeperiod_exceptions_from_changing_ics(mocked_get, caplog):
agenda = Agenda(label=u'Test 11 agenda') agenda = Agenda(label=u'Test 11 agenda')
agenda.save() 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() desk.save()
mocked_response = mock.Mock() mocked_response = mock.Mock()
mocked_response.text = ICS_SAMPLE mocked_response.text = ICS_SAMPLE
@ -397,6 +435,7 @@ END:VCALENDAR"""
call_command('sync_desks_timeperiod_exceptions') call_command('sync_desks_timeperiod_exceptions')
assert not TimePeriodException.objects.filter(desk=desk).exists() assert not TimePeriodException.objects.filter(desk=desk).exists()
def test_base_meeting_duration(): def test_base_meeting_duration():
agenda = Agenda(label='Meeting', kind='meetings') agenda = Agenda(label='Meeting', kind='meetings')
agenda.save() 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) exceptions_count = desk.create_timeperiod_exceptions_from_ics(ICS_SAMPLE_WITH_DURATION)
assert exceptions_count == 2 assert exceptions_count == 2
assert TimePeriodException.objects.filter(desk=desk).count() == 2 assert TimePeriodException.objects.filter(desk=desk).count() == 2
assert set(TimePeriodException.objects.values_list('start_datetime', flat=True)) == set([ 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)), 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('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') @pytest.mark.freeze_time('2017-12-01')
@ -447,8 +490,9 @@ def test_timeperiodexception_creation_from_ics_with_recurrences_in_the_past():
desk.save() desk.save()
assert desk.create_timeperiod_exceptions_from_ics(ICS_SAMPLE_WITH_RECURRENT_EVENT_IN_THE_PAST) == 2 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 TimePeriodException.objects.filter(desk=desk).count() == 2
assert set(TimePeriodException.objects.values_list('start_datetime', flat=True)) == set([ 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))]) [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 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 from chrono.api.utils import Response
@pytest.mark.parametrize('data, expected', [ @pytest.mark.parametrize(
(None, None), 'data, expected',
({}, {}), [
({'reason': 'foo'}, {'reason': 'foo'}), (None, None),
({'err_class': 'foo'}, {'err_class': 'foo', 'reason': 'foo'}), ({}, {}),
({'bar': 'foo'}, {'bar': 'foo'}), ({'reason': 'foo'}, {'reason': 'foo'}),
]) ({'err_class': 'foo'}, {'err_class': 'foo', 'reason': 'foo'}),
({'bar': 'foo'}, {'bar': 'foo'}),
],
)
def test_response_data(data, expected): def test_response_data(data, expected):
resp = Response(data=data) resp = Response(data=data)
assert resp.data == expected assert resp.data == expected

View File

@ -52,23 +52,31 @@ def test_timeperiod_data_migrations():
Event = old_apps.get_model(app, 'Event') Event = old_apps.get_model(app, 'Event')
agenda = Agenda.objects.create(label='foo', slug='foo', kind='meetings') agenda = Agenda.objects.create(label='foo', slug='foo', kind='meetings')
agenda2 = Agenda.objects.create(label='bar', slug='bar', kind='events') agenda2 = Agenda.objects.create(label='bar', slug='bar', kind='events')
TimePeriod.objects.create(agenda=agenda, weekday=1, TimePeriod.objects.create(
start_time=datetime.time(8, 0), agenda=agenda, weekday=1, start_time=datetime.time(8, 0), end_time=datetime.time(12, 0)
end_time=datetime.time(12, 0)) )
TimePeriod.objects.create(agenda=agenda, weekday=2, TimePeriod.objects.create(
start_time=datetime.time(8, 0), agenda=agenda, weekday=2, start_time=datetime.time(8, 0), end_time=datetime.time(10, 0)
end_time=datetime.time(10, 0)) )
TimePeriod.objects.create(agenda=agenda, weekday=3, TimePeriod.objects.create(
start_time=datetime.time(9, 0), agenda=agenda, weekday=3, start_time=datetime.time(9, 0), end_time=datetime.time(12, 0)
end_time=datetime.time(12, 0)) )
meeting_type = MeetingType.objects.create(agenda=agenda, label='foo', meeting_type = MeetingType.objects.create(agenda=agenda, label='foo', slug='foo', duration=60)
slug='foo', duration=60) Event.objects.create(
Event.objects.create(agenda=agenda, places=1, meeting_type=meeting_type, agenda=agenda,
start_datetime=make_aware(datetime.datetime(2017, 5, 22, 9, 30))) places=1,
Event.objects.create(agenda=agenda, places=1, meeting_type=meeting_type, meeting_type=meeting_type,
start_datetime=make_aware(datetime.datetime(2017, 5, 22, 10, 0))) start_datetime=make_aware(datetime.datetime(2017, 5, 22, 9, 30)),
Event.objects.create(agenda=agenda2, places=5, )
start_datetime=make_aware(datetime.datetime(2017, 5, 22, 10, 0))) 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.loader.build_graph()
executor.migrate(migrate_to) executor.migrate(migrate_to)
new_apps = executor.loader.project_state(migrate_to).apps 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.encoding import force_bytes
from django.utils.timezone import make_aware from django.utils.timezone import make_aware
from chrono.agendas.models import (Agenda, Event, TimePeriod, Desk, from chrono.agendas.models import Agenda, Event, TimePeriod, Desk, TimePeriodException, AgendaImportError
TimePeriodException, AgendaImportError)
from chrono.manager.utils import import_site from chrono.manager.utils import import_site
from test_api import some_data, meetings_agenda, time_zone, mock_now 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() desk = meetings_agenda.desk_set.first()
tpx_start = make_aware(datetime.datetime(2017, 5, 22, 8, 0)) tpx_start = make_aware(datetime.datetime(2017, 5, 22, 8, 0))
tpx_end = make_aware(datetime.datetime(2017, 5, 22, 12, 30)) tpx_end = make_aware(datetime.datetime(2017, 5, 22, 12, 30))
TimePeriodException.objects.create( TimePeriodException.objects.create(desk=desk, start_datetime=tpx_start, end_datetime=tpx_end)
desk=desk,
start_datetime=tpx_start,
end_datetime=tpx_end)
output = get_output_of_command('export_site') output = get_output_of_command('export_site')
assert len(json.loads(output)['agendas']) == 3 assert len(json.loads(output)['agendas']) == 3
import_site(data={}, clean=True) import_site(data={}, clean=True)
@ -77,15 +73,14 @@ def test_import_export(app, some_data, meetings_agenda):
event.save() event.save()
desk, _ = Desk.objects.get_or_create(agenda=agenda2, label='Desk A', slug='desk-a') desk, _ = Desk.objects.get_or_create(agenda=agenda2, label='Desk A', slug='desk-a')
timeperiod = TimePeriod( timeperiod = TimePeriod(
desk=desk, desk=desk, weekday=2, start_time=datetime.time(10, 0), end_time=datetime.time(11, 0)
weekday=2, )
start_time=datetime.time(10, 0),
end_time=datetime.time(11, 0))
timeperiod.save() timeperiod.save()
exception = TimePeriodException( exception = TimePeriodException(
desk=desk, desk=desk,
start_datetime=make_aware(datetime.datetime(2017, 5, 22, 8, 0)), 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() exception.save()
import_site(json.loads(output), overwrite=True) 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 = Event(agenda=agenda1, start_datetime=make_aware(datetime.datetime.now()), places=10)
event.save() event.save()
timeperiod = TimePeriod( timeperiod = TimePeriod(
weekday=2, weekday=2, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(11, 0)
desk=desk, )
start_time=datetime.time(10, 0),
end_time=datetime.time(11, 0))
timeperiod.save() timeperiod.save()
exception = TimePeriodException( exception = TimePeriodException(
desk=desk, desk=desk,
start_datetime=make_aware(datetime.datetime(2017, 5, 22, 8, 0)), 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() exception.save()
import_site(json.loads(output), overwrite=False) import_site(json.loads(output), overwrite=False)
assert Event.objects.filter(id=event.id).count() == 1 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.wsgi import application
from chrono.agendas.models import (Agenda, Event, Booking, MeetingType, from chrono.agendas.models import Agenda, Event, Booking, MeetingType, TimePeriod, Desk, TimePeriodException
TimePeriod, Desk, TimePeriodException)
from chrono.manager.utils import export_site from chrono.manager.utils import export_site
pytestmark = pytest.mark.django_db pytestmark = pytest.mark.django_db
@pytest.fixture @pytest.fixture
def simple_user(): def simple_user():
try: try:
@ -30,6 +30,7 @@ def simple_user():
user = User.objects.create_user('user', password='user') user = User.objects.create_user('user', password='user')
return user return user
@pytest.fixture @pytest.fixture
def manager_user(): def manager_user():
try: try:
@ -42,6 +43,7 @@ def manager_user():
user.groups.set([group]) user.groups.set([group])
return user return user
@pytest.fixture @pytest.fixture
def admin_user(): def admin_user():
try: try:
@ -50,17 +52,20 @@ def admin_user():
user = User.objects.create_superuser('admin', email=None, password='admin') user = User.objects.create_superuser('admin', email=None, password='admin')
return user return user
@pytest.fixture @pytest.fixture
def api_user(): def api_user():
try: try:
user = User.objects.get(username='api-user') user = User.objects.get(username='api-user')
except User.DoesNotExist: except User.DoesNotExist:
user = User.objects.create(username='john.doe', user = User.objects.create(
first_name=u'John', last_name=u'Doe', email='john.doe@example.net') username='john.doe', first_name=u'John', last_name=u'Doe', email='john.doe@example.net'
)
user.set_password('password') user.set_password('password')
user.save() user.save()
return user return user
def login(app, username='admin', password='admin'): def login(app, username='admin', password='admin'):
login_page = app.get('/login/') login_page = app.get('/login/')
login_form = login_page.forms[0] login_form = login_page.forms[0]
@ -70,15 +75,18 @@ def login(app, username='admin', password='admin'):
assert resp.status_int == 302 assert resp.status_int == 302
return app return app
def test_unlogged_access(app): def test_unlogged_access(app):
# connect while not being logged in # connect while not being logged in
assert app.get('/manage/', status=302).location.endswith('/login/?next=/manage/') assert app.get('/manage/', status=302).location.endswith('/login/?next=/manage/')
def test_simple_user_access(app, simple_user): def test_simple_user_access(app, simple_user):
# connect while being logged as a simple user, access should be forbidden # connect while being logged as a simple user, access should be forbidden
app = login(app, username='user', password='user') app = login(app, username='user', password='user')
assert app.get('/manage/', status=403) assert app.get('/manage/', status=403)
def test_manager_user_access(app, manager_user): def test_manager_user_access(app, manager_user):
# connect while being logged as a manager user, access should be granted if # connect while being logged as a manager user, access should be granted if
# there's at least an agenda that is viewable or editable. # 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() agenda.save()
assert app.get('/manage/', status=200) assert app.get('/manage/', status=200)
def test_home_redirect(app): def test_home_redirect(app):
assert app.get('/', status=302).location.endswith('/manage/') assert app.get('/', status=302).location.endswith('/manage/')
def test_access(app, admin_user): def test_access(app, admin_user):
app = login(app) app = login(app)
resp = app.get('/manage/', status=200) resp = app.get('/manage/', status=200)
assert '<h2>Agendas</h2>' in resp.text assert '<h2>Agendas</h2>' in resp.text
assert "This site doesn't have any agenda yet." in resp.text assert "This site doesn't have any agenda yet." in resp.text
def test_logout(app, admin_user): def test_logout(app, admin_user):
app = login(app) app = login(app)
app.get('/logout/') app.get('/logout/')
assert app.get('/manage/', status=302).location.endswith('/login/?next=/manage/') assert app.get('/manage/', status=302).location.endswith('/login/?next=/manage/')
def test_menu_json(app, admin_user): def test_menu_json(app, admin_user):
app = login(app) app = login(app)
resp = app.get('/manage/menu.json', status=200) 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.text == 'Q(%s);' % resp.text
assert resp2.content_type == 'application/javascript' assert resp2.content_type == 'application/javascript'
def test_view_agendas_as_manager(app, manager_user): def test_view_agendas_as_manager(app, manager_user):
agenda = Agenda(label=u'Foo Bar') agenda = Agenda(label=u'Foo Bar')
agenda.view_role = manager_user.groups.all()[0] 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 # check it gives a 404 on unknown agendas
resp = app.get('/manage/agendas/%s/settings' % '9999', status=404) resp = app.get('/manage/agendas/%s/settings' % '9999', status=404)
def test_add_agenda(app, admin_user): def test_add_agenda(app, admin_user):
app = login(app) app = login(app)
resp = app.get('/manage/', status=200) resp = app.get('/manage/', status=200)
@ -169,6 +183,7 @@ def test_add_agenda(app, admin_user):
assert 'Foo bar' in resp.text assert 'Foo bar' in resp.text
assert '<h2>Settings' in resp.text assert '<h2>Settings' in resp.text
def test_add_agenda_as_manager(app, manager_user): def test_add_agenda_as_manager(app, manager_user):
# open /manage/ access to manager_user, and check agenda creation is not # open /manage/ access to manager_user, and check agenda creation is not
# allowed. # allowed.
@ -179,6 +194,7 @@ def test_add_agenda_as_manager(app, manager_user):
resp = app.get('/manage/', status=200) resp = app.get('/manage/', status=200)
resp = app.get('/manage/agendas/add/', status=403) resp = app.get('/manage/agendas/add/', status=403)
def test_options_agenda(app, admin_user): def test_options_agenda(app, admin_user):
agenda = Agenda(label=u'Foo bar') agenda = Agenda(label=u'Foo bar')
agenda.save() agenda.save()
@ -194,6 +210,7 @@ def test_options_agenda(app, admin_user):
assert 'Foo baz' in resp.text assert 'Foo baz' in resp.text
assert '<h2>Settings' in resp.text assert '<h2>Settings' in resp.text
def test_options_agenda_as_manager(app, manager_user): def test_options_agenda_as_manager(app, manager_user):
agenda = Agenda(label=u'Foo bar') agenda = Agenda(label=u'Foo bar')
agenda.view_role = manager_user.groups.all()[0] 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 'Foo baz' in resp.text
assert '<h2>Settings' in resp.text assert '<h2>Settings' in resp.text
def test_delete_agenda(app, admin_user): def test_delete_agenda(app, admin_user):
agenda = Agenda(label=u'Foo bar') agenda = Agenda(label=u'Foo bar')
agenda.save() agenda.save()
@ -239,11 +257,11 @@ def test_delete_agenda(app, admin_user):
resp = resp.follow() resp = resp.follow()
assert not 'Foo bar' in resp.text assert not 'Foo bar' in resp.text
def test_delete_busy_agenda(app, admin_user): def test_delete_busy_agenda(app, admin_user):
agenda = Agenda(label=u'Foo bar') agenda = Agenda(label=u'Foo bar')
agenda.save() agenda.save()
event = Event(start_datetime=now() + datetime.timedelta(days=10), event = Event(start_datetime=now() + datetime.timedelta(days=10), places=10, agenda=agenda)
places=10, agenda=agenda)
event.save() event.save()
app = login(app) app = login(app)
@ -272,6 +290,7 @@ def test_delete_busy_agenda(app, admin_user):
booking.save() booking.save()
resp = resp.form.submit(status=403) resp = resp.form.submit(status=403)
def test_delete_agenda_as_manager(app, manager_user): def test_delete_agenda_as_manager(app, manager_user):
agenda = Agenda(label=u'Foo bar') agenda = Agenda(label=u'Foo bar')
agenda.edit_role = manager_user.groups.all()[0] 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_a = Desk.objects.create(agenda=agenda, label='Desk A')
desk_b = Desk.objects.create(agenda=agenda, label='Desk B') desk_b = Desk.objects.create(agenda=agenda, label='Desk B')
event = Event(start_datetime=now() + datetime.timedelta(days=10), event = Event(start_datetime=now() + datetime.timedelta(days=10), places=10, agenda=agenda, desk=desk_a)
places=10, agenda=agenda, desk=desk_a)
event.save() event.save()
app = login(app) app = login(app)
@ -354,6 +372,7 @@ def test_add_event_on_missing_agenda(app, admin_user):
app = login(app) app = login(app)
app.get('/manage/agendas/%s/add-event' % '999', status=404) app.get('/manage/agendas/%s/add-event' % '999', status=404)
def test_add_event_as_manager(app, manager_user): def test_add_event_as_manager(app, manager_user):
agenda = Agenda(label=u'Foo bar') agenda = Agenda(label=u'Foo bar')
agenda.view_role = manager_user.groups.all()[0] 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 'Feb. 15, 2016, 5 p.m.' in resp.text
assert '10 places' in resp.text assert '10 places' in resp.text
def test_edit_event(app, admin_user): def test_edit_event(app, admin_user):
agenda = Agenda(label=u'Foo bar') agenda = Agenda(label=u'Foo bar')
agenda.save() agenda.save()
event = Event( event = Event(start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)), places=20, agenda=agenda)
start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)),
places=20, agenda=agenda)
event.save() event.save()
app = login(app) app = login(app)
resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=200) 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 'Feb. 16, 2016, 5 p.m.' in resp.text
assert '20 places' in resp.text assert '20 places' in resp.text
def test_edit_missing_event(app, admin_user): def test_edit_missing_event(app, admin_user):
app = login(app) app = login(app)
app.get('/manage/agendas/999/', status=404) app.get('/manage/agendas/999/', status=404)
def test_edit_event_as_manager(app, manager_user): def test_edit_event_as_manager(app, manager_user):
agenda = Agenda(label=u'Foo bar') agenda = Agenda(label=u'Foo bar')
agenda.view_role = manager_user.groups.all()[0] agenda.view_role = manager_user.groups.all()[0]
agenda.save() agenda.save()
event = Event(start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)), event = Event(start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)), places=20, agenda=agenda)
places=20, agenda=agenda)
event.save() event.save()
app = login(app, username='manager', password='manager') app = login(app, username='manager', password='manager')
resp = app.get('/manage/events/%s/' % event.id, status=403) 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 'Feb. 16, 2016, 5 p.m.' in resp.text
assert '20 places' in resp.text assert '20 places' in resp.text
def test_booked_places(app, admin_user): def test_booked_places(app, admin_user):
agenda = Agenda(label=u'Foo bar') agenda = Agenda(label=u'Foo bar')
agenda.save() agenda.save()
event = Event(start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)), event = Event(start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)), places=10, agenda=agenda)
places=10, agenda=agenda)
event.save() event.save()
Booking(event=event).save() Booking(event=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 '10 places' in resp.text
assert '2 booked places' in resp.text assert '2 booked places' in resp.text
def test_event_classes(app, admin_user): def test_event_classes(app, admin_user):
agenda = Agenda(label=u'Foo bar') agenda = Agenda(label=u'Foo bar')
agenda.save() agenda.save()
event = Event(start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)), event = Event(start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)), places=10, agenda=agenda)
places=10, agenda=agenda)
event.save() event.save()
for i in range(2): for i in range(2):
Booking(event=event).save() Booking(event=event).save()
@ -463,11 +482,11 @@ def test_event_classes(app, admin_user):
assert 'full' in resp.text assert 'full' in resp.text
assert 'overbooking' in resp.text assert 'overbooking' in resp.text
def test_delete_event(app, admin_user): def test_delete_event(app, admin_user):
agenda = Agenda(label=u'Foo bar') agenda = Agenda(label=u'Foo bar')
agenda.save() agenda.save()
event = Event(start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)), event = Event(start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)), places=10, agenda=agenda)
places=10, agenda=agenda)
event.save() event.save()
app = login(app) 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 resp.location.endswith('/manage/agendas/%s/settings' % agenda.id)
assert Event.objects.count() == 0 assert Event.objects.count() == 0
def test_delete_busy_event(app, admin_user): def test_delete_busy_event(app, admin_user):
agenda = Agenda(label=u'Foo bar') agenda = Agenda(label=u'Foo bar')
agenda.save() agenda.save()
event = Event(start_datetime=now() + datetime.timedelta(days=10), event = Event(start_datetime=now() + datetime.timedelta(days=10), places=10, agenda=agenda)
places=10, agenda=agenda)
event.save() event.save()
app = login(app) app = login(app)
@ -511,12 +530,12 @@ def test_delete_busy_event(app, admin_user):
booking.save() booking.save()
resp = resp.form.submit(status=403) resp = resp.form.submit(status=403)
def test_delete_event_as_manager(app, manager_user): def test_delete_event_as_manager(app, manager_user):
agenda = Agenda(label=u'Foo bar') agenda = Agenda(label=u'Foo bar')
agenda.edit_role = manager_user.groups.all()[0] agenda.edit_role = manager_user.groups.all()[0]
agenda.save() agenda.save()
event = Event(start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)), event = Event(start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)), places=10, agenda=agenda)
places=10, agenda=agenda)
event.save() event.save()
app = login(app, username='manager', password='manager') 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 resp.location.endswith('/manage/agendas/%s/settings' % agenda.id)
assert Event.objects.count() == 0 assert Event.objects.count() == 0
def test_import_events(app, admin_user): def test_import_events(app, admin_user):
agenda = Agenda(label=u'Foo bar') agenda = Agenda(label=u'Foo bar')
agenda.save() agenda.save()
@ -590,8 +610,9 @@ def test_import_events(app, admin_user):
Event.objects.all().delete() Event.objects.all().delete()
resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200) resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200)
resp.form['events_csv_file'] = Upload('t.csv', resp.form['events_csv_file'] = Upload(
u'2016-09-16,18:00,10,5,éléphant'.encode('utf-8'), 'text/csv') 't.csv', u'2016-09-16,18:00,10,5,éléphant'.encode('utf-8'), 'text/csv'
)
resp = resp.form.submit(status=302) resp = resp.form.submit(status=302)
assert Event.objects.count() == 1 assert Event.objects.count() == 1
assert Event.objects.all()[0].start_datetime == make_aware(datetime.datetime(2016, 9, 16, 18, 0)) 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() Event.objects.all().delete()
resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200) resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200)
resp.form['events_csv_file'] = Upload('t.csv', resp.form['events_csv_file'] = Upload(
u'2016-09-16,18:00,10,5,éléphant'.encode('iso-8859-15'), 'text/csv') 't.csv', u'2016-09-16,18:00,10,5,éléphant'.encode('iso-8859-15'), 'text/csv'
)
resp = resp.form.submit(status=302) resp = resp.form.submit(status=302)
assert Event.objects.count() == 1 assert Event.objects.count() == 1
assert Event.objects.all()[0].start_datetime == make_aware(datetime.datetime(2016, 9, 16, 18, 0)) 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() Event.objects.all().delete()
resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200) resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200)
resp.form['events_csv_file'] = Upload('t.csv', resp.form['events_csv_file'] = Upload(
u'2016-09-16,18:00,10,5,éléphant'.encode('eucjp'), 'text/csv') 't.csv', u'2016-09-16,18:00,10,5,éléphant'.encode('eucjp'), 'text/csv'
)
resp = resp.form.submit(status=302) resp = resp.form.submit(status=302)
assert Event.objects.count() == 1 assert Event.objects.count() == 1
assert Event.objects.all()[0].start_datetime == make_aware(datetime.datetime(2016, 9, 16, 18, 0)) 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() Event.objects.all().delete()
resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200) 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' resp.form['events_csv_file'] = Upload(
b'2016-09-16,18:00,10,5,bla bla bla\n' 't.csv',
b'\n' 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',
b'2016-09-19,18:00,10', 'text/csv') 'text/csv',
)
resp = resp.form.submit(status=302) resp = resp.form.submit(status=302)
assert Event.objects.count() == 2 assert Event.objects.count() == 2
Event.objects.all().delete() Event.objects.all().delete()
resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200) 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' resp.form['events_csv_file'] = Upload(
'"2016-09-16"\t"18:00"\t"10"\t"5"\t"éléphant"\n' 't.csv',
'"2016-09-19"\t"18:00"\t"10"'.encode('iso-8859-15'), 'text/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) resp = resp.form.submit(status=302)
assert Event.objects.count() == 2 assert Event.objects.count() == 2
Event.objects.all().delete() Event.objects.all().delete()
@ -671,6 +699,7 @@ def test_add_meetings_agenda(app, admin_user):
agenda = Agenda.objects.get(label='Foo bar') agenda = Agenda.objects.get(label='Foo bar')
assert agenda.kind == 'meetings' assert agenda.kind == 'meetings'
def test_meetings_agenda_add_meeting_type(app, admin_user): def test_meetings_agenda_add_meeting_type(app, admin_user):
agenda = Agenda(label=u'Foo bar', kind='meetings') agenda = Agenda(label=u'Foo bar', kind='meetings')
agenda.save() agenda.save()
@ -693,6 +722,7 @@ def test_meetings_agenda_add_meeting_type(app, admin_user):
resp = resp.form.submit() resp = resp.form.submit()
assert MeetingType.objects.get(agenda=agenda).duration == 30 assert MeetingType.objects.get(agenda=agenda).duration == 30
def test_meetings_agenda_delete_meeting_type(app, admin_user): def test_meetings_agenda_delete_meeting_type(app, admin_user):
agenda = Agenda(label=u'Foo bar', kind='meetings') agenda = Agenda(label=u'Foo bar', kind='meetings')
agenda.save() 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 resp.location.endswith('/manage/agendas/%s/settings' % agenda.id)
assert MeetingType.objects.count() == 0 assert MeetingType.objects.count() == 0
def test_meetings_agenda_add_time_period(app, admin_user): def test_meetings_agenda_add_time_period(app, admin_user):
agenda = Agenda(label=u'Foo bar', kind='meetings') agenda = Agenda(label=u'Foo bar', kind='meetings')
agenda.save() agenda.save()
@ -771,6 +802,7 @@ def test_meetings_agenda_add_time_period(app, admin_user):
resp = resp.form.submit() resp = resp.form.submit()
assert TimePeriod.objects.filter(desk=desk).count() == 4 assert TimePeriod.objects.filter(desk=desk).count() == 4
def test_meetings_agenda_delete_time_period(app, admin_user): def test_meetings_agenda_delete_time_period(app, admin_user):
agenda = Agenda(label=u'Foo bar', kind='meetings') agenda = Agenda(label=u'Foo bar', kind='meetings')
agenda.save() agenda.save()
@ -778,9 +810,9 @@ def test_meetings_agenda_delete_time_period(app, admin_user):
MeetingType(agenda=agenda, label='Blah').save() MeetingType(agenda=agenda, label='Blah').save()
desk = Desk.objects.create(agenda=agenda, label='Desk A') desk = Desk.objects.create(agenda=agenda, label='Desk A')
time_period = TimePeriod(desk=desk, weekday=2, time_period = TimePeriod(
start_time=datetime.time(10, 0), desk=desk, weekday=2, start_time=datetime.time(10, 0), end_time=datetime.time(18, 0)
end_time=datetime.time(18, 0)) )
time_period.save() time_period.save()
app = login(app) 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) resp = app.get('/manage/agendas/%d/settings' % agenda.id, status=403)
MeetingType(agenda=agenda, label='Blah').save() MeetingType(agenda=agenda, label='Blah').save()
app.get('/manage/agendas/%d/desk/%d/add-time-period' % (agenda.id, desk.id), status=403) 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), time_period = TimePeriod(
end_time=datetime.time(12, 0)) desk=desk, weekday=0, start_time=datetime.time(9, 0), end_time=datetime.time(12, 0)
)
time_period.save() time_period.save()
resp = app.get('/manage/agendas/%d/' % agenda.id) resp = app.get('/manage/agendas/%d/' % agenda.id)
app.get('/manage/timeperiods/%d/edit' % time_period.id, status=403) 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() resp = resp.form.submit().follow()
assert TimePeriodException.objects.count() == 1 assert TimePeriodException.objects.count() == 1
time_period_exception = TimePeriodException.objects.first() 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.start_datetime).strftime(dt_format) == tomorrow.replace(
assert localtime(time_period_exception.end_datetime).strftime(dt_format) == tomorrow.replace(hour=16).strftime(dt_format) 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 # add an exception beyond 2 weeks and make sure it isn't listed
resp = resp.click('Add a time period exception', index=1) resp = resp.click('Add a time period exception', index=1)
future = tomorrow + datetime.timedelta(days=15) 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 TimePeriodException.objects.count() == 2
assert 'Exception 1' in resp.text assert 'Exception 1' in resp.text
assert 'Exception 2' not 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 1' in resp.text
assert 'Exception 2' 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') agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Desk A') desk = Desk.objects.create(agenda=agenda, label='Desk A')
MeetingType(agenda=agenda, label='Blah').save() MeetingType(agenda=agenda, label='Blah').save()
TimePeriod.objects.create(weekday=1, desk=desk, TimePeriod.objects.create(
start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)) 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))) 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) Booking.objects.create(event=event)
login(app) login(app)
resp = app.get('/manage/agendas/%d/' % agenda.pk).follow() resp = app.get('/manage/agendas/%d/' % agenda.pk).follow()
resp = resp.click('Settings') resp = resp.click('Settings')
resp = resp.click('Add a time period exception') 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 # fields should be marked with errors
assert resp.text.count('This field is required.') == 2 assert resp.text.count('This field is required.') == 2
# try again with data in fields # 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 # check it's possible to add an exception on another desk
desk = Desk.objects.create(agenda=agenda, label='Desk B') desk = Desk.objects.create(agenda=agenda, label='Desk B')
TimePeriod.objects.create(weekday=1, desk=desk, TimePeriod.objects.create(
start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)) 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 = app.get('/manage/agendas/%d/' % agenda.pk).follow()
resp = resp.click('Settings') resp = resp.click('Settings')
resp = resp.click('Add a time period exception', href='desk/%s/' % desk.id) 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() resp = resp.form.submit()
assert TimePeriodException.objects.count() == 1 assert TimePeriodException.objects.count() == 1
def test_meetings_agenda_add_time_period_exception_when_cancelled_booking_exists(app, admin_user): def test_meetings_agenda_add_time_period_exception_when_cancelled_booking_exists(app, admin_user):
agenda = Agenda.objects.create(label='Foo bar', kind='meetings') agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Desk A') desk = Desk.objects.create(agenda=agenda, label='Desk A')
MeetingType(agenda=agenda, label='Blah').save() MeetingType(agenda=agenda, label='Blah').save()
TimePeriod.objects.create(weekday=1, desk=desk, TimePeriod.objects.create(
start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)) 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))) event = Event.objects.create(
Booking.objects.create(event=event, agenda=agenda, places=1, start_datetime=make_aware(datetime.datetime(2017, 5, 22, 10, 30))
cancellation_datetime=make_aware(datetime.datetime(2017, 5, 20, 10, 30))) )
Booking.objects.create(
event=event, cancellation_datetime=make_aware(datetime.datetime(2017, 5, 20, 10, 30))
)
login(app) login(app)
resp = app.get('/manage/agendas/%d/' % agenda.pk).follow() resp = app.get('/manage/agendas/%d/' % agenda.pk).follow()
resp = resp.click('Settings') 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 'One or several bookings exists within this time slot.' not in resp.text
assert TimePeriodException.objects.count() == 1 assert TimePeriodException.objects.count() == 1
def test_meetings_agenda_add_invalid_time_period_exception(app, admin_user): def test_meetings_agenda_add_invalid_time_period_exception(app, admin_user):
agenda = Agenda.objects.create(label='Foo bar', kind='meetings') agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Desk A') desk = Desk.objects.create(agenda=agenda, label='Desk A')
MeetingType(agenda=agenda, label='Blah').save() MeetingType(agenda=agenda, label='Blah').save()
TimePeriod.objects.create(weekday=1, desk=desk, TimePeriod.objects.create(
start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)) weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)
)
login(app) login(app)
resp = app.get('/manage/agendas/%d/' % agenda.pk).follow() resp = app.get('/manage/agendas/%d/' % agenda.pk).follow()
resp = resp.click('Settings') 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') agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Desk A') desk = Desk.objects.create(agenda=agenda, label='Desk A')
MeetingType(agenda=agenda, label='Blah').save() MeetingType(agenda=agenda, label='Blah').save()
TimePeriod.objects.create(weekday=1, desk=desk, TimePeriod.objects.create(
start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)) weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)
)
login(app) login(app)
resp = app.get('/manage/agendas/%d/' % agenda.pk).follow() resp = app.get('/manage/agendas/%d/' % agenda.pk).follow()
resp = resp.click('Settings') resp = resp.click('Settings')
@ -1022,8 +1071,12 @@ def test_meetings_agenda_delete_time_period_exception(app, admin_user):
resp = resp.form.submit().follow() resp = resp.form.submit().follow()
assert TimePeriodException.objects.count() == 1 assert TimePeriodException.objects.count() == 1
time_period_exception = TimePeriodException.objects.first() 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.start_datetime).strftime(dt_format) == tomorrow.replace(
assert localtime(time_period_exception.end_datetime).strftime(dt_format) == tomorrow.replace(hour=16).strftime(dt_format) 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(href='/manage/time-period-exceptions/%d/edit' % time_period_exception.id)
resp = resp.click('Delete') resp = resp.click('Delete')
resp = resp.form.submit().follow() resp = resp.form.submit().follow()
@ -1035,7 +1088,8 @@ def test_meetings_agenda_delete_time_period_exception(app, admin_user):
label='Future Exception', label='Future Exception',
desk=desk, desk=desk,
start_datetime=now() + datetime.timedelta(days=1), 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 = 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.click(href='/manage/time-period-exceptions/%d/delete' % time_period_exception.pk)
resp = resp.form.submit( resp = resp.form.submit(
@ -1048,23 +1102,27 @@ def test_exception_list(app, admin_user):
agenda = Agenda.objects.create(label='Foo bar', kind='meetings') agenda = Agenda.objects.create(label='Foo bar', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Desk A') desk = Desk.objects.create(agenda=agenda, label='Desk A')
MeetingType(agenda=agenda, label='Blah').save() MeetingType(agenda=agenda, label='Blah').save()
TimePeriod.objects.create(weekday=1, desk=desk, TimePeriod.objects.create(
start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)) weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)
)
past_exception = TimePeriodException.objects.create( past_exception = TimePeriodException.objects.create(
label='Past Exception', label='Past Exception',
desk=desk, desk=desk,
start_datetime=now() - datetime.timedelta(days=2), 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( current_exception = TimePeriodException.objects.create(
label='Current Exception', label='Current Exception',
desk=desk, desk=desk,
start_datetime=now() - datetime.timedelta(days=1), 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( future_exception = TimePeriodException.objects.create(
label='Future Exception', label='Future Exception',
desk=desk, desk=desk,
start_datetime=now() + datetime.timedelta(days=1), start_datetime=now() + datetime.timedelta(days=1),
end_datetime=now() + datetime.timedelta(days=2)) end_datetime=now() + datetime.timedelta(days=2),
)
login(app) login(app)
resp = app.get('/manage/agendas/%d/settings' % agenda.pk) 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') resp = resp.click('Settings')
assert 'Import exceptions from .ics' not in resp.text assert 'Import exceptions from .ics' not in resp.text
TimePeriod.objects.create(weekday=1, desk=desk, TimePeriod.objects.create(
start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)) 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 = app.get('/manage/agendas/%d/' % agenda.pk).follow()
resp = resp.click('Settings') 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') agenda = Agenda.objects.create(label='Example', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Test Desk') desk = Desk.objects.create(agenda=agenda, label='Test Desk')
MeetingType(agenda=agenda, label='Foo').save() MeetingType(agenda=agenda, label='Foo').save()
TimePeriod.objects.create(weekday=1, desk=desk, TimePeriod.objects.create(
start_time=datetime.time(10, 0), weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)
end_time=datetime.time(12, 0)) )
login(app) login(app)
resp = app.get('/manage/agendas/%d/' % agenda.pk).follow() resp = app.get('/manage/agendas/%d/' % agenda.pk).follow()
resp = resp.click('Settings') 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') resp = resp.click('Settings')
assert 'Import exceptions from .ics' not in resp.text assert 'Import exceptions from .ics' not in resp.text
TimePeriod.objects.create(weekday=1, desk=desk, TimePeriod.objects.create(
start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)) 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 = app.get('/manage/agendas/%d/' % agenda.pk).follow()
resp = resp.click('Settings') 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 assert 'ics_url' in resp.form.fields
resp.form['ics_url'] = 'http://example.com/foo.ics' resp.form['ics_url'] = 'http://example.com/foo.ics'
mocked_response = mock.Mock() mocked_response = mock.Mock()
mocked_response.text = """BEGIN:VCALENDAR mocked_response.text = """BEGIN:VCALENDAR
VERSION:2.0 VERSION:2.0
PRODID:-//foo.bar//EN PRODID:-//foo.bar//EN
BEGIN:VEVENT BEGIN:VEVENT
@ -1213,8 +1273,10 @@ END:VCALENDAR"""
resp = resp.click('upload') resp = resp.click('upload')
resp.form['ics_url'] = '' resp.form['ics_url'] = ''
resp = resp.form.submit(status=302) resp = resp.form.submit(status=302)
assert not TimePeriodException.objects.filter(desk=desk, assert not TimePeriodException.objects.filter(
external_id='desk-%s:random-event-id' % desk.id).exists() desk=desk, external_id='desk-%s:random-event-id' % desk.id
).exists()
@mock.patch('chrono.agendas.models.requests.get') @mock.patch('chrono.agendas.models.requests.get')
def test_agenda_import_time_period_exception_with_remote_ics_no_events(mocked_get, app, admin_user): 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') resp = resp.click('Settings')
assert 'Import exceptions from .ics' not in resp.text assert 'Import exceptions from .ics' not in resp.text
TimePeriod.objects.create(weekday=1, desk=desk, TimePeriod.objects.create(
start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)) 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 = app.get('/manage/agendas/%d/' % agenda.pk).follow()
resp = resp.click('Settings') resp = resp.click('Settings')
resp = resp.click('upload') resp = resp.click('upload')
resp.form['ics_url'] = 'http://example.com/foo.ics' resp.form['ics_url'] = 'http://example.com/foo.ics'
mocked_response = mock.Mock() mocked_response = mock.Mock()
mocked_response.text = """BEGIN:VCALENDAR mocked_response.text = """BEGIN:VCALENDAR
VERSION:2.0 VERSION:2.0
PRODID:-//foo.bar//EN PRODID:-//foo.bar//EN
BEGIN:VEVENT BEGIN:VEVENT
@ -1257,8 +1320,7 @@ END:VCALENDAR"""
resp = resp.click('Settings') resp = resp.click('Settings')
resp = resp.click('upload') resp = resp.click('upload')
resp = resp.form.submit(status=302) resp = resp.form.submit(status=302)
assert not TimePeriodException.objects.filter(desk=desk, assert not TimePeriodException.objects.filter(desk=desk, external_id='random-event-id').exists()
external_id='random-event-id').exists()
@mock.patch('chrono.agendas.models.requests.get') @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') resp = resp.click('Settings')
assert 'Import exceptions from .ics' not in resp.text assert 'Import exceptions from .ics' not in resp.text
TimePeriod.objects.create(weekday=1, desk=desk, TimePeriod.objects.create(
start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)) 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 = app.get('/manage/agendas/%d/' % agenda.pk).follow()
resp = resp.click('Settings') resp = resp.click('Settings')
resp = resp.click('upload') resp = resp.click('upload')
resp.form['ics_url'] = 'http://example.com/foo.ics' resp.form['ics_url'] = 'http://example.com/foo.ics'
mocked_response = mock.Mock() mocked_response = mock.Mock()
mocked_response.text = """BEGIN:VCALENDAR mocked_response.text = """BEGIN:VCALENDAR
VERSION:2.0 VERSION:2.0
PRODID:-//foo.bar//EN PRODID:-//foo.bar//EN
BEGIN:VEVENT BEGIN:VEVENT
@ -1316,8 +1379,11 @@ END:VCALENDAR"""
resp = resp.form.submit(status=302) resp = resp.form.submit(status=302)
assert TimePeriodException.objects.filter(desk=desk).count() == 1 assert TimePeriodException.objects.filter(desk=desk).count() == 1
@mock.patch('chrono.agendas.models.requests.get') @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') agenda = Agenda.objects.create(label='New Example', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='New Desk') desk = Desk.objects.create(agenda=agenda, label='New Desk')
MeetingType(agenda=agenda, label='Bar').save() 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') resp = resp.click('Settings')
assert 'Import exceptions from .ics' not in resp.text assert 'Import exceptions from .ics' not in resp.text
TimePeriod.objects.create(weekday=1, desk=desk, TimePeriod.objects.create(
start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)) 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 = app.get('/manage/agendas/%d/' % agenda.pk).follow()
resp = resp.click('Settings') 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' resp.form['ics_url'] = 'http://example.com/foo.ics'
mocked_response = mock.Mock() mocked_response = mock.Mock()
mocked_get.return_value = mocked_response mocked_get.return_value = mocked_response
def mocked_requests_connection_error(*args, **kwargs): def mocked_requests_connection_error(*args, **kwargs):
raise requests.exceptions.ConnectionError('unreachable') raise requests.exceptions.ConnectionError('unreachable')
mocked_get.side_effect = mocked_requests_connection_error mocked_get.side_effect = mocked_requests_connection_error
resp = resp.form.submit(status=200) resp = resp.form.submit(status=200)
assert 'Failed to retrieve remote calendar (http://example.com/foo.ics, unreachable).' in resp.text assert 'Failed to retrieve remote calendar (http://example.com/foo.ics, unreachable).' in resp.text
@mock.patch('chrono.agendas.models.requests.get') @mock.patch('chrono.agendas.models.requests.get')
def test_agenda_import_time_period_exception_from_forbidden_remote_ics(mocked_get, app, admin_user): 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') 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') resp = resp.click('Settings')
assert 'Import exceptions from .ics' not in resp.text assert 'Import exceptions from .ics' not in resp.text
TimePeriod.objects.create(weekday=1, desk=desk, TimePeriod.objects.create(
start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)) 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 = app.get('/manage/agendas/%d/' % agenda.pk).follow()
resp = resp.click('Settings') 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 = mock.Mock()
mocked_response.status_code = 403 mocked_response.status_code = 403
mocked_get.return_value = mocked_response mocked_get.return_value = mocked_response
def mocked_requests_http_forbidden_error(*args, **kwargs): def mocked_requests_http_forbidden_error(*args, **kwargs):
raise requests.exceptions.HTTPError(response=mocked_response) raise requests.exceptions.HTTPError(response=mocked_response)
mocked_get.side_effect = mocked_requests_http_forbidden_error mocked_get.side_effect = mocked_requests_http_forbidden_error
resp = resp.form.submit(status=200) resp = resp.form.submit(status=200)
assert 'Failed to retrieve remote calendar (http://example.com/foo.ics, HTTP error 403).' in resp.text assert 'Failed to retrieve remote calendar (http://example.com/foo.ics, HTTP error 403).' in resp.text
@mock.patch('chrono.agendas.models.requests.get') @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): 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') 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 = app.get('/manage/agendas/%d/' % agenda.pk).follow()
resp = resp.click('Settings') resp = resp.click('Settings')
assert 'Import exceptions from .ics' not in resp.text assert 'Import exceptions from .ics' not in resp.text
TimePeriod.objects.create(weekday=1, desk=desk, TimePeriod.objects.create(
start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)) 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 = app.get('/manage/agendas/%d/' % agenda.pk).follow()
resp = resp.click('Settings') 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' resp.form['ics_url'] = 'https://example.com/foo.ics'
mocked_response = mock.Mock() mocked_response = mock.Mock()
mocked_get.return_value = mocked_response mocked_get.return_value = mocked_response
def mocked_requests_http_ssl_error(*args, **kwargs): def mocked_requests_http_ssl_error(*args, **kwargs):
raise requests.exceptions.SSLError('SSL error') raise requests.exceptions.SSLError('SSL error')
mocked_get.side_effect = mocked_requests_http_ssl_error mocked_get.side_effect = mocked_requests_http_ssl_error
resp = resp.form.submit(status=200) resp = resp.form.submit(status=200)
assert 'Failed to retrieve remote calendar (https://example.com/foo.ics, SSL error).' in resp.text 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): def test_agenda_day_view(app, admin_user, manager_user, api_user):
agenda = Agenda.objects.create(label='New Example', kind='meetings') agenda = Agenda.objects.create(label='New Example', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='New Desk') 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() resp = resp.follow()
assert 'No opening hours this day.' in resp.text # no time pediod assert 'No opening hours this day.' in resp.text # no time pediod
timeperiod = TimePeriod(desk=desk, weekday=today.weekday(), timeperiod = TimePeriod(
start_time=datetime.time(10, 0), desk=desk, weekday=today.weekday(), start_time=datetime.time(10, 0), end_time=datetime.time(18, 0)
end_time=datetime.time(18, 0)) )
timeperiod.save() timeperiod.save()
resp = app.get('/manage/agendas/%s/' % agenda.id, status=302).follow() resp = app.get('/manage/agendas/%s/' % agenda.id, status=302).follow()
assert not 'No opening hours this day.' in resp.text 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_url = resp.json['data'][0]['api']['fillslot_url']
booking_url2 = resp.json['data'][2]['api']['fillslot_url'] booking_url2 = resp.json['data'][2]['api']['fillslot_url']
resp = app.post(booking_url) resp = app.post(booking_url)
resp = app.post_json(booking_url2, resp = app.post_json(booking_url2, params={'label': 'foo', 'user': 'bar', 'url': 'http://baz/'})
params={'label': 'foo', 'user': 'bar', 'url': 'http://baz/'})
app.reset() app.reset()
login(app) login(app)
date = Booking.objects.all()[0].event.start_datetime date = Booking.objects.all()[0].event.start_datetime
resp = app.get('/manage/agendas/%s/%d/%d/%d/' % ( resp = app.get('/manage/agendas/%s/%d/%d/%d/' % (agenda.id, date.year, date.month, date.day))
agenda.id, date.year, date.month, date.day))
assert resp.text.count('div class="booking') == 2 assert resp.text.count('div class="booking') == 2
assert 'hourspan-2' in resp.text # table CSS class assert 'hourspan-2' in resp.text # table CSS class
assert 'height: 50%; top: 0%;' in resp.text # booking cells 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) # (and visually this will give more room for events)
meetingtype = MeetingType(agenda=agenda, label='Baz', duration=15) meetingtype = MeetingType(agenda=agenda, label='Baz', duration=15)
meetingtype.save() meetingtype.save()
resp = app.get('/manage/agendas/%s/%d/%d/%d/' % ( resp = app.get('/manage/agendas/%s/%d/%d/%d/' % (agenda.id, date.year, date.month, date.day))
agenda.id, date.year, date.month, date.day))
assert resp.text.count('div class="booking') == 2 assert resp.text.count('div class="booking') == 2
assert 'hourspan-4' in resp.text # table CSS class 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() app.reset()
login(app) login(app)
resp = app.get('/manage/agendas/%s/%d/%d/%d/' % ( resp = app.get('/manage/agendas/%s/%d/%d/%d/' % (agenda.id, date.year, date.month, date.day))
agenda.id, date.year, date.month, date.day))
assert resp.text.count('div class="booking') == 1 assert resp.text.count('div class="booking') == 1
# wrong type # wrong type
agenda2 = Agenda(label=u'Foo bar') agenda2 = Agenda(label=u'Foo bar')
agenda2.save() agenda2.save()
resp = app.get('/manage/agendas/%s/%d/%d/%d/' % ( resp = app.get('/manage/agendas/%s/%d/%d/%d/' % (agenda2.id, date.year, date.month, date.day), status=404)
agenda2.id, date.year, date.month, date.day), status=404)
# not enough permissions # not enough permissions
agenda2.view_role = manager_user.groups.all()[0] agenda2.view_role = manager_user.groups.all()[0]
agenda2.save() agenda2.save()
app.reset() app.reset()
app = login(app, username='manager', password='manager') app = login(app, username='manager', password='manager')
resp = app.get('/manage/agendas/%s/%d/%d/%d/' % ( resp = app.get('/manage/agendas/%s/%d/%d/%d/' % (agenda.id, date.year, date.month, date.day), status=403)
agenda.id, date.year, date.month, date.day), status=403)
# just enough permissions # just enough permissions
agenda.view_role = manager_user.groups.all()[0] agenda.view_role = manager_user.groups.all()[0]
agenda.save() agenda.save()
resp = app.get('/manage/agendas/%s/%d/%d/%d/' % ( resp = app.get('/manage/agendas/%s/%d/%d/%d/' % (agenda.id, date.year, date.month, date.day), status=200)
agenda.id, date.year, date.month, date.day), status=200)
def test_agenda_day_view_late_meeting(app, admin_user, manager_user, api_user): def test_agenda_day_view_late_meeting(app, admin_user, manager_user, api_user):
agenda = Agenda.objects.create(label='New Example', kind='meetings') 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() today = datetime.date.today()
timeperiod = TimePeriod(desk=desk, weekday=today.weekday(), timeperiod = TimePeriod(
start_time=datetime.time(10, 0), desk=desk, weekday=today.weekday(), start_time=datetime.time(10, 0), end_time=datetime.time(23, 30)
end_time=datetime.time(23, 30)) )
timeperiod.save() timeperiod.save()
login(app) 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 resp.text.count('<tr') == 15
assert '<th class="hour">11 p.m.</th>' in resp.text assert '<th class="hour">11 p.m.</th>' in resp.text
def test_agenda_invalid_day_view(app, admin_user, manager_user, api_user): def test_agenda_invalid_day_view(app, admin_user, manager_user, api_user):
agenda = Agenda.objects.create(label='New Example', kind='meetings') agenda = Agenda.objects.create(label='New Example', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='New Desk') 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) resp = app.get('/manage/agendas/%s/%d/%d/%d/' % (agenda.id, 2018, 11, 31), status=302)
assert resp.location.endswith('2018/11/30/') assert resp.location.endswith('2018/11/30/')
def test_agenda_month_view(app, admin_user, manager_user, api_user): def test_agenda_month_view(app, admin_user, manager_user, api_user):
agenda = Agenda.objects.create(label='Passeports', kind='meetings') agenda = Agenda.objects.create(label='Passeports', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Desk A') 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() today = datetime.date.today()
assert resp.request.url.endswith('%s/%s/' % (today.year, today.month)) 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 assert 'No opening hours this month.' in resp.text
today = datetime.date(2018, 11, 10) # fixed day today = datetime.date(2018, 11, 10) # fixed day
timeperiod_weekday = today.weekday() timeperiod_weekday = today.weekday()
timeperiod = TimePeriod(desk=desk, weekday=timeperiod_weekday, timeperiod = TimePeriod(
start_time=datetime.time(10, 0), desk=desk, weekday=timeperiod_weekday, start_time=datetime.time(10, 0), end_time=datetime.time(18, 0)
end_time=datetime.time(18, 0)) )
timeperiod.save() timeperiod.save()
resp = app.get('/manage/agendas/%s/%s/%s/' % (agenda.id, today.year, today.month)) 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 'No opening hours this month.' in resp.text
assert not '<div class="booking' in resp.text assert not '<div class="booking' in resp.text
first_month_day = today.replace(day=1) 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] start_week_number = first_month_day.isocalendar()[1]
end_week_number = last_month_day.isocalendar()[1] end_week_number = last_month_day.isocalendar()[1]
weeks_number = end_week_number - start_week_number + 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_url = resp.json['data'][0]['api']['fillslot_url']
booking_url2 = resp.json['data'][2]['api']['fillslot_url'] booking_url2 = resp.json['data'][2]['api']['fillslot_url']
booking = app.post(booking_url) booking = app.post(booking_url)
booking_2 = app.post_json(booking_url2, booking_2 = app.post_json(booking_url2, params={'label': 'foo', 'user': 'bar', 'url': 'http://baz/'})
params={'label': 'foo', 'user': 'bar', 'url': 'http://baz/'})
app.reset() app.reset()
login(app) login(app)
date = Booking.objects.all()[0].event.start_datetime date = Booking.objects.all()[0].event.start_datetime
resp = app.get('/manage/agendas/%s/%d/%d/' % ( resp = app.get('/manage/agendas/%s/%d/%d/' % (agenda.id, date.year, date.month))
agenda.id, date.year, date.month)) assert resp.text.count('<div class="booking" style="left:1.0%;height:33.0%;') == 2 # booking cells
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') desk = Desk.objects.create(agenda=agenda, label='Desk B')
timeperiod = TimePeriod(desk=desk, weekday=timeperiod_weekday, timeperiod = TimePeriod(
start_time=datetime.time(10, 0), desk=desk, weekday=timeperiod_weekday, start_time=datetime.time(10, 0), end_time=datetime.time(18, 0)
end_time=datetime.time(18, 0)) )
timeperiod.save() timeperiod.save()
app.reset() 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)) 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 'No opening hours this month.' in resp.text
def test_agenda_month_view_dst_change(app, admin_user, manager_user, api_user): def test_agenda_month_view_dst_change(app, admin_user, manager_user, api_user):
agenda = Agenda.objects.create(label='Passeports', kind='meetings') agenda = Agenda.objects.create(label='Passeports', kind='meetings')
desk = Desk.objects.create(agenda=agenda, label='Desk A') 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() meetingtype.save()
for weekday in range(0, 7): # open all mornings for weekday in range(0, 7): # open all mornings
TimePeriod(desk=desk, weekday=weekday, TimePeriod(
start_time=datetime.time(9, 0), desk=desk, weekday=weekday, start_time=datetime.time(9, 0), end_time=datetime.time(12, 0)
end_time=datetime.time(12, 0)).save() ).save()
login(app) login(app)
for date in ('2019-10-01', '2019-10-31'): 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/', status=200)
resp = app.get('/manage/agendas/import/', status=403) resp = app.get('/manage/agendas/import/', status=403)
def test_import_agenda(app, admin_user): def test_import_agenda(app, admin_user):
agenda = Agenda(label=u'Foo bar') agenda = Agenda(label=u'Foo bar')
agenda.save() agenda.save()

View File

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

View File

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

View File

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