0001-misc-black-29209.patch
chrono/agendas/management/commands/sync_desks_timeperiod_exceptions.py | ||
---|---|---|
18 | 18 | |
19 | 19 |
import sys |
20 | 20 | |
21 |
from chrono.agendas.models import Desk, ICSError |
|
22 | 21 |
from django.core.management.base import BaseCommand |
23 | 22 | |
23 |
from chrono.agendas.models import Desk, ICSError |
|
24 | ||
24 | 25 | |
25 | 26 |
class Command(BaseCommand): |
26 | 27 |
help = 'Synchronize time period exceptions from desks remote ics' |
chrono/agendas/migrations/0001_initial.py | ||
---|---|---|
1 | 1 |
# -*- coding: utf-8 -*- |
2 | 2 |
from __future__ import unicode_literals |
3 | 3 | |
4 |
from django.db import models, migrations
|
|
4 |
from django.db import migrations, models
|
|
5 | 5 | |
6 | 6 | |
7 | 7 |
class Migration(migrations.Migration): |
8 | 8 | |
9 |
dependencies = [ |
|
10 |
] |
|
9 |
dependencies = [] |
|
11 | 10 | |
12 | 11 |
operations = [ |
13 | 12 |
migrations.CreateModel( |
14 | 13 |
name='Agenda', |
15 | 14 |
fields=[ |
16 |
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), |
|
15 |
( |
|
16 |
'id', |
|
17 |
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True), |
|
18 |
), |
|
17 | 19 |
('label', models.CharField(max_length=50, verbose_name='Label')), |
18 | 20 |
('slug', models.SlugField(verbose_name='Identifier')), |
19 | 21 |
], |
20 |
options={ |
|
21 |
'ordering': ['label'], |
|
22 |
}, |
|
22 |
options={'ordering': ['label'],}, |
|
23 | 23 |
bases=(models.Model,), |
24 | 24 |
), |
25 | 25 |
] |
chrono/agendas/migrations/0002_event.py | ||
---|---|---|
1 | 1 |
# -*- coding: utf-8 -*- |
2 | 2 |
from __future__ import unicode_literals |
3 | 3 | |
4 |
from django.db import models, migrations
|
|
4 |
from django.db import migrations, models
|
|
5 | 5 | |
6 | 6 | |
7 | 7 |
class Migration(migrations.Migration): |
... | ... | |
14 | 14 |
migrations.CreateModel( |
15 | 15 |
name='Event', |
16 | 16 |
fields=[ |
17 |
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), |
|
17 |
( |
|
18 |
'id', |
|
19 |
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True), |
|
20 |
), |
|
18 | 21 |
('start_datetime', models.DateTimeField(verbose_name='Date/time')), |
19 | 22 |
('places', models.PositiveIntegerField(verbose_name='Places')), |
20 | 23 |
('agenda', models.ForeignKey(to='agendas.Agenda', on_delete=models.CASCADE)), |
21 | 24 |
], |
22 |
options={ |
|
23 |
'ordering': ['agenda', 'start_datetime'], |
|
24 |
}, |
|
25 |
options={'ordering': ['agenda', 'start_datetime'],}, |
|
25 | 26 |
bases=(models.Model,), |
26 | 27 |
), |
27 | 28 |
] |
chrono/agendas/migrations/0003_booking.py | ||
---|---|---|
1 | 1 |
# -*- coding: utf-8 -*- |
2 | 2 |
from __future__ import unicode_literals |
3 | 3 | |
4 |
from django.db import models, migrations |
|
5 | 4 |
import jsonfield.fields |
5 |
from django.db import migrations, models |
|
6 | 6 | |
7 | 7 | |
8 | 8 |
class Migration(migrations.Migration): |
... | ... | |
15 | 15 |
migrations.CreateModel( |
16 | 16 |
name='Booking', |
17 | 17 |
fields=[ |
18 |
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), |
|
18 |
( |
|
19 |
'id', |
|
20 |
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True), |
|
21 |
), |
|
19 | 22 |
('extra_data', jsonfield.fields.JSONField(null=True)), |
20 | 23 |
('event', models.ForeignKey(to='agendas.Event', on_delete=models.CASCADE)), |
21 | 24 |
], |
22 |
options={ |
|
23 |
}, |
|
25 |
options={}, |
|
24 | 26 |
bases=(models.Model,), |
25 | 27 |
), |
26 | 28 |
] |
chrono/agendas/migrations/0004_booking_cancellation_datetime.py | ||
---|---|---|
1 | 1 |
# -*- coding: utf-8 -*- |
2 | 2 |
from __future__ import unicode_literals |
3 | 3 | |
4 |
from django.db import models, migrations
|
|
4 |
from django.db import migrations, models
|
|
5 | 5 | |
6 | 6 | |
7 | 7 |
class Migration(migrations.Migration): |
chrono/agendas/migrations/0005_event_label.py | ||
---|---|---|
14 | 14 |
migrations.AddField( |
15 | 15 |
model_name='event', |
16 | 16 |
name='label', |
17 |
field=models.CharField(help_text='Optional label to identify this date.', max_length=50, null=True, blank=True, verbose_name='Label'), |
|
17 |
field=models.CharField( |
|
18 |
help_text='Optional label to identify this date.', |
|
19 |
max_length=50, |
|
20 |
null=True, |
|
21 |
blank=True, |
|
22 |
verbose_name='Label', |
|
23 |
), |
|
18 | 24 |
), |
19 | 25 |
] |
chrono/agendas/migrations/0006_auto_20160707_1357.py | ||
---|---|---|
1 | 1 |
# -*- coding: utf-8 -*- |
2 | 2 |
from __future__ import unicode_literals |
3 | 3 | |
4 |
from django.db import migrations, models |
|
5 | 4 |
import datetime |
5 | ||
6 |
from django.db import migrations, models |
|
6 | 7 |
from django.utils.timezone import utc |
7 | 8 | |
8 | 9 | |
... | ... | |
16 | 17 |
migrations.AddField( |
17 | 18 |
model_name='booking', |
18 | 19 |
name='creation_datetime', |
19 |
field=models.DateTimeField(default=datetime.datetime(2016, 7, 7, 13, 57, 47, 975893, tzinfo=utc), auto_now_add=True), |
|
20 |
field=models.DateTimeField( |
|
21 |
default=datetime.datetime(2016, 7, 7, 13, 57, 47, 975893, tzinfo=utc), auto_now_add=True |
|
22 |
), |
|
20 | 23 |
preserve_default=False, |
21 | 24 |
), |
22 | 25 |
migrations.AddField( |
23 |
model_name='booking', |
|
24 |
name='in_waiting_list', |
|
25 |
field=models.BooleanField(default=False), |
|
26 |
), |
|
27 |
migrations.AddField( |
|
28 |
model_name='event', |
|
29 |
name='full', |
|
30 |
field=models.BooleanField(default=False), |
|
26 |
model_name='booking', name='in_waiting_list', field=models.BooleanField(default=False), |
|
31 | 27 |
), |
28 |
migrations.AddField(model_name='event', name='full', field=models.BooleanField(default=False),), |
|
32 | 29 |
migrations.AddField( |
33 | 30 |
model_name='event', |
34 | 31 |
name='waiting_list_places', |
chrono/agendas/migrations/0007_auto_20160722_1135.py | ||
---|---|---|
12 | 12 | |
13 | 13 |
operations = [ |
14 | 14 |
migrations.AlterField( |
15 |
model_name='agenda', |
|
16 |
name='label', |
|
17 |
field=models.CharField(max_length=100, verbose_name='Label'), |
|
15 |
model_name='agenda', name='label', field=models.CharField(max_length=100, verbose_name='Label'), |
|
18 | 16 |
), |
19 | 17 |
] |
chrono/agendas/migrations/0008_auto_20160910_1319.py | ||
---|---|---|
14 | 14 |
migrations.CreateModel( |
15 | 15 |
name='MeetingType', |
16 | 16 |
fields=[ |
17 |
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), |
|
17 |
( |
|
18 |
'id', |
|
19 |
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True), |
|
20 |
), |
|
18 | 21 |
('label', models.CharField(max_length=100, verbose_name='Label')), |
19 | 22 |
('duration', models.IntegerField(default=30, verbose_name='Duration (in minutes)')), |
20 | 23 |
], |
21 |
options={ |
|
22 |
'ordering': ['label'], |
|
23 |
}, |
|
24 |
options={'ordering': ['label'],}, |
|
24 | 25 |
), |
25 | 26 |
migrations.CreateModel( |
26 | 27 |
name='TimePeriod', |
27 | 28 |
fields=[ |
28 |
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), |
|
29 |
('weekday', models.IntegerField(verbose_name='Week day', choices=[(0, 'Monday'), (1, 'Tuesday'), (2, 'Wednesday'), (3, 'Thursday'), (4, 'Friday'), (5, 'Saturday'), (6, 'Sunday')])), |
|
29 |
( |
|
30 |
'id', |
|
31 |
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True), |
|
32 |
), |
|
33 |
( |
|
34 |
'weekday', |
|
35 |
models.IntegerField( |
|
36 |
verbose_name='Week day', |
|
37 |
choices=[ |
|
38 |
(0, 'Monday'), |
|
39 |
(1, 'Tuesday'), |
|
40 |
(2, 'Wednesday'), |
|
41 |
(3, 'Thursday'), |
|
42 |
(4, 'Friday'), |
|
43 |
(5, 'Saturday'), |
|
44 |
(6, 'Sunday'), |
|
45 |
], |
|
46 |
), |
|
47 |
), |
|
30 | 48 |
('start_time', models.TimeField(verbose_name='Start')), |
31 | 49 |
('end_time', models.TimeField(verbose_name='End')), |
32 | 50 |
], |
33 |
options={ |
|
34 |
'ordering': ['weekday', 'start_time'], |
|
35 |
}, |
|
51 |
options={'ordering': ['weekday', 'start_time'],}, |
|
36 | 52 |
), |
37 | 53 |
migrations.AddField( |
38 | 54 |
model_name='agenda', |
39 | 55 |
name='kind', |
40 |
field=models.CharField(default=b'events', max_length=20, verbose_name='Kind', choices=[(b'events', 'Events'), (b'meetings', 'Meetings')]), |
|
56 |
field=models.CharField( |
|
57 |
default=b'events', |
|
58 |
max_length=20, |
|
59 |
verbose_name='Kind', |
|
60 |
choices=[(b'events', 'Events'), (b'meetings', 'Meetings')], |
|
61 |
), |
|
41 | 62 |
), |
42 | 63 |
migrations.AddField( |
43 | 64 |
model_name='timeperiod', |
chrono/agendas/migrations/0010_auto_20160918_1250.py | ||
---|---|---|
13 | 13 | |
14 | 14 |
operations = [ |
15 | 15 |
migrations.AlterModelOptions( |
16 |
name='event', |
|
17 |
options={'ordering': ['agenda', 'start_datetime', 'label']}, |
|
16 |
name='event', options={'ordering': ['agenda', 'start_datetime', 'label']}, |
|
18 | 17 |
), |
19 | 18 |
migrations.AddField( |
20 | 19 |
model_name='agenda', |
21 | 20 |
name='edit_role', |
22 |
field=models.ForeignKey(related_name='+', default=None, verbose_name='Edit Role', to='auth.Group', blank=True, null=True, on_delete=models.CASCADE), |
|
21 |
field=models.ForeignKey( |
|
22 |
related_name='+', |
|
23 |
default=None, |
|
24 |
verbose_name='Edit Role', |
|
25 |
to='auth.Group', |
|
26 |
blank=True, |
|
27 |
null=True, |
|
28 |
on_delete=models.CASCADE, |
|
29 |
), |
|
23 | 30 |
), |
24 | 31 |
migrations.AddField( |
25 | 32 |
model_name='agenda', |
26 | 33 |
name='view_role', |
27 |
field=models.ForeignKey(related_name='+', default=None, verbose_name='View Role', to='auth.Group', blank=True, null=True, on_delete=models.CASCADE), |
|
34 |
field=models.ForeignKey( |
|
35 |
related_name='+', |
|
36 |
default=None, |
|
37 |
verbose_name='View Role', |
|
38 |
to='auth.Group', |
|
39 |
blank=True, |
|
40 |
null=True, |
|
41 |
on_delete=models.CASCADE, |
|
42 |
), |
|
28 | 43 |
), |
29 | 44 |
] |
chrono/agendas/migrations/0013_auto_20161028_1603.py | ||
---|---|---|
12 | 12 | |
13 | 13 |
operations = [ |
14 | 14 |
migrations.AlterField( |
15 |
model_name='meetingtype', |
|
16 |
name='slug', |
|
17 |
field=models.SlugField(verbose_name='Identifier'), |
|
15 |
model_name='meetingtype', name='slug', field=models.SlugField(verbose_name='Identifier'), |
|
18 | 16 |
), |
19 | 17 |
] |
chrono/agendas/migrations/0014_booking_primary_booking.py | ||
---|---|---|
14 | 14 |
migrations.AddField( |
15 | 15 |
model_name='booking', |
16 | 16 |
name='primary_booking', |
17 |
field=models.ForeignKey(related_name='secondary_booking_set', to='agendas.Booking', null=True, on_delete=models.CASCADE), |
|
17 |
field=models.ForeignKey( |
|
18 |
related_name='secondary_booking_set', |
|
19 |
to='agendas.Booking', |
|
20 |
null=True, |
|
21 |
on_delete=models.CASCADE, |
|
22 |
), |
|
18 | 23 |
), |
19 | 24 |
] |
chrono/agendas/migrations/0015_auto_20170628_1137.py | ||
---|---|---|
12 | 12 | |
13 | 13 |
operations = [ |
14 | 14 |
migrations.AlterField( |
15 |
model_name='agenda', |
|
16 |
name='label', |
|
17 |
field=models.CharField(max_length=150, verbose_name='Label'), |
|
15 |
model_name='agenda', name='label', field=models.CharField(max_length=150, verbose_name='Label'), |
|
18 | 16 |
), |
19 | 17 |
migrations.AlterField( |
20 | 18 |
model_name='event', |
21 | 19 |
name='label', |
22 |
field=models.CharField(help_text='Optional label to identify this date.', max_length=150, null=True, verbose_name='Label', blank=True), |
|
20 |
field=models.CharField( |
|
21 |
help_text='Optional label to identify this date.', |
|
22 |
max_length=150, |
|
23 |
null=True, |
|
24 |
verbose_name='Label', |
|
25 |
blank=True, |
|
26 |
), |
|
23 | 27 |
), |
24 | 28 |
migrations.AlterField( |
25 | 29 |
model_name='meetingtype', |
chrono/agendas/migrations/0016_desk.py | ||
---|---|---|
14 | 14 |
migrations.CreateModel( |
15 | 15 |
name='Desk', |
16 | 16 |
fields=[ |
17 |
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), |
|
17 |
( |
|
18 |
'id', |
|
19 |
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True), |
|
20 |
), |
|
18 | 21 |
('label', models.CharField(max_length=150, verbose_name='Label')), |
19 | 22 |
('slug', models.SlugField(max_length=150, verbose_name='Identifier')), |
20 | 23 |
('agenda', models.ForeignKey(to='agendas.Agenda', on_delete=models.CASCADE)), |
21 | 24 |
], |
22 |
options={ |
|
23 |
'ordering': ['label'], |
|
24 |
}, |
|
25 |
options={'ordering': ['label'],}, |
|
25 | 26 |
), |
26 | 27 |
] |
chrono/agendas/migrations/0017_timeperiod_desk.py | ||
---|---|---|
9 | 9 |
Desk = apps.get_model('agendas', 'Desk') |
10 | 10 |
for time_period in TimePeriod.objects.all(): |
11 | 11 |
desk, created = Desk.objects.get_or_create( |
12 |
label='Guichet 1', slug='guichet-1', agenda=time_period.agenda) |
|
12 |
label='Guichet 1', slug='guichet-1', agenda=time_period.agenda |
|
13 |
) |
|
13 | 14 |
time_period.desk = desk |
14 | 15 |
time_period.save() |
15 | 16 | |
... | ... | |
36 | 37 |
name='desk', |
37 | 38 |
field=models.ForeignKey(to='agendas.Desk', on_delete=models.CASCADE), |
38 | 39 |
), |
39 |
migrations.RemoveField( |
|
40 |
model_name='timeperiod', |
|
41 |
name='agenda', |
|
42 |
), |
|
40 |
migrations.RemoveField(model_name='timeperiod', name='agenda',), |
|
43 | 41 |
] |
chrono/agendas/migrations/0018_event_desk.py | ||
---|---|---|
11 | 11 |
if not event.agenda.kind == 'meetings': |
12 | 12 |
continue |
13 | 13 | |
14 |
desk, created = Desk.objects.get_or_create( |
|
15 |
label='Guichet 1', slug='guichet-1', agenda=event.agenda) |
|
14 |
desk, created = Desk.objects.get_or_create(label='Guichet 1', slug='guichet-1', agenda=event.agenda) |
|
16 | 15 |
event.desk = desk |
17 | 16 |
event.save() |
18 | 17 | |
... | ... | |
33 | 32 |
name='desk', |
34 | 33 |
field=models.ForeignKey(to='agendas.Desk', null=True, on_delete=models.CASCADE), |
35 | 34 |
), |
36 |
migrations.RunPython(set_event_desk, unset_event_desk) |
|
35 |
migrations.RunPython(set_event_desk, unset_event_desk),
|
|
37 | 36 |
] |
chrono/agendas/migrations/0019_timeperiodexception.py | ||
---|---|---|
14 | 14 |
migrations.CreateModel( |
15 | 15 |
name='TimePeriodException', |
16 | 16 |
fields=[ |
17 |
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), |
|
18 |
('label', models.CharField(max_length=150, null=True, verbose_name='Optional Label', blank=True)), |
|
17 |
( |
|
18 |
'id', |
|
19 |
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True), |
|
20 |
), |
|
21 |
( |
|
22 |
'label', |
|
23 |
models.CharField(max_length=150, null=True, verbose_name='Optional Label', blank=True), |
|
24 |
), |
|
19 | 25 |
('start_datetime', models.DateTimeField(verbose_name='Exception start time')), |
20 | 26 |
('end_datetime', models.DateTimeField(verbose_name='Exception end time')), |
21 | 27 |
('desk', models.ForeignKey(to='agendas.Desk', on_delete=models.CASCADE)), |
22 | 28 |
], |
23 |
options={ |
|
24 |
'ordering': ['start_datetime'], |
|
25 |
}, |
|
29 |
options={'ordering': ['start_datetime'],}, |
|
26 | 30 |
), |
27 | 31 |
] |
chrono/agendas/migrations/0020_auto_20171102_1021.py | ||
---|---|---|
1 | 1 |
# -*- coding: utf-8 -*- |
2 | 2 |
from __future__ import unicode_literals |
3 | 3 | |
4 |
from django.db import migrations, models |
|
5 | 4 |
import datetime |
5 | ||
6 |
from django.db import migrations, models |
|
6 | 7 |
from django.utils.timezone import utc |
7 | 8 | |
8 | 9 | |
... | ... | |
26 | 27 |
migrations.AddField( |
27 | 28 |
model_name='timeperiodexception', |
28 | 29 |
name='update_datetime', |
29 |
field=models.DateTimeField(default=datetime.datetime(2017, 11, 2, 10, 21, 1, 826837, tzinfo=utc), auto_now=True), |
|
30 |
field=models.DateTimeField( |
|
31 |
default=datetime.datetime(2017, 11, 2, 10, 21, 1, 826837, tzinfo=utc), auto_now=True |
|
32 |
), |
|
30 | 33 |
preserve_default=False, |
31 | 34 |
), |
32 | 35 |
] |
chrono/agendas/migrations/0021_auto_20171126_1330.py | ||
---|---|---|
11 | 11 |
] |
12 | 12 | |
13 | 13 |
operations = [ |
14 |
migrations.AddField(model_name='booking', name='backoffice_url', field=models.URLField(blank=True),), |
|
14 | 15 |
migrations.AddField( |
15 |
model_name='booking', |
|
16 |
name='backoffice_url', |
|
17 |
field=models.URLField(blank=True), |
|
16 |
model_name='booking', name='label', field=models.CharField(max_length=250, blank=True), |
|
18 | 17 |
), |
19 | 18 |
migrations.AddField( |
20 |
model_name='booking', |
|
21 |
name='label', |
|
22 |
field=models.CharField(max_length=250, blank=True), |
|
23 |
), |
|
24 |
migrations.AddField( |
|
25 |
model_name='booking', |
|
26 |
name='user_name', |
|
27 |
field=models.CharField(max_length=250, blank=True), |
|
19 |
model_name='booking', name='user_name', field=models.CharField(max_length=250, blank=True), |
|
28 | 20 |
), |
29 | 21 |
] |
chrono/agendas/migrations/0022_auto_20171202_1828.py | ||
---|---|---|
17 | 17 |
field=models.SlugField(max_length=160, verbose_name='Identifier'), |
18 | 18 |
), |
19 | 19 |
migrations.AlterField( |
20 |
model_name='desk', |
|
21 |
name='slug', |
|
22 |
field=models.SlugField(max_length=160, verbose_name='Identifier'), |
|
20 |
model_name='desk', name='slug', field=models.SlugField(max_length=160, verbose_name='Identifier'), |
|
23 | 21 |
), |
24 | 22 |
migrations.AlterField( |
25 | 23 |
model_name='meetingtype', |
chrono/agendas/migrations/0023_auto_20171202_1835.py | ||
---|---|---|
14 | 14 |
migrations.AlterField( |
15 | 15 |
model_name='desk', |
16 | 16 |
name='timeperiod_exceptions_remote_url', |
17 |
field=models.URLField(max_length=500, verbose_name='URL to fetch time period exceptions from', blank=True), |
|
17 |
field=models.URLField( |
|
18 |
max_length=500, verbose_name='URL to fetch time period exceptions from', blank=True |
|
19 |
), |
|
18 | 20 |
), |
19 | 21 |
] |
chrono/agendas/migrations/0024_auto_20180426_1127.py | ||
---|---|---|
12 | 12 |
] |
13 | 13 | |
14 | 14 |
operations = [ |
15 |
migrations.AlterModelOptions( |
|
16 |
name='meetingtype', |
|
17 |
options={'ordering': ['duration', 'label']}, |
|
18 |
), |
|
15 |
migrations.AlterModelOptions(name='meetingtype', options={'ordering': ['duration', 'label']},), |
|
19 | 16 |
migrations.AddField( |
20 | 17 |
model_name='timeperiodexception', |
21 | 18 |
name='recurrence_id', |
chrono/agendas/migrations/0025_auto_20181206_1252.py | ||
---|---|---|
2 | 2 |
# Generated by Django 1.11.12 on 2018-12-06 12:52 |
3 | 3 |
from __future__ import unicode_literals |
4 | 4 | |
5 |
from django.db import migrations, models |
|
6 | 5 |
import django.db.models.deletion |
6 |
from django.db import migrations, models |
|
7 | 7 | |
8 | 8 | |
9 | 9 |
class Migration(migrations.Migration): |
... | ... | |
16 | 16 |
migrations.AlterField( |
17 | 17 |
model_name='agenda', |
18 | 18 |
name='edit_role', |
19 |
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'), |
|
19 |
field=models.ForeignKey( |
|
20 |
blank=True, |
|
21 |
default=None, |
|
22 |
null=True, |
|
23 |
on_delete=django.db.models.deletion.SET_NULL, |
|
24 |
related_name='+', |
|
25 |
to='auth.Group', |
|
26 |
verbose_name='Edit Role', |
|
27 |
), |
|
20 | 28 |
), |
21 | 29 |
migrations.AlterField( |
22 | 30 |
model_name='agenda', |
23 | 31 |
name='view_role', |
24 |
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'), |
|
32 |
field=models.ForeignKey( |
|
33 |
blank=True, |
|
34 |
default=None, |
|
35 |
null=True, |
|
36 |
on_delete=django.db.models.deletion.SET_NULL, |
|
37 |
related_name='+', |
|
38 |
to='auth.Group', |
|
39 |
verbose_name='View Role', |
|
40 |
), |
|
25 | 41 |
), |
26 | 42 |
] |
chrono/agendas/migrations/0027_event_description.py | ||
---|---|---|
15 | 15 |
migrations.AddField( |
16 | 16 |
model_name='event', |
17 | 17 |
name='description', |
18 |
field=models.TextField(blank=True, help_text='Optional event description.', null=True, verbose_name='Description'), |
|
18 |
field=models.TextField( |
|
19 |
blank=True, help_text='Optional event description.', null=True, verbose_name='Description' |
|
20 |
), |
|
19 | 21 |
), |
20 | 22 |
] |
chrono/agendas/migrations/0028_event_slug.py | ||
---|---|---|
15 | 15 |
migrations.AddField( |
16 | 16 |
model_name='event', |
17 | 17 |
name='slug', |
18 |
field=models.SlugField(default=None, null=True, blank=True, max_length=160, verbose_name='Identifier'), |
|
18 |
field=models.SlugField( |
|
19 |
default=None, null=True, blank=True, max_length=160, verbose_name='Identifier' |
|
20 |
), |
|
19 | 21 |
preserve_default=False, |
20 | 22 |
), |
21 | 23 |
] |
chrono/agendas/migrations/0029_auto_20191106_1320.py | ||
---|---|---|
15 | 15 |
migrations.AlterField( |
16 | 16 |
model_name='event', |
17 | 17 |
name='slug', |
18 |
field=models.SlugField(default=None, null=True, blank=True, max_length=160, verbose_name='Identifier'), |
|
18 |
field=models.SlugField( |
|
19 |
default=None, null=True, blank=True, max_length=160, verbose_name='Identifier' |
|
20 |
), |
|
19 | 21 |
), |
20 | 22 |
] |
chrono/agendas/migrations/0030_auto_20191107_1200.py | ||
---|---|---|
39 | 39 |
def set_slug_on_meetingtypes(apps, schema_editor): |
40 | 40 |
MeetingType = apps.get_model('agendas', 'MeetingType') |
41 | 41 |
for meetingtype in MeetingType.objects.all().order_by('-pk'): |
42 |
if not MeetingType.objects.filter(slug=meetingtype.slug, agenda=meetingtype.agenda).exclude(pk=meetingtype.pk).exists(): |
|
42 |
if ( |
|
43 |
not MeetingType.objects.filter(slug=meetingtype.slug, agenda=meetingtype.agenda) |
|
44 |
.exclude(pk=meetingtype.pk) |
|
45 |
.exists() |
|
46 |
): |
|
43 | 47 |
continue |
44 | 48 |
meetingtype.slug = generate_slug(meetingtype, agenda=meetingtype.agenda) |
45 | 49 |
meetingtype.save(update_fields=['slug']) |
chrono/agendas/migrations/0031_auto_20191107_1225.py | ||
---|---|---|
17 | 17 |
name='slug', |
18 | 18 |
field=models.SlugField(max_length=160, unique=True, verbose_name='Identifier'), |
19 | 19 |
), |
20 |
migrations.AlterUniqueTogether( |
|
21 |
name='desk', |
|
22 |
unique_together=set([('agenda', 'slug')]), |
|
23 |
), |
|
24 |
migrations.AlterUniqueTogether( |
|
25 |
name='meetingtype', |
|
26 |
unique_together=set([('agenda', 'slug')]), |
|
27 |
), |
|
20 |
migrations.AlterUniqueTogether(name='desk', unique_together=set([('agenda', 'slug')]),), |
|
21 |
migrations.AlterUniqueTogether(name='meetingtype', unique_together=set([('agenda', 'slug')]),), |
|
28 | 22 |
] |
chrono/agendas/migrations/0032_auto_20191127_0919.py | ||
---|---|---|
15 | 15 |
migrations.AlterField( |
16 | 16 |
model_name='event', |
17 | 17 |
name='slug', |
18 |
field=models.SlugField(default=None, null=True, blank=True, max_length=160, verbose_name='Identifier') |
|
19 |
), |
|
20 |
migrations.AlterUniqueTogether( |
|
21 |
name='event', |
|
22 |
unique_together=set([('agenda', 'slug')]), |
|
18 |
field=models.SlugField( |
|
19 |
default=None, null=True, blank=True, max_length=160, verbose_name='Identifier' |
|
20 |
), |
|
23 | 21 |
), |
22 |
migrations.AlterUniqueTogether(name='event', unique_together=set([('agenda', 'slug')]),), |
|
24 | 23 |
] |
chrono/agendas/models.py | ||
---|---|---|
17 | 17 | |
18 | 18 |
import datetime |
19 | 19 |
import math |
20 | ||
20 | 21 |
import requests |
21 | 22 |
import vobject |
22 | ||
23 | 23 |
from django.conf import settings |
24 | 24 |
from django.contrib.auth.models import Group |
25 | 25 |
from django.core.exceptions import ValidationError |
... | ... | |
30 | 30 |
from django.utils.encoding import force_text, python_2_unicode_compatible |
31 | 31 |
from django.utils.formats import date_format |
32 | 32 |
from django.utils.text import slugify |
33 |
from django.utils.timezone import localtime, now, make_aware, make_naive, is_aware
|
|
33 |
from django.utils.timezone import is_aware, localtime, make_aware, make_naive, now
|
|
34 | 34 |
from django.utils.translation import ugettext_lazy as _ |
35 | ||
36 | 35 |
from jsonfield import JSONField |
37 | 36 | |
38 | 37 |
from ..interval import Intervals |
39 | 38 | |
40 | ||
41 | 39 |
AGENDA_KINDS = ( |
42 | 40 |
('events', _('Events')), |
43 | 41 |
('meetings', _('Meetings')), |
... | ... | |
71 | 69 |
label = models.CharField(_('Label'), max_length=150) |
72 | 70 |
slug = models.SlugField(_('Identifier'), max_length=160, unique=True) |
73 | 71 |
kind = models.CharField(_('Kind'), max_length=20, choices=AGENDA_KINDS, default='events') |
74 |
minimal_booking_delay = models.PositiveIntegerField( |
|
75 |
_('Minimal booking delay (in days)'), default=1) |
|
72 |
minimal_booking_delay = models.PositiveIntegerField(_('Minimal booking delay (in days)'), default=1) |
|
76 | 73 |
maximal_booking_delay = models.PositiveIntegerField( |
77 |
_('Maximal booking delay (in days)'), default=56) # eight weeks |
|
78 |
edit_role = models.ForeignKey(Group, blank=True, null=True, default=None, |
|
79 |
related_name='+', verbose_name=_('Edit Role'), |
|
80 |
on_delete=models.SET_NULL) |
|
81 |
view_role = models.ForeignKey(Group, blank=True, null=True, default=None, |
|
82 |
related_name='+', verbose_name=_('View Role'), |
|
83 |
on_delete=models.SET_NULL) |
|
74 |
_('Maximal booking delay (in days)'), default=56 |
|
75 |
) # eight weeks |
|
76 |
edit_role = models.ForeignKey( |
|
77 |
Group, |
|
78 |
blank=True, |
|
79 |
null=True, |
|
80 |
default=None, |
|
81 |
related_name='+', |
|
82 |
verbose_name=_('Edit Role'), |
|
83 |
on_delete=models.SET_NULL, |
|
84 |
) |
|
85 |
view_role = models.ForeignKey( |
|
86 |
Group, |
|
87 |
blank=True, |
|
88 |
null=True, |
|
89 |
default=None, |
|
90 |
related_name='+', |
|
91 |
verbose_name=_('View Role'), |
|
92 |
on_delete=models.SET_NULL, |
|
93 |
) |
|
84 | 94 | |
85 | 95 |
class Meta: |
86 | 96 |
ordering = ['label'] |
... | ... | |
124 | 134 |
'permissions': { |
125 | 135 |
'view': self.view_role.name if self.view_role else None, |
126 | 136 |
'edit': self.edit_role.name if self.edit_role else None, |
127 |
} |
|
137 |
},
|
|
128 | 138 |
} |
129 | 139 |
if self.kind == 'events': |
130 | 140 |
agenda['events'] = [x.export_json() for x in self.event_set.all()] |
... | ... | |
170 | 180 |
Desk.import_json(desk).save() |
171 | 181 |
return created |
172 | 182 | |
183 | ||
173 | 184 |
WEEKDAYS_LIST = sorted(WEEKDAYS.items(), key=lambda x: x[0]) |
174 | 185 | |
175 | 186 | |
... | ... | |
198 | 209 | |
199 | 210 |
def __str__(self): |
200 | 211 |
return u'%s / %s → %s' % ( |
201 |
force_text(WEEKDAYS[self.weekday]), |
|
202 |
date_format(self.start_time, 'TIME_FORMAT'), |
|
203 |
date_format(self.end_time, 'TIME_FORMAT')) |
|
212 |
force_text(WEEKDAYS[self.weekday]), |
|
213 |
date_format(self.start_time, 'TIME_FORMAT'), |
|
214 |
date_format(self.end_time, 'TIME_FORMAT'), |
|
215 |
) |
|
204 | 216 | |
205 | 217 |
@property |
206 | 218 |
def weekday_str(self): |
... | ... | |
212 | 224 | |
213 | 225 |
def export_json(self): |
214 | 226 |
return { |
215 |
'weekday': self.weekday, |
|
216 |
'start_time': self.start_time.strftime('%H:%M'), |
|
217 |
'end_time': self.end_time.strftime('%H:%M'), |
|
227 |
'weekday': self.weekday,
|
|
228 |
'start_time': self.start_time.strftime('%H:%M'),
|
|
229 |
'end_time': self.end_time.strftime('%H:%M'),
|
|
218 | 230 |
} |
219 | 231 | |
220 | 232 |
def get_time_slots(self, min_datetime, max_datetime, meeting_type): |
... | ... | |
223 | 235 |
min_datetime = make_naive(min_datetime) |
224 | 236 |
max_datetime = make_naive(max_datetime) |
225 | 237 | |
226 |
real_min_datetime = min_datetime + datetime.timedelta( |
|
227 |
days=self.weekday - min_datetime.weekday()) |
|
238 |
real_min_datetime = min_datetime + datetime.timedelta(days=self.weekday - min_datetime.weekday()) |
|
228 | 239 |
if real_min_datetime < min_datetime: |
229 | 240 |
real_min_datetime += datetime.timedelta(days=7) |
230 | 241 | |
231 |
event_datetime = real_min_datetime.replace(hour=self.start_time.hour, |
|
232 |
minute=self.start_time.minute, second=0, microsecond=0) |
|
242 |
event_datetime = real_min_datetime.replace( |
|
243 |
hour=self.start_time.hour, minute=self.start_time.minute, second=0, microsecond=0 |
|
244 |
) |
|
233 | 245 |
while event_datetime < max_datetime: |
234 | 246 |
end_time = event_datetime + meeting_duration |
235 | 247 |
next_time = event_datetime + duration |
236 | 248 |
if end_time.time() > self.end_time or event_datetime.date() != next_time.date(): |
237 | 249 |
# back to morning |
238 |
event_datetime = event_datetime.replace(hour=self.start_time.hour, minute=self.start_time.minute) |
|
250 |
event_datetime = event_datetime.replace( |
|
251 |
hour=self.start_time.hour, minute=self.start_time.minute |
|
252 |
) |
|
239 | 253 |
# but next week |
240 | 254 |
event_datetime += datetime.timedelta(days=7) |
241 | 255 |
next_time = event_datetime + duration |
... | ... | |
243 | 257 |
if event_datetime > max_datetime: |
244 | 258 |
break |
245 | 259 | |
246 |
yield TimeSlot(start_datetime=make_aware(event_datetime), meeting_type=meeting_type, desk=self.desk) |
|
260 |
yield TimeSlot( |
|
261 |
start_datetime=make_aware(event_datetime), meeting_type=meeting_type, desk=self.desk |
|
262 |
) |
|
247 | 263 |
event_datetime = next_time |
248 | 264 | |
249 | 265 | |
... | ... | |
265 | 281 |
@classmethod |
266 | 282 |
def import_json(cls, data): |
267 | 283 |
meeting_type, created = cls.objects.get_or_create( |
268 |
slug=data['slug'], agenda=data['agenda'], defaults=data) |
|
284 |
slug=data['slug'], agenda=data['agenda'], defaults=data |
|
285 |
) |
|
269 | 286 |
if not created: |
270 | 287 |
for k, v in data.items(): |
271 | 288 |
setattr(meeting_type, k, v) |
... | ... | |
273 | 290 | |
274 | 291 |
def export_json(self): |
275 | 292 |
return { |
276 |
'label': self.label, |
|
277 |
'slug': self.slug, |
|
278 |
'duration': self.duration, |
|
293 |
'label': self.label,
|
|
294 |
'slug': self.slug,
|
|
295 |
'duration': self.duration,
|
|
279 | 296 |
} |
280 | 297 | |
281 | 298 | |
... | ... | |
284 | 301 |
agenda = models.ForeignKey(Agenda, on_delete=models.CASCADE) |
285 | 302 |
start_datetime = models.DateTimeField(_('Date/time')) |
286 | 303 |
places = models.PositiveIntegerField(_('Places')) |
287 |
waiting_list_places = models.PositiveIntegerField( |
|
288 |
_('Places in waiting list'), default=0) |
|
289 |
label = models.CharField(_('Label'), max_length=150, null=True, blank=True, |
|
290 |
help_text=_('Optional label to identify this date.')) |
|
304 |
waiting_list_places = models.PositiveIntegerField(_('Places in waiting list'), default=0) |
|
305 |
label = models.CharField( |
|
306 |
_('Label'), |
|
307 |
max_length=150, |
|
308 |
null=True, |
|
309 |
blank=True, |
|
310 |
help_text=_('Optional label to identify this date.'), |
|
311 |
) |
|
291 | 312 |
slug = models.SlugField(_('Identifier'), max_length=160, null=True, blank=True, default=None) |
292 |
description = models.TextField(_('Description'), null=True, blank=True, |
|
293 |
help_text=_('Optional event description.')) |
|
313 |
description = models.TextField( |
|
314 |
_('Description'), null=True, blank=True, help_text=_('Optional event description.') |
|
315 |
) |
|
294 | 316 |
full = models.BooleanField(default=False) |
295 | 317 |
meeting_type = models.ForeignKey(MeetingType, null=True, on_delete=models.CASCADE) |
296 | 318 |
desk = models.ForeignKey('Desk', null=True, on_delete=models.CASCADE) |
... | ... | |
310 | 332 | |
311 | 333 |
def check_full(self): |
312 | 334 |
self.full = bool( |
313 |
(self.booked_places >= self.places and self.waiting_list_places == 0) or |
|
314 |
(self.waiting_list_places and self.waiting_list >= self.waiting_list_places)) |
|
335 |
(self.booked_places >= self.places and self.waiting_list_places == 0) |
|
336 |
or (self.waiting_list_places and self.waiting_list >= self.waiting_list_places) |
|
337 |
) |
|
315 | 338 | |
316 | 339 |
def in_bookable_period(self): |
317 |
if localtime(now()).date() > localtime(self.start_datetime - datetime.timedelta(days=self.agenda.minimal_booking_delay)).date(): |
|
340 |
if ( |
|
341 |
localtime(now()).date() |
|
342 |
> localtime( |
|
343 |
self.start_datetime - datetime.timedelta(days=self.agenda.minimal_booking_delay) |
|
344 |
).date() |
|
345 |
): |
|
318 | 346 |
return False |
319 | 347 |
if self.agenda.maximal_booking_delay and ( |
320 |
localtime(now()).date() <= localtime(self.start_datetime - datetime.timedelta(days=self.agenda.maximal_booking_delay)).date()): |
|
348 |
localtime(now()).date() |
|
349 |
<= localtime( |
|
350 |
self.start_datetime - datetime.timedelta(days=self.agenda.maximal_booking_delay) |
|
351 |
).date() |
|
352 |
): |
|
321 | 353 |
return False |
322 | 354 |
if self.start_datetime < now(): |
323 | 355 |
# past the event date, we may want in the future to allow for some |
... | ... | |
327 | 359 | |
328 | 360 |
@property |
329 | 361 |
def booked_places(self): |
330 |
return self.booking_set.filter(cancellation_datetime__isnull=True, |
|
331 |
in_waiting_list=False).count() |
|
362 |
return self.booking_set.filter(cancellation_datetime__isnull=True, in_waiting_list=False).count() |
|
332 | 363 | |
333 | 364 |
@property |
334 | 365 |
def waiting_list(self): |
335 |
return self.booking_set.filter(cancellation_datetime__isnull=True, |
|
336 |
in_waiting_list=True).count() |
|
366 |
return self.booking_set.filter(cancellation_datetime__isnull=True, in_waiting_list=True).count() |
|
337 | 367 | |
338 | 368 |
@property |
339 | 369 |
def end_datetime(self): |
... | ... | |
344 | 374 | |
345 | 375 |
@classmethod |
346 | 376 |
def import_json(cls, data): |
347 |
data['start_datetime'] = make_aware(datetime.datetime.strptime( |
|
348 |
data['start_datetime'], '%Y-%m-%d %H:%M:%S')) |
|
377 |
data['start_datetime'] = make_aware( |
|
378 |
datetime.datetime.strptime(data['start_datetime'], '%Y-%m-%d %H:%M:%S') |
|
379 |
) |
|
349 | 380 |
if data.get('slug'): |
350 | 381 |
event, created = cls.objects.get_or_create(slug=data['slug'], defaults=data) |
351 | 382 |
if not created: |
... | ... | |
372 | 403 |
in_waiting_list = models.BooleanField(default=False) |
373 | 404 |
creation_datetime = models.DateTimeField(auto_now_add=True) |
374 | 405 |
# primary booking is used to group multiple bookings together |
375 |
primary_booking = models.ForeignKey('self', null=True, |
|
376 |
on_delete=models.CASCADE, related_name='secondary_booking_set') |
|
406 |
primary_booking = models.ForeignKey( |
|
407 |
'self', null=True, on_delete=models.CASCADE, related_name='secondary_booking_set' |
|
408 |
) |
|
377 | 409 | |
378 | 410 |
label = models.CharField(max_length=250, blank=True) |
379 |
user_display_label = models.CharField(verbose_name=_('Label displayed to user'), max_length=250, blank=True) |
|
411 |
user_display_label = models.CharField( |
|
412 |
verbose_name=_('Label displayed to user'), max_length=250, blank=True |
|
413 |
) |
|
380 | 414 |
user_name = models.CharField(max_length=250, blank=True) |
381 | 415 |
backoffice_url = models.URLField(blank=True) |
382 | 416 | |
... | ... | |
405 | 439 |
ics = vobject.iCalendar() |
406 | 440 |
ics.add('prodid').value = '-//Entr\'ouvert//NON SGML Publik' |
407 | 441 |
vevent = vobject.newFromBehavior('vevent') |
408 |
vevent.add('uid').value = '%s-%s-%s' % (self.event.start_datetime.isoformat(), self.event.agenda.pk, self.pk) |
|
442 |
vevent.add('uid').value = '%s-%s-%s' % ( |
|
443 |
self.event.start_datetime.isoformat(), |
|
444 |
self.event.agenda.pk, |
|
445 |
self.pk, |
|
446 |
) |
|
409 | 447 | |
410 | 448 |
vevent.add('summary').value = self.user_display_label or self.label |
411 | 449 |
vevent.add('dtstart').value = self.event.start_datetime |
412 | 450 |
if self.user_name: |
413 | 451 |
vevent.add('attendee').value = self.user_name |
414 | 452 |
if self.event.meeting_type: |
415 |
vevent.add('dtend').value = self.event.start_datetime + datetime.timedelta(minutes=self.event.meeting_type.duration) |
|
453 |
vevent.add('dtend').value = self.event.start_datetime + datetime.timedelta( |
|
454 |
minutes=self.event.meeting_type.duration |
|
455 |
) |
|
416 | 456 | |
417 | 457 |
for field in ('description', 'location', 'comment', 'url'): |
418 | 458 |
field_value = request and request.GET.get(field) or self.extra_data.get(field) |
... | ... | |
422 | 462 |
return ics.serialize() |
423 | 463 | |
424 | 464 | |
425 | ||
426 | 465 |
@python_2_unicode_compatible |
427 | 466 |
class Desk(models.Model): |
428 | 467 |
agenda = models.ForeignKey(Agenda, on_delete=models.CASCADE) |
429 | 468 |
label = models.CharField(_('Label'), max_length=150) |
430 | 469 |
slug = models.SlugField(_('Identifier'), max_length=160) |
431 | 470 |
timeperiod_exceptions_remote_url = models.URLField( |
432 |
_('URL to fetch time period exceptions from'),
|
|
433 |
blank=True, max_length=500)
|
|
471 |
_('URL to fetch time period exceptions from'), blank=True, max_length=500
|
|
472 |
) |
|
434 | 473 | |
435 | 474 |
def __str__(self): |
436 | 475 |
return self.label |
... | ... | |
448 | 487 |
def import_json(cls, data): |
449 | 488 |
timeperiods = data.pop('timeperiods') |
450 | 489 |
exceptions = data.pop('exceptions') |
451 |
instance, created = cls.objects.get_or_create( |
|
452 |
slug=data['slug'], agenda=data['agenda'], defaults=data) |
|
490 |
instance, created = cls.objects.get_or_create(slug=data['slug'], agenda=data['agenda'], defaults=data) |
|
453 | 491 |
if not created: |
454 | 492 |
for k, v in data.items(): |
455 | 493 |
setattr(instance, k, v) |
... | ... | |
462 | 500 |
return instance |
463 | 501 | |
464 | 502 |
def export_json(self): |
465 |
return {'label': self.label, |
|
466 |
'slug': self.slug, |
|
467 |
'timeperiods': [time_period.export_json() for time_period in self.timeperiod_set.all()], |
|
468 |
'exceptions': [exception.export_json() for exception in self.timeperiodexception_set.all()] |
|
469 |
} |
|
503 |
return { |
|
504 |
'label': self.label, |
|
505 |
'slug': self.slug, |
|
506 |
'timeperiods': [time_period.export_json() for time_period in self.timeperiod_set.all()], |
|
507 |
'exceptions': [exception.export_json() for exception in self.timeperiodexception_set.all()], |
|
508 |
} |
|
470 | 509 | |
471 | 510 |
def get_exceptions_within_two_weeks(self): |
472 | 511 |
in_two_weeks = make_aware(datetime.datetime.today() + datetime.timedelta(days=14)) |
473 | 512 |
exceptions = self.timeperiodexception_set.filter(end_datetime__gte=now()).filter( |
474 |
Q(end_datetime__lte=in_two_weeks) | Q(start_datetime__lt=now())) |
|
513 |
Q(end_datetime__lte=in_two_weeks) | Q(start_datetime__lt=now()) |
|
514 |
) |
|
475 | 515 |
if exceptions.exists(): |
476 | 516 |
return exceptions |
477 | 517 |
# if none found within the 2 coming weeks, return the next one |
478 |
next_exception = self.timeperiodexception_set.filter( |
|
479 |
start_datetime__gte=now()).order_by('start_datetime').first() |
|
518 |
next_exception = ( |
|
519 |
self.timeperiodexception_set.filter(start_datetime__gte=now()).order_by('start_datetime').first() |
|
520 |
) |
|
480 | 521 |
if next_exception: |
481 | 522 |
return [next_exception] |
482 | 523 |
return [] |
... | ... | |
491 | 532 |
response.raise_for_status() |
492 | 533 |
except requests.HTTPError as e: |
493 | 534 |
raise ICSError( |
494 |
_('Failed to retrieve remote calendar (%(url)s, HTTP error %(status_code)s).') % |
|
495 |
{'url': url, 'status_code': e.response.status_code}) |
|
535 |
_('Failed to retrieve remote calendar (%(url)s, HTTP error %(status_code)s).') |
|
536 |
% {'url': url, 'status_code': e.response.status_code} |
|
537 |
) |
|
496 | 538 |
except requests.RequestException as e: |
497 | 539 |
raise ICSError( |
498 |
_('Failed to retrieve remote calendar (%(url)s, %(exception)s).') % |
|
499 |
{'url': url, 'exception': e}) |
|
540 |
_('Failed to retrieve remote calendar (%(url)s, %(exception)s).') |
|
541 |
% {'url': url, 'exception': e} |
|
542 |
) |
|
500 | 543 | |
501 | 544 |
return self.create_timeperiod_exceptions_from_ics(response.text, keep_synced_by_uid=True) |
502 | 545 | |
... | ... | |
582 | 625 |
if end_dt < update_datetime: |
583 | 626 |
TimePeriodException.objects.filter(**kwargs).update(**event) |
584 | 627 |
else: |
585 |
obj, created = TimePeriodException.objects.update_or_create(defaults=event, **kwargs) |
|
628 |
obj, created = TimePeriodException.objects.update_or_create( |
|
629 |
defaults=event, **kwargs |
|
630 |
) |
|
586 | 631 |
if created: |
587 | 632 |
total_created += 1 |
588 | 633 |
# delete unseen occurrences |
... | ... | |
591 | 636 | |
592 | 637 |
if keep_synced_by_uid: |
593 | 638 |
# delete all outdated exceptions from remote calendar |
594 |
TimePeriodException.objects.filter(update_datetime__lt=update_datetime, |
|
595 |
desk=self).exclude(external_id='').delete() |
|
639 |
TimePeriodException.objects.filter(update_datetime__lt=update_datetime, desk=self).exclude( |
|
640 |
external_id='' |
|
641 |
).delete() |
|
596 | 642 | |
597 | 643 |
return total_created |
598 | 644 | |
... | ... | |
606 | 652 |
aware_date = make_aware(datetime.datetime(date.year, date.month, date.day)) |
607 | 653 |
aware_next_date = aware_date + datetime.timedelta(days=1) |
608 | 654 |
for exception in self.timeperiodexception_set.filter( |
609 |
start_datetime__lt=aware_next_date,
|
|
610 |
end_datetime__gt=aware_date):
|
|
655 |
start_datetime__lt=aware_next_date, end_datetime__gt=aware_date
|
|
656 |
): |
|
611 | 657 |
openslots.remove(exception.start_datetime, exception.end_datetime) |
612 | 658 | |
613 | 659 |
return openslots.search(aware_date, aware_next_date) |
... | ... | |
634 | 680 |
exc_repr = u'%s' % date_format(localtime(self.start_datetime), 'SHORT_DATE_FORMAT') |
635 | 681 |
else: |
636 | 682 |
exc_repr = u'%s → %s' % ( |
637 |
date_format(localtime(self.start_datetime), 'SHORT_DATE_FORMAT'), |
|
638 |
date_format(localtime(self.end_datetime), 'SHORT_DATE_FORMAT')) |
|
683 |
date_format(localtime(self.start_datetime), 'SHORT_DATE_FORMAT'), |
|
684 |
date_format(localtime(self.end_datetime), 'SHORT_DATE_FORMAT'), |
|
685 |
) |
|
639 | 686 |
else: |
640 | 687 |
if localtime(self.start_datetime).date() == localtime(self.end_datetime).date(): |
641 | 688 |
# same day |
642 | 689 |
exc_repr = u'%s → %s' % ( |
643 |
date_format(localtime(self.start_datetime), 'SHORT_DATETIME_FORMAT'), |
|
644 |
date_format(localtime(self.end_datetime), 'TIME_FORMAT')) |
|
690 |
date_format(localtime(self.start_datetime), 'SHORT_DATETIME_FORMAT'), |
|
691 |
date_format(localtime(self.end_datetime), 'TIME_FORMAT'), |
|
692 |
) |
|
645 | 693 |
else: |
646 | 694 |
exc_repr = u'%s → %s' % ( |
647 |
date_format(localtime(self.start_datetime), 'SHORT_DATETIME_FORMAT'), |
|
648 |
date_format(localtime(self.end_datetime), 'SHORT_DATETIME_FORMAT')) |
|
695 |
date_format(localtime(self.start_datetime), 'SHORT_DATETIME_FORMAT'), |
|
696 |
date_format(localtime(self.end_datetime), 'SHORT_DATETIME_FORMAT'), |
|
697 |
) |
|
649 | 698 | |
650 | 699 |
if self.label: |
651 | 700 |
exc_repr = u'%s (%s)' % (self.label, exc_repr) |
... | ... | |
662 | 711 |
# incomplete time period, can't tell |
663 | 712 |
return False |
664 | 713 |
for event in Event.objects.filter( |
665 |
desk=self.desk, |
|
666 |
booking__isnull=False, |
|
667 |
booking__cancellation_datetime__isnull=True): |
|
714 |
desk=self.desk, booking__isnull=False, booking__cancellation_datetime__isnull=True |
|
715 |
): |
|
668 | 716 |
if self.start_datetime <= event.start_datetime < self.end_datetime: |
669 | 717 |
return True |
670 | 718 |
return False |
chrono/api/urls.py | ||
---|---|---|
21 | 21 |
urlpatterns = [ |
22 | 22 |
url(r'agenda/$', views.agendas), |
23 | 23 |
url(r'agenda/(?P<agenda_identifier>[\w-]+)/$', views.agenda_detail), |
24 | ||
25 | 24 |
url(r'agenda/(?P<agenda_identifier>[\w-]+)/datetimes/$', views.datetimes, name='api-agenda-datetimes'), |
26 |
url(r'agenda/(?P<agenda_identifier>[\w-]+)/fillslot/(?P<event_identifier>[\w:-]+)/$', |
|
27 |
views.fillslot, name='api-fillslot'), |
|
28 |
url(r'agenda/(?P<agenda_identifier>[\w-]+)/fillslots/$', |
|
29 |
views.fillslots, name='api-agenda-fillslots'), |
|
30 |
url(r'agenda/(?P<agenda_identifier>[\w-]+)/status/(?P<event_identifier>[\w-]+)/$', views.slot_status, |
|
31 |
name='api-event-status'), |
|
32 | ||
33 |
url(r'agenda/meetings/(?P<meeting_identifier>[\w-]+)/datetimes/$', |
|
34 |
views.meeting_datetimes, name='api-agenda-meeting-datetimes-legacy'), |
|
35 |
url(r'agenda/(?P<agenda_identifier>[\w-]+)/meetings/$', |
|
36 |
views.meeting_list, name='api-agenda-meetings'), |
|
37 |
url(r'agenda/(?P<agenda_identifier>[\w-]+)/desks/$', |
|
38 |
views.agenda_desk_list, name='api-agenda-desks'), |
|
39 |
url(r'agenda/(?P<agenda_identifier>[\w-]+)/meetings/(?P<meeting_identifier>[\w-]+)/datetimes/$', |
|
40 |
views.meeting_datetimes, name='api-agenda-meeting-datetimes'), |
|
41 | ||
25 |
url( |
|
26 |
r'agenda/(?P<agenda_identifier>[\w-]+)/fillslot/(?P<event_identifier>[\w:-]+)/$', |
|
27 |
views.fillslot, |
|
28 |
name='api-fillslot', |
|
29 |
), |
|
30 |
url(r'agenda/(?P<agenda_identifier>[\w-]+)/fillslots/$', views.fillslots, name='api-agenda-fillslots'), |
|
31 |
url( |
|
32 |
r'agenda/(?P<agenda_identifier>[\w-]+)/status/(?P<event_identifier>[\w-]+)/$', |
|
33 |
views.slot_status, |
|
34 |
name='api-event-status', |
|
35 |
), |
|
36 |
url( |
|
37 |
r'agenda/meetings/(?P<meeting_identifier>[\w-]+)/datetimes/$', |
|
38 |
views.meeting_datetimes, |
|
39 |
name='api-agenda-meeting-datetimes-legacy', |
|
40 |
), |
|
41 |
url(r'agenda/(?P<agenda_identifier>[\w-]+)/meetings/$', views.meeting_list, name='api-agenda-meetings'), |
|
42 |
url(r'agenda/(?P<agenda_identifier>[\w-]+)/desks/$', views.agenda_desk_list, name='api-agenda-desks'), |
|
43 |
url( |
|
44 |
r'agenda/(?P<agenda_identifier>[\w-]+)/meetings/(?P<meeting_identifier>[\w-]+)/datetimes/$', |
|
45 |
views.meeting_datetimes, |
|
46 |
name='api-agenda-meeting-datetimes', |
|
47 |
), |
|
42 | 48 |
url(r'booking/(?P<booking_pk>\w+)/$', views.booking), |
43 |
url(r'booking/(?P<booking_pk>\w+)/cancel/$', views.cancel_booking, |
|
44 |
name='api-cancel-booking'), |
|
45 |
url(r'booking/(?P<booking_pk>\w+)/accept/$', views.accept_booking, |
|
46 |
name='api-accept-booking'), |
|
47 |
url(r'booking/(?P<booking_pk>\w+)/ics/$', views.booking_ics, |
|
48 |
name='api-booking-ics'), |
|
49 |
url(r'booking/(?P<booking_pk>\w+)/cancel/$', views.cancel_booking, name='api-cancel-booking'), |
|
50 |
url(r'booking/(?P<booking_pk>\w+)/accept/$', views.accept_booking, name='api-accept-booking'), |
|
51 |
url(r'booking/(?P<booking_pk>\w+)/ics/$', views.booking_ics, name='api-booking-ics'), |
|
49 | 52 |
] |
chrono/api/views.py | ||
---|---|---|
14 | 14 |
# You should have received a copy of the GNU Affero General Public License |
15 | 15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 |
from collections import defaultdict |
|
18 | 17 |
import datetime |
18 |
from collections import defaultdict |
|
19 | 19 | |
20 | 20 |
from django.db import transaction |
21 | 21 |
from django.http import Http404, HttpResponse |
... | ... | |
23 | 23 |
from django.urls import reverse |
24 | 24 |
from django.utils.dateparse import parse_date |
25 | 25 |
from django.utils.encoding import force_text |
26 |
from django.utils.timezone import now, make_aware, localtime
|
|
26 |
from django.utils.timezone import localtime, make_aware, now
|
|
27 | 27 |
from django.utils.translation import gettext_noop |
28 | 28 |
from django.utils.translation import ugettext_lazy as _ |
29 | ||
30 | 29 |
from rest_framework import permissions, serializers, status |
31 | ||
32 | 30 |
from rest_framework.views import APIView |
33 | 31 | |
34 | 32 |
from chrono.api.utils import Response |
35 |
from ..agendas.models import (Agenda, Event, Booking, MeetingType, |
|
36 |
TimePeriod, Desk) |
|
33 | ||
34 |
from ..agendas.models import Agenda, Booking, Desk, Event, MeetingType, TimePeriod |
|
37 | 35 |
from ..interval import Intervals |
38 | 36 | |
39 | 37 | |
... | ... | |
44 | 42 |
def get_exceptions_by_desk(agenda): |
45 | 43 |
exceptions_by_desk = {} |
46 | 44 |
for desk in Desk.objects.filter(agenda=agenda).prefetch_related('timeperiodexception_set'): |
47 |
exceptions_by_desk[desk.id] = [(exc.start_datetime, exc.end_datetime) for exc in desk.timeperiodexception_set.all()] |
|
45 |
exceptions_by_desk[desk.id] = [ |
|
46 |
(exc.start_datetime, exc.end_datetime) for exc in desk.timeperiodexception_set.all() |
|
47 |
] |
|
48 | 48 |
return exceptions_by_desk |
49 | 49 | |
50 | 50 | |
... | ... | |
58 | 58 |
time_period_filters = { |
59 | 59 |
'min_datetime': min_datetime, |
60 | 60 |
'max_datetime': max_datetime, |
61 |
'meeting_type': meeting_type |
|
61 |
'meeting_type': meeting_type,
|
|
62 | 62 |
} |
63 | 63 | |
64 | 64 |
base_date = now().date() |
65 | 65 |
open_slots_by_desk = defaultdict(lambda: Intervals()) |
66 | 66 |
for time_period in TimePeriod.objects.filter(desk__agenda=agenda): |
67 |
duration = (datetime.datetime.combine(base_date, time_period.end_time) - |
|
68 |
datetime.datetime.combine(base_date, time_period.start_time)).seconds / 60 |
|
67 |
duration = ( |
|
68 |
datetime.datetime.combine(base_date, time_period.end_time) |
|
69 |
- datetime.datetime.combine(base_date, time_period.start_time) |
|
70 |
).seconds / 60 |
|
69 | 71 |
if duration < meeting_type.duration: |
70 | 72 |
# skip time period that can't even hold a single meeting |
71 | 73 |
continue |
... | ... | |
80 | 82 |
begin, end = interval |
81 | 83 |
open_slots_by_desk[desk].remove_overlap(localtime(begin), localtime(end)) |
82 | 84 | |
83 |
for event in agenda.event_set.filter( |
|
84 |
agenda=agenda, start_datetime__gte=min_datetime, |
|
85 |
start_datetime__lte=max_datetime + datetime.timedelta(meeting_type.duration)).select_related( |
|
86 |
'meeting_type').exclude( |
|
87 |
booking__cancellation_datetime__isnull=False): |
|
85 |
for event in ( |
|
86 |
agenda.event_set.filter( |
|
87 |
agenda=agenda, |
|
88 |
start_datetime__gte=min_datetime, |
|
89 |
start_datetime__lte=max_datetime + datetime.timedelta(meeting_type.duration), |
|
90 |
) |
|
91 |
.select_related('meeting_type') |
|
92 |
.exclude(booking__cancellation_datetime__isnull=False) |
|
93 |
): |
|
88 | 94 |
for slot in open_slots_by_desk[event.desk_id].search_data(event.start_datetime, event.end_datetime): |
89 | 95 |
slot.full = True |
90 | 96 | |
... | ... | |
98 | 104 |
def get_agenda_detail(request, agenda): |
99 | 105 |
agenda_detail = { |
100 | 106 |
'id': agenda.slug, |
101 |
'slug': agenda.slug, # kept for compatibility |
|
107 |
'slug': agenda.slug, # kept for compatibility
|
|
102 | 108 |
'text': agenda.label, |
103 | 109 |
'kind': agenda.kind, |
104 | 110 |
'minimal_booking_delay': agenda.minimal_booking_delay, |
... | ... | |
108 | 114 |
if agenda.kind == 'events': |
109 | 115 |
agenda_detail['api'] = { |
110 | 116 |
'datetimes_url': request.build_absolute_uri( |
111 |
reverse('api-agenda-datetimes', |
|
112 |
kwargs={'agenda_identifier': agenda.slug}))
|
|
117 |
reverse('api-agenda-datetimes', kwargs={'agenda_identifier': agenda.slug})
|
|
118 |
) |
|
113 | 119 |
} |
114 | 120 |
elif agenda.kind == 'meetings': |
115 | 121 |
agenda_detail['api'] = { |
116 | 122 |
'meetings_url': request.build_absolute_uri( |
117 |
reverse('api-agenda-meetings', |
|
118 |
kwargs={'agenda_identifier': agenda.slug})),
|
|
123 |
reverse('api-agenda-meetings', kwargs={'agenda_identifier': agenda.slug})
|
|
124 |
), |
|
119 | 125 |
'desks_url': request.build_absolute_uri( |
120 |
reverse('api-agenda-desks', |
|
121 |
kwargs={'agenda_identifier': agenda.slug}))
|
|
126 |
reverse('api-agenda-desks', kwargs={'agenda_identifier': agenda.slug})
|
|
127 |
),
|
|
122 | 128 |
} |
123 | 129 |
agenda_detail['api']['fillslots_url'] = request.build_absolute_uri( |
124 |
reverse('api-agenda-fillslots', |
|
125 |
kwargs={'agenda_identifier': agenda.slug}))
|
|
130 |
reverse('api-agenda-fillslots', kwargs={'agenda_identifier': agenda.slug})
|
|
131 |
) |
|
126 | 132 | |
127 | 133 |
return agenda_detail |
128 | 134 | |
... | ... | |
136 | 142 |
if event.waiting_list_places: |
137 | 143 |
places['waiting_list_total'] = event.waiting_list_places |
138 | 144 |
places['waiting_list_reserved'] = event.waiting_list |
139 |
places['waiting_list_available'] = (event.waiting_list_places - event.waiting_list)
|
|
145 |
places['waiting_list_available'] = event.waiting_list_places - event.waiting_list
|
|
140 | 146 |
return places |
141 | 147 | |
142 | 148 | |
... | ... | |
144 | 150 |
permission_classes = () |
145 | 151 | |
146 | 152 |
def get(self, request, format=None): |
147 |
agendas = [get_agenda_detail(request, agenda) |
|
148 |
for agenda in Agenda.objects.all().order_by('label')] |
|
153 |
agendas = [get_agenda_detail(request, agenda) for agenda in Agenda.objects.all().order_by('label')] |
|
149 | 154 |
return Response({'data': agendas}) |
150 | 155 | |
156 | ||
151 | 157 |
agendas = Agendas.as_view() |
152 | 158 | |
153 | 159 | |
... | ... | |
155 | 161 |
''' |
156 | 162 |
Retrieve an agenda instance. |
157 | 163 |
''' |
164 | ||
158 | 165 |
permission_classes = () |
159 | 166 | |
160 | 167 |
def get(self, request, agenda_identifier): |
161 | 168 |
agenda = get_object_or_404(Agenda, slug=agenda_identifier) |
162 | 169 |
return Response({'data': get_agenda_detail(request, agenda)}) |
163 | 170 | |
171 | ||
164 | 172 |
agenda_detail = AgendaDetail.as_view() |
165 | 173 | |
166 | 174 | |
... | ... | |
186 | 194 | |
187 | 195 |
if agenda.minimal_booking_delay: |
188 | 196 |
entries = entries.filter( |
189 |
start_datetime__gte=localtime(now() + datetime.timedelta(days=agenda.minimal_booking_delay)).replace(hour=0, minute=0)) |
|
197 |
start_datetime__gte=localtime( |
|
198 |
now() + datetime.timedelta(days=agenda.minimal_booking_delay) |
|
199 |
).replace(hour=0, minute=0) |
|
200 |
) |
|
190 | 201 | |
191 | 202 |
if agenda.maximal_booking_delay: |
192 | 203 |
entries = entries.filter( |
193 |
start_datetime__lt=localtime(now() + datetime.timedelta(days=agenda.maximal_booking_delay)).replace(hour=0, minute=0)) |
|
204 |
start_datetime__lt=localtime( |
|
205 |
now() + datetime.timedelta(days=agenda.maximal_booking_delay) |
|
206 |
).replace(hour=0, minute=0) |
|
207 |
) |
|
194 | 208 | |
195 | 209 |
if 'date_start' in request.GET: |
196 |
entries = entries.filter(start_datetime__gte=make_aware( |
|
197 |
datetime.datetime.combine(parse_date(request.GET['date_start']), datetime.time(0, 0)))) |
|
210 |
entries = entries.filter( |
|
211 |
start_datetime__gte=make_aware( |
|
212 |
datetime.datetime.combine(parse_date(request.GET['date_start']), datetime.time(0, 0)) |
|
213 |
) |
|
214 |
) |
|
198 | 215 | |
199 | 216 |
if 'date_end' in request.GET: |
200 |
entries = entries.filter(start_datetime__lt=make_aware( |
|
201 |
datetime.datetime.combine(parse_date(request.GET['date_end']), datetime.time(0, 0)))) |
|
202 | ||
203 |
response = {'data': [{'id': x.id, |
|
204 |
'slug': x.slug, |
|
205 |
'text': force_text(x), |
|
206 |
'datetime': format_response_datetime(x.start_datetime), |
|
207 |
'description': x.description, |
|
208 |
'disabled': bool(x.full), |
|
209 |
'api': { |
|
210 |
'fillslot_url': request.build_absolute_uri( |
|
211 |
reverse('api-fillslot', |
|
212 |
kwargs={ |
|
213 |
'agenda_identifier': agenda.slug, |
|
214 |
'event_identifier': x.slug or x.id, |
|
215 |
})), |
|
216 |
'status_url': request.build_absolute_uri( |
|
217 |
reverse('api-event-status', |
|
218 |
kwargs={ |
|
219 |
'agenda_identifier': agenda.slug, |
|
220 |
'event_identifier': x.slug or x.id, |
|
221 |
})) |
|
222 |
}, |
|
223 |
} for x in entries]} |
|
217 |
entries = entries.filter( |
|
218 |
start_datetime__lt=make_aware( |
|
219 |
datetime.datetime.combine(parse_date(request.GET['date_end']), datetime.time(0, 0)) |
|
220 |
) |
|
221 |
) |
|
222 | ||
223 |
response = { |
|
224 |
'data': [ |
|
225 |
{ |
|
226 |
'id': x.id, |
|
227 |
'slug': x.slug, |
|
228 |
'text': force_text(x), |
|
229 |
'datetime': format_response_datetime(x.start_datetime), |
|
230 |
'description': x.description, |
|
231 |
'disabled': bool(x.full), |
|
232 |
'api': { |
|
233 |
'fillslot_url': request.build_absolute_uri( |
|
234 |
reverse( |
|
235 |
'api-fillslot', |
|
236 |
kwargs={ |
|
237 |
'agenda_identifier': agenda.slug, |
|
238 |
'event_identifier': x.slug or x.id, |
|
239 |
}, |
|
240 |
) |
|
241 |
), |
|
242 |
'status_url': request.build_absolute_uri( |
|
243 |
reverse( |
|
244 |
'api-event-status', |
|
245 |
kwargs={ |
|
246 |
'agenda_identifier': agenda.slug, |
|
247 |
'event_identifier': x.slug or x.id, |
|
248 |
}, |
|
249 |
) |
|
250 |
), |
|
251 |
}, |
|
252 |
} |
|
253 |
for x in entries |
|
254 |
] |
|
255 |
} |
|
224 | 256 |
return Response(response) |
225 | 257 | |
258 | ||
226 | 259 |
datetimes = Datetimes.as_view() |
227 | 260 | |
228 | 261 | |
... | ... | |
235 | 268 |
# legacy access by meeting id |
236 | 269 |
meeting_type = MeetingType.objects.get(id=meeting_identifier) |
237 | 270 |
else: |
238 |
meeting_type = MeetingType.objects.get(slug=meeting_identifier, |
|
239 |
agenda__slug=agenda_identifier) |
|
271 |
meeting_type = MeetingType.objects.get( |
|
272 |
slug=meeting_identifier, agenda__slug=agenda_identifier |
|
273 |
) |
|
240 | 274 |
except (ValueError, MeetingType.DoesNotExist): |
241 | 275 |
raise Http404() |
242 | 276 | |
... | ... | |
259 | 293 |
# to request.build_absolute_uri() |
260 | 294 |
fake_event_identifier = '__event_identifier__' |
261 | 295 |
fillslot_url = request.build_absolute_uri( |
262 |
reverse('api-fillslot', |
|
263 |
kwargs={ |
|
264 |
'agenda_identifier': agenda.slug, |
|
265 |
'event_identifier': fake_event_identifier, |
|
266 |
})) |
|
267 | ||
268 |
response = {'data': [{'id': x.id, |
|
269 |
'datetime': format_response_datetime(x.start_datetime), |
|
270 |
'text': force_text(x), |
|
271 |
'disabled': bool(x.full), |
|
272 |
'api': { |
|
273 |
'fillslot_url': fillslot_url.replace(fake_event_identifier, str(x.id)), |
|
274 |
}, |
|
275 |
} for x in slots]} |
|
296 |
reverse( |
|
297 |
'api-fillslot', |
|
298 |
kwargs={'agenda_identifier': agenda.slug, 'event_identifier': fake_event_identifier,}, |
|
299 |
) |
|
300 |
) |
|
301 | ||
302 |
response = { |
|
303 |
'data': [ |
|
304 |
{ |
|
305 |
'id': x.id, |
|
306 |
'datetime': format_response_datetime(x.start_datetime), |
|
307 |
'text': force_text(x), |
|
308 |
'disabled': bool(x.full), |
|
309 |
'api': {'fillslot_url': fillslot_url.replace(fake_event_identifier, str(x.id)),}, |
|
310 |
} |
|
311 |
for x in slots |
|
312 |
] |
|
313 |
} |
|
276 | 314 |
return Response(response) |
277 | 315 | |
316 | ||
278 | 317 |
meeting_datetimes = MeetingDatetimes.as_view() |
279 | 318 | |
280 | 319 | |
... | ... | |
291 | 330 | |
292 | 331 |
meeting_types = [] |
293 | 332 |
for meeting_type in agenda.meetingtype_set.all(): |
294 |
meeting_types.append({ |
|
295 |
'text': meeting_type.label, |
|
296 |
'id': meeting_type.slug, |
|
297 |
'duration': meeting_type.duration, |
|
298 |
'api': { |
|
299 |
'datetimes_url': request.build_absolute_uri( |
|
300 |
reverse('api-agenda-meeting-datetimes', |
|
301 |
kwargs={'agenda_identifier': agenda.slug, |
|
302 |
'meeting_identifier': meeting_type.slug})), |
|
303 |
} |
|
304 |
}) |
|
333 |
meeting_types.append( |
|
334 |
{ |
|
335 |
'text': meeting_type.label, |
|
336 |
'id': meeting_type.slug, |
|
337 |
'duration': meeting_type.duration, |
|
338 |
'api': { |
|
339 |
'datetimes_url': request.build_absolute_uri( |
|
340 |
reverse( |
|
341 |
'api-agenda-meeting-datetimes', |
|
342 |
kwargs={ |
|
343 |
'agenda_identifier': agenda.slug, |
|
344 |
'meeting_identifier': meeting_type.slug, |
|
345 |
}, |
|
346 |
) |
|
347 |
), |
|
348 |
}, |
|
349 |
} |
|
350 |
) |
|
305 | 351 | |
306 | 352 |
return Response({'data': meeting_types}) |
307 | 353 | |
354 | ||
308 | 355 |
meeting_list = MeetingList.as_view() |
309 | 356 | |
310 | 357 | |
... | ... | |
322 | 369 |
desks = [{'id': x.slug, 'text': x.label} for x in agenda.desk_set.all()] |
323 | 370 |
return Response({'data': desks}) |
324 | 371 | |
372 | ||
325 | 373 |
agenda_desk_list = AgendaDeskList.as_view() |
326 | 374 | |
327 | 375 | |
... | ... | |
329 | 377 |
''' |
330 | 378 |
payload to fill one slot. The slot (event id) is in the URL. |
331 | 379 |
''' |
380 | ||
332 | 381 |
label = serializers.CharField(max_length=250, allow_blank=True) |
333 | 382 |
user_name = serializers.CharField(max_length=250, allow_blank=True) |
334 | 383 |
user_display_label = serializers.CharField(max_length=250, allow_blank=True) |
... | ... | |
342 | 391 |
payload to fill multiple slots: same as SlotSerializer, but the |
343 | 392 |
slots list is in the payload. |
344 | 393 |
''' |
345 |
slots = serializers.ListField(required=True, |
|
346 |
child=serializers.CharField(max_length=64, allow_blank=False)) |
|
394 | ||
395 |
slots = serializers.ListField( |
|
396 |
required=True, child=serializers.CharField(max_length=64, allow_blank=False) |
|
397 |
) |
|
347 | 398 | |
348 | 399 | |
349 | 400 |
class Fillslots(APIView): |
... | ... | |
351 | 402 |
serializer_class = SlotsSerializer |
352 | 403 | |
353 | 404 |
def post(self, request, agenda_identifier=None, event_identifier=None, format=None): |
354 |
return self.fillslot(request=request, agenda_identifier=agenda_identifier, |
|
355 |
format=format) |
|
405 |
return self.fillslot(request=request, agenda_identifier=agenda_identifier, format=format) |
|
356 | 406 | |
357 | 407 |
def fillslot(self, request, agenda_identifier=None, slots=[], format=None): |
358 | 408 |
multiple_booking = bool(not slots) |
... | ... | |
367 | 417 | |
368 | 418 |
serializer = self.serializer_class(data=request.data, partial=True) |
369 | 419 |
if not serializer.is_valid(): |
370 |
return Response({ |
|
371 |
'err': 1, |
|
372 |
'err_class': 'invalid payload', |
|
373 |
'err_desc': _('invalid payload'), |
|
374 |
'errors': serializer.errors |
|
375 |
}, status=status.HTTP_400_BAD_REQUEST) |
|
420 |
return Response( |
|
421 |
{ |
|
422 |
'err': 1, |
|
423 |
'err_class': 'invalid payload', |
|
424 |
'err_desc': _('invalid payload'), |
|
425 |
'errors': serializer.errors, |
|
426 |
}, |
|
427 |
status=status.HTTP_400_BAD_REQUEST, |
|
428 |
) |
|
376 | 429 |
payload = serializer.validated_data |
377 | 430 | |
378 | 431 |
if 'slots' in payload: |
379 | 432 |
slots = payload['slots'] |
380 | 433 |
if not slots: |
381 |
return Response({ |
|
382 |
'err': 1, |
|
383 |
'err_class': 'slots list cannot be empty', |
|
384 |
'err_desc': _('slots list cannot be empty'), |
|
385 |
}, status=status.HTTP_400_BAD_REQUEST) |
|
434 |
return Response( |
|
435 |
{ |
|
436 |
'err': 1, |
|
437 |
'err_class': 'slots list cannot be empty', |
|
438 |
'err_desc': _('slots list cannot be empty'), |
|
439 |
}, |
|
440 |
status=status.HTTP_400_BAD_REQUEST, |
|
441 |
) |
|
386 | 442 | |
387 | 443 |
if 'count' in payload: |
388 | 444 |
places_count = payload['count'] |
... | ... | |
391 | 447 |
try: |
392 | 448 |
places_count = int(request.query_params['count']) |
393 | 449 |
except ValueError: |
394 |
return Response({ |
|
395 |
'err': 1, |
|
396 |
'err_class': 'invalid value for count (%s)' % request.query_params['count'], |
|
397 |
'err_desc': _('invalid value for count (%s)') % request.query_params['count'], |
|
398 |
}, status=status.HTTP_400_BAD_REQUEST) |
|
450 |
return Response( |
|
451 |
{ |
|
452 |
'err': 1, |
|
453 |
'err_class': 'invalid value for count (%s)' % request.query_params['count'], |
|
454 |
'err_desc': _('invalid value for count (%s)') % request.query_params['count'], |
|
455 |
}, |
|
456 |
status=status.HTTP_400_BAD_REQUEST, |
|
457 |
) |
|
399 | 458 |
else: |
400 | 459 |
places_count = 1 |
401 | 460 | |
402 | 461 |
if places_count <= 0: |
403 |
return Response({ |
|
404 |
'err': 1, |
|
405 |
'err_class': 'count cannot be less than or equal to zero', |
|
406 |
'err_desc': _('count cannot be less than or equal to zero'), |
|
407 |
}, status=status.HTTP_400_BAD_REQUEST) |
|
462 |
return Response( |
|
463 |
{ |
|
464 |
'err': 1, |
|
465 |
'err_class': 'count cannot be less than or equal to zero', |
|
466 |
'err_desc': _('count cannot be less than or equal to zero'), |
|
467 |
}, |
|
468 |
status=status.HTTP_400_BAD_REQUEST, |
|
469 |
) |
|
408 | 470 | |
409 | 471 |
to_cancel_booking = None |
410 | 472 |
cancel_booking_id = None |
... | ... | |
412 | 474 |
try: |
413 | 475 |
cancel_booking_id = int(payload.get('cancel_booking_id')) |
414 | 476 |
except (ValueError, TypeError): |
415 |
return Response({ |
|
416 |
'err': 1, |
|
417 |
'err_class': 'cancel_booking_id is not an integer', |
|
418 |
'err_desc': _('cancel_booking_id is not an integer'), |
|
419 |
}, status=status.HTTP_400_BAD_REQUEST) |
|
477 |
return Response( |
|
478 |
{ |
|
479 |
'err': 1, |
|
480 |
'err_class': 'cancel_booking_id is not an integer', |
|
481 |
'err_desc': _('cancel_booking_id is not an integer'), |
|
482 |
}, |
|
483 |
status=status.HTTP_400_BAD_REQUEST, |
|
484 |
) |
|
420 | 485 | |
421 | 486 |
if cancel_booking_id is not None: |
422 | 487 |
cancel_error = None |
... | ... | |
432 | 497 |
cancel_error = gettext_noop('cancel booking: booking does no exist') |
433 | 498 | |
434 | 499 |
if cancel_error: |
435 |
return Response({ |
|
436 |
'err': 1, |
|
437 |
'err_class': cancel_error, |
|
438 |
'err_desc': _(cancel_error), |
|
439 |
}) |
|
500 |
return Response({'err': 1, 'err_class': cancel_error, 'err_desc': _(cancel_error),}) |
|
440 | 501 | |
441 | 502 |
extra_data = {} |
442 | 503 |
for k, v in request.data.items(): |
... | ... | |
454 | 515 |
try: |
455 | 516 |
meeting_type_id_, datetime_str = slot.split(':') |
456 | 517 |
except ValueError: |
457 |
return Response({ |
|
458 |
'err': 1, |
|
459 |
'err_class': 'invalid slot: %s' % slot, |
|
460 |
'err_desc': _('invalid slot: %s') % slot, |
|
461 |
}, status=status.HTTP_400_BAD_REQUEST) |
|
518 |
return Response( |
|
519 |
{ |
|
520 |
'err': 1, |
|
521 |
'err_class': 'invalid slot: %s' % slot, |
|
522 |
'err_desc': _('invalid slot: %s') % slot, |
|
523 |
}, |
|
524 |
status=status.HTTP_400_BAD_REQUEST, |
|
525 |
) |
|
462 | 526 |
if meeting_type_id_ != meeting_type_id: |
463 |
return Response({ |
|
464 |
'err': 1, |
|
465 |
'err_class': 'all slots must have the same meeting type id (%s)' % meeting_type_id, |
|
466 |
'err_desc': _('all slots must have the same meeting type id (%s)') % meeting_type_id, |
|
467 |
}, status=status.HTTP_400_BAD_REQUEST) |
|
527 |
return Response( |
|
528 |
{ |
|
529 |
'err': 1, |
|
530 |
'err_class': 'all slots must have the same meeting type id (%s)' |
|
531 |
% meeting_type_id, |
|
532 |
'err_desc': _('all slots must have the same meeting type id (%s)') |
|
533 |
% meeting_type_id, |
|
534 |
}, |
|
535 |
status=status.HTTP_400_BAD_REQUEST, |
|
536 |
) |
|
468 | 537 |
datetimes.add(make_aware(datetime.datetime.strptime(datetime_str, '%Y-%m-%d-%H%M'))) |
469 | 538 | |
470 | 539 |
# get all free slots and separate them by desk |
... | ... | |
480 | 549 |
available_desk = Desk.objects.get(id=available_desk_id) |
481 | 550 |
break |
482 | 551 |
else: |
483 |
return Response({ |
|
484 |
'err': 1, |
|
485 |
'err_class': 'no more desk available', |
|
486 |
'err_desc': _('no more desk available'), |
|
487 |
}) |
|
552 |
return Response( |
|
553 |
{ |
|
554 |
'err': 1, |
|
555 |
'err_class': 'no more desk available', |
|
556 |
'err_desc': _('no more desk available'), |
|
557 |
} |
|
558 |
) |
|
488 | 559 | |
489 | 560 |
# all datetimes are free, book them in order |
490 | 561 |
datetimes = list(datetimes) |
... | ... | |
494 | 565 |
# create them now, with data from the slots and the desk we found. |
495 | 566 |
events = [] |
496 | 567 |
for start_datetime in datetimes: |
497 |
events.append(Event.objects.create(agenda=agenda, |
|
568 |
events.append( |
|
569 |
Event.objects.create( |
|
570 |
agenda=agenda, |
|
498 | 571 |
meeting_type_id=meeting_type_id, |
499 | 572 |
start_datetime=start_datetime, |
500 |
full=False, places=1, |
|
501 |
desk=available_desk)) |
|
573 |
full=False, |
|
574 |
places=1, |
|
575 |
desk=available_desk, |
|
576 |
) |
|
577 |
) |
|
502 | 578 |
else: |
503 | 579 |
try: |
504 | 580 |
events = Event.objects.filter(id__in=[int(s) for s in slots]).order_by('start_datetime') |
... | ... | |
514 | 590 |
# in the waiting list. |
515 | 591 |
in_waiting_list = True |
516 | 592 |
if (event.waiting_list + places_count) > event.waiting_list_places: |
517 |
return Response({ |
|
518 |
'err': 1, |
|
519 |
'err_class': 'sold out', |
|
520 |
'err_desc': _('sold out'), |
|
521 |
}) |
|
593 |
return Response({'err': 1, 'err_class': 'sold out', 'err_desc': _('sold out'),}) |
|
522 | 594 |
else: |
523 | 595 |
if (event.booked_places + places_count) > event.places: |
524 |
return Response({ |
|
525 |
'err': 1, |
|
526 |
'err_class': 'sold out', |
|
527 |
'err_desc': _('sold out') |
|
528 |
}) |
|
596 |
return Response({'err': 1, 'err_class': 'sold out', 'err_desc': _('sold out')}) |
|
529 | 597 | |
530 | 598 |
with transaction.atomic(): |
531 | 599 |
if to_cancel_booking: |
... | ... | |
536 | 604 |
primary_booking = None |
537 | 605 |
for event in events: |
538 | 606 |
for i in range(places_count): |
539 |
new_booking = Booking(event_id=event.id, |
|
540 |
in_waiting_list=in_waiting_list, |
|
541 |
label=payload.get('label', ''), |
|
542 |
user_name=payload.get('user_name', ''), |
|
543 |
backoffice_url=payload.get('backoffice_url', ''), |
|
544 |
user_display_label=payload.get('user_display_label', ''), |
|
545 |
extra_data=extra_data) |
|
607 |
new_booking = Booking( |
|
608 |
event_id=event.id, |
|
609 |
in_waiting_list=in_waiting_list, |
|
610 |
label=payload.get('label', ''), |
|
611 |
user_name=payload.get('user_name', ''), |
|
612 |
backoffice_url=payload.get('backoffice_url', ''), |
|
613 |
user_display_label=payload.get('user_display_label', ''), |
|
614 |
extra_data=extra_data, |
|
615 |
) |
|
546 | 616 |
if primary_booking is not None: |
547 | 617 |
new_booking.primary_booking = primary_booking |
548 | 618 |
new_booking.save() |
... | ... | |
556 | 626 |
'datetime': format_response_datetime(events[0].start_datetime), |
557 | 627 |
'api': { |
558 | 628 |
'cancel_url': request.build_absolute_uri( |
559 |
reverse('api-cancel-booking', kwargs={'booking_pk': primary_booking.id})), |
|
629 |
reverse('api-cancel-booking', kwargs={'booking_pk': primary_booking.id}) |
|
630 |
), |
|
560 | 631 |
'ics_url': request.build_absolute_uri( |
561 |
reverse('api-booking-ics', kwargs={'booking_pk': primary_booking.id})), |
|
562 |
} |
|
632 |
reverse('api-booking-ics', kwargs={'booking_pk': primary_booking.id}) |
|
633 |
), |
|
634 |
}, |
|
563 | 635 |
} |
564 | 636 |
if in_waiting_list: |
565 | 637 |
response['api']['accept_url'] = request.build_absolute_uri( |
566 |
reverse('api-accept-booking', kwargs={'booking_pk': primary_booking.id})) |
|
638 |
reverse('api-accept-booking', kwargs={'booking_pk': primary_booking.id}) |
|
639 |
) |
|
567 | 640 |
if agenda.kind == 'meetings': |
568 | 641 |
response['end_datetime'] = format_response_datetime(events[-1].end_datetime) |
569 | 642 |
response['duration'] = (events[-1].end_datetime - events[-1].start_datetime).seconds // 60 |
570 | 643 |
if available_desk: |
571 |
response['desk'] = { |
|
572 |
'label': available_desk.label, |
|
573 |
'slug': available_desk.slug} |
|
644 |
response['desk'] = {'label': available_desk.label, 'slug': available_desk.slug} |
|
574 | 645 |
if to_cancel_booking: |
575 | 646 |
response['cancelled_booking_id'] = cancelled_booking_id |
576 | 647 |
if agenda.kind == 'events' and not multiple_booking: |
... | ... | |
579 | 650 | |
580 | 651 |
return Response(response) |
581 | 652 | |
653 | ||
582 | 654 |
fillslots = Fillslots.as_view() |
583 | 655 | |
584 | 656 | |
... | ... | |
586 | 658 |
serializer_class = SlotSerializer |
587 | 659 | |
588 | 660 |
def post(self, request, agenda_identifier=None, event_identifier=None, format=None): |
589 |
return self.fillslot(request=request, |
|
590 |
agenda_identifier=agenda_identifier, |
|
591 |
slots=[event_identifier], # fill a "list on one slot" |
|
592 |
format=format) |
|
661 |
return self.fillslot( |
|
662 |
request=request, |
|
663 |
agenda_identifier=agenda_identifier, |
|
664 |
slots=[event_identifier], # fill a "list on one slot" |
|
665 |
format=format, |
|
666 |
) |
|
667 | ||
593 | 668 | |
594 | 669 |
fillslot = Fillslot.as_view() |
595 | 670 | |
... | ... | |
599 | 674 | |
600 | 675 |
def initial(self, request, *args, **kwargs): |
601 | 676 |
super(BookingAPI, self).initial(request, *args, **kwargs) |
602 |
self.booking = Booking.objects.get(id=kwargs.get('booking_pk'), |
|
603 |
cancellation_datetime__isnull=True) |
|
677 |
self.booking = Booking.objects.get(id=kwargs.get('booking_pk'), cancellation_datetime__isnull=True) |
|
604 | 678 | |
605 | 679 |
def delete(self, request, *args, **kwargs): |
606 | 680 |
self.booking.cancel() |
607 | 681 |
response = {'err': 0, 'booking_id': self.booking.id} |
608 | 682 |
return Response(response) |
609 | 683 | |
684 | ||
610 | 685 |
booking = BookingAPI.as_view() |
611 | 686 | |
612 | 687 | |
... | ... | |
616 | 691 | |
617 | 692 |
It will return an error (code 1) if the booking was already cancelled. |
618 | 693 |
''' |
694 | ||
619 | 695 |
permission_classes = (permissions.IsAuthenticated,) |
620 | 696 | |
621 | 697 |
def post(self, request, booking_pk=None, format=None): |
... | ... | |
631 | 707 |
response = {'err': 0, 'booking_id': booking.id} |
632 | 708 |
return Response(response) |
633 | 709 | |
710 | ||
634 | 711 |
cancel_booking = CancelBooking.as_view() |
635 | 712 | |
636 | 713 | |
... | ... | |
641 | 718 |
It will return error codes if the booking was cancelled before (code 1) and |
642 | 719 |
if the booking was not in waiting list (code 2). |
643 | 720 |
''' |
721 | ||
644 | 722 |
permission_classes = (permissions.IsAuthenticated,) |
645 | 723 | |
646 | 724 |
def post(self, request, booking_pk=None, format=None): |
... | ... | |
663 | 741 |
response = {'err': 0, 'booking_id': booking.id} |
664 | 742 |
return Response(response) |
665 | 743 | |
744 | ||
666 | 745 |
accept_booking = AcceptBooking.as_view() |
667 | 746 | |
668 | 747 | |
... | ... | |
699 | 778 |
response = HttpResponse(booking.get_ics(request), content_type='text/calendar') |
700 | 779 |
return response |
701 | 780 | |
781 | ||
702 | 782 |
booking_ics = BookingICS.as_view() |
chrono/interval.py | ||
---|---|---|
63 | 63 |
10: [a], |
64 | 64 |
} |
65 | 65 |
''' |
66 | ||
66 | 67 |
def __init__(self): |
67 | 68 |
self.points = [] |
68 | 69 |
self.container = [] |
chrono/manager/forms.py | ||
---|---|---|
26 | 26 |
from django.utils.timezone import make_aware |
27 | 27 |
from django.utils.translation import ugettext_lazy as _ |
28 | 28 | |
29 |
from chrono.agendas.models import (Agenda, Event, MeetingType, TimePeriod, Desk, |
|
30 |
TimePeriodException, WEEKDAYS_LIST) |
|
29 |
from chrono.agendas.models import ( |
|
30 |
WEEKDAYS_LIST, |
|
31 |
Agenda, |
|
32 |
Desk, |
|
33 |
Event, |
|
34 |
MeetingType, |
|
35 |
TimePeriod, |
|
36 |
TimePeriodException, |
|
37 |
) |
|
31 | 38 | |
32 | 39 |
from . import widgets |
33 | 40 | |
34 | ||
35 | 41 |
DATETIME_OPTIONS = { |
36 |
'weekStart': 1,
|
|
37 |
'autoclose': True,
|
|
38 |
}
|
|
42 |
'weekStart': 1, |
|
43 |
'autoclose': True, |
|
44 |
} |
|
39 | 45 | |
40 | 46 | |
41 | 47 |
class DateTimeWidget(widgets.DateTimeWidget): |
... | ... | |
49 | 55 |
fields = ['label', 'kind', 'edit_role', 'view_role'] |
50 | 56 | |
51 | 57 |
edit_role = forms.ModelChoiceField( |
52 |
label=_('Edit Role'), required=False,
|
|
53 |
queryset=Group.objects.all().order_by('name'))
|
|
58 |
label=_('Edit Role'), required=False, queryset=Group.objects.all().order_by('name')
|
|
59 |
) |
|
54 | 60 |
view_role = forms.ModelChoiceField( |
55 |
label=_('View Role'), required=False,
|
|
56 |
queryset=Group.objects.all().order_by('name'))
|
|
61 |
label=_('View Role'), required=False, queryset=Group.objects.all().order_by('name')
|
|
62 |
) |
|
57 | 63 | |
58 | 64 | |
59 | 65 |
class AgendaEditForm(AgendaAddForm): |
... | ... | |
66 | 72 |
class Meta: |
67 | 73 |
model = Event |
68 | 74 |
widgets = { |
69 |
'agenda': forms.HiddenInput(),
|
|
70 |
'start_datetime': DateTimeWidget(),
|
|
75 |
'agenda': forms.HiddenInput(), |
|
76 |
'start_datetime': DateTimeWidget(), |
|
71 | 77 |
} |
72 | 78 |
exclude = ['full', 'meeting_type', 'desk', 'slug'] |
73 | 79 | |
... | ... | |
76 | 82 |
class Meta: |
77 | 83 |
model = Event |
78 | 84 |
widgets = { |
79 |
'agenda': forms.HiddenInput(),
|
|
80 |
'start_datetime': DateTimeWidget(),
|
|
85 |
'agenda': forms.HiddenInput(), |
|
86 |
'start_datetime': DateTimeWidget(), |
|
81 | 87 |
} |
82 | 88 |
exclude = ['full', 'meeting_type', 'desk'] |
83 | 89 | |
... | ... | |
86 | 92 |
class Meta: |
87 | 93 |
model = MeetingType |
88 | 94 |
widgets = { |
89 |
'agenda': forms.HiddenInput(),
|
|
95 |
'agenda': forms.HiddenInput(), |
|
90 | 96 |
} |
91 | 97 |
exclude = ['slug'] |
92 | 98 | |
... | ... | |
95 | 101 |
class Meta: |
96 | 102 |
model = MeetingType |
97 | 103 |
widgets = { |
98 |
'agenda': forms.HiddenInput(),
|
|
104 |
'agenda': forms.HiddenInput(), |
|
99 | 105 |
} |
100 | 106 |
exclude = [] |
101 | 107 | |
102 | 108 | |
103 | 109 |
class TimePeriodAddForm(forms.Form): |
104 | 110 |
weekdays = forms.MultipleChoiceField( |
105 |
label=_('Days'), |
|
106 |
widget=widgets.WeekdaysWidget(), |
|
107 |
choices=WEEKDAYS_LIST) |
|
111 |
label=_('Days'), widget=widgets.WeekdaysWidget(), choices=WEEKDAYS_LIST |
|
112 |
) |
|
108 | 113 |
start_time = forms.TimeField(label=_('Start Time'), widget=widgets.TimeWidget()) |
109 | 114 |
end_time = forms.TimeField(label=_('End Time'), widget=widgets.TimeWidget()) |
110 | 115 | |
... | ... | |
118 | 123 |
class Meta: |
119 | 124 |
model = TimePeriod |
120 | 125 |
widgets = { |
121 |
'start_time': widgets.TimeWidget(),
|
|
122 |
'end_time': widgets.TimeWidget(),
|
|
123 |
'desk': forms.HiddenInput(),
|
|
126 |
'start_time': widgets.TimeWidget(), |
|
127 |
'end_time': widgets.TimeWidget(), |
|
128 |
'desk': forms.HiddenInput(), |
|
124 | 129 |
} |
125 | 130 |
exclude = [] |
126 | 131 | |
... | ... | |
166 | 171 | |
167 | 172 |
class ImportEventsForm(forms.Form): |
168 | 173 |
events_csv_file = forms.FileField( |
169 |
label=_('Events File'), |
|
170 |
required=True, |
|
171 |
help_text=_('CSV file with date, time, number of places, ' |
|
172 |
'number of places in waiting list, and label ' |
|
173 |
'as columns.')) |
|
174 |
label=_('Events File'), |
|
175 |
required=True, |
|
176 |
help_text=_( |
|
177 |
'CSV file with date, time, number of places, ' |
|
178 |
'number of places in waiting list, and label ' |
|
179 |
'as columns.' |
|
180 |
), |
|
181 |
) |
|
174 | 182 |
events = None |
175 | 183 | |
176 | 184 |
def __init__(self, agenda_pk, **kwargs): |
... | ... | |
201 | 209 |
if not csvline: |
202 | 210 |
continue |
203 | 211 |
if len(csvline) < 3: |
204 |
raise ValidationError(_('Invalid file format. (line %d)') % (i+1))
|
|
212 |
raise ValidationError(_('Invalid file format. (line %d)') % (i + 1))
|
|
205 | 213 |
if i == 0 and csvline[0].strip('#') in ('date', 'Date', _('date'), _('Date')): |
206 | 214 |
continue |
207 | 215 |
event = Event() |
208 | 216 |
event.agenda_id = self.agenda_pk |
209 |
for datetime_fmt in ('%Y-%m-%d %H:%M', '%d/%m/%Y %H:%M', |
|
210 |
'%d/%m/%Y %Hh%M', '%Y-%m-%d %H:%M:%S', '%d/%m/%Y %H:%M:%S'): |
|
217 |
for datetime_fmt in ( |
|
218 |
'%Y-%m-%d %H:%M', |
|
219 |
'%d/%m/%Y %H:%M', |
|
220 |
'%d/%m/%Y %Hh%M', |
|
221 |
'%Y-%m-%d %H:%M:%S', |
|
222 |
'%d/%m/%Y %H:%M:%S', |
|
223 |
): |
|
211 | 224 |
try: |
212 |
event_datetime = datetime.datetime.strptime( |
|
213 |
'%s %s' % tuple(csvline[:2]), datetime_fmt) |
|
225 |
event_datetime = datetime.datetime.strptime('%s %s' % tuple(csvline[:2]), datetime_fmt) |
|
214 | 226 |
except ValueError: |
215 | 227 |
continue |
216 | 228 |
event.start_datetime = make_aware(event_datetime) |
217 | 229 |
break |
218 | 230 |
else: |
219 |
raise ValidationError(_('Invalid file format. (date/time format, line %d)') % (i+1))
|
|
231 |
raise ValidationError(_('Invalid file format. (date/time format, line %d)') % (i + 1))
|
|
220 | 232 |
try: |
221 | 233 |
event.places = int(csvline[2]) |
222 | 234 |
except ValueError: |
223 |
raise ValidationError(_('Invalid file format. (number of places, line %d)') % (i+1))
|
|
235 |
raise ValidationError(_('Invalid file format. (number of places, line %d)') % (i + 1))
|
|
224 | 236 |
if len(csvline) >= 4: |
225 | 237 |
try: |
226 | 238 |
event.waiting_list_places = int(csvline[3]) |
227 | 239 |
except ValueError: |
228 |
raise ValidationError(_('Invalid file format. (number of places in waiting list, line %d)') % (i+1)) |
|
240 |
raise ValidationError( |
|
241 |
_('Invalid file format. (number of places in waiting list, line %d)') % (i + 1) |
|
242 |
) |
|
229 | 243 |
if len(csvline) >= 5: |
230 | 244 |
event.label = force_text(csvline[4]) |
231 | 245 |
exclude = ['desk', 'meeting_type'] |
... | ... | |
237 | 251 |
event.full_clean(exclude=exclude) |
238 | 252 |
except ValidationError as e: |
239 | 253 |
errors = [ |
240 |
_('Invalid file format. (%(label)s: %(errors)s, line %(line)d)') % { |
|
241 |
'label': label, |
|
242 |
'errors': u', '.join(field_errors), |
|
243 |
'line': i + 1 |
|
244 |
} for label, field_errors in e.message_dict.items()] |
|
254 |
_('Invalid file format. (%(label)s: %(errors)s, line %(line)d)') |
|
255 |
% {'label': label, 'errors': u', '.join(field_errors), 'line': i + 1} |
|
256 |
for label, field_errors in e.message_dict.items() |
|
257 |
] |
|
245 | 258 |
raise ValidationError(errors) |
246 | 259 |
events.append(event) |
247 | 260 |
self.events = events |
... | ... | |
252 | 265 |
model = Desk |
253 | 266 |
fields = [] |
254 | 267 | |
255 |
ics_file = forms.FileField(label=_('ICS File'), required=False, |
|
256 |
help_text=_('ICS file containing events which will be considered as exceptions.')) |
|
257 |
ics_url = forms.URLField(label=_('URL'), required=False, |
|
258 |
help_text=_('URL to remote calendar which will be synchronised hourly.')) |
|
268 |
ics_file = forms.FileField( |
|
269 |
label=_('ICS File'), |
|
270 |
required=False, |
|
271 |
help_text=_('ICS file containing events which will be considered as exceptions.'), |
|
272 |
) |
|
273 |
ics_url = forms.URLField( |
|
274 |
label=_('URL'), |
|
275 |
required=False, |
|
276 |
help_text=_('URL to remote calendar which will be synchronised hourly.'), |
|
277 |
) |
|
259 | 278 | |
260 | 279 | |
261 | 280 |
class AgendasImportForm(forms.Form): |
chrono/manager/management/commands/export_site.py | ||
---|---|---|
27 | 27 | |
28 | 28 |
def add_arguments(self, parser): |
29 | 29 |
parser.add_argument( |
30 |
'--output', metavar='FILE', default=None,
|
|
31 |
help='name of a file to write output to')
|
|
30 |
'--output', metavar='FILE', default=None, help='name of a file to write output to'
|
|
31 |
) |
|
32 | 32 | |
33 | 33 |
def handle(self, *args, **options): |
34 | 34 |
if options['output']: |
chrono/manager/management/commands/import_site.py | ||
---|---|---|
27 | 27 |
help = 'Import an exported site' |
28 | 28 | |
29 | 29 |
def add_arguments(self, parser): |
30 |
parser.add_argument('filename', metavar='FILENAME', type=str, |
|
31 |
help='name of file to import')
|
|
30 |
parser.add_argument('filename', metavar='FILENAME', type=str, help='name of file to import')
|
|
31 |
parser.add_argument('--clean', action='store_true', default=False, help='Clean site before importing')
|
|
32 | 32 |
parser.add_argument( |
33 |
'--clean', action='store_true', default=False, |
|
34 |
help='Clean site before importing') |
|
35 |
parser.add_argument( |
|
36 |
'--if-empty', action='store_true', default=False, |
|
37 |
help='Import only if site is empty') |
|
38 |
parser.add_argument( |
|
39 |
'--overwrite', action='store_true', default=False, |
|
40 |
help='Overwrite existing data') |
|
33 |
'--if-empty', action='store_true', default=False, help='Import only if site is empty' |
|
34 |
) |
|
35 |
parser.add_argument('--overwrite', action='store_true', default=False, help='Overwrite existing data') |
|
41 | 36 | |
42 | 37 |
def handle(self, filename, **options): |
43 | 38 |
if filename == '-': |
... | ... | |
45 | 40 |
else: |
46 | 41 |
fd = open(filename) |
47 | 42 |
try: |
48 |
import_site(json.load(fd), |
|
49 |
if_empty=options['if_empty'], |
|
50 |
clean=options['clean'], |
|
51 |
overwrite=options['overwrite']) |
|
43 |
import_site( |
|
44 |
json.load(fd), |
|
45 |
if_empty=options['if_empty'], |
|
46 |
clean=options['clean'], |
|
47 |
overwrite=options['overwrite'], |
|
48 |
) |
|
52 | 49 |
except AgendaImportError as exc: |
53 | 50 |
raise CommandError(u'%s' % exc) |
chrono/manager/urls.py | ||
---|---|---|
19 | 19 |
from . import views |
20 | 20 | |
21 | 21 |
urlpatterns = [ |
22 |
url(r'^$', views.homepage, name='chrono-manager-homepage'), |
|
23 |
url(r'^agendas/add/$', views.agenda_add, |
|
24 |
name='chrono-manager-agenda-add'), |
|
25 |
url(r'^agendas/import/$', views.agendas_import, |
|
26 |
name='chrono-manager-agendas-import'), |
|
27 |
url(r'^agendas/(?P<pk>\d+)/$', views.agenda_view, |
|
28 |
name='chrono-manager-agenda-view'), |
|
29 |
url(r'^agendas/(?P<pk>\d+)/(?P<year>[0-9]{4})/(?P<month>[0-9]+)/$', views.agenda_monthly_view, |
|
30 |
name='chrono-manager-agenda-month-view'), |
|
31 |
url(r'^agendas/(?P<pk>\d+)/(?P<year>[0-9]{4})/(?P<month>[0-9]+)/(?P<day>[0-9]+)/$', views.agenda_day_view, |
|
32 |
name='chrono-manager-agenda-day-view'), |
|
33 |
url(r'^agendas/(?P<pk>\d+)/settings$', views.agenda_settings, |
|
34 |
name='chrono-manager-agenda-settings'), |
|
35 |
url(r'^agendas/(?P<pk>\d+)/edit$', views.agenda_edit, |
|
36 |
name='chrono-manager-agenda-edit'), |
|
37 |
url(r'^agendas/(?P<pk>\d+)/delete$', views.agenda_delete, |
|
38 |
name='chrono-manager-agenda-delete'), |
|
39 |
url(r'^agendas/(?P<pk>\d+)/export$', views.agenda_export, |
|
40 |
name='chrono-manager-agenda-export'), |
|
41 |
url(r'^agendas/(?P<pk>\d+)/add-event$', views.agenda_add_event, |
|
42 |
name='chrono-manager-agenda-add-event'), |
|
43 |
url(r'^agendas/(?P<pk>\d+)/import-events$', views.agenda_import_events, |
|
44 |
name='chrono-manager-agenda-import-events'), |
|
45 |
url(r'^events/(?P<pk>\d+)/$', views.event_edit, |
|
46 |
name='chrono-manager-event-edit'), |
|
47 |
url(r'^events/(?P<pk>\d+)/delete$', views.event_delete, |
|
48 |
name='chrono-manager-event-delete'), |
|
49 | ||
50 |
url(r'^agendas/(?P<pk>\d+)/add-meeting-type$', views.agenda_add_meeting_type, |
|
51 |
name='chrono-manager-agenda-add-meeting-type'), |
|
52 |
url(r'^meetingtypes/(?P<pk>\d+)/edit$', views.meeting_type_edit, |
|
53 |
name='chrono-manager-meeting-type-edit'), |
|
54 |
url(r'^meetingtypes/(?P<pk>\d+)/delete$', views.meeting_type_delete, |
|
55 |
name='chrono-manager-meeting-type-delete'), |
|
56 | ||
57 |
url(r'^agendas/(?P<agenda_pk>\d+)/desk/(?P<pk>\d+)/add-time-period$', views.agenda_add_time_period, |
|
58 |
name='chrono-manager-agenda-add-time-period'), |
|
59 |
url(r'^timeperiods/(?P<pk>\d+)/edit$', views.time_period_edit, |
|
60 |
name='chrono-manager-time-period-edit'), |
|
61 |
url(r'^timeperiods/(?P<pk>\d+)/delete$', views.time_period_delete, |
|
62 |
name='chrono-manager-time-period-delete'), |
|
63 | ||
64 |
url(r'^agendas/(?P<pk>\d+)/add-desk$', views.agenda_add_desk, |
|
65 |
name='chrono-manager-agenda-add-desk'), |
|
66 |
url(r'^desks/(?P<pk>\d+)/edit$', views.desk_edit, |
|
67 |
name='chrono-manager-desk-edit'), |
|
68 |
url(r'^desks/(?P<pk>\d+)/delete$', views.desk_delete, |
|
69 |
name='chrono-manager-desk-delete'), |
|
70 | ||
71 |
url(r'^agendas/(?P<agenda_pk>\d+)/desk/(?P<pk>\d+)/add-time-period-exception$', views.agenda_add_time_period_exception, |
|
72 |
name='chrono-manager-agenda-add-time-period-exception'), |
|
73 |
url(r'^agendas/desk/(?P<pk>\d+)/import-exceptions-from-ics/$', views.desk_import_time_period_exceptions, |
|
74 |
name='chrono-manager-desk-add-import-time-period-exceptions'), |
|
75 |
url(r'^time-period-exceptions/(?P<pk>\d+)/edit$', views.time_period_exception_edit, |
|
76 |
name='chrono-manager-time-period-exception-edit'), |
|
77 |
url(r'^time-period-exceptions/(?P<pk>\d+)/delete$', views.time_period_exception_delete, |
|
78 |
name='chrono-manager-time-period-exception-delete'), |
|
79 |
url(r'^time-period-exceptions/(?P<pk>\d+)/exception-extract-list$', views.time_period_exception_extract_list, |
|
80 |
name='chrono-manager-time-period-exception-extract-list'), |
|
81 |
url(r'^time-period-exceptions/(?P<pk>\d+)/exception-list$', views.time_period_exception_list, |
|
82 |
name='chrono-manager-time-period-exception-list'), |
|
83 | ||
84 |
url(r'^agendas/events.csv$', views.agenda_import_events_sample_csv, |
|
85 |
name='chrono-manager-sample-events-csv'), |
|
86 | ||
87 |
url(r'^menu.json$', views.menu_json), |
|
22 |
url(r'^$', views.homepage, name='chrono-manager-homepage'), |
|
23 |
url(r'^agendas/add/$', views.agenda_add, name='chrono-manager-agenda-add'), |
|
24 |
url(r'^agendas/import/$', views.agendas_import, name='chrono-manager-agendas-import'), |
|
25 |
url(r'^agendas/(?P<pk>\d+)/$', views.agenda_view, name='chrono-manager-agenda-view'), |
|
26 |
url( |
|
27 |
r'^agendas/(?P<pk>\d+)/(?P<year>[0-9]{4})/(?P<month>[0-9]+)/$', |
|
28 |
views.agenda_monthly_view, |
|
29 |
name='chrono-manager-agenda-month-view', |
|
30 |
), |
|
31 |
url( |
|
32 |
r'^agendas/(?P<pk>\d+)/(?P<year>[0-9]{4})/(?P<month>[0-9]+)/(?P<day>[0-9]+)/$', |
|
33 |
views.agenda_day_view, |
|
34 |
name='chrono-manager-agenda-day-view', |
|
35 |
), |
|
36 |
url(r'^agendas/(?P<pk>\d+)/settings$', views.agenda_settings, name='chrono-manager-agenda-settings'), |
|
37 |
url(r'^agendas/(?P<pk>\d+)/edit$', views.agenda_edit, name='chrono-manager-agenda-edit'), |
|
38 |
url(r'^agendas/(?P<pk>\d+)/delete$', views.agenda_delete, name='chrono-manager-agenda-delete'), |
|
39 |
url(r'^agendas/(?P<pk>\d+)/export$', views.agenda_export, name='chrono-manager-agenda-export'), |
|
40 |
url(r'^agendas/(?P<pk>\d+)/add-event$', views.agenda_add_event, name='chrono-manager-agenda-add-event'), |
|
41 |
url( |
|
42 |
r'^agendas/(?P<pk>\d+)/import-events$', |
|
43 |
views.agenda_import_events, |
|
44 |
name='chrono-manager-agenda-import-events', |
|
45 |
), |
|
46 |
url(r'^events/(?P<pk>\d+)/$', views.event_edit, name='chrono-manager-event-edit'), |
|
47 |
url(r'^events/(?P<pk>\d+)/delete$', views.event_delete, name='chrono-manager-event-delete'), |
|
48 |
url( |
|
49 |
r'^agendas/(?P<pk>\d+)/add-meeting-type$', |
|
50 |
views.agenda_add_meeting_type, |
|
51 |
name='chrono-manager-agenda-add-meeting-type', |
|
52 |
), |
|
53 |
url(r'^meetingtypes/(?P<pk>\d+)/edit$', views.meeting_type_edit, name='chrono-manager-meeting-type-edit'), |
|
54 |
url( |
|
55 |
r'^meetingtypes/(?P<pk>\d+)/delete$', |
|
56 |
views.meeting_type_delete, |
|
57 |
name='chrono-manager-meeting-type-delete', |
|
58 |
), |
|
59 |
url( |
|
60 |
r'^agendas/(?P<agenda_pk>\d+)/desk/(?P<pk>\d+)/add-time-period$', |
|
61 |
views.agenda_add_time_period, |
|
62 |
name='chrono-manager-agenda-add-time-period', |
|
63 |
), |
|
64 |
url(r'^timeperiods/(?P<pk>\d+)/edit$', views.time_period_edit, name='chrono-manager-time-period-edit'), |
|
65 |
url( |
|
66 |
r'^timeperiods/(?P<pk>\d+)/delete$', |
|
67 |
views.time_period_delete, |
|
68 |
name='chrono-manager-time-period-delete', |
|
69 |
), |
|
70 |
url(r'^agendas/(?P<pk>\d+)/add-desk$', views.agenda_add_desk, name='chrono-manager-agenda-add-desk'), |
|
71 |
url(r'^desks/(?P<pk>\d+)/edit$', views.desk_edit, name='chrono-manager-desk-edit'), |
|
72 |
url(r'^desks/(?P<pk>\d+)/delete$', views.desk_delete, name='chrono-manager-desk-delete'), |
|
73 |
url( |
|
74 |
r'^agendas/(?P<agenda_pk>\d+)/desk/(?P<pk>\d+)/add-time-period-exception$', |
|
75 |
views.agenda_add_time_period_exception, |
|
76 |
name='chrono-manager-agenda-add-time-period-exception', |
|
77 |
), |
|
78 |
url( |
|
79 |
r'^agendas/desk/(?P<pk>\d+)/import-exceptions-from-ics/$', |
|
80 |
views.desk_import_time_period_exceptions, |
|
81 |
name='chrono-manager-desk-add-import-time-period-exceptions', |
|
82 |
), |
|
83 |
url( |
|
84 |
r'^time-period-exceptions/(?P<pk>\d+)/edit$', |
|
85 |
views.time_period_exception_edit, |
|
86 |
name='chrono-manager-time-period-exception-edit', |
|
87 |
), |
|
88 |
url( |
|
89 |
r'^time-period-exceptions/(?P<pk>\d+)/delete$', |
|
90 |
views.time_period_exception_delete, |
|
91 |
name='chrono-manager-time-period-exception-delete', |
|
92 |
), |
|
93 |
url( |
|
94 |
r'^time-period-exceptions/(?P<pk>\d+)/exception-extract-list$', |
|
95 |
views.time_period_exception_extract_list, |
|
96 |
name='chrono-manager-time-period-exception-extract-list', |
|
97 |
), |
|
98 |
url( |
|
99 |
r'^time-period-exceptions/(?P<pk>\d+)/exception-list$', |
|
100 |
views.time_period_exception_list, |
|
101 |
name='chrono-manager-time-period-exception-list', |
|
102 |
), |
|
103 |
url( |
|
104 |
r'^agendas/events.csv$', |
|
105 |
views.agenda_import_events_sample_csv, |
|
106 |
name='chrono-manager-sample-events-csv', |
|
107 |
), |
|
108 |
url(r'^menu.json$', views.menu_json), |
|
88 | 109 |
] |
chrono/manager/views.py | ||
---|---|---|
20 | 20 |
from django.contrib import messages |
21 | 21 |
from django.core.exceptions import PermissionDenied |
22 | 22 |
from django.db.models import Q |
23 |
from django.http import HttpResponse, Http404, HttpResponseRedirect
|
|
23 |
from django.http import Http404, HttpResponse, HttpResponseRedirect
|
|
24 | 24 |
from django.shortcuts import get_object_or_404 |
25 | 25 |
from django.urls import reverse, reverse_lazy |
26 | 26 |
from django.utils.dates import MONTHS |
27 |
from django.utils.timezone import now, make_aware, make_naive |
|
27 |
from django.utils.encoding import force_text |
|
28 |
from django.utils.timezone import make_aware, make_naive, now |
|
28 | 29 |
from django.utils.translation import ugettext_lazy as _ |
29 | 30 |
from django.utils.translation import ungettext |
30 |
from django.utils.encoding import force_text |
|
31 |
from django.views.generic import (DetailView, CreateView, UpdateView, |
|
32 |
ListView, DeleteView, FormView, TemplateView, DayArchiveView, |
|
33 |
MonthArchiveView) |
|
34 | ||
35 |
from chrono.agendas.models import (Agenda, Event, MeetingType, TimePeriod, |
|
36 |
Booking, Desk, TimePeriodException, |
|
37 |
ICSError, AgendaImportError) |
|
38 | ||
39 |
from .forms import (AgendaAddForm, AgendaEditForm, NewEventForm, EventForm, NewMeetingTypeForm, MeetingTypeForm, |
|
40 |
TimePeriodForm, ImportEventsForm, NewDeskForm, DeskForm, TimePeriodExceptionForm, |
|
41 |
ExceptionsImportForm, AgendasImportForm, TimePeriodAddForm) |
|
31 |
from django.views.generic import ( |
|
32 |
CreateView, |
|
33 |
DayArchiveView, |
|
34 |
DeleteView, |
|
35 |
DetailView, |
|
36 |
FormView, |
|
37 |
ListView, |
|
38 |
MonthArchiveView, |
|
39 |
TemplateView, |
|
40 |
UpdateView, |
|
41 |
) |
|
42 | ||
43 |
from chrono.agendas.models import ( |
|
44 |
Agenda, |
|
45 |
AgendaImportError, |
|
46 |
Booking, |
|
47 |
Desk, |
|
48 |
Event, |
|
49 |
ICSError, |
|
50 |
MeetingType, |
|
51 |
TimePeriod, |
|
52 |
TimePeriodException, |
|
53 |
) |
|
54 | ||
55 |
from .forms import ( |
|
56 |
AgendaAddForm, |
|
57 |
AgendaEditForm, |
|
58 |
AgendasImportForm, |
|
59 |
DeskForm, |
|
60 |
EventForm, |
|
61 |
ExceptionsImportForm, |
|
62 |
ImportEventsForm, |
|
63 |
MeetingTypeForm, |
|
64 |
NewDeskForm, |
|
65 |
NewEventForm, |
|
66 |
NewMeetingTypeForm, |
|
67 |
TimePeriodAddForm, |
|
68 |
TimePeriodExceptionForm, |
|
69 |
TimePeriodForm, |
|
70 |
) |
|
42 | 71 |
from .utils import import_site |
43 | 72 | |
44 | 73 | |
... | ... | |
53 | 82 |
queryset = queryset.filter(Q(view_role_id__in=group_ids) | Q(edit_role_id__in=group_ids)) |
54 | 83 |
return queryset |
55 | 84 | |
85 | ||
56 | 86 |
homepage = HomepageView.as_view() |
57 | 87 | |
58 | 88 | |
... | ... | |
76 | 106 |
def get_success_url(self): |
77 | 107 |
return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.object.id}) |
78 | 108 | |
109 | ||
79 | 110 |
agenda_add = AgendaAddView.as_view() |
80 | 111 | |
81 | 112 | |
... | ... | |
108 | 139 |
if results.get('created') == 0: |
109 | 140 |
message1 = _('No agenda created.') |
110 | 141 |
else: |
111 |
message1 = ungettext('An agenda has been created.',
|
|
112 |
'%(count)d agendas have been created.', results['created']) % {
|
|
113 |
'count': results['created']}
|
|
142 |
message1 = ungettext( |
|
143 |
'An agenda has been created.', '%(count)d agendas have been created.', results['created']
|
|
144 |
) % {'count': results['created']}
|
|
114 | 145 | |
115 | 146 |
if results.get('updated') == 0: |
116 | 147 |
message2 = _('No agenda updated.') |
117 | 148 |
else: |
118 |
message2 = ungettext('An agenda has been updated.',
|
|
119 |
'%(count)d agendas have been updated.', results['updated']) % {
|
|
120 |
'count': results['updated']}
|
|
149 |
message2 = ungettext( |
|
150 |
'An agenda has been updated.', '%(count)d agendas have been updated.', results['updated']
|
|
151 |
) % {'count': results['updated']}
|
|
121 | 152 |
messages.info(self.request, u'%s %s' % (message1, message2)) |
122 | 153 | |
123 | 154 |
return super(AgendasImportView, self).form_valid(form) |
124 | 155 | |
156 | ||
125 | 157 |
agendas_import = AgendasImportView.as_view() |
126 | 158 | |
127 | 159 | |
... | ... | |
139 | 171 |
def get_success_url(self): |
140 | 172 |
return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.object.id}) |
141 | 173 | |
174 | ||
142 | 175 |
agenda_edit = AgendaEditView.as_view() |
143 | 176 | |
144 | 177 | |
... | ... | |
155 | 188 |
def get_context_data(self, **kwargs): |
156 | 189 |
context = super(AgendaDeleteView, self).get_context_data(**kwargs) |
157 | 190 |
context['cannot_delete'] = Booking.objects.filter( |
158 |
event__agenda=self.get_object(), |
|
159 |
event__start_datetime__gt=now(), |
|
160 |
cancellation_datetime__isnull=True).exists() |
|
191 |
event__agenda=self.get_object(), |
|
192 |
event__start_datetime__gt=now(), |
|
193 |
cancellation_datetime__isnull=True, |
|
194 |
).exists() |
|
161 | 195 |
return context |
162 | 196 | |
163 | 197 |
def delete(self, request, *args, **kwargs): |
... | ... | |
167 | 201 |
raise PermissionDenied() |
168 | 202 |
return super(AgendaDeleteView, self).delete(request, *args, **kwargs) |
169 | 203 | |
204 | ||
170 | 205 |
agenda_delete = AgendaDeleteView.as_view() |
171 | 206 | |
172 | 207 | |
... | ... | |
184 | 219 |
if agenda.kind == 'meetings': |
185 | 220 |
# redirect to today view |
186 | 221 |
today = datetime.date.today() |
187 |
return HttpResponseRedirect(reverse('chrono-manager-agenda-day-view', |
|
188 |
kwargs={'pk': agenda.id, |
|
189 |
'year': today.year, |
|
190 |
'month': today.month, |
|
191 |
'day': today.day})) |
|
222 |
return HttpResponseRedirect( |
|
223 |
reverse( |
|
224 |
'chrono-manager-agenda-day-view', |
|
225 |
kwargs={'pk': agenda.id, 'year': today.year, 'month': today.month, 'day': today.day}, |
|
226 |
) |
|
227 |
) |
|
192 | 228 | |
193 | 229 |
# redirect to settings |
194 |
return HttpResponseRedirect( |
|
195 |
reverse('chrono-manager-agenda-settings', kwargs={'pk': agenda.id})) |
|
230 |
return HttpResponseRedirect(reverse('chrono-manager-agenda-settings', kwargs={'pk': agenda.id}))
|
|
231 | ||
196 | 232 | |
197 | 233 |
agenda_view = AgendaView.as_view() |
198 | 234 | |
... | ... | |
214 | 250 |
# specify 6am time to get the expected timezone on daylight saving time |
215 | 251 |
# days. |
216 | 252 |
try: |
217 |
self.date = make_aware(datetime.datetime.strptime( |
|
218 |
'%s-%s-%s 06:00' % (self.get_year(), self.get_month(), self.get_day()), |
|
219 |
'%Y-%m-%d %H:%M')) |
|
253 |
self.date = make_aware( |
|
254 |
datetime.datetime.strptime( |
|
255 |
'%s-%s-%s 06:00' % (self.get_year(), self.get_month(), self.get_day()), '%Y-%m-%d %H:%M' |
|
256 |
) |
|
257 |
) |
|
220 | 258 |
except ValueError: # day is out of range for month |
221 | 259 |
# redirect to last day of month |
222 | 260 |
date = datetime.date(int(self.get_year()), int(self.get_month()), 1) |
223 | 261 |
date += datetime.timedelta(days=40) |
224 | 262 |
date = date.replace(day=1) |
225 | 263 |
date -= datetime.timedelta(days=1) |
226 |
return HttpResponseRedirect(reverse('chrono-manager-agenda-day-view', |
|
227 |
kwargs={'pk': self.agenda.id, |
|
228 |
'year': date.year, |
|
229 |
'month': date.month, |
|
230 |
'day': date.day})) |
|
264 |
return HttpResponseRedirect( |
|
265 |
reverse( |
|
266 |
'chrono-manager-agenda-day-view', |
|
267 |
kwargs={'pk': self.agenda.id, 'year': date.year, 'month': date.month, 'day': date.day}, |
|
268 |
) |
|
269 |
) |
|
231 | 270 |
return super(AgendaDateView, self).dispatch(request, *args, **kwargs) |
232 | 271 | |
233 | 272 |
def get_context_data(self, **kwargs): |
... | ... | |
253 | 292 | |
254 | 293 |
def get_years(self): |
255 | 294 |
year = now().year |
256 |
return [str(x) for x in range(year-1, year+5)]
|
|
295 |
return [str(x) for x in range(year - 1, year + 5)]
|
|
257 | 296 | |
258 | 297 | |
259 | 298 |
class AgendaDayView(AgendaDateView, DayArchiveView): |
... | ... | |
261 | 300 | |
262 | 301 |
def get_previous_day_url(self): |
263 | 302 |
previous_day = self.date.date() - datetime.timedelta(days=1) |
264 |
return reverse('chrono-manager-agenda-day-view', |
|
265 |
kwargs={'pk': self.agenda.id, |
|
266 |
'year': previous_day.year, |
|
267 |
'month': previous_day.month, |
|
268 |
'day': previous_day.day}) |
|
303 |
return reverse( |
|
304 |
'chrono-manager-agenda-day-view', |
|
305 |
kwargs={ |
|
306 |
'pk': self.agenda.id, |
|
307 |
'year': previous_day.year, |
|
308 |
'month': previous_day.month, |
|
309 |
'day': previous_day.day, |
|
310 |
}, |
|
311 |
) |
|
269 | 312 | |
270 | 313 |
def get_next_day_url(self): |
271 | 314 |
next_day = self.date.date() + datetime.timedelta(days=1) |
272 |
return reverse('chrono-manager-agenda-day-view', |
|
273 |
kwargs={'pk': self.agenda.id, |
|
274 |
'year': next_day.year, |
|
275 |
'month': next_day.month, |
|
276 |
'day': next_day.day}) |
|
315 |
return reverse( |
|
316 |
'chrono-manager-agenda-day-view', |
|
317 |
kwargs={ |
|
318 |
'pk': self.agenda.id, |
|
319 |
'year': next_day.year, |
|
320 |
'month': next_day.month, |
|
321 |
'day': next_day.day, |
|
322 |
}, |
|
323 |
) |
|
277 | 324 | |
278 | 325 |
def get_timetable_infos(self): |
279 |
timeperiods = TimePeriod.objects.filter( |
|
280 |
desk__agenda=self.agenda, |
|
281 |
weekday=self.date.weekday(), |
|
282 |
) |
|
326 |
timeperiods = TimePeriod.objects.filter(desk__agenda=self.agenda, weekday=self.date.weekday(),) |
|
283 | 327 |
if not timeperiods: |
284 | 328 |
return |
285 | 329 | |
... | ... | |
307 | 351 |
# use first row to include opening hours |
308 | 352 |
info['opening_hours'] = opening_hours = [] |
309 | 353 |
for opening_hour in desk.get_opening_hours(current_date.date()): |
310 |
opening_hours.append({ |
|
311 |
'css_top': 100 * (opening_hour.begin - start_date).seconds // 3600, |
|
312 |
'css_height': 100 * (opening_hour.end - opening_hour.begin).seconds // 3600, |
|
313 |
}) |
|
354 |
opening_hours.append( |
|
355 |
{ |
|
356 |
'css_top': 100 * (opening_hour.begin - start_date).seconds // 3600, |
|
357 |
'css_height': 100 * (opening_hour.end - opening_hour.begin).seconds // 3600, |
|
358 |
} |
|
359 |
) |
|
314 | 360 |
infos.append(info) |
315 | 361 |
info['bookings'] = bookings = [] # bookings for this desk |
316 | 362 |
finish_datetime = current_date + interval |
317 |
for event in [x for x in self.object_list if x.desk_id == desk.id and |
|
318 |
x.start_datetime >= current_date and x.start_datetime < finish_datetime]: |
|
363 |
for event in [ |
|
364 |
x |
|
365 |
for x in self.object_list |
|
366 |
if x.desk_id == desk.id |
|
367 |
and x.start_datetime >= current_date |
|
368 |
and x.start_datetime < finish_datetime |
|
369 |
]: |
|
319 | 370 |
# don't consider cancelled bookings |
320 | 371 |
for booking in [x for x in event.booking_set.all() if not x.cancellation_datetime]: |
321 | 372 |
booking.css_top = int(100 * event.start_datetime.minute / 60) |
... | ... | |
326 | 377 |
current_date += interval |
327 | 378 |
first = False |
328 | 379 | |
380 | ||
329 | 381 |
agenda_day_view = AgendaDayView.as_view() |
330 | 382 | |
331 | 383 | |
... | ... | |
334 | 386 | |
335 | 387 |
def get_previous_month_url(self): |
336 | 388 |
previous_month = self.get_previous_month(self.date.date()) |
337 |
return reverse('chrono-manager-agenda-month-view',
|
|
338 |
kwargs={'pk': self.agenda.id,
|
|
339 |
'year': previous_month.year,
|
|
340 |
'month': previous_month.month})
|
|
389 |
return reverse( |
|
390 |
'chrono-manager-agenda-month-view',
|
|
391 |
kwargs={'pk': self.agenda.id, 'year': previous_month.year, 'month': previous_month.month},
|
|
392 |
) |
|
341 | 393 | |
342 | 394 |
def get_next_month_url(self): |
343 | 395 |
next_month = self.get_next_month(self.date.date()) |
344 |
return reverse('chrono-manager-agenda-month-view',
|
|
345 |
kwargs={'pk': self.agenda.id,
|
|
346 |
'year': next_month.year,
|
|
347 |
'month': next_month.month})
|
|
396 |
return reverse( |
|
397 |
'chrono-manager-agenda-month-view',
|
|
398 |
kwargs={'pk': self.agenda.id, 'year': next_month.year, 'month': next_month.month},
|
|
399 |
) |
|
348 | 400 | |
349 | 401 |
def get_day(self): |
350 | 402 |
return '1' |
... | ... | |
362 | 414 |
last_week_number = 53 |
363 | 415 | |
364 | 416 |
for week_number in range(first_week_number, last_week_number + 1): |
365 |
yield self.get_week_timetable_infos(week_number-first_week_number, timeperiods)
|
|
417 |
yield self.get_week_timetable_infos(week_number - first_week_number, timeperiods)
|
|
366 | 418 | |
367 | 419 |
def get_week_timetable_infos(self, week_index, timeperiods): |
368 | 420 | |
369 |
date = self.date + datetime.timedelta(week_index*7)
|
|
421 |
date = self.date + datetime.timedelta(week_index * 7)
|
|
370 | 422 |
year, week_number, dow = date.isocalendar() |
371 | 423 |
start_date = date - datetime.timedelta(dow) |
372 | 424 | |
... | ... | |
382 | 434 |
periods.append(period) |
383 | 435 |
period = period + interval |
384 | 436 | |
385 |
return {'days': [self.get_day_timetable_infos(start_date + datetime.timedelta(i), interval) for i in range(1, 8)], |
|
386 |
'periods': periods} |
|
437 |
return { |
|
438 |
'days': [ |
|
439 |
self.get_day_timetable_infos(start_date + datetime.timedelta(i), interval) |
|
440 |
for i in range(1, 8) |
|
441 |
], |
|
442 |
'periods': periods, |
|
443 |
} |
|
387 | 444 | |
388 | 445 |
def get_day_timetable_infos(self, day, interval): |
389 | 446 |
day = make_aware(make_naive(day)) # give day correct timezone |
390 | 447 |
period = current_date = day.replace(hour=self.min_timeperiod.hour, minute=0) |
391 |
timetable = {'date': current_date, |
|
392 |
'today': day.date() == datetime.date.today(), |
|
393 |
'other_month': day.month != self.date.month, |
|
394 |
'infos': {'opening_hours': [], 'booked_slots': []}} |
|
448 |
timetable = { |
|
449 |
'date': current_date, |
|
450 |
'today': day.date() == datetime.date.today(), |
|
451 |
'other_month': day.month != self.date.month, |
|
452 |
'infos': {'opening_hours': [], 'booked_slots': []}, |
|
453 |
} |
|
395 | 454 | |
396 | 455 |
desks = self.agenda.desk_set.all() |
397 | 456 |
desks_len = len(desks) |
... | ... | |
406 | 465 |
period_end = period + interval |
407 | 466 |
for desk_index, desk in enumerate(desks): |
408 | 467 |
width = (98.0 / desks_len) - 1 |
409 |
for event in [x for x in self.object_list if x.desk_id == desk.id and |
|
410 |
x.start_datetime >= period and x.start_datetime < period_end]: |
|
468 |
for event in [ |
|
469 |
x |
|
470 |
for x in self.object_list |
|
471 |
if x.desk_id == desk.id and x.start_datetime >= period and x.start_datetime < period_end |
|
472 |
]: |
|
411 | 473 |
# don't consider cancelled bookings |
412 | 474 |
bookings = [x for x in event.booking_set.all() if not x.cancellation_datetime] |
413 | 475 |
if not bookings: |
414 | 476 |
continue |
415 |
booking = {'css_top': 100 * (event.start_datetime - current_date).seconds // 3600, |
|
416 |
'css_height': 100 * event.meeting_type.duration // 60, |
|
417 |
'css_width': width, |
|
418 |
'css_left': left, |
|
419 |
'desk': desk, |
|
420 |
'booking': bookings[0] |
|
477 |
booking = { |
|
478 |
'css_top': 100 * (event.start_datetime - current_date).seconds // 3600, |
|
479 |
'css_height': 100 * event.meeting_type.duration // 60, |
|
480 |
'css_width': width, |
|
481 |
'css_left': left, |
|
482 |
'desk': desk, |
|
483 |
'booking': bookings[0], |
|
421 | 484 |
} |
422 | 485 |
timetable['infos']['booked_slots'].append(booking) |
423 | 486 | |
424 | 487 |
# get desks opening hours on last period iteration |
425 | 488 |
if period == max_date: |
426 | 489 |
for hour in desk.get_opening_hours(current_date): |
427 |
timetable['infos']['opening_hours'].append({ |
|
428 |
'css_top': 100 * (hour.begin - current_date).seconds // 3600, |
|
429 |
'css_height': 100 * (hour.end - hour.begin).seconds // 3600, |
|
430 |
'css_width': width, |
|
431 |
'css_left': left, |
|
432 |
}) |
|
490 |
timetable['infos']['opening_hours'].append( |
|
491 |
{ |
|
492 |
'css_top': 100 * (hour.begin - current_date).seconds // 3600, |
|
493 |
'css_height': 100 * (hour.end - hour.begin).seconds // 3600, |
|
494 |
'css_width': width, |
|
495 |
'css_left': left, |
|
496 |
} |
|
497 |
) |
|
433 | 498 |
left += width + 1 |
434 | 499 |
period += interval |
435 | 500 | |
436 | 501 |
return timetable |
437 | 502 | |
503 | ||
438 | 504 |
agenda_monthly_view = AgendaMonthView.as_view() |
439 | 505 | |
440 | 506 | |
... | ... | |
550 | 616 |
context['user_can_manage'] = self.get_object().can_be_managed(self.request.user) |
551 | 617 |
return context |
552 | 618 | |
619 | ||
553 | 620 |
agenda_settings = AgendaSettings.as_view() |
554 | 621 | |
555 | 622 | |
... | ... | |
561 | 628 |
json.dump({'agendas': [self.get_object().export_json()]}, response, indent=2) |
562 | 629 |
return response |
563 | 630 | |
631 | ||
564 | 632 |
agenda_export = AgendaExport.as_view() |
565 | 633 | |
566 | 634 | |
... | ... | |
569 | 637 |
model = Event |
570 | 638 |
form_class = NewEventForm |
571 | 639 | |
640 | ||
572 | 641 |
agenda_add_event = AgendaAddEventView.as_view() |
573 | 642 | |
574 | 643 | |
... | ... | |
583 | 652 |
context['some_future_date'] = some_future_date |
584 | 653 |
return context |
585 | 654 | |
655 | ||
586 | 656 |
agenda_import_events_sample_csv = AgendaImportEventsSampleView.as_view() |
587 | 657 | |
588 | 658 | |
... | ... | |
600 | 670 |
if form.events: |
601 | 671 |
for event in form.events: |
602 | 672 |
event.agenda_id = self.kwargs['pk'] |
603 |
if event.slug and Event.objects.filter( |
|
604 |
agenda_id=event.agenda_id, |
|
605 |
slug=event.slug).exists(): |
|
673 |
if event.slug and Event.objects.filter(agenda_id=event.agenda_id, slug=event.slug).exists(): |
|
606 | 674 |
raise ValidationError(_('Duplicated event identifier')) |
607 | 675 |
event.save() |
608 | 676 |
messages.info(self.request, _('%d events have been imported.') % len(form.events)) |
609 | 677 |
return super(AgendaImportEventsView, self).form_valid(form) |
610 | 678 | |
679 | ||
611 | 680 |
agenda_import_events = AgendaImportEventsView.as_view() |
612 | 681 | |
613 | 682 | |
... | ... | |
616 | 685 |
model = Event |
617 | 686 |
form_class = EventForm |
618 | 687 | |
688 | ||
619 | 689 |
event_edit = EventEditView.as_view() |
620 | 690 | |
621 | 691 | |
... | ... | |
626 | 696 |
def get_context_data(self, **kwargs): |
627 | 697 |
context = super(EventDeleteView, self).get_context_data(**kwargs) |
628 | 698 |
context['cannot_delete'] = bool( |
629 |
self.object.booking_set.filter(cancellation_datetime__isnull=True).exists() and |
|
630 |
self.object.start_datetime > now()) |
|
699 |
self.object.booking_set.filter(cancellation_datetime__isnull=True).exists() |
|
700 |
and self.object.start_datetime > now() |
|
701 |
) |
|
631 | 702 |
return context |
632 | 703 | |
633 | 704 |
def delete(self, request, *args, **kwargs): |
... | ... | |
637 | 708 |
raise PermissionDenied() |
638 | 709 |
return super(EventDeleteView, self).delete(request, *args, **kwargs) |
639 | 710 | |
711 | ||
640 | 712 |
event_delete = EventDeleteView.as_view() |
641 | 713 | |
642 | 714 | |
... | ... | |
645 | 717 |
model = Event |
646 | 718 |
form_class = NewMeetingTypeForm |
647 | 719 | |
720 | ||
648 | 721 |
agenda_add_meeting_type = AgendaAddMeetingTypeView.as_view() |
649 | 722 | |
723 | ||
650 | 724 |
class MeetingTypeEditView(ManagedAgendaSubobjectMixin, UpdateView): |
651 | 725 |
template_name = 'chrono/manager_meeting_type_form.html' |
652 | 726 |
model = MeetingType |
653 | 727 |
form_class = MeetingTypeForm |
654 | 728 | |
729 | ||
655 | 730 |
meeting_type_edit = MeetingTypeEditView.as_view() |
656 | 731 | |
657 | 732 | |
... | ... | |
659 | 734 |
template_name = 'chrono/manager_confirm_delete.html' |
660 | 735 |
model = MeetingType |
661 | 736 | |
737 | ||
662 | 738 |
meeting_type_delete = MeetingTypeDeleteView.as_view() |
663 | 739 | |
664 | 740 | |
... | ... | |
669 | 745 |
def form_valid(self, form): |
670 | 746 |
for weekday in form.cleaned_data.get('weekdays'): |
671 | 747 |
period = TimePeriod( |
672 |
weekday=weekday, |
|
673 |
start_time=form.cleaned_data['start_time'], |
|
674 |
end_time=form.cleaned_data['end_time'], |
|
675 |
desk=self.desk) |
|
748 |
weekday=weekday, |
|
749 |
start_time=form.cleaned_data['start_time'], |
|
750 |
end_time=form.cleaned_data['end_time'], |
|
751 |
desk=self.desk, |
|
752 |
) |
|
676 | 753 |
period.save() |
677 | 754 |
return super(AgendaAddTimePeriodView, self).form_valid(form) |
678 | 755 | |
756 | ||
679 | 757 |
agenda_add_time_period = AgendaAddTimePeriodView.as_view() |
680 | 758 | |
681 | 759 | |
... | ... | |
684 | 762 |
model = TimePeriod |
685 | 763 |
form_class = TimePeriodForm |
686 | 764 | |
765 | ||
687 | 766 |
time_period_edit = TimePeriodEditView.as_view() |
688 | 767 | |
689 | 768 | |
... | ... | |
691 | 770 |
template_name = 'chrono/manager_confirm_delete.html' |
692 | 771 |
model = TimePeriod |
693 | 772 | |
773 | ||
694 | 774 |
time_period_delete = TimePeriodDeleteView.as_view() |
695 | 775 | |
696 | 776 | |
... | ... | |
719 | 799 |
def get_context_data(self, **kwargs): |
720 | 800 |
context = super(DeskDeleteView, self).get_context_data(**kwargs) |
721 | 801 |
context['cannot_delete'] = Booking.objects.filter( |
722 |
event__desk=self.get_object(), |
|
723 |
event__start_datetime__gt=now(), |
|
724 |
cancellation_datetime__isnull=True).exists() |
|
802 |
event__desk=self.get_object(), event__start_datetime__gt=now(), cancellation_datetime__isnull=True |
|
803 |
).exists() |
|
725 | 804 |
return context |
726 | 805 | |
727 | 806 |
def delete(self, request, *args, **kwargs): |
... | ... | |
812 | 891 |
ics_file_content = force_text(form.cleaned_data['ics_file'].read()) |
813 | 892 |
exceptions = form.instance.create_timeperiod_exceptions_from_ics(ics_file_content) |
814 | 893 |
elif form.cleaned_data['ics_url']: |
815 |
exceptions = form.instance.create_timeperiod_exceptions_from_remote_ics(form.cleaned_data['ics_url']) |
|
894 |
exceptions = form.instance.create_timeperiod_exceptions_from_remote_ics( |
|
895 |
form.cleaned_data['ics_url'] |
|
896 |
) |
|
816 | 897 |
else: |
817 | 898 |
form.instance.remove_timeperiod_exceptions_from_remote_ics() |
818 | 899 |
except ICSError as e: |
... | ... | |
821 | 902 |
form.instance.timeperiod_exceptions_remote_url = form.cleaned_data['ics_url'] |
822 | 903 |
form.instance.save() |
823 | 904 |
if exceptions is not None: |
824 |
message = ungettext('An exception has been imported.', |
|
825 |
'%(count)d exceptions have been imported.', exceptions) |
|
905 |
message = ungettext( |
|
906 |
'An exception has been imported.', '%(count)d exceptions have been imported.', exceptions |
|
907 |
) |
|
826 | 908 |
message = message % {'count': exceptions} |
827 | 909 |
messages.info(self.request, message) |
828 | 910 |
return super(DeskImportTimePeriodExceptionsView, self).form_valid(form) |
829 | 911 | |
912 | ||
830 | 913 |
desk_import_time_period_exceptions = DeskImportTimePeriodExceptionsView.as_view() |
831 | 914 | |
832 | 915 | |
833 | 916 |
def menu_json(request): |
834 | 917 |
label = _('Agendas') |
835 |
json_str = json.dumps([{'label': force_text(label), |
|
836 |
'slug': 'calendar', |
|
837 |
'url': request.build_absolute_uri(reverse('chrono-manager-homepage')) |
|
838 |
}]) |
|
918 |
json_str = json.dumps( |
|
919 |
[ |
|
920 |
{ |
|
921 |
'label': force_text(label), |
|
922 |
'slug': 'calendar', |
|
923 |
'url': request.build_absolute_uri(reverse('chrono-manager-homepage')), |
|
924 |
} |
|
925 |
] |
|
926 |
) |
|
839 | 927 |
content_type = 'application/json' |
840 | 928 |
for variable in ('jsonpCallback', 'callback'): |
841 | 929 |
if variable in request.GET: |
chrono/manager/widgets.py | ||
---|---|---|
11 | 11 |
import re |
12 | 12 |
import uuid |
13 | 13 | |
14 |
from django.forms.widgets import DateTimeInput, TimeInput, SelectMultiple
|
|
14 |
from django.forms.widgets import DateTimeInput, SelectMultiple, TimeInput
|
|
15 | 15 |
from django.utils.formats import get_language |
16 | 16 |
from django.utils.safestring import mark_safe |
17 | 17 | |
... | ... | |
39 | 39 |
'%Y': 'yyyy', |
40 | 40 |
'%y': 'yy', |
41 | 41 |
'%p': 'P', |
42 |
'%S': 'ss' |
|
42 |
'%S': 'ss',
|
|
43 | 43 |
} |
44 | 44 | |
45 | 45 |
DATE_FORMAT_TO_JS_REGEX = re.compile(r'(?<!\w)(' + '|'.join(DATE_FORMAT_PY_JS_MAPPING.keys()) + r')\b') |
... | ... | |
77 | 77 |
# with a default, and convert it to a Python data format for later string parsing |
78 | 78 |
date_format = self.options['format'] |
79 | 79 |
self.format = DATE_FORMAT_TO_PYTHON_REGEX.sub( |
80 |
lambda x: DATE_FORMAT_JS_PY_MAPPING[x.group()], |
|
81 |
date_format |
|
82 |
) |
|
80 |
lambda x: DATE_FORMAT_JS_PY_MAPPING[x.group()], date_format |
|
81 |
) |
|
83 | 82 | |
84 | 83 |
super(PickerWidgetMixin, self).__init__(attrs, format=self.format) |
85 | 84 | |
... | ... | |
87 | 86 |
final_attrs = self.build_attrs(attrs) |
88 | 87 |
rendered_widget = super(PickerWidgetMixin, self).render(name, value, final_attrs, renderer=renderer) |
89 | 88 | |
90 |
#if not set, autoclose have to be true. |
|
89 |
# if not set, autoclose have to be true.
|
|
91 | 90 |
self.options.setdefault('autoclose', True) |
92 | 91 | |
93 | 92 |
# Build javascript options out of python dictionary |
... | ... | |
100 | 99 |
# Use provided id or generate hex to avoid collisions in document |
101 | 100 |
id = final_attrs.get('id', uuid.uuid4().hex) |
102 | 101 | |
103 |
return mark_safe(BOOTSTRAP_INPUT_TEMPLATE % dict( |
|
104 |
id=id, |
|
105 |
rendered_widget=rendered_widget, |
|
106 |
clear_button=CLEAR_BTN_TEMPLATE if self.options.get('clearBtn') else '', |
|
107 |
glyphicon=self.glyphicon, |
|
108 |
options=js_options |
|
109 |
) |
|
102 |
return mark_safe( |
|
103 |
BOOTSTRAP_INPUT_TEMPLATE |
|
104 |
% dict( |
|
105 |
id=id, |
|
106 |
rendered_widget=rendered_widget, |
|
107 |
clear_button=CLEAR_BTN_TEMPLATE if self.options.get('clearBtn') else '', |
|
108 |
glyphicon=self.glyphicon, |
|
109 |
options=js_options, |
|
110 |
) |
|
110 | 111 |
) |
111 | 112 | |
112 | 113 | |
... | ... | |
136 | 137 |
input type and has a bit of a fallback mechanism with the presence |
137 | 138 |
of the pattern attribute in case a standard text input is used. |
138 | 139 |
""" |
140 | ||
139 | 141 |
input_type = 'time' |
140 | 142 | |
141 | 143 |
def __init__(self, **kwargs): |
... | ... | |
149 | 151 |
s = [] |
150 | 152 |
value = value or [] |
151 | 153 |
for choice_id, choice_label in self.choices: |
152 |
s.append('<li><label><input type="checkbox" ' |
|
153 |
' name="%(name)s-%(choice_id)s" %(checked)s>' |
|
154 |
'<span>%(choice_label)s</span></label></li>' % { |
|
155 |
'name': name, |
|
156 |
'checked': 'checked' if choice_id in value else '', |
|
157 |
'choice_id': choice_id, |
|
158 |
'choice_label': choice_label}) |
|
154 |
s.append( |
|
155 |
'<li><label><input type="checkbox" ' |
|
156 |
' name="%(name)s-%(choice_id)s" %(checked)s>' |
|
157 |
'<span>%(choice_label)s</span></label></li>' |
|
158 |
% { |
|
159 |
'name': name, |
|
160 |
'checked': 'checked' if choice_id in value else '', |
|
161 |
'choice_id': choice_id, |
|
162 |
'choice_label': choice_label, |
|
163 |
} |
|
164 |
) |
|
159 | 165 |
return mark_safe('<ul id="%(id)s">' % attrs + '\n'.join(s) + '</ul>') |
160 | 166 | |
161 | 167 |
def value_from_datadict(self, data, files, name): |
chrono/settings.py | ||
---|---|---|
24 | 24 |
""" |
25 | 25 | |
26 | 26 |
import os |
27 | ||
27 | 28 |
from django.conf.global_settings import STATICFILES_FINDERS |
28 | 29 | |
29 | 30 |
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) |
... | ... | |
77 | 78 |
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases |
78 | 79 | |
79 | 80 |
DATABASES = { |
80 |
'default': { |
|
81 |
'ENGINE': 'django.db.backends.sqlite3', |
|
82 |
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), |
|
83 |
} |
|
81 |
'default': {'ENGINE': 'django.db.backends.sqlite3', 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),} |
|
84 | 82 |
} |
85 | 83 | |
86 | 84 |
# Internationalization |
... | ... | |
96 | 94 | |
97 | 95 |
USE_TZ = True |
98 | 96 | |
99 |
LOCALE_PATHS = (os.path.join(BASE_DIR, 'chrono', 'locale'), )
|
|
97 |
LOCALE_PATHS = (os.path.join(BASE_DIR, 'chrono', 'locale'),) |
|
100 | 98 | |
101 | 99 |
FORMAT_MODULE_PATH = 'chrono.formats' |
102 | 100 | |
... | ... | |
104 | 102 |
TEMPLATES = [ |
105 | 103 |
{ |
106 | 104 |
'BACKEND': 'django.template.backends.django.DjangoTemplates', |
107 |
'DIRS': [ |
|
108 |
], |
|
105 |
'DIRS': [], |
|
109 | 106 |
'APP_DIRS': True, |
110 | 107 |
'OPTIONS': { |
111 | 108 |
'context_processors': [ |
... | ... | |
165 | 162 |
# (see http://docs.python-requests.org/en/master/user/advanced/#proxies) |
166 | 163 |
REQUESTS_PROXIES = None |
167 | 164 | |
168 |
local_settings_file = os.environ.get('CHRONO_SETTINGS_FILE', |
|
169 |
os.path.join(os.path.dirname(__file__), 'local_settings.py')) |
|
165 |
local_settings_file = os.environ.get( |
|
166 |
'CHRONO_SETTINGS_FILE', os.path.join(os.path.dirname(__file__), 'local_settings.py') |
|
167 |
) |
|
170 | 168 |
if os.path.exists(local_settings_file): |
171 | 169 |
exec(open(local_settings_file).read()) |
chrono/urls.py | ||
---|---|---|
19 | 19 |
from django.conf.urls.static import static |
20 | 20 |
from django.contrib.staticfiles.urls import staticfiles_urlpatterns |
21 | 21 | |
22 |
from .urls_utils import decorated_includes, manager_required |
|
23 | ||
24 |
from .views import homepage, LoginView, LogoutView |
|
25 | 22 |
from .api.urls import urlpatterns as chrono_api_urls |
26 | 23 |
from .manager.urls import urlpatterns as chrono_manager_urls |
27 | ||
24 |
from .urls_utils import decorated_includes, manager_required |
|
25 |
from .views import LoginView, LogoutView, homepage |
|
28 | 26 | |
29 | 27 |
urlpatterns = [ |
30 | 28 |
url(r'^$', homepage, name='home'), |
31 |
url(r'^manage/', decorated_includes(manager_required, |
|
32 |
include(chrono_manager_urls))), |
|
29 |
url(r'^manage/', decorated_includes(manager_required, include(chrono_manager_urls))), |
|
33 | 30 |
url(r'^api/', include(chrono_api_urls)), |
34 | 31 |
url(r'^logout/$', LogoutView.as_view(), name='auth_logout'), |
35 | 32 |
url(r'^login/$', LoginView.as_view(), name='auth_login'), |
chrono/urls_utils.py | ||
---|---|---|
17 | 17 |
# Decorating URL includes, <https://djangosnippets.org/snippets/2532/> |
18 | 18 | |
19 | 19 |
import django |
20 | ||
21 | 20 |
from django.contrib.auth.decorators import user_passes_test |
22 | 21 |
from django.core.exceptions import PermissionDenied |
23 | 22 |
from django.db.models import Q |
... | ... | |
37 | 36 |
result.func = self._decorate_with(result.func) |
38 | 37 |
return result |
39 | 38 | |
39 | ||
40 | 40 |
def decorated_includes(func, includes, *args, **kwargs): |
41 | 41 |
urlconf_module, app_name, namespace = includes |
42 | 42 | |
... | ... | |
59 | 59 |
raise PermissionDenied() |
60 | 60 |
# As the last resort, show the login form |
61 | 61 |
return False |
62 | ||
62 | 63 |
actual_decorator = user_passes_test(check_manager, login_url=login_url) |
63 | 64 |
if function: |
64 | 65 |
return actual_decorator(function) |
chrono/views.py | ||
---|---|---|
23 | 23 |
from django.utils.decorators import method_decorator |
24 | 24 |
from django.views.decorators.cache import never_cache |
25 | 25 | |
26 | ||
27 | 26 |
if 'mellon' in settings.INSTALLED_APPS: |
28 | 27 |
from mellon.utils import get_idps |
29 | 28 |
else: |
... | ... | |
35 | 34 |
if any(get_idps()): |
36 | 35 |
if not 'next' in request.GET: |
37 | 36 |
return HttpResponseRedirect(resolve_url('mellon_login')) |
38 |
return HttpResponseRedirect(resolve_url('mellon_login') + '?next=' |
|
39 |
+ quote(request.GET.get('next'))) |
|
37 |
return HttpResponseRedirect( |
|
38 |
resolve_url('mellon_login') + '?next=' + quote(request.GET.get('next')) |
|
39 |
) |
|
40 | 40 |
return super(LoginView, self).get(request, *args, **kwargs) |
41 | 41 | |
42 | 42 |
chrono/wsgi.py | ||
---|---|---|
8 | 8 |
""" |
9 | 9 | |
10 | 10 |
import os |
11 |
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "chrono.settings") |
|
12 | 11 | |
13 | 12 |
from django.core.wsgi import get_wsgi_application |
13 | ||
14 |
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "chrono.settings") |
|
15 | ||
16 | ||
14 | 17 |
application = get_wsgi_application() |
tests/conftest.py | ||
---|---|---|
1 |
import django_webtest |
|
1 | 2 |
import pytest |
2 | ||
3 | 3 |
from django.contrib.auth.models import User |
4 | 4 | |
5 |
import django_webtest |
|
6 | 5 | |
7 | 6 |
@pytest.fixture |
8 | 7 |
def app(request): |
tests/test_agendas.py | ||
---|---|---|
1 |
import pytest |
|
2 | 1 |
import datetime |
3 |
import mock |
|
4 | 2 |
import re |
5 |
import requests |
|
6 | ||
7 | 3 | |
8 |
from django.utils.timezone import now, make_aware, localtime |
|
4 |
import mock |
|
5 |
import pytest |
|
6 |
import requests |
|
9 | 7 |
from django.contrib.auth.models import Group |
10 | 8 |
from django.core.management import call_command |
11 |
from django.core.management.base import CommandError
|
|
9 |
from django.utils.timezone import localtime, make_aware, now
|
|
12 | 10 | |
13 |
from chrono.agendas.models import (Agenda, Event, Booking, MeetingType, |
|
14 |
Desk, TimePeriod, TimePeriodException, ICSError) |
|
11 |
from chrono.agendas.models import Agenda, Booking, Desk, Event, ICSError, MeetingType, TimePeriodException |
|
15 | 12 | |
16 | 13 |
pytestmark = pytest.mark.django_db |
17 | 14 | |
... | ... | |
154 | 151 |
booking.save() |
155 | 152 |
assert Event.objects.all()[0].booked_places == 0 |
156 | 153 | |
154 | ||
157 | 155 |
def test_event_bookable_period(): |
158 | 156 |
agenda = Agenda(label=u'Foo bar') |
159 | 157 |
agenda.save() |
... | ... | |
179 | 177 |
event.save() |
180 | 178 |
assert event.in_bookable_period() is False |
181 | 179 | |
180 | ||
182 | 181 |
def test_meeting_type_slugs(): |
183 | 182 |
agenda1 = Agenda(label=u'Foo bar') |
184 | 183 |
agenda1.save() |
... | ... | |
197 | 196 |
meeting_type3.save() |
198 | 197 |
assert meeting_type3.slug == 'baz' |
199 | 198 | |
199 | ||
200 | 200 |
def test_timeperiodexception_creation_from_ics(): |
201 | 201 |
agenda = Agenda(label=u'Test 1 agenda') |
202 | 202 |
agenda.save() |
... | ... | |
206 | 206 |
assert exceptions_count == 2 |
207 | 207 |
assert TimePeriodException.objects.filter(desk=desk).count() == 2 |
208 | 208 | |
209 | ||
209 | 210 |
def test_timeperiodexception_creation_from_ics_without_startdt(): |
210 | 211 |
agenda = Agenda(label=u'Test 2 agenda') |
211 | 212 |
agenda.save() |
... | ... | |
222 | 223 |
exceptions_count = desk.create_timeperiod_exceptions_from_ics(ics_sample) |
223 | 224 |
assert 'Event "Event 1" has no start date.' == str(e.value) |
224 | 225 | |
226 | ||
225 | 227 |
def test_timeperiodexception_creation_from_ics_without_enddt(): |
226 | 228 |
agenda = Agenda(label=u'Test 3 agenda') |
227 | 229 |
agenda.save() |
... | ... | |
248 | 250 |
desk.save() |
249 | 251 |
assert desk.create_timeperiod_exceptions_from_ics(ICS_SAMPLE_WITH_RECURRENT_EVENT) == 3 |
250 | 252 |
assert TimePeriodException.objects.filter(desk=desk).count() == 3 |
251 |
assert set(TimePeriodException.objects.values_list('start_datetime', flat=True)) == set([ |
|
252 |
make_aware(datetime.datetime(2018, 1, 1)), make_aware(datetime.datetime(2019, 1, 1))]) |
|
253 |
assert set(TimePeriodException.objects.values_list('start_datetime', flat=True)) == set( |
|
254 |
[make_aware(datetime.datetime(2018, 1, 1)), make_aware(datetime.datetime(2019, 1, 1))] |
|
255 |
) |
|
253 | 256 |
assert desk.create_timeperiod_exceptions_from_ics(ICS_SAMPLE_WITH_RECURRENT_EVENT) == 0 |
254 | 257 |
# verify occurences are cleaned when count changed |
255 | 258 |
assert desk.create_timeperiod_exceptions_from_ics(ICS_SAMPLE_WITH_RECURRENT_EVENT_2) == 0 |
256 | 259 |
assert TimePeriodException.objects.filter(desk=desk).count() == 2 |
257 |
assert set(TimePeriodException.objects.values_list('start_datetime', flat=True)) == set([ |
|
258 |
make_aware(datetime.datetime(2018, 1, 1)), make_aware(datetime.datetime(2018, 1, 2))]) |
|
260 |
assert set(TimePeriodException.objects.values_list('start_datetime', flat=True)) == set( |
|
261 |
[make_aware(datetime.datetime(2018, 1, 1)), make_aware(datetime.datetime(2018, 1, 2))] |
|
262 |
) |
|
263 | ||
259 | 264 | |
260 | 265 |
def test_timeexception_creation_from_ics_with_dates(): |
261 | 266 |
agenda = Agenda(label=u'Test 5 agenda') |
... | ... | |
275 | 280 |
assert localtime(exception.start_datetime) == make_aware(datetime.datetime(2018, 1, 1, 0, 0)) |
276 | 281 |
assert localtime(exception.end_datetime) == make_aware(datetime.datetime(2018, 1, 1, 0, 0)) |
277 | 282 | |
283 | ||
278 | 284 |
def test_timeexception_create_from_invalid_ics(): |
279 | 285 |
agenda = Agenda(label=u'Test 6 agenda') |
280 | 286 |
agenda.save() |
... | ... | |
284 | 290 |
exceptions_count = desk.create_timeperiod_exceptions_from_ics(INVALID_ICS_SAMPLE) |
285 | 291 |
assert str(e.value) == 'File format is invalid.' |
286 | 292 | |
293 | ||
287 | 294 |
def test_timeexception_create_from_ics_with_no_events(): |
288 | 295 |
agenda = Agenda(label=u'Test 7 agenda') |
289 | 296 |
agenda.save() |
... | ... | |
293 | 300 |
exceptions_count = desk.create_timeperiod_exceptions_from_ics(ICS_SAMPLE_WITH_NO_EVENTS) |
294 | 301 |
assert str(e.value) == "The file doesn't contain any events." |
295 | 302 | |
303 | ||
296 | 304 |
@mock.patch('chrono.agendas.models.requests.get') |
297 | 305 |
def test_timeperiodexception_creation_from_remote_ics(mocked_get): |
298 | 306 |
agenda = Agenda(label=u'Test 8 agenda') |
... | ... | |
316 | 324 |
assert exceptions_count == 0 |
317 | 325 |
TimePeriodException.objects.filter(external_id='desk-%s:' % desk.id).count() == 0 |
318 | 326 | |
327 | ||
319 | 328 |
@mock.patch('chrono.agendas.models.requests.get') |
320 | 329 |
def test_timeperiodexception_creation_from_unreachable_remote_ics(mocked_get): |
321 | 330 |
agenda = Agenda(label=u'Test 9 agenda') |
... | ... | |
325 | 334 |
mocked_response = mock.Mock() |
326 | 335 |
mocked_response.text = ICS_SAMPLE |
327 | 336 |
mocked_get.return_value = mocked_response |
337 | ||
328 | 338 |
def mocked_requests_connection_error(*args, **kwargs): |
329 | 339 |
raise requests.ConnectionError('unreachable') |
340 | ||
330 | 341 |
mocked_get.side_effect = mocked_requests_connection_error |
331 | 342 |
with pytest.raises(ICSError) as e: |
332 | 343 |
exceptions_count = desk.create_timeperiod_exceptions_from_remote_ics('http://example.com/sample.ics') |
333 | 344 |
assert str(e.value) == "Failed to retrieve remote calendar (http://example.com/sample.ics, unreachable)." |
334 | 345 | |
346 | ||
335 | 347 |
@mock.patch('chrono.agendas.models.requests.get') |
336 | 348 |
def test_timeperiodexception_creation_from_forbidden_remote_ics(mocked_get): |
337 | 349 |
agenda = Agenda(label=u'Test 10 agenda') |
... | ... | |
341 | 353 |
mocked_response = mock.Mock() |
342 | 354 |
mocked_response.status_code = 403 |
343 | 355 |
mocked_get.return_value = mocked_response |
356 | ||
344 | 357 |
def mocked_requests_http_forbidden_error(*args, **kwargs): |
345 | 358 |
raise requests.HTTPError(response=mocked_response) |
359 | ||
346 | 360 |
mocked_get.side_effect = mocked_requests_http_forbidden_error |
347 | 361 | |
348 | 362 |
with pytest.raises(ICSError) as e: |
349 | 363 |
exceptions_count = desk.create_timeperiod_exceptions_from_remote_ics('http://example.com/sample.ics') |
350 |
assert str(e.value) == "Failed to retrieve remote calendar (http://example.com/sample.ics, HTTP error 403)." |
|
364 |
assert ( |
|
365 |
str(e.value) == "Failed to retrieve remote calendar (http://example.com/sample.ics, HTTP error 403)." |
|
366 |
) |
|
367 | ||
351 | 368 | |
352 | 369 |
@mock.patch('chrono.agendas.models.requests.get') |
353 | 370 |
def test_sync_desks_timeperiod_exceptions_from_ics(mocked_get, capsys): |
354 | 371 |
agenda = Agenda(label=u'Test 11 agenda') |
355 | 372 |
agenda.save() |
356 |
desk = Desk(label='Test 11 desk', agenda=agenda, timeperiod_exceptions_remote_url='http://example.com/sample.ics') |
|
373 |
desk = Desk( |
|
374 |
label='Test 11 desk', agenda=agenda, timeperiod_exceptions_remote_url='http://example.com/sample.ics' |
|
375 |
) |
|
357 | 376 |
desk.save() |
358 | 377 |
mocked_response = mock.Mock() |
359 | 378 |
mocked_response.status_code = 403 |
360 | 379 |
mocked_get.return_value = mocked_response |
380 | ||
361 | 381 |
def mocked_requests_http_forbidden_error(*args, **kwargs): |
362 | 382 |
raise requests.HTTPError(response=mocked_response) |
383 | ||
363 | 384 |
mocked_get.side_effect = mocked_requests_http_forbidden_error |
364 | 385 |
call_command('sync_desks_timeperiod_exceptions') |
365 | 386 |
out, err = capsys.readouterr() |
366 |
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' |
|
387 |
assert ( |
|
388 |
err |
|
389 |
== 'unable to create timeperiod exceptions for "Test 11 desk": Failed to retrieve remote calendar (http://example.com/sample.ics, HTTP error 403).\n' |
|
390 |
) |
|
391 | ||
367 | 392 | |
368 | 393 |
@mock.patch('chrono.agendas.models.requests.get') |
369 | 394 |
def test_sync_desks_timeperiod_exceptions_from_changing_ics(mocked_get, caplog): |
370 | 395 |
agenda = Agenda(label=u'Test 11 agenda') |
371 | 396 |
agenda.save() |
372 |
desk = Desk(label='Test 11 desk', agenda=agenda, timeperiod_exceptions_remote_url='http:example.com/sample.ics') |
|
397 |
desk = Desk( |
|
398 |
label='Test 11 desk', agenda=agenda, timeperiod_exceptions_remote_url='http:example.com/sample.ics' |
|
399 |
) |
|
373 | 400 |
desk.save() |
374 | 401 |
mocked_response = mock.Mock() |
375 | 402 |
mocked_response.text = ICS_SAMPLE |
... | ... | |
397 | 424 |
call_command('sync_desks_timeperiod_exceptions') |
398 | 425 |
assert not TimePeriodException.objects.filter(desk=desk).exists() |
399 | 426 | |
427 | ||
400 | 428 |
def test_base_meeting_duration(): |
401 | 429 |
agenda = Agenda(label='Meeting', kind='meetings') |
402 | 430 |
agenda.save() |
... | ... | |
427 | 455 |
exceptions_count = desk.create_timeperiod_exceptions_from_ics(ICS_SAMPLE_WITH_DURATION) |
428 | 456 |
assert exceptions_count == 2 |
429 | 457 |
assert TimePeriodException.objects.filter(desk=desk).count() == 2 |
430 |
assert set(TimePeriodException.objects.values_list('start_datetime', flat=True)) == set([ |
|
431 |
make_aware(datetime.datetime(2017, 8, 31, 19, 8, 0)), |
|
432 |
make_aware(datetime.datetime(2017, 8, 30, 20, 8, 0)), |
|
433 |
]) |
|
434 |
assert set(TimePeriodException.objects.values_list('end_datetime', flat=True)) == set([ |
|
435 |
make_aware(datetime.datetime(2017, 8, 31, 22, 34, 0)), |
|
436 |
make_aware(datetime.datetime(2017, 9, 1, 0, 34, 0)), |
|
437 |
]) |
|
458 |
assert set(TimePeriodException.objects.values_list('start_datetime', flat=True)) == set( |
|
459 |
[ |
|
460 |
make_aware(datetime.datetime(2017, 8, 31, 19, 8, 0)), |
|
461 |
make_aware(datetime.datetime(2017, 8, 30, 20, 8, 0)), |
|
462 |
] |
|
463 |
) |
|
464 |
assert set(TimePeriodException.objects.values_list('end_datetime', flat=True)) == set( |
|
465 |
[ |
|
466 |
make_aware(datetime.datetime(2017, 8, 31, 22, 34, 0)), |
|
467 |
make_aware(datetime.datetime(2017, 9, 1, 0, 34, 0)), |
|
468 |
] |
|
469 |
) |
|
438 | 470 | |
439 | 471 | |
440 | 472 |
@pytest.mark.freeze_time('2017-12-01') |
... | ... | |
447 | 479 |
desk.save() |
448 | 480 |
assert desk.create_timeperiod_exceptions_from_ics(ICS_SAMPLE_WITH_RECURRENT_EVENT_IN_THE_PAST) == 2 |
449 | 481 |
assert TimePeriodException.objects.filter(desk=desk).count() == 2 |
450 |
assert set(TimePeriodException.objects.values_list('start_datetime', flat=True)) == set([ |
|
451 |
make_aware(datetime.datetime(2018, 1, 1)), make_aware(datetime.datetime(2019, 1, 1))]) |
|
482 |
assert set(TimePeriodException.objects.values_list('start_datetime', flat=True)) == set( |
|
483 |
[make_aware(datetime.datetime(2018, 1, 1)), make_aware(datetime.datetime(2019, 1, 1))] |
|
484 |
) |
|
452 | 485 |
assert desk.create_timeperiod_exceptions_from_ics(ICS_SAMPLE_WITH_RECURRENT_EVENT_IN_THE_PAST) == 0 |
453 | 486 | |
454 | 487 |
tests/test_api.py | ||
---|---|---|
1 | 1 |
import datetime |
2 |
import urllib.parse as urlparse |
|
3 |
import pytest |
|
4 | 2 |
import sys |
3 |
import urllib.parse as urlparse |
|
5 | 4 | |
5 |
import pytest |
|
6 | 6 |
from django.contrib.auth import get_user_model |
7 | 7 |
from django.db import connection |
8 | 8 |
from django.test import override_settings |
9 | 9 |
from django.test.utils import CaptureQueriesContext |
10 |
from django.utils.timezone import now, make_aware, localtime
|
|
10 |
from django.utils.timezone import localtime, make_aware, now
|
|
11 | 11 | |
12 |
from chrono.agendas.models import (Agenda, Event, Booking, |
|
13 |
MeetingType, TimePeriod, Desk, |
|
14 |
TimePeriodException) |
|
15 | 12 |
import chrono.api.views |
16 | ||
13 |
from chrono.agendas.models import Agenda, Booking, Desk, Event, MeetingType, TimePeriod, TimePeriodException |
|
17 | 14 | |
18 | 15 |
pytestmark = pytest.mark.django_db |
19 | 16 | |
... | ... | |
25 | 22 |
@pytest.fixture |
26 | 23 |
def user(): |
27 | 24 |
User = get_user_model() |
28 |
user = User.objects.create(username='john.doe', |
|
29 |
first_name=u'John', last_name=u'Doe', email='john.doe@example.net') |
|
25 |
user = User.objects.create( |
|
26 |
username='john.doe', first_name=u'John', last_name=u'Doe', email='john.doe@example.net' |
|
27 |
) |
|
30 | 28 |
user.set_password('password') |
31 | 29 |
user.save() |
32 | 30 |
return user |
33 | 31 | |
32 | ||
34 | 33 |
@pytest.fixture(params=['Europe/Brussels', 'Asia/Kolkata', 'Brazil/East']) |
35 | 34 |
def time_zone(request, settings): |
36 | 35 |
settings.TIME_ZONE = request.param |
37 | 36 | |
38 | 37 | |
39 |
@pytest.fixture(params=[ |
|
40 |
datetime.datetime(2017, 5, 20, 1, 12), |
|
41 |
datetime.datetime(2017, 5, 20, 11, 42), |
|
42 |
datetime.datetime(2017, 5, 20, 23, 17)]) |
|
38 |
@pytest.fixture( |
|
39 |
params=[ |
|
40 |
datetime.datetime(2017, 5, 20, 1, 12), |
|
41 |
datetime.datetime(2017, 5, 20, 11, 42), |
|
42 |
datetime.datetime(2017, 5, 20, 23, 17), |
|
43 |
] |
|
44 |
) |
|
43 | 45 |
def mock_now(request, monkeypatch): |
44 | 46 |
def mockreturn(): |
45 | 47 |
return make_aware(request.param) |
48 | ||
46 | 49 |
monkeypatch.setattr(chrono.api.views, 'now', mockreturn) |
47 | 50 |
monkeypatch.setattr(chrono.agendas.models, 'now', mockreturn) |
48 | 51 |
monkeypatch.setattr(sys.modules[__name__], 'now', mockreturn) |
49 | 52 |
return mockreturn() |
50 | 53 | |
54 | ||
51 | 55 |
@pytest.fixture |
52 | 56 |
def some_data(time_zone, mock_now): |
53 | 57 |
agenda = Agenda(label=u'Foo bar') |
... | ... | |
55 | 59 |
first_date = localtime(now()).replace(hour=17, minute=0, second=0, microsecond=0) |
56 | 60 |
first_date += datetime.timedelta(days=1) |
57 | 61 |
for i in range(3): |
58 |
event = Event(start_datetime=first_date + datetime.timedelta(days=i), |
|
59 |
places=20, agenda=agenda) |
|
62 |
event = Event(start_datetime=first_date + datetime.timedelta(days=i), places=20, agenda=agenda) |
|
60 | 63 |
event.save() |
61 | 64 | |
62 | 65 |
agenda2 = Agenda(label=u'Foo bar2') |
... | ... | |
64 | 67 |
first_date = localtime(now()).replace(hour=20, minute=0, second=0, microsecond=0) |
65 | 68 |
first_date += datetime.timedelta(days=1) |
66 | 69 |
for i in range(2): |
67 |
event = Event(start_datetime=first_date + datetime.timedelta(days=i), |
|
68 |
places=20, agenda=agenda2) |
|
70 |
event = Event(start_datetime=first_date + datetime.timedelta(days=i), places=20, agenda=agenda2) |
|
69 | 71 |
event.save() |
70 | 72 | |
71 | 73 |
# a date in the past |
72 |
event = Event(start_datetime=first_date - datetime.timedelta(days=10), |
|
73 |
places=10, agenda=agenda) |
|
74 |
event = Event(start_datetime=first_date - datetime.timedelta(days=10), places=10, agenda=agenda) |
|
74 | 75 |
event.save() |
75 | 76 | |
77 | ||
76 | 78 |
@pytest.fixture |
77 | 79 |
def meetings_agenda(time_zone, mock_now): |
78 |
agenda = Agenda(label=u'Foo bar Meeting', kind='meetings', |
|
79 |
minimal_booking_delay=1, maximal_booking_delay=56) |
|
80 |
agenda = Agenda( |
|
81 |
label=u'Foo bar Meeting', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=56 |
|
82 |
) |
|
80 | 83 |
agenda.save() |
81 | 84 |
meeting_type = MeetingType(agenda=agenda, label='Blah', duration=30) |
82 | 85 |
meeting_type.save() |
... | ... | |
86 | 89 | |
87 | 90 |
default_desk, created = Desk.objects.get_or_create(agenda=agenda, label='Desk 1') |
88 | 91 | |
89 |
time_period = TimePeriod(weekday=test_1st_weekday, |
|
90 |
start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=default_desk) |
|
92 |
time_period = TimePeriod( |
|
93 |
weekday=test_1st_weekday, |
|
94 |
start_time=datetime.time(10, 0), |
|
95 |
end_time=datetime.time(12, 0), |
|
96 |
desk=default_desk, |
|
97 |
) |
|
91 | 98 |
time_period.save() |
92 |
time_period = TimePeriod(weekday=test_2nd_weekday, |
|
93 |
start_time=datetime.time(10, 0), end_time=datetime.time(17, 0), desk=default_desk) |
|
99 |
time_period = TimePeriod( |
|
100 |
weekday=test_2nd_weekday, |
|
101 |
start_time=datetime.time(10, 0), |
|
102 |
end_time=datetime.time(17, 0), |
|
103 |
desk=default_desk, |
|
104 |
) |
|
94 | 105 |
time_period.save() |
95 | 106 |
return agenda |
96 | 107 | |
108 | ||
97 | 109 |
def test_agendas_api(app, some_data, meetings_agenda): |
98 | 110 |
agenda1 = Agenda.objects.filter(label=u'Foo bar')[0] |
99 | 111 |
agenda2 = Agenda.objects.filter(label=u'Foo bar2')[0] |
100 | 112 |
resp = app.get('/api/agenda/') |
101 |
assert resp.json == {'data': [ |
|
102 |
{'text': 'Foo bar', 'id': u'foo-bar', 'slug': 'foo-bar', 'kind': 'events', |
|
103 |
'minimal_booking_delay': 1, 'maximal_booking_delay': 56, |
|
104 |
'api': {'datetimes_url': 'http://testserver/api/agenda/%s/datetimes/' % agenda1.slug, |
|
105 |
'fillslots_url': 'http://testserver/api/agenda/%s/fillslots/' % agenda1.slug}}, |
|
106 |
{'text': 'Foo bar Meeting', 'id': u'foo-bar-meeting', 'slug': 'foo-bar-meeting', |
|
107 |
'minimal_booking_delay': 1, 'maximal_booking_delay': 56, |
|
108 |
'kind': 'meetings', |
|
109 |
'api': {'meetings_url': 'http://testserver/api/agenda/%s/meetings/' % meetings_agenda.slug, |
|
110 |
'desks_url': 'http://testserver/api/agenda/%s/desks/' % meetings_agenda.slug, |
|
111 |
'fillslots_url': 'http://testserver/api/agenda/%s/fillslots/' % meetings_agenda.slug, |
|
113 |
assert resp.json == { |
|
114 |
'data': [ |
|
115 |
{ |
|
116 |
'text': 'Foo bar', |
|
117 |
'id': u'foo-bar', |
|
118 |
'slug': 'foo-bar', |
|
119 |
'kind': 'events', |
|
120 |
'minimal_booking_delay': 1, |
|
121 |
'maximal_booking_delay': 56, |
|
122 |
'api': { |
|
123 |
'datetimes_url': 'http://testserver/api/agenda/%s/datetimes/' % agenda1.slug, |
|
124 |
'fillslots_url': 'http://testserver/api/agenda/%s/fillslots/' % agenda1.slug, |
|
112 | 125 |
}, |
113 |
}, |
|
114 |
{'text': 'Foo bar2', 'id': u'foo-bar2', 'kind': 'events', 'slug': 'foo-bar2', |
|
115 |
'minimal_booking_delay': 1, 'maximal_booking_delay': 56, |
|
116 |
'api': {'datetimes_url': 'http://testserver/api/agenda/%s/datetimes/' % agenda2.slug, |
|
117 |
'fillslots_url': 'http://testserver/api/agenda/%s/fillslots/' % agenda2.slug}} |
|
118 |
]} |
|
126 |
}, |
|
127 |
{ |
|
128 |
'text': 'Foo bar Meeting', |
|
129 |
'id': u'foo-bar-meeting', |
|
130 |
'slug': 'foo-bar-meeting', |
|
131 |
'minimal_booking_delay': 1, |
|
132 |
'maximal_booking_delay': 56, |
|
133 |
'kind': 'meetings', |
|
134 |
'api': { |
|
135 |
'meetings_url': 'http://testserver/api/agenda/%s/meetings/' % meetings_agenda.slug, |
|
136 |
'desks_url': 'http://testserver/api/agenda/%s/desks/' % meetings_agenda.slug, |
|
137 |
'fillslots_url': 'http://testserver/api/agenda/%s/fillslots/' % meetings_agenda.slug, |
|
138 |
}, |
|
139 |
}, |
|
140 |
{ |
|
141 |
'text': 'Foo bar2', |
|
142 |
'id': u'foo-bar2', |
|
143 |
'kind': 'events', |
|
144 |
'slug': 'foo-bar2', |
|
145 |
'minimal_booking_delay': 1, |
|
146 |
'maximal_booking_delay': 56, |
|
147 |
'api': { |
|
148 |
'datetimes_url': 'http://testserver/api/agenda/%s/datetimes/' % agenda2.slug, |
|
149 |
'fillslots_url': 'http://testserver/api/agenda/%s/fillslots/' % agenda2.slug, |
|
150 |
}, |
|
151 |
}, |
|
152 |
] |
|
153 |
} |
|
154 | ||
119 | 155 | |
120 | 156 |
def test_agendas_meetingtypes_api(app, some_data, meetings_agenda): |
121 | 157 |
resp = app.get('/api/agenda/%s/meetings/' % meetings_agenda.slug) |
122 |
assert resp.json == {'data': [ |
|
123 |
{'text': 'Blah', |
|
124 |
'id': 'blah', |
|
125 |
'duration': 30, |
|
126 |
'api': { |
|
127 |
'datetimes_url': 'http://testserver/api/agenda/foo-bar-meeting/meetings/blah/datetimes/', |
|
128 |
} |
|
129 |
} |
|
130 |
]} |
|
158 |
assert resp.json == { |
|
159 |
'data': [ |
|
160 |
{ |
|
161 |
'text': 'Blah', |
|
162 |
'id': 'blah', |
|
163 |
'duration': 30, |
|
164 |
'api': { |
|
165 |
'datetimes_url': 'http://testserver/api/agenda/foo-bar-meeting/meetings/blah/datetimes/', |
|
166 |
}, |
|
167 |
} |
|
168 |
] |
|
169 |
} |
|
131 | 170 | |
132 | 171 |
# wrong kind |
133 | 172 |
agenda1 = Agenda.objects.filter(label=u'Foo bar')[0] |
... | ... | |
139 | 178 | |
140 | 179 |
def test_agendas_desks_api(app, some_data, meetings_agenda): |
141 | 180 |
resp = app.get('/api/agenda/%s/desks/' % meetings_agenda.slug) |
142 |
assert resp.json == {'data': [ |
|
143 |
{'text': 'Desk 1', |
|
144 |
'id': 'desk-1', |
|
145 |
} |
|
146 |
]} |
|
181 |
assert resp.json == {'data': [{'text': 'Desk 1', 'id': 'desk-1',}]} |
|
147 | 182 | |
148 | 183 |
# wrong kind |
149 | 184 |
agenda1 = Agenda.objects.filter(label=u'Foo bar')[0] |
... | ... | |
198 | 233 |
resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug) |
199 | 234 |
assert resp.json['data'][0]['description'] |
200 | 235 | |
236 | ||
201 | 237 |
def test_datetimes_api_wrong_kind(app, some_data): |
202 | 238 |
agenda = Agenda.objects.filter(label=u'Foo bar')[0] |
203 | 239 |
agenda.kind = 'meetings' |
204 | 240 |
agenda.save() |
205 | 241 |
resp = app.get('/api/agenda/%s/datetimes/' % agenda.id, status=404) |
206 | 242 | |
243 | ||
207 | 244 |
def test_datetime_api_fr(app, some_data): |
208 | 245 |
agenda_id = Agenda.objects.filter(label=u'Foo bar')[0].id |
209 | 246 |
with override_settings(LANGUAGE_CODE='fr-fr'): |
... | ... | |
213 | 250 |
assert resp.json['data'][0]['datetime'].endswith(' 17:00:00') |
214 | 251 |
assert 'data' in resp.json |
215 | 252 | |
253 | ||
216 | 254 |
def test_datetime_api_label(app, some_data): |
217 | 255 |
agenda_id = Agenda.objects.filter(label=u'Foo bar2')[0].id |
218 | 256 |
event = Event.objects.filter(agenda=agenda_id)[0] |
... | ... | |
221 | 259 |
resp = app.get('/api/agenda/%s/datetimes/' % agenda_id) |
222 | 260 |
assert 'Hello world' in [x['text'] for x in resp.json['data']] |
223 | 261 | |
262 | ||
224 | 263 |
def test_datetime_api_status_url(app, some_data): |
225 | 264 |
agenda = Agenda.objects.get(label=u'Foo bar2') |
226 | 265 |
resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug) |
227 | 266 |
for datum in resp.json['data']: |
228 | 267 |
assert urlparse.urlparse(datum['api']['status_url']).path == '/api/agenda/%s/status/%s/' % ( |
229 |
agenda.slug, datum['slug'] or datum['id']) |
|
268 |
agenda.slug, |
|
269 |
datum['slug'] or datum['id'], |
|
270 |
) |
|
271 | ||
230 | 272 | |
231 | 273 |
def test_datetimes_api_meetings_agenda(app, meetings_agenda): |
232 | 274 |
meeting_type = MeetingType.objects.get(agenda=meetings_agenda) |
233 |
api_url = '/api/agenda/%s/meetings/%s/datetimes/' % ( |
|
234 |
meeting_type.agenda.slug, meeting_type.slug) |
|
275 |
api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (meeting_type.agenda.slug, meeting_type.slug) |
|
235 | 276 | |
236 | 277 |
resp = app.get('/api/agenda/%s/meetings/xxx/datetimes/' % meeting_type.agenda.slug, status=404) |
237 | 278 | |
... | ... | |
253 | 294 | |
254 | 295 |
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) |
255 | 296 |
dt = datetime.datetime.strptime(resp.json['data'][2]['id'].split(':')[1], '%Y-%m-%d-%H%M') |
256 |
ev = Event(agenda=meetings_agenda, meeting_type=meeting_type, |
|
257 |
places=1, full=False, start_datetime=make_aware(dt), desk=Desk.objects.first()) |
|
297 |
ev = Event( |
|
298 |
agenda=meetings_agenda, |
|
299 |
meeting_type=meeting_type, |
|
300 |
places=1, |
|
301 |
full=False, |
|
302 |
start_datetime=make_aware(dt), |
|
303 |
desk=Desk.objects.first(), |
|
304 |
) |
|
258 | 305 |
ev.save() |
259 | 306 |
booking = Booking(event=ev) |
260 | 307 |
booking.save() |
... | ... | |
277 | 324 |
default_desk, _ = Desk.objects.get_or_create(agenda=meetings_agenda, slug='desk-1') |
278 | 325 |
TimePeriod.objects.filter(desk=default_desk).delete() |
279 | 326 |
start_time = localtime(now()) - datetime.timedelta(minutes=10) |
280 |
time_period = TimePeriod(weekday=localtime(now()).weekday(), |
|
281 |
start_time=start_time, |
|
282 |
end_time=start_time + datetime.timedelta(hours=1), desk=default_desk) |
|
327 |
time_period = TimePeriod( |
|
328 |
weekday=localtime(now()).weekday(), |
|
329 |
start_time=start_time, |
|
330 |
end_time=start_time + datetime.timedelta(hours=1), |
|
331 |
desk=default_desk, |
|
332 |
) |
|
283 | 333 |
time_period.save() |
284 | 334 |
meetings_agenda.minimal_booking_delay = 0 |
285 | 335 |
meetings_agenda.maximal_booking_delay = 10 |
... | ... | |
287 | 337 |
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) |
288 | 338 |
assert len(resp.json['data']) == 3 |
289 | 339 | |
340 | ||
290 | 341 |
def test_datetimes_api_meetings_agenda_short_time_periods(app, meetings_agenda, user): |
291 | 342 |
meetings_agenda.minimal_booking_delay = 0 |
292 | 343 |
meetings_agenda.maximal_booking_delay = 10 |
... | ... | |
294 | 345 | |
295 | 346 |
default_desk, _ = Desk.objects.get_or_create(agenda=meetings_agenda, slug='desk-1') |
296 | 347 |
meeting_type = MeetingType.objects.get(agenda=meetings_agenda) |
297 |
api_url = '/api/agenda/%s/meetings/%s/datetimes/' % ( |
|
298 |
meeting_type.agenda.slug, meeting_type.slug) |
|
348 |
api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (meeting_type.agenda.slug, meeting_type.slug) |
|
299 | 349 | |
300 | 350 |
# test with short time periods |
301 | 351 |
TimePeriod.objects.filter(desk=default_desk).delete() |
302 | 352 |
test_1st_weekday = (localtime(now()).weekday() + 2) % 7 |
303 |
time_period = TimePeriod(weekday=test_1st_weekday, |
|
304 |
start_time=datetime.time(10, 0), end_time=datetime.time(10, 30), desk=default_desk) |
|
353 |
time_period = TimePeriod( |
|
354 |
weekday=test_1st_weekday, |
|
355 |
start_time=datetime.time(10, 0), |
|
356 |
end_time=datetime.time(10, 30), |
|
357 |
desk=default_desk, |
|
358 |
) |
|
305 | 359 |
time_period.save() |
306 | 360 |
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) |
307 | 361 |
assert len(resp.json['data']) == 2 |
... | ... | |
328 | 382 |
assert resp.json['err_class'] == 'no more desk available' |
329 | 383 |
assert resp.json['err_desc'] == 'no more desk available' |
330 | 384 | |
385 | ||
331 | 386 |
def test_booking_api(app, some_data, user): |
332 | 387 |
agenda = Agenda.objects.filter(label=u'Foo bar')[0] |
333 | 388 |
event = [x for x in Event.objects.filter(agenda=agenda) if x.in_bookable_period()][0] |
... | ... | |
337 | 392 | |
338 | 393 |
for agenda_key in (agenda.slug, agenda.id): # acces datetimes via agenda slug or id (legacy) |
339 | 394 |
resp_datetimes = app.get('/api/agenda/%s/datetimes/' % agenda_key) |
340 |
event_fillslot_url = [x for x in resp_datetimes.json['data'] if x['id'] == event.id][0]['api']['fillslot_url'] |
|
341 |
assert urlparse.urlparse(event_fillslot_url).path == '/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.slug or event.id) |
|
395 |
event_fillslot_url = [x for x in resp_datetimes.json['data'] if x['id'] == event.id][0]['api'][ |
|
396 |
'fillslot_url' |
|
397 |
] |
|
398 |
assert urlparse.urlparse(event_fillslot_url).path == '/api/agenda/%s/fillslot/%s/' % ( |
|
399 |
agenda.slug, |
|
400 |
event.slug or event.id, |
|
401 |
) |
|
342 | 402 | |
343 | 403 |
app.authorization = ('Basic', ('john.doe', 'password')) |
344 | 404 |
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, event.id)) |
... | ... | |
359 | 419 |
assert Booking.objects.filter(event__agenda=agenda).count() == 2 |
360 | 420 | |
361 | 421 |
# test with additional data |
362 |
resp = app.post_json('/api/agenda/%s/fillslot/%s/' % (agenda.id, event.id), |
|
363 |
params={'label': 'foo', 'user_name': 'bar', 'backoffice_url': 'http://example.net/'}) |
|
422 |
resp = app.post_json( |
|
423 |
'/api/agenda/%s/fillslot/%s/' % (agenda.id, event.id), |
|
424 |
params={'label': 'foo', 'user_name': 'bar', 'backoffice_url': 'http://example.net/'}, |
|
425 |
) |
|
364 | 426 |
assert Booking.objects.get(id=resp.json['booking_id']).label == 'foo' |
365 | 427 |
assert Booking.objects.get(id=resp.json['booking_id']).user_name == 'bar' |
366 | 428 |
assert Booking.objects.get(id=resp.json['booking_id']).backoffice_url == 'http://example.net/' |
367 | 429 | |
368 | 430 |
# blank data are OK |
369 |
resp = app.post_json('/api/agenda/%s/fillslot/%s/' % (agenda.id, event.id), |
|
370 |
params={'label': '', 'user_name': '', 'backoffice_url': ''}) |
|
431 |
resp = app.post_json( |
|
432 |
'/api/agenda/%s/fillslot/%s/' % (agenda.id, event.id), |
|
433 |
params={'label': '', 'user_name': '', 'backoffice_url': ''}, |
|
434 |
) |
|
371 | 435 |
assert Booking.objects.get(id=resp.json['booking_id']).label == '' |
372 | 436 |
assert Booking.objects.get(id=resp.json['booking_id']).user_name == '' |
373 | 437 |
assert Booking.objects.get(id=resp.json['booking_id']).backoffice_url == '' |
374 | 438 | |
375 | 439 |
# extra data stored in extra_data field |
376 |
resp = app.post_json('/api/agenda/%s/fillslot/%s/' % (agenda.id, event.id), |
|
377 |
params={'label': 'l', 'user_name': 'u', 'backoffice_url': '', 'foo': 'bar'}) |
|
440 |
resp = app.post_json( |
|
441 |
'/api/agenda/%s/fillslot/%s/' % (agenda.id, event.id), |
|
442 |
params={'label': 'l', 'user_name': 'u', 'backoffice_url': '', 'foo': 'bar'}, |
|
443 |
) |
|
378 | 444 |
assert Booking.objects.get(id=resp.json['booking_id']).label == 'l' |
379 | 445 |
assert Booking.objects.get(id=resp.json['booking_id']).user_name == 'u' |
380 | 446 |
assert Booking.objects.get(id=resp.json['booking_id']).backoffice_url == '' |
381 | 447 |
assert Booking.objects.get(id=resp.json['booking_id']).extra_data == {'foo': 'bar'} |
382 | 448 | |
383 | 449 |
# test invalid data are refused |
384 |
resp = app.post_json('/api/agenda/%s/fillslot/%s/' % (agenda.id, event.id), |
|
385 |
params={'user_name': {'foo': 'bar'}}, status=400) |
|
450 |
resp = app.post_json( |
|
451 |
'/api/agenda/%s/fillslot/%s/' % (agenda.id, event.id), |
|
452 |
params={'user_name': {'foo': 'bar'}}, |
|
453 |
status=400, |
|
454 |
) |
|
386 | 455 |
assert resp.json['err'] == 1 |
387 | 456 |
assert resp.json['reason'] == 'invalid payload' # legacy |
388 | 457 |
assert resp.json['err_class'] == 'invalid payload' |
... | ... | |
394 | 463 | |
395 | 464 |
resp = app.post('/api/agenda/233/fillslot/%s/' % event.id, status=404) |
396 | 465 | |
466 | ||
397 | 467 |
def test_booking_ics(app, some_data, meetings_agenda, user): |
398 | 468 |
agenda = Agenda.objects.filter(label=u'Foo bar')[0] |
399 | 469 |
event = [x for x in Event.objects.filter(agenda=agenda) if x.in_bookable_period()][0] |
... | ... | |
407 | 477 | |
408 | 478 |
formatted_start_date = event.start_datetime.strftime('%Y%m%dT%H%M%S') |
409 | 479 |
booking_ics = Booking.objects.get(id=resp.json['booking_id']).get_ics() |
410 |
assert 'UID:%s-%s-%s\r\n' % (event.start_datetime.isoformat(), agenda.pk, resp.json['booking_id']) in booking_ics |
|
480 |
assert ( |
|
481 |
'UID:%s-%s-%s\r\n' % (event.start_datetime.isoformat(), agenda.pk, resp.json['booking_id']) |
|
482 |
in booking_ics |
|
483 |
) |
|
411 | 484 |
assert 'SUMMARY:\r\n' in booking_ics |
412 | 485 |
assert 'DTSTART:%sZ\r\n' % formatted_start_date in booking_ics |
413 | 486 |
assert 'DTEDND:' not in booking_ics |
414 | 487 | |
415 | 488 |
# test with additional data |
416 |
resp = app.post_json('/api/agenda/%s/fillslot/%s/' % (agenda.id, event.id), |
|
417 |
params={'label': 'foo', 'user_name': 'bar', 'backoffice_url': 'http://example.net/', |
|
418 |
'url': 'http://example.com/booking'}) |
|
489 |
resp = app.post_json( |
|
490 |
'/api/agenda/%s/fillslot/%s/' % (agenda.id, event.id), |
|
491 |
params={ |
|
492 |
'label': 'foo', |
|
493 |
'user_name': 'bar', |
|
494 |
'backoffice_url': 'http://example.net/', |
|
495 |
'url': 'http://example.com/booking', |
|
496 |
}, |
|
497 |
) |
|
419 | 498 |
assert Booking.objects.count() == 2 |
420 | 499 |
booking_ics = Booking.objects.get(id=resp.json['booking_id']).get_ics() |
421 | 500 |
assert 'SUMMARY:foo\r\n' in booking_ics |
... | ... | |
423 | 502 |
assert 'URL:http://example.com/booking\r\n' in booking_ics |
424 | 503 | |
425 | 504 |
# test with user_label in additionnal data |
426 |
resp = app.post_json('/api/agenda/%s/fillslot/%s/' % (agenda.id, event.id), |
|
427 |
params={'label': 'foo', 'user_name': 'bar', 'backoffice_url': 'http://example.net/', |
|
428 |
'url': 'http://example.com/booking', 'user_display_label': 'your booking'}) |
|
505 |
resp = app.post_json( |
|
506 |
'/api/agenda/%s/fillslot/%s/' % (agenda.id, event.id), |
|
507 |
params={ |
|
508 |
'label': 'foo', |
|
509 |
'user_name': 'bar', |
|
510 |
'backoffice_url': 'http://example.net/', |
|
511 |
'url': 'http://example.com/booking', |
|
512 |
'user_display_label': 'your booking', |
|
513 |
}, |
|
514 |
) |
|
429 | 515 |
assert Booking.objects.count() == 3 |
430 | 516 |
booking_ics = Booking.objects.get(id=resp.json['booking_id']).get_ics() |
431 | 517 |
assert 'SUMMARY:your booking\r\n' in booking_ics |
... | ... | |
433 | 519 |
assert 'URL:http://example.com/booking\r\n' in booking_ics |
434 | 520 | |
435 | 521 |
# extra data stored in extra_data field |
436 |
resp = app.post_json('/api/agenda/%s/fillslot/%s/' % (agenda.id, event.id), |
|
437 |
params={'label': 'l', 'user_name': 'u', 'backoffice_url': '', 'location': 'bar', |
|
438 |
'comment': 'booking comment', 'description': 'booking description'}) |
|
522 |
resp = app.post_json( |
|
523 |
'/api/agenda/%s/fillslot/%s/' % (agenda.id, event.id), |
|
524 |
params={ |
|
525 |
'label': 'l', |
|
526 |
'user_name': 'u', |
|
527 |
'backoffice_url': '', |
|
528 |
'location': 'bar', |
|
529 |
'comment': 'booking comment', |
|
530 |
'description': 'booking description', |
|
531 |
}, |
|
532 |
) |
|
439 | 533 |
assert Booking.objects.count() == 4 |
440 | 534 |
booking_id = resp.json['booking_id'] |
441 | 535 |
booking = Booking.objects.get(id=booking_id) |
... | ... | |
452 | 546 |
resp = app.get('/api/booking/%s/ics/' % resp.json['booking_id']) |
453 | 547 |
assert resp.headers['Content-Type'] == 'text/calendar' |
454 | 548 | |
455 |
params = {'description': 'custom booking description', 'location': 'custom booking location', |
|
456 |
'comment': 'custom comment', 'url': 'http://example.com/custom'} |
|
549 |
params = { |
|
550 |
'description': 'custom booking description', |
|
551 |
'location': 'custom booking location', |
|
552 |
'comment': 'custom comment', |
|
553 |
'url': 'http://example.com/custom', |
|
554 |
} |
|
457 | 555 |
resp = app.get('/api/booking/%s/ics/' % booking_id, params=params) |
458 | 556 |
assert 'DESCRIPTION:custom booking description\r\n' in resp.text |
459 | 557 |
assert 'LOCATION:custom booking location\r\n' in resp.text |
... | ... | |
470 | 568 |
booking = Booking.objects.get(id=resp.json['booking_id']) |
471 | 569 |
booking_ics = booking.get_ics() |
472 | 570 |
start = booking.event.start_datetime.strftime('%Y%m%dT%H%M%S') |
473 |
end = (booking.event.start_datetime + datetime.timedelta(minutes=booking.event.meeting_type.duration)).strftime('%Y%m%dT%H%M%S') |
|
571 |
end = ( |
|
572 |
booking.event.start_datetime + datetime.timedelta(minutes=booking.event.meeting_type.duration) |
|
573 |
).strftime('%Y%m%dT%H%M%S') |
|
474 | 574 |
assert "DTSTART:%sZ\r\n" % start in booking_ics |
475 | 575 |
assert "DTEND:%sZ\r\n" % end in booking_ics |
476 | 576 | |
577 | ||
477 | 578 |
def test_booking_api_fillslots(app, some_data, user): |
478 | 579 |
agenda = Agenda.objects.filter(label=u'Foo bar')[0] |
479 | 580 |
events_ids = [x.id for x in Event.objects.filter(agenda=agenda) if x.in_bookable_period()] |
... | ... | |
520 | 621 |
assert Booking.objects.filter(event__agenda=agenda, primary_booking=primary_booking_id_2).count() == 2 |
521 | 622 | |
522 | 623 |
# test with additional data |
523 |
resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.id, |
|
524 |
params={'slots': events_ids, |
|
525 |
'label': 'foo', 'user_name': 'bar', 'backoffice_url': 'http://example.net/'}) |
|
624 |
resp = app.post_json( |
|
625 |
'/api/agenda/%s/fillslots/' % agenda.id, |
|
626 |
params={ |
|
627 |
'slots': events_ids, |
|
628 |
'label': 'foo', |
|
629 |
'user_name': 'bar', |
|
630 |
'backoffice_url': 'http://example.net/', |
|
631 |
}, |
|
632 |
) |
|
526 | 633 |
booking_id = resp.json['booking_id'] |
527 | 634 |
assert Booking.objects.get(id=booking_id).label == 'foo' |
528 | 635 |
assert Booking.objects.get(id=booking_id).user_name == 'bar' |
... | ... | |
538 | 645 |
assert Booking.objects.get(id=booking_id).cancellation_datetime is not None |
539 | 646 | |
540 | 647 |
# extra data stored in extra_data field |
541 |
resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.id, |
|
542 |
params={'slots': events_ids, |
|
543 |
'label': 'l', 'user_name': 'u', 'backoffice_url': '', 'foo': 'bar'}) |
|
648 |
resp = app.post_json( |
|
649 |
'/api/agenda/%s/fillslots/' % agenda.id, |
|
650 |
params={'slots': events_ids, 'label': 'l', 'user_name': 'u', 'backoffice_url': '', 'foo': 'bar'}, |
|
651 |
) |
|
544 | 652 |
assert Booking.objects.get(id=resp.json['booking_id']).label == 'l' |
545 | 653 |
assert Booking.objects.get(id=resp.json['booking_id']).user_name == 'u' |
546 | 654 |
assert Booking.objects.get(id=resp.json['booking_id']).backoffice_url == '' |
... | ... | |
549 | 657 |
assert booking.extra_data == {'foo': 'bar'} |
550 | 658 | |
551 | 659 |
# test invalid data are refused |
552 |
resp = app.post_json('/api/agenda/%s/fillslots/' % agenda.id, |
|
553 |
params={'slots': events_ids, |
|
554 |
'user_name': {'foo': 'bar'}}, status=400) |
|
660 |
resp = app.post_json( |
|
661 |
'/api/agenda/%s/fillslots/' % agenda.id, |
|
662 |
params={'slots': events_ids, 'user_name': {'foo': 'bar'}}, |
|
663 |
status=400, |
|
664 |
) |
|
555 | 665 |
assert resp.json['err'] == 1 |
556 | 666 |
assert resp.json['reason'] == 'invalid payload' # legacy |
557 | 667 |
assert resp.json['err_class'] == 'invalid payload' |
... | ... | |
583 | 693 |
resp = app.post('/api/agenda/foobar/fillslots/', status=404) |
584 | 694 |
resp = app.post('/api/agenda/233/fillslots/', status=404) |
585 | 695 | |
696 | ||
586 | 697 |
def test_booking_api_meeting(app, meetings_agenda, user): |
587 | 698 |
agenda_id = meetings_agenda.slug |
588 | 699 |
meeting_type = MeetingType.objects.get(agenda=meetings_agenda) |
589 | 700 |
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) |
590 | 701 |
event_id = resp.json['data'][2]['id'] |
591 |
assert urlparse.urlparse(resp.json['data'][2]['api']['fillslot_url'] |
|
592 |
).path == '/api/agenda/%s/fillslot/%s/' % (meetings_agenda.slug, event_id) |
|
702 |
assert urlparse.urlparse( |
|
703 |
resp.json['data'][2]['api']['fillslot_url'] |
|
704 |
).path == '/api/agenda/%s/fillslot/%s/' % (meetings_agenda.slug, event_id) |
|
593 | 705 | |
594 | 706 |
app.authorization = ('Basic', ('john.doe', 'password')) |
595 | 707 | |
... | ... | |
600 | 712 |
# make a booking |
601 | 713 |
resp_booking = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id)) |
602 | 714 |
assert Booking.objects.count() == 1 |
603 |
assert resp_booking.json['datetime'] == localtime(Booking.objects.all()[0].event.start_datetime |
|
604 |
).strftime('%Y-%m-%d %H:%M:%S') |
|
605 |
assert resp_booking.json['end_datetime'] == localtime(Booking.objects.all()[0].event.end_datetime |
|
606 |
).strftime('%Y-%m-%d %H:%M:%S') |
|
715 |
assert resp_booking.json['datetime'] == localtime(Booking.objects.all()[0].event.start_datetime).strftime( |
|
716 |
'%Y-%m-%d %H:%M:%S' |
|
717 |
) |
|
718 |
assert resp_booking.json['end_datetime'] == localtime( |
|
719 |
Booking.objects.all()[0].event.end_datetime |
|
720 |
).strftime('%Y-%m-%d %H:%M:%S') |
|
607 | 721 |
assert resp_booking.json['duration'] == 30 |
608 | 722 | |
609 | 723 |
resp2 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) |
... | ... | |
622 | 736 |
assert resp.json['err'] == 0 |
623 | 737 |
assert Booking.objects.count() == 2 |
624 | 738 | |
739 | ||
625 | 740 |
def test_booking_api_meeting_fillslots(app, meetings_agenda, user): |
626 | 741 |
agenda_id = meetings_agenda.slug |
627 | 742 |
meeting_type = MeetingType.objects.get(agenda=meetings_agenda) |
... | ... | |
633 | 748 |
assert Booking.objects.count() == 2 |
634 | 749 |
primary_booking = Booking.objects.filter(primary_booking__isnull=True).first() |
635 | 750 |
secondary_booking = Booking.objects.filter(primary_booking=primary_booking.id).first() |
636 |
assert resp_booking.json['datetime'] == localtime(primary_booking.event.start_datetime |
|
637 |
).strftime('%Y-%m-%d %H:%M:%S') |
|
638 |
assert resp_booking.json['end_datetime'] == localtime(secondary_booking.event.end_datetime |
|
639 |
).strftime('%Y-%m-%d %H:%M:%S') |
|
751 |
assert resp_booking.json['datetime'] == localtime(primary_booking.event.start_datetime).strftime( |
|
752 |
'%Y-%m-%d %H:%M:%S' |
|
753 |
) |
|
754 |
assert resp_booking.json['end_datetime'] == localtime(secondary_booking.event.end_datetime).strftime( |
|
755 |
'%Y-%m-%d %H:%M:%S' |
|
756 |
) |
|
640 | 757 | |
641 | 758 |
resp2 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) |
642 | 759 |
assert len(resp.json['data']) == len([x for x in resp2.json['data'] if not x.get('disabled')]) + 2 |
... | ... | |
672 | 789 |
assert Booking.objects.filter(cancellation_datetime__isnull=False).count() == 2 |
673 | 790 | |
674 | 791 |
impossible_slots = ['1:2017-05-22-1130', '2:2017-05-22-1100'] |
675 |
resp = app.post('/api/agenda/%s/fillslots/' % agenda_id, |
|
676 |
params={'slots': impossible_slots}, |
|
677 |
status=400) |
|
792 |
resp = app.post('/api/agenda/%s/fillslots/' % agenda_id, params={'slots': impossible_slots}, status=400) |
|
678 | 793 |
assert resp.json['err'] == 1 |
679 | 794 |
assert resp.json['reason'] == 'all slots must have the same meeting type id (1)' # legacy |
680 | 795 |
assert resp.json['err_class'] == 'all slots must have the same meeting type id (1)' |
681 | 796 |
assert resp.json['err_desc'] == 'all slots must have the same meeting type id (1)' |
682 | 797 | |
798 | ||
683 | 799 |
def test_booking_api_meeting_across_daylight_saving_time(app, meetings_agenda, user): |
684 | 800 |
meetings_agenda.maximal_booking_delay = 365 |
685 | 801 |
meetings_agenda.save() |
... | ... | |
689 | 805 |
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) |
690 | 806 |
event_index = 26 * 18 |
691 | 807 |
event_id = resp.json['data'][event_index]['id'] |
692 |
assert event_id[-4:] == resp.json['data'][2*18]['id'][-4:] |
|
693 |
assert urlparse.urlparse(resp.json['data'][event_index]['api']['fillslot_url'] |
|
694 |
).path == '/api/agenda/%s/fillslot/%s/' % (meetings_agenda.slug, event_id) |
|
808 |
assert event_id[-4:] == resp.json['data'][2 * 18]['id'][-4:] |
|
809 |
assert urlparse.urlparse( |
|
810 |
resp.json['data'][event_index]['api']['fillslot_url'] |
|
811 |
).path == '/api/agenda/%s/fillslot/%s/' % (meetings_agenda.slug, event_id) |
|
695 | 812 | |
696 | 813 |
app.authorization = ('Basic', ('john.doe', 'password')) |
697 | 814 |
resp_booking = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id)) |
698 | 815 |
assert Booking.objects.count() == 1 |
699 |
assert resp_booking.json['datetime'] == localtime(Booking.objects.all()[0].event.start_datetime |
|
700 |
).strftime('%Y-%m-%d %H:%M:%S') |
|
816 |
assert resp_booking.json['datetime'] == localtime(Booking.objects.all()[0].event.start_datetime).strftime( |
|
817 |
'%Y-%m-%d %H:%M:%S' |
|
818 |
) |
|
701 | 819 | |
702 | 820 |
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) |
703 | 821 |
assert resp.json['data'][event_index]['disabled'] |
704 | 822 | |
823 | ||
705 | 824 |
def test_booking_api_meeting_different_durations_book_short(app, meetings_agenda, user): |
706 | 825 |
agenda_id = meetings_agenda.id |
707 | 826 |
meeting_type = MeetingType.objects.get(agenda=meetings_agenda) |
... | ... | |
722 | 841 | |
723 | 842 |
# the longer event at the same time shouldn't be available anymore |
724 | 843 |
resp_long2 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) |
725 |
assert len(resp_long.json['data']) == len([x for x in resp_long2.json['data'] if not x.get('disabled')]) + 1 |
|
844 |
assert ( |
|
845 |
len(resp_long.json['data']) == len([x for x in resp_long2.json['data'] if not x.get('disabled')]) + 1 |
|
846 |
) |
|
726 | 847 |
assert resp_long.json['data'][1:] == [x for x in resp_long2.json['data'] if not x.get('disabled')] |
727 | 848 | |
849 | ||
728 | 850 |
def test_booking_api_meeting_different_durations_book_long(app, meetings_agenda, user): |
729 | 851 |
agenda_id = meetings_agenda.id |
730 | 852 |
meeting_type = MeetingType.objects.get(agenda=meetings_agenda) |
... | ... | |
745 | 867 | |
746 | 868 |
# this should have removed two short events |
747 | 869 |
resp_short2 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_2.id) |
748 |
assert len(resp_short.json['data']) == len([x for x in resp_short2.json['data'] if not x.get('disabled')]) + 2 |
|
870 |
assert ( |
|
871 |
len(resp_short.json['data']) |
|
872 |
== len([x for x in resp_short2.json['data'] if not x.get('disabled')]) + 2 |
|
873 |
) |
|
749 | 874 | |
750 | 875 |
# book another long event |
751 | 876 |
event_id = resp.json['data'][10]['id'] |
... | ... | |
754 | 879 |
assert Booking.objects.count() == 2 |
755 | 880 | |
756 | 881 |
resp_short2 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_2.id) |
757 |
assert len(resp_short.json['data']) == len([x for x in resp_short2.json['data'] if not x.get('disabled')]) + 4 |
|
882 |
assert ( |
|
883 |
len(resp_short.json['data']) |
|
884 |
== len([x for x in resp_short2.json['data'] if not x.get('disabled')]) + 4 |
|
885 |
) |
|
886 | ||
758 | 887 | |
759 | 888 |
def test_booking_api_with_data(app, some_data, user): |
760 | 889 |
agenda_id = Agenda.objects.filter(label=u'Foo bar')[0].id |
761 | 890 |
event = Event.objects.filter(agenda_id=agenda_id)[0] |
762 | 891 | |
763 | 892 |
app.authorization = ('Basic', ('john.doe', 'password')) |
764 |
resp = app.post_json('/api/agenda/%s/fillslot/%s/' % (agenda_id, event.id), |
|
765 |
params={'hello': 'world'}) |
|
893 |
resp = app.post_json('/api/agenda/%s/fillslot/%s/' % (agenda_id, event.id), params={'hello': 'world'}) |
|
766 | 894 |
assert Booking.objects.count() == 1 |
767 | 895 |
assert Booking.objects.all()[0].extra_data == {'hello': 'world'} |
768 | 896 | |
... | ... | |
801 | 929 |
assert 'places' not in resp.json |
802 | 930 | |
803 | 931 |
# not for multiple booking |
804 |
events = [x for x in Event.objects.filter(agenda=agenda).order_by('start_datetime') if x.in_bookable_period()][:2] |
|
932 |
events = [ |
|
933 |
x for x in Event.objects.filter(agenda=agenda).order_by('start_datetime') if x.in_bookable_period() |
|
934 |
][:2] |
|
805 | 935 |
slots = [x.pk for x in events] |
806 | 936 | |
807 |
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, |
|
808 |
params={'slots': slots, 'count': '3'}) |
|
937 |
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots, 'count': '3'}) |
|
809 | 938 |
assert resp.json['err'] == 0 |
810 | 939 |
assert 'places' not in resp.json |
811 | 940 | |
... | ... | |
822 | 951 |
# Book a new event and cancel previous booking |
823 | 952 |
resp = app.post_json( |
824 | 953 |
'/api/agenda/%s/fillslot/%s/' % (agenda_id, event_1.id), |
825 |
params={'cancel_booking_id': first_booking.pk} |
|
954 |
params={'cancel_booking_id': first_booking.pk},
|
|
826 | 955 |
) |
827 | 956 |
assert resp.json['err'] == 0 |
828 | 957 |
assert resp.json['cancelled_booking_id'] == first_booking.pk |
... | ... | |
833 | 962 |
# Cancelling an already cancelled booking returns an error |
834 | 963 |
resp = app.post_json( |
835 | 964 |
'/api/agenda/%s/fillslot/%s/' % (agenda_id, event_1.id), |
836 |
params={'cancel_booking_id': first_booking.pk} |
|
965 |
params={'cancel_booking_id': first_booking.pk},
|
|
837 | 966 |
) |
838 | 967 |
assert resp.json['err'] == 1 |
839 | 968 |
assert resp.json['reason'] == 'cancel booking: booking already cancelled' # legacy |
... | ... | |
843 | 972 | |
844 | 973 |
# Cancelling a non existent booking returns an error |
845 | 974 |
resp = app.post_json( |
846 |
'/api/agenda/%s/fillslot/%s/' % (agenda_id, event_1.id), |
|
847 |
params={'cancel_booking_id': '-1'} |
|
975 |
'/api/agenda/%s/fillslot/%s/' % (agenda_id, event_1.id), params={'cancel_booking_id': '-1'} |
|
848 | 976 |
) |
849 | 977 |
assert resp.json['err'] == 1 |
850 | 978 |
assert resp.json['reason'] == 'cancel booking: booking does no exist' # legacy |
... | ... | |
853 | 981 |
assert Booking.objects.count() == 2 |
854 | 982 | |
855 | 983 |
# Cancelling booking with different count than new booking |
856 |
resp = app.post_json( |
|
857 |
'/api/agenda/%s/fillslot/%s/' % (agenda_id, event_2.id), |
|
858 |
params={'count': 2} |
|
859 |
) |
|
984 |
resp = app.post_json('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_2.id), params={'count': 2}) |
|
860 | 985 |
assert resp.json['err'] == 0 |
861 | 986 |
assert Booking.objects.count() == 4 |
862 | 987 |
booking_id = resp.json['booking_id'] |
863 | 988 | |
864 | 989 |
resp = app.post_json( |
865 | 990 |
'/api/agenda/%s/fillslot/%s/' % (agenda_id, event_3.id), |
866 |
params={'cancel_booking_id': booking_id, 'count': 1} |
|
991 |
params={'cancel_booking_id': booking_id, 'count': 1},
|
|
867 | 992 |
) |
868 | 993 |
assert resp.json['err'] == 1 |
869 | 994 |
assert resp.json['reason'] == 'cancel booking: count is different' # legacy |
... | ... | |
875 | 1000 |
app.post_json( |
876 | 1001 |
'/api/agenda/%s/fillslot/%s/' % (agenda_id, event_0.id), |
877 | 1002 |
params={'cancel_booking_id': 'no an integer'}, |
878 |
status=400) |
|
1003 |
status=400, |
|
1004 |
) |
|
879 | 1005 | |
880 | 1006 |
# cancel_booking_id can be empty or null |
881 | 1007 |
resp = app.post_json( |
882 |
'/api/agenda/%s/fillslot/%s/' % (agenda_id, event_0.id), |
|
883 |
params={'cancel_booking_id': ''})
|
|
1008 |
'/api/agenda/%s/fillslot/%s/' % (agenda_id, event_0.id), params={'cancel_booking_id': ''}
|
|
1009 |
) |
|
884 | 1010 |
assert resp.json['err'] == 0 |
885 | 1011 |
assert 'cancelled_booking_id' not in resp.json |
886 | 1012 |
assert Booking.objects.count() == 5 |
887 | 1013 | |
888 | 1014 |
resp = app.post_json( |
889 |
'/api/agenda/%s/fillslot/%s/' % (agenda_id, event_0.id), |
|
890 |
params={'cancel_booking_id': None})
|
|
1015 |
'/api/agenda/%s/fillslot/%s/' % (agenda_id, event_0.id), params={'cancel_booking_id': None}
|
|
1016 |
) |
|
891 | 1017 |
assert resp.json['err'] == 0 |
892 | 1018 |
assert 'cancelled_booking_id' not in resp.json |
893 | 1019 |
assert Booking.objects.count() == 6 |
894 | 1020 | |
1021 | ||
895 | 1022 |
def test_booking_cancellation_api(app, some_data, user): |
896 | 1023 |
agenda_id = Agenda.objects.filter(label=u'Foo bar')[0].id |
897 | 1024 |
event = Event.objects.filter(agenda_id=agenda_id)[0] |
... | ... | |
904 | 1031 |
resp = app.delete('/api/booking/%s/' % booking_id) |
905 | 1032 |
assert Booking.objects.filter(cancellation_datetime__isnull=False).count() == 1 |
906 | 1033 | |
1034 | ||
907 | 1035 |
def test_booking_cancellation_post_api(app, some_data, user): |
908 | 1036 |
agenda_id = Agenda.objects.filter(label=u'Foo bar')[0].id |
909 | 1037 |
event = Event.objects.filter(agenda_id=agenda_id)[0] |
... | ... | |
924 | 1052 |
resp = app.post('/api/booking/%s/cancel/' % booking_id, status=200) |
925 | 1053 |
assert resp.json['err'] == 1 |
926 | 1054 | |
1055 | ||
927 | 1056 |
def test_booking_cancellation_post_meeting_api(app, meetings_agenda, user): |
928 | 1057 |
agenda_id = Agenda.objects.filter(label=u'Foo bar Meeting')[0].id |
929 | 1058 |
meeting_type = MeetingType.objects.get(agenda=meetings_agenda) |
... | ... | |
948 | 1077 |
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) |
949 | 1078 |
assert len([x for x in resp.json['data'] if not x.get('disabled')]) == nb_events - 1 |
950 | 1079 | |
1080 | ||
951 | 1081 |
def test_soldout(app, some_data, user): |
952 | 1082 |
agenda_id = Agenda.objects.filter(label=u'Foo bar')[0].id |
953 | 1083 |
event = Event.objects.filter(agenda_id=agenda_id).exclude(start_datetime__lt=now())[0] |
... | ... | |
971 | 1101 |
assert resp.json['err_class'] == 'sold out' |
972 | 1102 |
assert resp.json['err_desc'] == 'sold out' |
973 | 1103 | |
1104 | ||
974 | 1105 |
def test_status(app, some_data, user): |
975 | 1106 |
agenda_id = Agenda.objects.filter(label=u'Foo bar')[0].id |
976 | 1107 |
event = Event.objects.filter(agenda_id=agenda_id)[0] |
... | ... | |
1033 | 1164 |
assert not event.id in [x['id'] for x in resp.json['data'] if not x.get('disabled')] |
1034 | 1165 |
assert event.id in [x['id'] for x in resp.json['data'] if x.get('disabled')] |
1035 | 1166 | |
1167 | ||
1036 | 1168 |
def test_waiting_list_booking(app, some_data, user): |
1037 | 1169 |
agenda_id = Agenda.objects.filter(label=u'Foo bar')[0].id |
1038 | 1170 |
event = Event.objects.filter(agenda_id=agenda_id).exclude(start_datetime__lt=now())[0] |
... | ... | |
1074 | 1206 |
assert resp.json['err_class'] == 'sold out' |
1075 | 1207 |
assert resp.json['err_desc'] == 'sold out' |
1076 | 1208 | |
1209 | ||
1077 | 1210 |
def test_accept_booking(app, some_data, user): |
1078 | 1211 |
agenda_id = Agenda.objects.filter(label=u'Foo bar')[0].id |
1079 | 1212 |
event = Event.objects.filter(agenda_id=agenda_id).exclude(start_datetime__lt=now())[0] |
... | ... | |
1107 | 1240 |
assert Booking.objects.filter(in_waiting_list=True).count() == 1 |
1108 | 1241 |
assert Booking.objects.filter(in_waiting_list=False).count() == 0 |
1109 | 1242 | |
1243 | ||
1110 | 1244 |
def test_multiple_booking_api(app, some_data, user): |
1111 | 1245 |
agenda = Agenda.objects.filter(label=u'Foo bar')[0] |
1112 | 1246 |
event = [x for x in Event.objects.filter(agenda=agenda) if x.in_bookable_period()][0] |
1113 | 1247 | |
1114 | 1248 |
resp_datetimes = app.get('/api/agenda/%s/datetimes/' % agenda.id) |
1115 |
event_fillslot_url = [x for x in resp_datetimes.json['data'] if x['id'] == event.id][0]['api']['fillslot_url'] |
|
1249 |
event_fillslot_url = [x for x in resp_datetimes.json['data'] if x['id'] == event.id][0]['api'][ |
|
1250 |
'fillslot_url' |
|
1251 |
] |
|
1116 | 1252 | |
1117 | 1253 |
app.authorization = ('Basic', ('john.doe', 'password')) |
1118 | 1254 |
resp = app.post('/api/agenda/%s/fillslot/%s/?count=NaN' % (agenda.slug, event.id), status=400) |
... | ... | |
1199 | 1335 |
assert Event.objects.get(id=event.id).booked_places == 3 |
1200 | 1336 |
assert Event.objects.get(id=event.id).waiting_list == 2 |
1201 | 1337 | |
1338 | ||
1202 | 1339 |
def test_multiple_booking_api_fillslots(app, some_data, user): |
1203 | 1340 |
agenda = Agenda.objects.filter(label=u'Foo bar')[0] |
1204 | 1341 |
# get slots of first 2 events |
1205 |
events = [x for x in Event.objects.filter(agenda=agenda).order_by('start_datetime') if x.in_bookable_period()][:2] |
|
1342 |
events = [ |
|
1343 |
x for x in Event.objects.filter(agenda=agenda).order_by('start_datetime') if x.in_bookable_period() |
|
1344 |
][:2] |
|
1206 | 1345 |
events_ids = [x.id for x in events] |
1207 | 1346 |
resp_datetimes = app.get('/api/agenda/%s/datetimes/' % agenda.id) |
1208 | 1347 |
slots = [x['id'] for x in resp_datetimes.json['data'] if x['id'] in events_ids] |
... | ... | |
1214 | 1353 |
assert resp.json['err_class'] == "invalid value for count (NaN)" |
1215 | 1354 |
assert resp.json['err_desc'] == "invalid value for count (NaN)" |
1216 | 1355 | |
1217 |
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, |
|
1218 |
params={'slots': slots, 'count': 'NaN'}, status=400) |
|
1356 |
resp = app.post( |
|
1357 |
'/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots, 'count': 'NaN'}, status=400 |
|
1358 |
) |
|
1219 | 1359 |
assert resp.json['err'] == 1 |
1220 | 1360 |
assert resp.json['reason'] == "invalid payload" # legacy |
1221 | 1361 |
assert resp.json['err_class'] == "invalid payload" |
... | ... | |
1223 | 1363 |
assert 'count' in resp.json['errors'] |
1224 | 1364 | |
1225 | 1365 |
# get 3 places on 2 slots |
1226 |
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, |
|
1227 |
params={'slots': slots, 'count': '3'}) |
|
1366 |
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots, 'count': '3'}) |
|
1228 | 1367 |
# one booking with 5 children |
1229 | 1368 |
booking = Booking.objects.get(id=resp.json['booking_id']) |
1230 | 1369 |
cancel_url = resp.json['api']['cancel_url'] |
... | ... | |
1236 | 1375 |
for event in events: |
1237 | 1376 |
assert Event.objects.get(id=event.id).booked_places == 3 |
1238 | 1377 | |
1239 |
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, |
|
1240 |
params={'slots': slots, 'count': 2}) |
|
1378 |
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots, 'count': 2}) |
|
1241 | 1379 |
for event in events: |
1242 | 1380 |
assert Event.objects.get(id=event.id).booked_places == 5 |
1243 | 1381 | |
... | ... | |
1251 | 1389 |
events[0].waiting_list_places = 8 |
1252 | 1390 |
events[0].save() |
1253 | 1391 | |
1254 |
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, |
|
1255 |
params={'slots': slots, 'count': 5}) |
|
1392 |
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots, 'count': 5}) |
|
1256 | 1393 |
for event in events: |
1257 | 1394 |
assert Event.objects.get(id=event.id).booked_places == 2 |
1258 | 1395 |
assert Event.objects.get(id=event.id).waiting_list == 5 |
... | ... | |
1260 | 1397 | |
1261 | 1398 |
return |
1262 | 1399 | |
1263 |
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, |
|
1264 |
params={'slots': slots, 'count': 5}) |
|
1400 |
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots, 'count': 5}) |
|
1265 | 1401 |
assert resp.json['err'] == 1 |
1266 | 1402 |
assert resp.json['reason'] == 'sold out' # legacy |
1267 | 1403 |
assert resp.json['err_class'] == 'sold out' |
... | ... | |
1283 | 1419 |
events[0].waiting_list_places = 2 |
1284 | 1420 |
events[0].save() |
1285 | 1421 | |
1286 |
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, |
|
1287 |
params={'slots': slots, 'count': 5}) |
|
1422 |
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots, 'count': 5}) |
|
1288 | 1423 |
assert resp.json['err'] == 1 |
1289 | 1424 |
assert resp.json['reason'] == 'sold out' # legacy |
1290 | 1425 |
assert resp.json['err_class'] == 'sold out' |
1291 | 1426 |
assert resp.json['err_desc'] == 'sold out' |
1292 | 1427 | |
1293 |
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, |
|
1294 |
params={'slots': slots, 'count': 3}) |
|
1428 |
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots, 'count': 3}) |
|
1295 | 1429 |
assert resp.json['err'] == 0 |
1296 | 1430 |
for event in events: |
1297 | 1431 |
assert Event.objects.get(id=event.id).booked_places == 3 |
1298 | 1432 |
assert Event.objects.get(id=event.id).waiting_list == 0 |
1299 | 1433 | |
1300 |
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, |
|
1301 |
params={'slots': slots, 'count': 3}) |
|
1434 |
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots, 'count': 3}) |
|
1302 | 1435 |
assert resp.json['err'] == 1 |
1303 | 1436 |
assert resp.json['reason'] == 'sold out' # legacy |
1304 | 1437 |
assert resp.json['err_class'] == 'sold out' |
1305 | 1438 |
assert resp.json['err_desc'] == 'sold out' |
1306 | 1439 | |
1307 |
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, |
|
1308 |
params={'slots': slots, 'count': '2'}) |
|
1440 |
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots, 'count': '2'}) |
|
1309 | 1441 |
assert resp.json['err'] == 0 |
1310 | 1442 |
for event in events: |
1311 | 1443 |
assert Event.objects.get(id=event.id).booked_places == 3 |
1312 | 1444 |
assert Event.objects.get(id=event.id).waiting_list == 2 |
1313 | 1445 | |
1446 | ||
1314 | 1447 |
def test_agenda_detail_api(app, some_data): |
1315 | 1448 |
agenda = Agenda.objects.get(slug='foo-bar') |
1316 | 1449 |
resp = app.get('/api/agenda/%s/' % agenda.slug) |
... | ... | |
1324 | 1457 |
# unknown |
1325 | 1458 |
app.get('/api/agenda/whatever/', status=404) |
1326 | 1459 | |
1460 | ||
1327 | 1461 |
def test_agenda_api_date_range(app, some_data): |
1328 | 1462 |
# test range limitation |
1329 | 1463 |
agenda2 = Agenda.objects.get(slug='foo-bar2') |
... | ... | |
1339 | 1473 |
day_events = ['8:00'] |
1340 | 1474 |
day = base_date + datetime.timedelta(days=idx) |
1341 | 1475 |
for event in day_events: |
1342 |
event_dt = datetime.datetime.combine( |
|
1343 |
day, datetime.datetime.strptime(event, '%H:%M').time()) |
|
1476 |
event_dt = datetime.datetime.combine(day, datetime.datetime.strptime(event, '%H:%M').time()) |
|
1344 | 1477 |
Event.objects.create(agenda=agenda2, start_datetime=make_aware(event_dt), places=2) |
1345 | 1478 | |
1346 | 1479 |
params = {'date_start': base_date.isoformat()} |
... | ... | |
1380 | 1513 |
assert resp.json['data'][0]['datetime'] == '2017-05-30 09:00:00' |
1381 | 1514 |
assert resp.json['data'][-1]['datetime'] == '2017-05-30 11:00:00' |
1382 | 1515 | |
1516 | ||
1383 | 1517 |
def test_agenda_meeting_api_multiple_desk(app, meetings_agenda, user): |
1384 | 1518 |
app.authorization = ('Basic', ('john.doe', 'password')) |
1385 | 1519 |
agenda_id = meetings_agenda.slug |
... | ... | |
1396 | 1530 |
time_period = meetings_agenda.desk_set.first().timeperiod_set.first() |
1397 | 1531 |
desk2 = Desk.objects.create(label='Desk 2', agenda=meetings_agenda) |
1398 | 1532 |
TimePeriod.objects.create( |
1399 |
start_time=time_period.start_time, end_time=time_period.end_time, |
|
1400 |
weekday=time_period.weekday, desk=desk2) |
|
1533 |
start_time=time_period.start_time, |
|
1534 |
end_time=time_period.end_time, |
|
1535 |
weekday=time_period.weekday, |
|
1536 |
desk=desk2, |
|
1537 |
) |
|
1401 | 1538 | |
1402 | 1539 |
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) |
1403 | 1540 |
event_id = resp.json['data'][1]['id'] |
1404 | 1541 |
resp_booking = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id)) |
1405 | 1542 |
assert Booking.objects.count() == 2 |
1406 |
assert resp_booking.json['datetime'] == localtime(Booking.objects.last().event.start_datetime |
|
1407 |
).strftime('%Y-%m-%d %H:%M:%S') |
|
1543 |
assert resp_booking.json['datetime'] == localtime(Booking.objects.last().event.start_datetime).strftime( |
|
1544 |
'%Y-%m-%d %H:%M:%S' |
|
1545 |
) |
|
1408 | 1546 | |
1409 | 1547 |
resp2 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) |
1410 | 1548 |
assert len(resp.json['data']) == len([x for x in resp2.json['data'] if not x['disabled']]) + 1 |
... | ... | |
1430 | 1568 |
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id)) |
1431 | 1569 |
queries_count_fillslot1 = len(ctx.captured_queries) |
1432 | 1570 | |
1433 |
assert resp_booking.json['datetime'] == localtime(Booking.objects.last().event.start_datetime |
|
1434 |
).strftime('%Y-%m-%d %H:%M:%S') |
|
1571 |
assert resp_booking.json['datetime'] == localtime(Booking.objects.last().event.start_datetime).strftime( |
|
1572 |
'%Y-%m-%d %H:%M:%S' |
|
1573 |
) |
|
1435 | 1574 |
cancel_url = resp.json['api']['cancel_url'] |
1436 | 1575 | |
1437 | 1576 |
resp3 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) |
... | ... | |
1446 | 1585 |
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id)) |
1447 | 1586 |
assert Booking.objects.count() == 4 |
1448 | 1587 |
assert Booking.objects.exclude(cancellation_datetime__isnull=True).count() == 2 |
1449 |
assert resp_booking.json['datetime'] == localtime(Booking.objects.last().event.start_datetime |
|
1450 |
).strftime('%Y-%m-%d %H:%M:%S') |
|
1588 |
assert resp_booking.json['datetime'] == localtime(Booking.objects.last().event.start_datetime).strftime( |
|
1589 |
'%Y-%m-%d %H:%M:%S' |
|
1590 |
) |
|
1451 | 1591 | |
1452 | 1592 |
# try booking the same timeslot again and fail |
1453 | 1593 |
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id)) |
... | ... | |
1467 | 1607 |
app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) |
1468 | 1608 |
assert queries_count_datetime1 == len(ctx.captured_queries) |
1469 | 1609 | |
1610 | ||
1470 | 1611 |
def test_agenda_meeting_api_fillslots_multiple_desks(app, meetings_agenda, user): |
1471 | 1612 |
app.authorization = ('Basic', ('john.doe', 'password')) |
1472 | 1613 |
agenda_id = meetings_agenda.slug |
... | ... | |
1476 | 1617 |
time_period = meetings_agenda.desk_set.first().timeperiod_set.first() |
1477 | 1618 |
desk2 = Desk.objects.create(label='Desk 2', agenda=meetings_agenda) |
1478 | 1619 |
TimePeriod.objects.create( |
1479 |
start_time=time_period.start_time, end_time=time_period.end_time, |
|
1480 |
weekday=time_period.weekday, desk=desk2) |
|
1620 |
start_time=time_period.start_time, |
|
1621 |
end_time=time_period.end_time, |
|
1622 |
weekday=time_period.weekday, |
|
1623 |
desk=desk2, |
|
1624 |
) |
|
1481 | 1625 | |
1482 | 1626 |
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) |
1483 | 1627 |
slots = [x['id'] for x in resp.json['data'][:3]] |
... | ... | |
1485 | 1629 |
def get_free_places(): |
1486 | 1630 |
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) |
1487 | 1631 |
return len([x for x in resp.json['data'] if not x['disabled']]) |
1632 | ||
1488 | 1633 |
start_free_places = get_free_places() |
1489 | 1634 | |
1490 | 1635 |
# booking 3 slots on desk 1 |
... | ... | |
1541 | 1686 |
assert resp.json['desk']['slug'] == desk1 |
1542 | 1687 |
assert get_free_places() == start_free_places - len(slots) |
1543 | 1688 | |
1689 | ||
1544 | 1690 |
def test_agenda_meeting_same_day(app, meetings_agenda, mock_now, user): |
1545 | 1691 |
app.authorization = ('Basic', ('john.doe', 'password')) |
1546 | 1692 |
agenda = Agenda(label='Foo', kind='meetings') |
... | ... | |
1553 | 1699 |
desk2 = Desk.objects.create(label='bar', agenda=agenda) |
1554 | 1700 |
for weekday in range(7): |
1555 | 1701 |
TimePeriod.objects.create( |
1556 |
weekday=weekday, start_time=datetime.time(11, 0), end_time=datetime.time(12, 30), |
|
1557 |
desk=desk1)
|
|
1702 |
weekday=weekday, start_time=datetime.time(11, 0), end_time=datetime.time(12, 30), desk=desk1
|
|
1703 |
) |
|
1558 | 1704 |
TimePeriod.objects.create( |
1559 |
weekday=weekday, start_time=datetime.time(11, 0), end_time=datetime.time(12, 30), |
|
1560 |
desk=desk2)
|
|
1705 |
weekday=weekday, start_time=datetime.time(11, 0), end_time=datetime.time(12, 30), desk=desk2
|
|
1706 |
) |
|
1561 | 1707 |
resp = app.get(datetime_url) |
1562 | 1708 |
event_data = resp.json['data'][0] |
1563 | 1709 |
# check first proposed date is on the same day unless we're past the last |
1564 | 1710 |
# open timeperiod. |
1565 | 1711 |
event_datetime = datetime.datetime.strptime(event_data['datetime'], '%Y-%m-%d %H:%M:%S').timetuple() |
1566 |
assert (event_datetime[:3] == mock_now.timetuple()[:3] and |
|
1567 |
event_datetime[3:5] >= mock_now.timetuple()[3:5]) or ( |
|
1568 |
event_datetime[:3] > mock_now.timetuple()[:3] and |
|
1569 |
event_datetime[3:5] < mock_now.timetuple()[3:5]) |
|
1712 |
assert ( |
|
1713 |
event_datetime[:3] == mock_now.timetuple()[:3] and event_datetime[3:5] >= mock_now.timetuple()[3:5] |
|
1714 |
) or (event_datetime[:3] > mock_now.timetuple()[:3] and event_datetime[3:5] < mock_now.timetuple()[3:5]) |
|
1570 | 1715 | |
1571 | 1716 |
# check booking works |
1572 | 1717 |
first_booking_url = resp.json['data'][0]['api']['fillslot_url'] |
... | ... | |
1598 | 1743 |
desk = Desk.objects.create(label='foo', agenda=agenda) |
1599 | 1744 |
for weekday in range(7): |
1600 | 1745 |
time_period = TimePeriod.objects.create( |
1601 |
weekday=weekday, start_time=datetime.time(11, 0), end_time=datetime.time(12, 30), |
|
1602 |
desk=desk)
|
|
1746 |
weekday=weekday, start_time=datetime.time(11, 0), end_time=datetime.time(12, 30), desk=desk
|
|
1747 |
) |
|
1603 | 1748 |
resp = app.get(datetime_url) |
1604 | 1749 |
event_data = resp.json['data'][0] |
1605 | 1750 |
# check all proposed dates are on the next day |
... | ... | |
1631 | 1776 |
desk = meetings_agenda.desk_set.first() |
1632 | 1777 |
# test exception at the lowest limit |
1633 | 1778 |
excp1 = TimePeriodException.objects.create( |
1634 |
desk=desk, start_datetime=make_aware(datetime.datetime(2017, 5, 22, 10, 0)), |
|
1635 |
end_datetime=make_aware(datetime.datetime(2017, 5, 22, 12, 0))) |
|
1779 |
desk=desk, |
|
1780 |
start_datetime=make_aware(datetime.datetime(2017, 5, 22, 10, 0)), |
|
1781 |
end_datetime=make_aware(datetime.datetime(2017, 5, 22, 12, 0)), |
|
1782 |
) |
|
1636 | 1783 |
resp2 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) |
1637 | 1784 |
assert len(resp.json['data']) == len(resp2.json['data']) + 4 |
1638 | 1785 | |
... | ... | |
1650 | 1797 |
TimePeriodException.objects.create( |
1651 | 1798 |
desk=excp1.desk, |
1652 | 1799 |
start_datetime=make_aware(datetime.datetime(2017, 5, 22, 15, 0)), |
1653 |
end_datetime=make_aware(datetime.datetime(2017, 5, 23, 9, 0))) |
|
1800 |
end_datetime=make_aware(datetime.datetime(2017, 5, 23, 9, 0)), |
|
1801 |
) |
|
1654 | 1802 | |
1655 | 1803 |
resp2 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) |
1656 | 1804 |
assert len(resp.json['data']) == len(resp2.json['data']) + 6 |
... | ... | |
1659 | 1807 |
desk2 = Desk.objects.create(label='Desk 2', agenda=meetings_agenda) |
1660 | 1808 |
time_period = desk.timeperiod_set.first() |
1661 | 1809 |
TimePeriod.objects.create( |
1662 |
desk=desk2, start_time=time_period.start_time, end_time=time_period.end_time, |
|
1663 |
weekday=time_period.weekday) |
|
1810 |
desk=desk2, |
|
1811 |
start_time=time_period.start_time, |
|
1812 |
end_time=time_period.end_time, |
|
1813 |
weekday=time_period.weekday, |
|
1814 |
) |
|
1664 | 1815 |
resp3 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) |
1665 | 1816 |
assert len(resp.json['data']) == len(resp3.json['data']) + 2 # +2 because excp1 changed |
1666 | 1817 | |
... | ... | |
1668 | 1819 |
TimePeriodException.objects.create( |
1669 | 1820 |
desk=desk2, |
1670 | 1821 |
start_datetime=make_aware(datetime.datetime(2017, 5, 22, 9, 0)), |
1671 |
end_datetime=make_aware(datetime.datetime(2017, 5, 22, 12, 0))) |
|
1822 |
end_datetime=make_aware(datetime.datetime(2017, 5, 22, 12, 0)), |
|
1823 |
) |
|
1672 | 1824 |
booking_url = resp3.json['data'][0]['api']['fillslot_url'] |
1673 | 1825 |
resp = app.post(booking_url) |
1674 | 1826 |
assert resp.json['err'] == 1 |
... | ... | |
1683 | 1835 |
TimePeriodException.objects.create( |
1684 | 1836 |
desk=desk, |
1685 | 1837 |
start_datetime=make_aware(datetime.datetime(2017, 5, 22, 10, 0)), |
1686 |
end_datetime=make_aware(datetime.datetime(2017, 5, 22, 12, 0))) |
|
1838 |
end_datetime=make_aware(datetime.datetime(2017, 5, 22, 12, 0)), |
|
1839 |
) |
|
1687 | 1840 |
resp2 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) |
1688 | 1841 |
assert len(resp.json['data']) == len(resp2.json['data']) + 4 |
1689 | 1842 |
# exclude slots on 2017-05-30 and 2017-07-10 |
1690 | 1843 |
date_2017_05_30 = datetime.datetime(2017, 5, 30).date() |
1691 | 1844 |
date_2017_07_10 = datetime.datetime(2017, 7, 10).date() |
1692 |
count_on_2017_05_30 = len([ |
|
1693 |
datum for datum in resp.json['data'] if datetime_from_str(datum['datetime']).date() == date_2017_05_30]) |
|
1694 |
count_on_2017_07_10 = len([ |
|
1695 |
datum for datum in resp.json['data'] if datetime_from_str(datum['datetime']).date() == date_2017_07_10]) |
|
1845 |
count_on_2017_05_30 = len( |
|
1846 |
[ |
|
1847 |
datum |
|
1848 |
for datum in resp.json['data'] |
|
1849 |
if datetime_from_str(datum['datetime']).date() == date_2017_05_30 |
|
1850 |
] |
|
1851 |
) |
|
1852 |
count_on_2017_07_10 = len( |
|
1853 |
[ |
|
1854 |
datum |
|
1855 |
for datum in resp.json['data'] |
|
1856 |
if datetime_from_str(datum['datetime']).date() == date_2017_07_10 |
|
1857 |
] |
|
1858 |
) |
|
1696 | 1859 |
TimePeriodException.objects.create( |
1697 | 1860 |
desk=desk, |
1698 | 1861 |
start_datetime=make_aware(datetime.datetime(2017, 5, 30, 8, 0)), |
1699 |
end_datetime=make_aware(datetime.datetime(2017, 5, 30, 18, 0))) |
|
1862 |
end_datetime=make_aware(datetime.datetime(2017, 5, 30, 18, 0)), |
|
1863 |
) |
|
1700 | 1864 |
TimePeriodException.objects.create( |
1701 | 1865 |
desk=desk, |
1702 | 1866 |
start_datetime=make_aware(datetime.datetime(2017, 7, 10, 8, 0)), |
1703 |
end_datetime=make_aware(datetime.datetime(2017, 7, 10, 18, 0))) |
|
1867 |
end_datetime=make_aware(datetime.datetime(2017, 7, 10, 18, 0)), |
|
1868 |
) |
|
1704 | 1869 |
resp3 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) |
1705 | 1870 |
assert len(resp2.json['data']) == len(resp3.json['data']) + count_on_2017_05_30 + count_on_2017_07_10 |
1706 |
assert len([datum for datum in resp3.json['data'] if datetime_from_str(datum['datetime']).date() == date_2017_05_30]) == 0 |
|
1707 |
assert len([datum for datum in resp3.json['data'] if datetime_from_str(datum['datetime']).date() == date_2017_07_10]) == 0 |
|
1871 |
assert ( |
|
1872 |
len( |
|
1873 |
[ |
|
1874 |
datum |
|
1875 |
for datum in resp3.json['data'] |
|
1876 |
if datetime_from_str(datum['datetime']).date() == date_2017_05_30 |
|
1877 |
] |
|
1878 |
) |
|
1879 |
== 0 |
|
1880 |
) |
|
1881 |
assert ( |
|
1882 |
len( |
|
1883 |
[ |
|
1884 |
datum |
|
1885 |
for datum in resp3.json['data'] |
|
1886 |
if datetime_from_str(datum['datetime']).date() == date_2017_07_10 |
|
1887 |
] |
|
1888 |
) |
|
1889 |
== 0 |
|
1890 |
) |
|
1708 | 1891 |
# with a second desk with the same time periods |
1709 | 1892 |
desk2 = Desk.objects.create(label='Desk 2', agenda=meetings_agenda) |
1710 | 1893 |
for time_period in desk.timeperiod_set.all(): |
1711 | 1894 |
TimePeriod.objects.create( |
1712 |
desk=desk2, start_time=time_period.start_time, end_time=time_period.end_time, |
|
1713 |
weekday=time_period.weekday) |
|
1895 |
desk=desk2, |
|
1896 |
start_time=time_period.start_time, |
|
1897 |
end_time=time_period.end_time, |
|
1898 |
weekday=time_period.weekday, |
|
1899 |
) |
|
1714 | 1900 |
resp4 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) |
1715 | 1901 |
assert len(resp.json['data']) == len(resp4.json['data']) |
1716 | 1902 | |
... | ... | |
1722 | 1908 |
desk2 = Desk.objects.create(label='Desk 2', agenda=meetings_agenda) |
1723 | 1909 |
for time_period in desk.timeperiod_set.all(): |
1724 | 1910 |
TimePeriod.objects.create( |
1725 |
desk=desk2, start_time=time_period.start_time, end_time=time_period.end_time, |
|
1726 |
weekday=time_period.weekday) |
|
1911 |
desk=desk2, |
|
1912 |
start_time=time_period.start_time, |
|
1913 |
end_time=time_period.end_time, |
|
1914 |
weekday=time_period.weekday, |
|
1915 |
) |
|
1727 | 1916 |
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) |
1728 | 1917 |
booking_url = resp.json['data'][0]['api']['fillslot_url'] |
1729 | 1918 |
booking_url2 = resp.json['data'][3]['api']['fillslot_url'] |
... | ... | |
1792 | 1981 |
assert Booking.objects.count() == 4 |
1793 | 1982 | |
1794 | 1983 |
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_20.id) |
1795 |
assert [x for x in resp.json['data'] if not x.get('disabled')][0]['datetime'].startswith('2017-05-22 12:00:00') |
|
1984 |
assert [x for x in resp.json['data'] if not x.get('disabled')][0]['datetime'].startswith( |
|
1985 |
'2017-05-22 12:00:00' |
|
1986 |
) |
|
1796 | 1987 |
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_30.id) |
1797 |
assert [x for x in resp.json['data'] if not x.get('disabled')][0]['datetime'].startswith('2017-05-22 12:00:00') |
|
1988 |
assert [x for x in resp.json['data'] if not x.get('disabled')][0]['datetime'].startswith( |
|
1989 |
'2017-05-22 12:00:00' |
|
1990 |
) |
|
1798 | 1991 | |
1799 | 1992 | |
1800 | 1993 |
def test_agenda_meeting_gcd_durations_and_exceptions(app, meetings_agenda, user): |
... | ... | |
1820 | 2013 |
TimePeriodException.objects.create( |
1821 | 2014 |
desk=desk, |
1822 | 2015 |
start_datetime=make_aware(datetime.datetime(2017, 5, 22, 10, 20)), |
1823 |
end_datetime=make_aware(datetime.datetime(2017, 5, 22, 12, 0))) |
|
2016 |
end_datetime=make_aware(datetime.datetime(2017, 5, 22, 12, 0)), |
|
2017 |
) |
|
1824 | 2018 | |
1825 | 2019 |
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type_20.id) |
1826 | 2020 |
assert len(resp.json['data']) == 1 |
... | ... | |
1830 | 2024 | |
1831 | 2025 |
def test_datetimes_api_meetings_agenda_start_hour_change(app, meetings_agenda): |
1832 | 2026 |
meeting_type = MeetingType.objects.get(agenda=meetings_agenda) |
1833 |
api_url = '/api/agenda/%s/meetings/%s/datetimes/' % ( |
|
1834 |
meeting_type.agenda.slug, meeting_type.slug) |
|
2027 |
api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (meeting_type.agenda.slug, meeting_type.slug) |
|
1835 | 2028 | |
1836 | 2029 |
resp = app.get(api_url) |
1837 | 2030 |
dt = datetime.datetime.strptime(resp.json['data'][2]['id'].split(':')[1], '%Y-%m-%d-%H%M') |
1838 |
ev = Event(agenda=meetings_agenda, meeting_type=meeting_type, |
|
1839 |
places=1, full=False, start_datetime=make_aware(dt), desk=Desk.objects.first()) |
|
2031 |
ev = Event( |
|
2032 |
agenda=meetings_agenda, |
|
2033 |
meeting_type=meeting_type, |
|
2034 |
places=1, |
|
2035 |
full=False, |
|
2036 |
start_datetime=make_aware(dt), |
|
2037 |
desk=Desk.objects.first(), |
|
2038 |
) |
|
1840 | 2039 |
ev.save() |
1841 | 2040 |
booking = Booking(event=ev) |
1842 | 2041 |
booking.save() |
tests/test_api_utils.py | ||
---|---|---|
3 | 3 |
from chrono.api.utils import Response |
4 | 4 | |
5 | 5 | |
6 |
@pytest.mark.parametrize('data, expected', [ |
|
7 |
(None, None), |
|
8 |
({}, {}), |
|
9 |
({'reason': 'foo'}, {'reason': 'foo'}), |
|
10 |
({'err_class': 'foo'}, {'err_class': 'foo', 'reason': 'foo'}), |
|
11 |
({'bar': 'foo'}, {'bar': 'foo'}), |
|
12 |
]) |
|
6 |
@pytest.mark.parametrize( |
|
7 |
'data, expected', |
|
8 |
[ |
|
9 |
(None, None), |
|
10 |
({}, {}), |
|
11 |
({'reason': 'foo'}, {'reason': 'foo'}), |
|
12 |
({'err_class': 'foo'}, {'err_class': 'foo', 'reason': 'foo'}), |
|
13 |
({'bar': 'foo'}, {'bar': 'foo'}), |
|
14 |
], |
|
15 |
) |
|
13 | 16 |
def test_response_data(data, expected): |
14 | 17 |
resp = Response(data=data) |
15 | 18 |
assert resp.data == expected |
tests/test_data_migrations.py | ||
---|---|---|
1 | 1 |
import datetime |
2 |
import pytest |
|
3 | 2 | |
4 | 3 |
import django |
4 |
import pytest |
|
5 | 5 |
from django.db import connection |
6 | 6 |
from django.db.migrations.executor import MigrationExecutor |
7 | 7 |
from django.utils.timezone import make_aware |
... | ... | |
52 | 52 |
Event = old_apps.get_model(app, 'Event') |
53 | 53 |
agenda = Agenda.objects.create(label='foo', slug='foo', kind='meetings') |
54 | 54 |
agenda2 = Agenda.objects.create(label='bar', slug='bar', kind='events') |
55 |
TimePeriod.objects.create(agenda=agenda, weekday=1, |
|
56 |
start_time=datetime.time(8, 0), |
|
57 |
end_time=datetime.time(12, 0)) |
|
58 |
TimePeriod.objects.create(agenda=agenda, weekday=2, |
|
59 |
start_time=datetime.time(8, 0), |
|
60 |
end_time=datetime.time(10, 0)) |
|
61 |
TimePeriod.objects.create(agenda=agenda, weekday=3, |
|
62 |
start_time=datetime.time(9, 0), |
|
63 |
end_time=datetime.time(12, 0)) |
|
64 |
meeting_type = MeetingType.objects.create(agenda=agenda, label='foo', |
|
65 |
slug='foo', duration=60) |
|
66 |
Event.objects.create(agenda=agenda, places=1, meeting_type=meeting_type, |
|
67 |
start_datetime=make_aware(datetime.datetime(2017, 5, 22, 9, 30))) |
|
68 |
Event.objects.create(agenda=agenda, places=1, meeting_type=meeting_type, |
|
69 |
start_datetime=make_aware(datetime.datetime(2017, 5, 22, 10, 0))) |
|
70 |
Event.objects.create(agenda=agenda2, places=5, |
|
71 |
start_datetime=make_aware(datetime.datetime(2017, 5, 22, 10, 0))) |
|
55 |
TimePeriod.objects.create( |
|
56 |
agenda=agenda, weekday=1, start_time=datetime.time(8, 0), end_time=datetime.time(12, 0) |
|
57 |
) |
|
58 |
TimePeriod.objects.create( |
|
59 |
agenda=agenda, weekday=2, start_time=datetime.time(8, 0), end_time=datetime.time(10, 0) |
|
60 |
) |
|
61 |
TimePeriod.objects.create( |
|
62 |
agenda=agenda, weekday=3, start_time=datetime.time(9, 0), end_time=datetime.time(12, 0) |
|
63 |
) |
|
64 |
meeting_type = MeetingType.objects.create(agenda=agenda, label='foo', slug='foo', duration=60) |
|
65 |
Event.objects.create( |
|
66 |
agenda=agenda, |
|
67 |
places=1, |
|
68 |
meeting_type=meeting_type, |
|
69 |
start_datetime=make_aware(datetime.datetime(2017, 5, 22, 9, 30)), |
|
70 |
) |
|
71 |
Event.objects.create( |
|
72 |
agenda=agenda, |
|
73 |
places=1, |
|
74 |
meeting_type=meeting_type, |
|
75 |
start_datetime=make_aware(datetime.datetime(2017, 5, 22, 10, 0)), |
|
76 |
) |
|
77 |
Event.objects.create( |
|
78 |
agenda=agenda2, places=5, start_datetime=make_aware(datetime.datetime(2017, 5, 22, 10, 0)) |
|
79 |
) |
|
72 | 80 |
executor.loader.build_graph() |
73 | 81 |
executor.migrate(migrate_to) |
74 | 82 |
new_apps = executor.loader.project_state(migrate_to).apps |
tests/test_import_export.py | ||
---|---|---|
3 | 3 |
from __future__ import unicode_literals |
4 | 4 | |
5 | 5 |
import datetime |
6 |
from io import StringIO |
|
7 | 6 |
import json |
8 | 7 |
import os |
9 | 8 |
import shutil |
10 | 9 |
import sys |
11 | 10 |
import tempfile |
11 |
from io import StringIO |
|
12 | 12 | |
13 | 13 |
import pytest |
14 | 14 |
from django.contrib.auth.models import Group |
15 |
from django.core.management import call_command, CommandError
|
|
15 |
from django.core.management import CommandError, call_command
|
|
16 | 16 |
from django.utils.encoding import force_bytes |
17 | 17 |
from django.utils.timezone import make_aware |
18 | 18 | |
19 |
from chrono.agendas.models import (Agenda, Event, TimePeriod, Desk, |
|
20 |
TimePeriodException, AgendaImportError) |
|
19 |
from chrono.agendas.models import Agenda, AgendaImportError, Desk, Event, TimePeriod, TimePeriodException |
|
21 | 20 |
from chrono.manager.utils import import_site |
22 | ||
23 |
from test_api import some_data, meetings_agenda, time_zone, mock_now |
|
21 |
from test_api import meetings_agenda, mock_now, some_data, time_zone |
|
24 | 22 | |
25 | 23 |
pytestmark = pytest.mark.django_db |
26 | 24 | |
... | ... | |
40 | 38 |
desk = meetings_agenda.desk_set.first() |
41 | 39 |
tpx_start = make_aware(datetime.datetime(2017, 5, 22, 8, 0)) |
42 | 40 |
tpx_end = make_aware(datetime.datetime(2017, 5, 22, 12, 30)) |
43 |
TimePeriodException.objects.create( |
|
44 |
desk=desk, |
|
45 |
start_datetime=tpx_start, |
|
46 |
end_datetime=tpx_end) |
|
41 |
TimePeriodException.objects.create(desk=desk, start_datetime=tpx_start, end_datetime=tpx_end) |
|
47 | 42 |
output = get_output_of_command('export_site') |
48 | 43 |
assert len(json.loads(output)['agendas']) == 3 |
49 | 44 |
import_site(data={}, clean=True) |
... | ... | |
77 | 72 |
event.save() |
78 | 73 |
desk, _ = Desk.objects.get_or_create(agenda=agenda2, label='Desk A', slug='desk-a') |
79 | 74 |
timeperiod = TimePeriod( |
80 |
desk=desk, |
|
81 |
weekday=2, |
|
82 |
start_time=datetime.time(10, 0), |
|
83 |
end_time=datetime.time(11, 0)) |
|
75 |
desk=desk, weekday=2, start_time=datetime.time(10, 0), end_time=datetime.time(11, 0) |
|
76 |
) |
|
84 | 77 |
timeperiod.save() |
85 | 78 |
exception = TimePeriodException( |
86 | 79 |
desk=desk, |
87 | 80 |
start_datetime=make_aware(datetime.datetime(2017, 5, 22, 8, 0)), |
88 |
end_datetime=make_aware(datetime.datetime(2017, 5, 22, 12, 30))) |
|
81 |
end_datetime=make_aware(datetime.datetime(2017, 5, 22, 12, 30)), |
|
82 |
) |
|
89 | 83 |
exception.save() |
90 | 84 | |
91 | 85 |
import_site(json.loads(output), overwrite=True) |
... | ... | |
97 | 91 |
event = Event(agenda=agenda1, start_datetime=make_aware(datetime.datetime.now()), places=10) |
98 | 92 |
event.save() |
99 | 93 |
timeperiod = TimePeriod( |
100 |
weekday=2, |
|
101 |
desk=desk, |
|
102 |
start_time=datetime.time(10, 0), |
|
103 |
end_time=datetime.time(11, 0)) |
|
94 |
weekday=2, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(11, 0) |
|
95 |
) |
|
104 | 96 |
timeperiod.save() |
105 | 97 |
exception = TimePeriodException( |
106 | 98 |
desk=desk, |
107 | 99 |
start_datetime=make_aware(datetime.datetime(2017, 5, 22, 8, 0)), |
108 |
end_datetime=make_aware(datetime.datetime(2017, 5, 22, 12, 30))) |
|
100 |
end_datetime=make_aware(datetime.datetime(2017, 5, 22, 12, 30)), |
|
101 |
) |
|
109 | 102 |
exception.save() |
110 | 103 |
import_site(json.loads(output), overwrite=False) |
111 | 104 |
assert Event.objects.filter(id=event.id).count() == 1 |
tests/test_manager.py | ||
---|---|---|
1 | 1 |
# -*- coding: utf-8 -*- |
2 | 2 | |
3 | 3 |
from __future__ import unicode_literals |
4 | ||
4 | 5 |
import copy |
6 |
import datetime |
|
5 | 7 |
import json |
6 | 8 | |
7 |
from django.contrib.auth.models import User, Group |
|
8 |
from django.utils.encoding import force_text |
|
9 |
from django.utils.timezone import make_aware, now, localtime |
|
10 |
import datetime |
|
11 | 9 |
import freezegun |
12 | 10 |
import mock |
13 | 11 |
import pytest |
14 | 12 |
import requests |
13 |
from django.contrib.auth.models import Group, User |
|
14 |
from django.utils.encoding import force_text |
|
15 |
from django.utils.timezone import localtime, make_aware, now |
|
15 | 16 |
from webtest import Upload |
16 | 17 | |
17 |
from chrono.wsgi import application |
|
18 | ||
19 |
from chrono.agendas.models import (Agenda, Event, Booking, MeetingType, |
|
20 |
TimePeriod, Desk, TimePeriodException) |
|
18 |
from chrono.agendas.models import Agenda, Booking, Desk, Event, MeetingType, TimePeriod, TimePeriodException |
|
21 | 19 |
from chrono.manager.utils import export_site |
20 |
from chrono.wsgi import application |
|
22 | 21 | |
23 | 22 |
pytestmark = pytest.mark.django_db |
24 | 23 | |
24 | ||
25 | 25 |
@pytest.fixture |
26 | 26 |
def simple_user(): |
27 | 27 |
try: |
... | ... | |
30 | 30 |
user = User.objects.create_user('user', password='user') |
31 | 31 |
return user |
32 | 32 | |
33 | ||
33 | 34 |
@pytest.fixture |
34 | 35 |
def manager_user(): |
35 | 36 |
try: |
... | ... | |
42 | 43 |
user.groups.set([group]) |
43 | 44 |
return user |
44 | 45 | |
46 | ||
45 | 47 |
@pytest.fixture |
46 | 48 |
def admin_user(): |
47 | 49 |
try: |
... | ... | |
50 | 52 |
user = User.objects.create_superuser('admin', email=None, password='admin') |
51 | 53 |
return user |
52 | 54 | |
55 | ||
53 | 56 |
@pytest.fixture |
54 | 57 |
def api_user(): |
55 | 58 |
try: |
56 | 59 |
user = User.objects.get(username='api-user') |
57 | 60 |
except User.DoesNotExist: |
58 |
user = User.objects.create(username='john.doe', |
|
59 |
first_name=u'John', last_name=u'Doe', email='john.doe@example.net') |
|
61 |
user = User.objects.create( |
|
62 |
username='john.doe', first_name=u'John', last_name=u'Doe', email='john.doe@example.net' |
|
63 |
) |
|
60 | 64 |
user.set_password('password') |
61 | 65 |
user.save() |
62 | 66 |
return user |
63 | 67 | |
68 | ||
64 | 69 |
def login(app, username='admin', password='admin'): |
65 | 70 |
login_page = app.get('/login/') |
66 | 71 |
login_form = login_page.forms[0] |
... | ... | |
70 | 75 |
assert resp.status_int == 302 |
71 | 76 |
return app |
72 | 77 | |
78 | ||
73 | 79 |
def test_unlogged_access(app): |
74 | 80 |
# connect while not being logged in |
75 | 81 |
assert app.get('/manage/', status=302).location.endswith('/login/?next=/manage/') |
76 | 82 | |
83 | ||
77 | 84 |
def test_simple_user_access(app, simple_user): |
78 | 85 |
# connect while being logged as a simple user, access should be forbidden |
79 | 86 |
app = login(app, username='user', password='user') |
80 | 87 |
assert app.get('/manage/', status=403) |
81 | 88 | |
89 | ||
82 | 90 |
def test_manager_user_access(app, manager_user): |
83 | 91 |
# connect while being logged as a manager user, access should be granted if |
84 | 92 |
# there's at least an agenda that is viewable or editable. |
... | ... | |
99 | 107 |
agenda.save() |
100 | 108 |
assert app.get('/manage/', status=200) |
101 | 109 | |
110 | ||
102 | 111 |
def test_home_redirect(app): |
103 | 112 |
assert app.get('/', status=302).location.endswith('/manage/') |
104 | 113 | |
114 | ||
105 | 115 |
def test_access(app, admin_user): |
106 | 116 |
app = login(app) |
107 | 117 |
resp = app.get('/manage/', status=200) |
108 | 118 |
assert '<h2>Agendas</h2>' in resp.text |
109 | 119 |
assert "This site doesn't have any agenda yet." in resp.text |
110 | 120 | |
121 | ||
111 | 122 |
def test_logout(app, admin_user): |
112 | 123 |
app = login(app) |
113 | 124 |
app.get('/logout/') |
114 | 125 |
assert app.get('/manage/', status=302).location.endswith('/login/?next=/manage/') |
115 | 126 | |
127 | ||
116 | 128 |
def test_menu_json(app, admin_user): |
117 | 129 |
app = login(app) |
118 | 130 |
resp = app.get('/manage/menu.json', status=200) |
... | ... | |
123 | 135 |
assert resp2.text == 'Q(%s);' % resp.text |
124 | 136 |
assert resp2.content_type == 'application/javascript' |
125 | 137 | |
138 | ||
126 | 139 |
def test_view_agendas_as_manager(app, manager_user): |
127 | 140 |
agenda = Agenda(label=u'Foo Bar') |
128 | 141 |
agenda.view_role = manager_user.groups.all()[0] |
... | ... | |
156 | 169 |
# check it gives a 404 on unknown agendas |
157 | 170 |
resp = app.get('/manage/agendas/%s/settings' % '9999', status=404) |
158 | 171 | |
172 | ||
159 | 173 |
def test_add_agenda(app, admin_user): |
160 | 174 |
app = login(app) |
161 | 175 |
resp = app.get('/manage/', status=200) |
... | ... | |
169 | 183 |
assert 'Foo bar' in resp.text |
170 | 184 |
assert '<h2>Settings' in resp.text |
171 | 185 | |
186 | ||
172 | 187 |
def test_add_agenda_as_manager(app, manager_user): |
173 | 188 |
# open /manage/ access to manager_user, and check agenda creation is not |
174 | 189 |
# allowed. |
... | ... | |
179 | 194 |
resp = app.get('/manage/', status=200) |
180 | 195 |
resp = app.get('/manage/agendas/add/', status=403) |
181 | 196 | |
197 | ||
182 | 198 |
def test_options_agenda(app, admin_user): |
183 | 199 |
agenda = Agenda(label=u'Foo bar') |
184 | 200 |
agenda.save() |
... | ... | |
194 | 210 |
assert 'Foo baz' in resp.text |
195 | 211 |
assert '<h2>Settings' in resp.text |
196 | 212 | |
213 | ||
197 | 214 |
def test_options_agenda_as_manager(app, manager_user): |
198 | 215 |
agenda = Agenda(label=u'Foo bar') |
199 | 216 |
agenda.view_role = manager_user.groups.all()[0] |
... | ... | |
227 | 244 |
assert 'Foo baz' in resp.text |
228 | 245 |
assert '<h2>Settings' in resp.text |
229 | 246 | |
247 | ||
230 | 248 |
def test_delete_agenda(app, admin_user): |
231 | 249 |
agenda = Agenda(label=u'Foo bar') |
232 | 250 |
agenda.save() |
... | ... | |
239 | 257 |
resp = resp.follow() |
240 | 258 |
assert not 'Foo bar' in resp.text |
241 | 259 | |
260 | ||
242 | 261 |
def test_delete_busy_agenda(app, admin_user): |
243 | 262 |
agenda = Agenda(label=u'Foo bar') |
244 | 263 |
agenda.save() |
245 |
event = Event(start_datetime=now() + datetime.timedelta(days=10), |
|
246 |
places=10, agenda=agenda) |
|
264 |
event = Event(start_datetime=now() + datetime.timedelta(days=10), places=10, agenda=agenda) |
|
247 | 265 |
event.save() |
248 | 266 | |
249 | 267 |
app = login(app) |
... | ... | |
272 | 290 |
booking.save() |
273 | 291 |
resp = resp.form.submit(status=403) |
274 | 292 | |
293 | ||
275 | 294 |
def test_delete_agenda_as_manager(app, manager_user): |
276 | 295 |
agenda = Agenda(label=u'Foo bar') |
277 | 296 |
agenda.edit_role = manager_user.groups.all()[0] |
... | ... | |
290 | 309 |
desk_a = Desk.objects.create(agenda=agenda, label='Desk A') |
291 | 310 |
desk_b = Desk.objects.create(agenda=agenda, label='Desk B') |
292 | 311 | |
293 |
event = Event(start_datetime=now() + datetime.timedelta(days=10), |
|
294 |
places=10, agenda=agenda, desk=desk_a) |
|
312 |
event = Event(start_datetime=now() + datetime.timedelta(days=10), places=10, agenda=agenda, desk=desk_a) |
|
295 | 313 |
event.save() |
296 | 314 | |
297 | 315 |
app = login(app) |
... | ... | |
354 | 372 |
app = login(app) |
355 | 373 |
app.get('/manage/agendas/%s/add-event' % '999', status=404) |
356 | 374 | |
375 | ||
357 | 376 |
def test_add_event_as_manager(app, manager_user): |
358 | 377 |
agenda = Agenda(label=u'Foo bar') |
359 | 378 |
agenda.view_role = manager_user.groups.all()[0] |
... | ... | |
378 | 397 |
assert 'Feb. 15, 2016, 5 p.m.' in resp.text |
379 | 398 |
assert '10 places' in resp.text |
380 | 399 | |
400 | ||
381 | 401 |
def test_edit_event(app, admin_user): |
382 | 402 |
agenda = Agenda(label=u'Foo bar') |
383 | 403 |
agenda.save() |
384 |
event = Event( |
|
385 |
start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)), |
|
386 |
places=20, agenda=agenda) |
|
404 |
event = Event(start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)), places=20, agenda=agenda) |
|
387 | 405 |
event.save() |
388 | 406 |
app = login(app) |
389 | 407 |
resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=200) |
... | ... | |
397 | 415 |
assert 'Feb. 16, 2016, 5 p.m.' in resp.text |
398 | 416 |
assert '20 places' in resp.text |
399 | 417 | |
418 | ||
400 | 419 |
def test_edit_missing_event(app, admin_user): |
401 | 420 |
app = login(app) |
402 | 421 |
app.get('/manage/agendas/999/', status=404) |
403 | 422 | |
423 | ||
404 | 424 |
def test_edit_event_as_manager(app, manager_user): |
405 | 425 |
agenda = Agenda(label=u'Foo bar') |
406 | 426 |
agenda.view_role = manager_user.groups.all()[0] |
407 | 427 |
agenda.save() |
408 |
event = Event(start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)), |
|
409 |
places=20, agenda=agenda) |
|
428 |
event = Event(start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)), places=20, agenda=agenda) |
|
410 | 429 |
event.save() |
411 | 430 |
app = login(app, username='manager', password='manager') |
412 | 431 |
resp = app.get('/manage/events/%s/' % event.id, status=403) |
... | ... | |
424 | 443 |
assert 'Feb. 16, 2016, 5 p.m.' in resp.text |
425 | 444 |
assert '20 places' in resp.text |
426 | 445 | |
446 | ||
427 | 447 |
def test_booked_places(app, admin_user): |
428 | 448 |
agenda = Agenda(label=u'Foo bar') |
429 | 449 |
agenda.save() |
430 |
event = Event(start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)), |
|
431 |
places=10, agenda=agenda) |
|
450 |
event = Event(start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)), places=10, agenda=agenda) |
|
432 | 451 |
event.save() |
433 | 452 |
Booking(event=event).save() |
434 | 453 |
Booking(event=event).save() |
... | ... | |
437 | 456 |
assert '10 places' in resp.text |
438 | 457 |
assert '2 booked places' in resp.text |
439 | 458 | |
459 | ||
440 | 460 |
def test_event_classes(app, admin_user): |
441 | 461 |
agenda = Agenda(label=u'Foo bar') |
442 | 462 |
agenda.save() |
443 |
event = Event(start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)), |
|
444 |
places=10, agenda=agenda) |
|
463 |
event = Event(start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)), places=10, agenda=agenda) |
|
445 | 464 |
event.save() |
446 | 465 |
for i in range(2): |
447 | 466 |
Booking(event=event).save() |
... | ... | |
463 | 482 |
assert 'full' in resp.text |
464 | 483 |
assert 'overbooking' in resp.text |
465 | 484 | |
485 | ||
466 | 486 |
def test_delete_event(app, admin_user): |
467 | 487 |
agenda = Agenda(label=u'Foo bar') |
468 | 488 |
agenda.save() |
469 |
event = Event(start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)), |
|
470 |
places=10, agenda=agenda) |
|
489 |
event = Event(start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)), places=10, agenda=agenda) |
|
471 | 490 |
event.save() |
472 | 491 | |
473 | 492 |
app = login(app) |
... | ... | |
478 | 497 |
assert resp.location.endswith('/manage/agendas/%s/settings' % agenda.id) |
479 | 498 |
assert Event.objects.count() == 0 |
480 | 499 | |
500 | ||
481 | 501 |
def test_delete_busy_event(app, admin_user): |
482 | 502 |
agenda = Agenda(label=u'Foo bar') |
483 | 503 |
agenda.save() |
484 |
event = Event(start_datetime=now() + datetime.timedelta(days=10), |
|
485 |
places=10, agenda=agenda) |
|
504 |
event = Event(start_datetime=now() + datetime.timedelta(days=10), places=10, agenda=agenda) |
|
486 | 505 |
event.save() |
487 | 506 | |
488 | 507 |
app = login(app) |
... | ... | |
511 | 530 |
booking.save() |
512 | 531 |
resp = resp.form.submit(status=403) |
513 | 532 | |
533 | ||
514 | 534 |
def test_delete_event_as_manager(app, manager_user): |
515 | 535 |
agenda = Agenda(label=u'Foo bar') |
516 | 536 |
agenda.edit_role = manager_user.groups.all()[0] |
517 | 537 |
agenda.save() |
518 |
event = Event(start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)), |
|
519 |
places=10, agenda=agenda) |
|
538 |
event = Event(start_datetime=make_aware(datetime.datetime(2016, 2, 15, 17, 0)), places=10, agenda=agenda) |
|
520 | 539 |
event.save() |
521 | 540 | |
522 | 541 |
app = login(app, username='manager', password='manager') |
... | ... | |
527 | 546 |
assert resp.location.endswith('/manage/agendas/%s/settings' % agenda.id) |
528 | 547 |
assert Event.objects.count() == 0 |
529 | 548 | |
549 | ||
530 | 550 |
def test_import_events(app, admin_user): |
531 | 551 |
agenda = Agenda(label=u'Foo bar') |
532 | 552 |
agenda.save() |
... | ... | |
590 | 610 |
Event.objects.all().delete() |
591 | 611 | |
592 | 612 |
resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200) |
593 |
resp.form['events_csv_file'] = Upload('t.csv', |
|
594 |
u'2016-09-16,18:00,10,5,éléphant'.encode('utf-8'), 'text/csv') |
|
613 |
resp.form['events_csv_file'] = Upload( |
|
614 |
't.csv', u'2016-09-16,18:00,10,5,éléphant'.encode('utf-8'), 'text/csv' |
|
615 |
) |
|
595 | 616 |
resp = resp.form.submit(status=302) |
596 | 617 |
assert Event.objects.count() == 1 |
597 | 618 |
assert Event.objects.all()[0].start_datetime == make_aware(datetime.datetime(2016, 9, 16, 18, 0)) |
... | ... | |
601 | 622 |
Event.objects.all().delete() |
602 | 623 | |
603 | 624 |
resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200) |
604 |
resp.form['events_csv_file'] = Upload('t.csv', |
|
605 |
u'2016-09-16,18:00,10,5,éléphant'.encode('iso-8859-15'), 'text/csv') |
|
625 |
resp.form['events_csv_file'] = Upload( |
|
626 |
't.csv', u'2016-09-16,18:00,10,5,éléphant'.encode('iso-8859-15'), 'text/csv' |
|
627 |
) |
|
606 | 628 |
resp = resp.form.submit(status=302) |
607 | 629 |
assert Event.objects.count() == 1 |
608 | 630 |
assert Event.objects.all()[0].start_datetime == make_aware(datetime.datetime(2016, 9, 16, 18, 0)) |
... | ... | |
612 | 634 |
Event.objects.all().delete() |
613 | 635 | |
614 | 636 |
resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200) |
615 |
resp.form['events_csv_file'] = Upload('t.csv', |
|
616 |
u'2016-09-16,18:00,10,5,éléphant'.encode('eucjp'), 'text/csv') |
|
637 |
resp.form['events_csv_file'] = Upload( |
|
638 |
't.csv', u'2016-09-16,18:00,10,5,éléphant'.encode('eucjp'), 'text/csv' |
|
639 |
) |
|
617 | 640 |
resp = resp.form.submit(status=302) |
618 | 641 |
assert Event.objects.count() == 1 |
619 | 642 |
assert Event.objects.all()[0].start_datetime == make_aware(datetime.datetime(2016, 9, 16, 18, 0)) |
... | ... | |
623 | 646 |
Event.objects.all().delete() |
624 | 647 | |
625 | 648 |
resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200) |
626 |
resp.form['events_csv_file'] = Upload('t.csv', b'date,time,etc.\n' |
|
627 |
b'2016-09-16,18:00,10,5,bla bla bla\n' |
|
628 |
b'\n' |
|
629 |
b'2016-09-19,18:00,10', 'text/csv') |
|
649 |
resp.form['events_csv_file'] = Upload( |
|
650 |
't.csv', |
|
651 |
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', |
|
652 |
'text/csv', |
|
653 |
) |
|
630 | 654 |
resp = resp.form.submit(status=302) |
631 | 655 |
assert Event.objects.count() == 2 |
632 | 656 |
Event.objects.all().delete() |
633 | 657 | |
634 | 658 |
resp = app.get('/manage/agendas/%s/import-events' % agenda.id, status=200) |
635 |
resp.form['events_csv_file'] = Upload('t.csv', '"date"\t"time"\t"etc."\n' |
|
636 |
'"2016-09-16"\t"18:00"\t"10"\t"5"\t"éléphant"\n' |
|
637 |
'"2016-09-19"\t"18:00"\t"10"'.encode('iso-8859-15'), 'text/csv') |
|
659 |
resp.form['events_csv_file'] = Upload( |
|
660 |
't.csv', |
|
661 |
'"date"\t"time"\t"etc."\n' |
|
662 |
'"2016-09-16"\t"18:00"\t"10"\t"5"\t"éléphant"\n' |
|
663 |
'"2016-09-19"\t"18:00"\t"10"'.encode('iso-8859-15'), |
|
664 |
'text/csv', |
|
665 |
) |
|
638 | 666 |
resp = resp.form.submit(status=302) |
639 | 667 |
assert Event.objects.count() == 2 |
640 | 668 |
Event.objects.all().delete() |
... | ... | |
671 | 699 |
agenda = Agenda.objects.get(label='Foo bar') |
672 | 700 |
assert agenda.kind == 'meetings' |
673 | 701 | |
702 | ||
674 | 703 |
def test_meetings_agenda_add_meeting_type(app, admin_user): |
675 | 704 |
agenda = Agenda(label=u'Foo bar', kind='meetings') |
676 | 705 |
agenda.save() |
... | ... | |
693 | 722 |
resp = resp.form.submit() |
694 | 723 |
assert MeetingType.objects.get(agenda=agenda).duration == 30 |
695 | 724 | |
725 | ||
696 | 726 |
def test_meetings_agenda_delete_meeting_type(app, admin_user): |
697 | 727 |
agenda = Agenda(label=u'Foo bar', kind='meetings') |
698 | 728 |
agenda.save() |
... | ... | |
709 | 739 |
assert resp.location.endswith('/manage/agendas/%s/settings' % agenda.id) |
710 | 740 |
assert MeetingType.objects.count() == 0 |
711 | 741 | |
742 | ||
712 | 743 |
def test_meetings_agenda_add_time_period(app, admin_user): |
713 | 744 |
agenda = Agenda(label=u'Foo bar', kind='meetings') |
714 | 745 |
agenda.save() |
... | ... | |
771 | 802 |
resp = resp.form.submit() |
772 | 803 |
assert TimePeriod.objects.filter(desk=desk).count() == 4 |
773 | 804 | |
805 | ||
774 | 806 |
def test_meetings_agenda_delete_time_period(app, admin_user): |
775 | 807 |
agenda = Agenda(label=u'Foo bar', kind='meetings') |
776 | 808 |
agenda.save() |
... | ... | |
778 | 810 |
MeetingType(agenda=agenda, label='Blah').save() |
779 | 811 | |
780 | 812 |
desk = Desk.objects.create(agenda=agenda, label='Desk A') |
781 |
time_period = TimePeriod(desk=desk, weekday=2,
|
|
782 |
start_time=datetime.time(10, 0),
|
|
783 |
end_time=datetime.time(18, 0))
|
|
813 |
time_period = TimePeriod( |
|
814 |
desk=desk, weekday=2, start_time=datetime.time(10, 0), end_time=datetime.time(18, 0)
|
|
815 |
) |
|
784 | 816 |
time_period.save() |
785 | 817 | |
786 | 818 |
app = login(app) |
... | ... | |
811 | 843 |
resp = app.get('/manage/agendas/%d/settings' % agenda.id, status=403) |
812 | 844 |
MeetingType(agenda=agenda, label='Blah').save() |
813 | 845 |
app.get('/manage/agendas/%d/desk/%d/add-time-period' % (agenda.id, desk.id), status=403) |
814 |
time_period = TimePeriod(desk=desk, weekday=0, start_time=datetime.time(9, 0), |
|
815 |
end_time=datetime.time(12, 0)) |
|
846 |
time_period = TimePeriod( |
|
847 |
desk=desk, weekday=0, start_time=datetime.time(9, 0), end_time=datetime.time(12, 0) |
|
848 |
) |
|
816 | 849 |
time_period.save() |
817 | 850 |
resp = app.get('/manage/agendas/%d/' % agenda.id) |
818 | 851 |
app.get('/manage/timeperiods/%d/edit' % time_period.id, status=403) |
... | ... | |
915 | 948 |
resp = resp.form.submit().follow() |
916 | 949 |
assert TimePeriodException.objects.count() == 1 |
917 | 950 |
time_period_exception = TimePeriodException.objects.first() |
918 |
assert localtime(time_period_exception.start_datetime).strftime(dt_format) == tomorrow.replace(hour=8).strftime(dt_format) |
|
919 |
assert localtime(time_period_exception.end_datetime).strftime(dt_format) == tomorrow.replace(hour=16).strftime(dt_format) |
|
951 |
assert localtime(time_period_exception.start_datetime).strftime(dt_format) == tomorrow.replace( |
|
952 |
hour=8 |
|
953 |
).strftime(dt_format) |
|
954 |
assert localtime(time_period_exception.end_datetime).strftime(dt_format) == tomorrow.replace( |
|
955 |
hour=16 |
|
956 |
).strftime(dt_format) |
|
920 | 957 |
# add an exception beyond 2 weeks and make sure it isn't listed |
921 | 958 |
resp = resp.click('Add a time period exception', index=1) |
922 | 959 |
future = tomorrow + datetime.timedelta(days=15) |
... | ... | |
927 | 964 |
assert TimePeriodException.objects.count() == 2 |
928 | 965 |
assert 'Exception 1' in resp.text |
929 | 966 |
assert 'Exception 2' not in resp.text |
930 |
resp = resp.click(href="/manage/time-period-exceptions/%d/exception-extract-list" % agenda.desk_set.first().pk) |
|
967 |
resp = resp.click( |
|
968 |
href="/manage/time-period-exceptions/%d/exception-extract-list" % agenda.desk_set.first().pk |
|
969 |
) |
|
931 | 970 |
assert 'Exception 1' in resp.text |
932 | 971 |
assert 'Exception 2' in resp.text |
933 | 972 | |
... | ... | |
936 | 975 |
agenda = Agenda.objects.create(label='Foo bar', kind='meetings') |
937 | 976 |
desk = Desk.objects.create(agenda=agenda, label='Desk A') |
938 | 977 |
MeetingType(agenda=agenda, label='Blah').save() |
939 |
TimePeriod.objects.create(weekday=1, desk=desk, |
|
940 |
start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)) |
|
941 |
event = Event.objects.create(agenda=agenda, places=1, desk=desk, |
|
942 |
start_datetime=make_aware(datetime.datetime(2017, 5, 22, 10, 30))) |
|
978 |
TimePeriod.objects.create( |
|
979 |
weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0) |
|
980 |
) |
|
981 |
event = Event.objects.create( |
|
982 |
agenda=agenda, places=1, desk=desk, start_datetime=make_aware(datetime.datetime(2017, 5, 22, 10, 30)) |
|
983 |
) |
|
943 | 984 |
Booking.objects.create(event=event) |
944 | 985 |
login(app) |
945 | 986 |
resp = app.get('/manage/agendas/%d/' % agenda.pk).follow() |
946 | 987 |
resp = resp.click('Settings') |
947 | 988 |
resp = resp.click('Add a time period exception') |
948 |
resp = resp.form.submit() # submit empty form |
|
989 |
resp = resp.form.submit() # submit empty form
|
|
949 | 990 |
# fields should be marked with errors |
950 | 991 |
assert resp.text.count('This field is required.') == 2 |
951 | 992 |
# try again with data in fields |
... | ... | |
957 | 998 | |
958 | 999 |
# check it's possible to add an exception on another desk |
959 | 1000 |
desk = Desk.objects.create(agenda=agenda, label='Desk B') |
960 |
TimePeriod.objects.create(weekday=1, desk=desk, |
|
961 |
start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)) |
|
1001 |
TimePeriod.objects.create( |
|
1002 |
weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0) |
|
1003 |
) |
|
962 | 1004 |
resp = app.get('/manage/agendas/%d/' % agenda.pk).follow() |
963 | 1005 |
resp = resp.click('Settings') |
964 | 1006 |
resp = resp.click('Add a time period exception', href='desk/%s/' % desk.id) |
... | ... | |
967 | 1009 |
resp = resp.form.submit() |
968 | 1010 |
assert TimePeriodException.objects.count() == 1 |
969 | 1011 | |
1012 | ||
970 | 1013 |
def test_meetings_agenda_add_time_period_exception_when_cancelled_booking_exists(app, admin_user): |
971 | 1014 |
agenda = Agenda.objects.create(label='Foo bar', kind='meetings') |
972 | 1015 |
desk = Desk.objects.create(agenda=agenda, label='Desk A') |
973 | 1016 |
MeetingType(agenda=agenda, label='Blah').save() |
974 |
TimePeriod.objects.create(weekday=1, desk=desk, |
|
975 |
start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)) |
|
976 |
event = Event.objects.create(agenda=agenda, places=1, |
|
977 |
start_datetime=make_aware(datetime.datetime(2017, 5, 22, 10, 30))) |
|
978 |
Booking.objects.create(event=event, |
|
979 |
cancellation_datetime=make_aware(datetime.datetime(2017, 5, 20, 10, 30))) |
|
1017 |
TimePeriod.objects.create( |
|
1018 |
weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0) |
|
1019 |
) |
|
1020 |
event = Event.objects.create( |
|
1021 |
agenda=agenda, places=1, start_datetime=make_aware(datetime.datetime(2017, 5, 22, 10, 30)) |
|
1022 |
) |
|
1023 |
Booking.objects.create( |
|
1024 |
event=event, cancellation_datetime=make_aware(datetime.datetime(2017, 5, 20, 10, 30)) |
|
1025 |
) |
|
980 | 1026 |
login(app) |
981 | 1027 |
resp = app.get('/manage/agendas/%d/' % agenda.pk).follow() |
982 | 1028 |
resp = resp.click('Settings') |
... | ... | |
987 | 1033 |
assert 'One or several bookings exists within this time slot.' not in resp.text |
988 | 1034 |
assert TimePeriodException.objects.count() == 1 |
989 | 1035 | |
1036 | ||
990 | 1037 |
def test_meetings_agenda_add_invalid_time_period_exception(app, admin_user): |
991 | 1038 |
agenda = Agenda.objects.create(label='Foo bar', kind='meetings') |
992 | 1039 |
desk = Desk.objects.create(agenda=agenda, label='Desk A') |
993 | 1040 |
MeetingType(agenda=agenda, label='Blah').save() |
994 |
TimePeriod.objects.create(weekday=1, desk=desk, |
|
995 |
start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)) |
|
1041 |
TimePeriod.objects.create( |
|
1042 |
weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0) |
|
1043 |
) |
|
996 | 1044 |
login(app) |
997 | 1045 |
resp = app.get('/manage/agendas/%d/' % agenda.pk).follow() |
998 | 1046 |
resp = resp.click('Settings') |
... | ... | |
1007 | 1055 |
agenda = Agenda.objects.create(label='Foo bar', kind='meetings') |
1008 | 1056 |
desk = Desk.objects.create(agenda=agenda, label='Desk A') |
1009 | 1057 |
MeetingType(agenda=agenda, label='Blah').save() |
1010 |
TimePeriod.objects.create(weekday=1, desk=desk, |
|
1011 |
start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)) |
|
1058 |
TimePeriod.objects.create( |
|
1059 |
weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0) |
|
1060 |
) |
|
1012 | 1061 |
login(app) |
1013 | 1062 |
resp = app.get('/manage/agendas/%d/' % agenda.pk).follow() |
1014 | 1063 |
resp = resp.click('Settings') |
... | ... | |
1022 | 1071 |
resp = resp.form.submit().follow() |
1023 | 1072 |
assert TimePeriodException.objects.count() == 1 |
1024 | 1073 |
time_period_exception = TimePeriodException.objects.first() |
1025 |
assert localtime(time_period_exception.start_datetime).strftime(dt_format) == tomorrow.replace(hour=8).strftime(dt_format) |
|
1026 |
assert localtime(time_period_exception.end_datetime).strftime(dt_format) == tomorrow.replace(hour=16).strftime(dt_format) |
|
1074 |
assert localtime(time_period_exception.start_datetime).strftime(dt_format) == tomorrow.replace( |
|
1075 |
hour=8 |
|
1076 |
).strftime(dt_format) |
|
1077 |
assert localtime(time_period_exception.end_datetime).strftime(dt_format) == tomorrow.replace( |
|
1078 |
hour=16 |
|
1079 |
).strftime(dt_format) |
|
1027 | 1080 |
resp = resp.click(href='/manage/time-period-exceptions/%d/edit' % time_period_exception.id) |
1028 | 1081 |
resp = resp.click('Delete') |
1029 | 1082 |
resp = resp.form.submit().follow() |
... | ... | |
1035 | 1088 |
label='Future Exception', |
1036 | 1089 |
desk=desk, |
1037 | 1090 |
start_datetime=now() + datetime.timedelta(days=1), |
1038 |
end_datetime=now() + datetime.timedelta(days=2)) |
|
1091 |
end_datetime=now() + datetime.timedelta(days=2), |
|
1092 |
) |
|
1039 | 1093 |
resp = app.get('/manage/time-period-exceptions/%d/exception-list' % desk.pk) |
1040 | 1094 |
resp = resp.click(href='/manage/time-period-exceptions/%d/delete' % time_period_exception.pk) |
1041 | 1095 |
resp = resp.form.submit( |
... | ... | |
1048 | 1102 |
agenda = Agenda.objects.create(label='Foo bar', kind='meetings') |
1049 | 1103 |
desk = Desk.objects.create(agenda=agenda, label='Desk A') |
1050 | 1104 |
MeetingType(agenda=agenda, label='Blah').save() |
1051 |
TimePeriod.objects.create(weekday=1, desk=desk, |
|
1052 |
start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)) |
|
1105 |
TimePeriod.objects.create( |
|
1106 |
weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0) |
|
1107 |
) |
|
1053 | 1108 |
past_exception = TimePeriodException.objects.create( |
1054 | 1109 |
label='Past Exception', |
1055 | 1110 |
desk=desk, |
1056 | 1111 |
start_datetime=now() - datetime.timedelta(days=2), |
1057 |
end_datetime=now() - datetime.timedelta(days=1)) |
|
1112 |
end_datetime=now() - datetime.timedelta(days=1), |
|
1113 |
) |
|
1058 | 1114 |
current_exception = TimePeriodException.objects.create( |
1059 | 1115 |
label='Current Exception', |
1060 | 1116 |
desk=desk, |
1061 | 1117 |
start_datetime=now() - datetime.timedelta(days=1), |
1062 |
end_datetime=now() + datetime.timedelta(days=1)) |
|
1118 |
end_datetime=now() + datetime.timedelta(days=1), |
|
1119 |
) |
|
1063 | 1120 |
future_exception = TimePeriodException.objects.create( |
1064 | 1121 |
label='Future Exception', |
1065 | 1122 |
desk=desk, |
1066 | 1123 |
start_datetime=now() + datetime.timedelta(days=1), |
1067 |
end_datetime=now() + datetime.timedelta(days=2)) |
|
1124 |
end_datetime=now() + datetime.timedelta(days=2), |
|
1125 |
) |
|
1068 | 1126 | |
1069 | 1127 |
login(app) |
1070 | 1128 |
resp = app.get('/manage/agendas/%d/settings' % agenda.pk) |
... | ... | |
1092 | 1150 |
resp = resp.click('Settings') |
1093 | 1151 |
assert 'Import exceptions from .ics' not in resp.text |
1094 | 1152 | |
1095 |
TimePeriod.objects.create(weekday=1, desk=desk, |
|
1096 |
start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)) |
|
1153 |
TimePeriod.objects.create( |
|
1154 |
weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0) |
|
1155 |
) |
|
1097 | 1156 | |
1098 | 1157 |
resp = app.get('/manage/agendas/%d/' % agenda.pk).follow() |
1099 | 1158 |
resp = resp.click('Settings') |
... | ... | |
1150 | 1209 |
agenda = Agenda.objects.create(label='Example', kind='meetings') |
1151 | 1210 |
desk = Desk.objects.create(agenda=agenda, label='Test Desk') |
1152 | 1211 |
MeetingType(agenda=agenda, label='Foo').save() |
1153 |
TimePeriod.objects.create(weekday=1, desk=desk,
|
|
1154 |
start_time=datetime.time(10, 0),
|
|
1155 |
end_time=datetime.time(12, 0))
|
|
1212 |
TimePeriod.objects.create( |
|
1213 |
weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)
|
|
1214 |
) |
|
1156 | 1215 |
login(app) |
1157 | 1216 |
resp = app.get('/manage/agendas/%d/' % agenda.pk).follow() |
1158 | 1217 |
resp = resp.click('Settings') |
... | ... | |
1182 | 1241 |
resp = resp.click('Settings') |
1183 | 1242 |
assert 'Import exceptions from .ics' not in resp.text |
1184 | 1243 | |
1185 |
TimePeriod.objects.create(weekday=1, desk=desk, |
|
1186 |
start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)) |
|
1244 |
TimePeriod.objects.create( |
|
1245 |
weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0) |
|
1246 |
) |
|
1187 | 1247 | |
1188 | 1248 |
resp = app.get('/manage/agendas/%d/' % agenda.pk).follow() |
1189 | 1249 |
resp = resp.click('Settings') |
... | ... | |
1193 | 1253 |
assert 'ics_url' in resp.form.fields |
1194 | 1254 |
resp.form['ics_url'] = 'http://example.com/foo.ics' |
1195 | 1255 |
mocked_response = mock.Mock() |
1196 |
mocked_response.text = """BEGIN:VCALENDAR
|
|
1256 |
mocked_response.text = """BEGIN:VCALENDAR |
|
1197 | 1257 |
VERSION:2.0 |
1198 | 1258 |
PRODID:-//foo.bar//EN |
1199 | 1259 |
BEGIN:VEVENT |
... | ... | |
1213 | 1273 |
resp = resp.click('upload') |
1214 | 1274 |
resp.form['ics_url'] = '' |
1215 | 1275 |
resp = resp.form.submit(status=302) |
1216 |
assert not TimePeriodException.objects.filter(desk=desk, |
|
1217 |
external_id='desk-%s:random-event-id' % desk.id).exists() |
|
1276 |
assert not TimePeriodException.objects.filter( |
|
1277 |
desk=desk, external_id='desk-%s:random-event-id' % desk.id |
|
1278 |
).exists() |
|
1279 | ||
1218 | 1280 | |
1219 | 1281 |
@mock.patch('chrono.agendas.models.requests.get') |
1220 | 1282 |
def test_agenda_import_time_period_exception_with_remote_ics_no_events(mocked_get, app, admin_user): |
... | ... | |
1226 | 1288 |
resp = resp.click('Settings') |
1227 | 1289 |
assert 'Import exceptions from .ics' not in resp.text |
1228 | 1290 | |
1229 |
TimePeriod.objects.create(weekday=1, desk=desk, |
|
1230 |
start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)) |
|
1291 |
TimePeriod.objects.create( |
|
1292 |
weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0) |
|
1293 |
) |
|
1231 | 1294 | |
1232 | 1295 |
resp = app.get('/manage/agendas/%d/' % agenda.pk).follow() |
1233 | 1296 |
resp = resp.click('Settings') |
1234 | 1297 |
resp = resp.click('upload') |
1235 | 1298 |
resp.form['ics_url'] = 'http://example.com/foo.ics' |
1236 | 1299 |
mocked_response = mock.Mock() |
1237 |
mocked_response.text = """BEGIN:VCALENDAR
|
|
1300 |
mocked_response.text = """BEGIN:VCALENDAR |
|
1238 | 1301 |
VERSION:2.0 |
1239 | 1302 |
PRODID:-//foo.bar//EN |
1240 | 1303 |
BEGIN:VEVENT |
... | ... | |
1257 | 1320 |
resp = resp.click('Settings') |
1258 | 1321 |
resp = resp.click('upload') |
1259 | 1322 |
resp = resp.form.submit(status=302) |
1260 |
assert not TimePeriodException.objects.filter(desk=desk, |
|
1261 |
external_id='random-event-id').exists() |
|
1323 |
assert not TimePeriodException.objects.filter(desk=desk, external_id='random-event-id').exists() |
|
1262 | 1324 | |
1263 | 1325 | |
1264 | 1326 |
@mock.patch('chrono.agendas.models.requests.get') |
... | ... | |
1271 | 1333 |
resp = resp.click('Settings') |
1272 | 1334 |
assert 'Import exceptions from .ics' not in resp.text |
1273 | 1335 | |
1274 |
TimePeriod.objects.create(weekday=1, desk=desk, |
|
1275 |
start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)) |
|
1336 |
TimePeriod.objects.create( |
|
1337 |
weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0) |
|
1338 |
) |
|
1276 | 1339 | |
1277 | 1340 |
resp = app.get('/manage/agendas/%d/' % agenda.pk).follow() |
1278 | 1341 |
resp = resp.click('Settings') |
1279 | 1342 |
resp = resp.click('upload') |
1280 | 1343 |
resp.form['ics_url'] = 'http://example.com/foo.ics' |
1281 | 1344 |
mocked_response = mock.Mock() |
1282 |
mocked_response.text = """BEGIN:VCALENDAR
|
|
1345 |
mocked_response.text = """BEGIN:VCALENDAR |
|
1283 | 1346 |
VERSION:2.0 |
1284 | 1347 |
PRODID:-//foo.bar//EN |
1285 | 1348 |
BEGIN:VEVENT |
... | ... | |
1316 | 1379 |
resp = resp.form.submit(status=302) |
1317 | 1380 |
assert TimePeriodException.objects.filter(desk=desk).count() == 1 |
1318 | 1381 | |
1382 | ||
1319 | 1383 |
@mock.patch('chrono.agendas.models.requests.get') |
1320 |
def test_agenda_import_time_period_exception_from_remote_ics_with_connection_error(mocked_get, app, admin_user): |
|
1384 |
def test_agenda_import_time_period_exception_from_remote_ics_with_connection_error( |
|
1385 |
mocked_get, app, admin_user |
|
1386 |
): |
|
1321 | 1387 |
agenda = Agenda.objects.create(label='New Example', kind='meetings') |
1322 | 1388 |
desk = Desk.objects.create(agenda=agenda, label='New Desk') |
1323 | 1389 |
MeetingType(agenda=agenda, label='Bar').save() |
... | ... | |
1326 | 1392 |
resp = resp.click('Settings') |
1327 | 1393 |
assert 'Import exceptions from .ics' not in resp.text |
1328 | 1394 | |
1329 |
TimePeriod.objects.create(weekday=1, desk=desk, |
|
1330 |
start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)) |
|
1395 |
TimePeriod.objects.create( |
|
1396 |
weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0) |
|
1397 |
) |
|
1331 | 1398 | |
1332 | 1399 |
resp = app.get('/manage/agendas/%d/' % agenda.pk).follow() |
1333 | 1400 |
resp = resp.click('Settings') |
... | ... | |
1338 | 1405 |
resp.form['ics_url'] = 'http://example.com/foo.ics' |
1339 | 1406 |
mocked_response = mock.Mock() |
1340 | 1407 |
mocked_get.return_value = mocked_response |
1408 | ||
1341 | 1409 |
def mocked_requests_connection_error(*args, **kwargs): |
1342 | 1410 |
raise requests.exceptions.ConnectionError('unreachable') |
1411 | ||
1343 | 1412 |
mocked_get.side_effect = mocked_requests_connection_error |
1344 | 1413 |
resp = resp.form.submit(status=200) |
1345 | 1414 |
assert 'Failed to retrieve remote calendar (http://example.com/foo.ics, unreachable).' in resp.text |
1346 | 1415 | |
1416 | ||
1347 | 1417 |
@mock.patch('chrono.agendas.models.requests.get') |
1348 | 1418 |
def test_agenda_import_time_period_exception_from_forbidden_remote_ics(mocked_get, app, admin_user): |
1349 | 1419 |
agenda = Agenda.objects.create(label='New Example', kind='meetings') |
... | ... | |
1354 | 1424 |
resp = resp.click('Settings') |
1355 | 1425 |
assert 'Import exceptions from .ics' not in resp.text |
1356 | 1426 | |
1357 |
TimePeriod.objects.create(weekday=1, desk=desk, |
|
1358 |
start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)) |
|
1427 |
TimePeriod.objects.create( |
|
1428 |
weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0) |
|
1429 |
) |
|
1359 | 1430 | |
1360 | 1431 |
resp = app.get('/manage/agendas/%d/' % agenda.pk).follow() |
1361 | 1432 |
resp = resp.click('Settings') |
... | ... | |
1364 | 1435 |
mocked_response = mock.Mock() |
1365 | 1436 |
mocked_response.status_code = 403 |
1366 | 1437 |
mocked_get.return_value = mocked_response |
1438 | ||
1367 | 1439 |
def mocked_requests_http_forbidden_error(*args, **kwargs): |
1368 | 1440 |
raise requests.exceptions.HTTPError(response=mocked_response) |
1441 | ||
1369 | 1442 |
mocked_get.side_effect = mocked_requests_http_forbidden_error |
1370 | 1443 |
resp = resp.form.submit(status=200) |
1371 | 1444 |
assert 'Failed to retrieve remote calendar (http://example.com/foo.ics, HTTP error 403).' in resp.text |
1372 | 1445 | |
1446 | ||
1373 | 1447 |
@mock.patch('chrono.agendas.models.requests.get') |
1374 | 1448 |
def test_agenda_import_time_period_exception_from_remote_ics_with_ssl_error(mocked_get, app, admin_user): |
1375 | 1449 |
agenda = Agenda.objects.create(label='New Example', kind='meetings') |
... | ... | |
1379 | 1453 |
resp = app.get('/manage/agendas/%d/' % agenda.pk).follow() |
1380 | 1454 |
resp = resp.click('Settings') |
1381 | 1455 |
assert 'Import exceptions from .ics' not in resp.text |
1382 |
TimePeriod.objects.create(weekday=1, desk=desk, |
|
1383 |
start_time=datetime.time(10, 0), end_time=datetime.time(12, 0)) |
|
1456 |
TimePeriod.objects.create( |
|
1457 |
weekday=1, desk=desk, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0) |
|
1458 |
) |
|
1384 | 1459 | |
1385 | 1460 |
resp = app.get('/manage/agendas/%d/' % agenda.pk).follow() |
1386 | 1461 |
resp = resp.click('Settings') |
... | ... | |
1388 | 1463 |
resp.form['ics_url'] = 'https://example.com/foo.ics' |
1389 | 1464 |
mocked_response = mock.Mock() |
1390 | 1465 |
mocked_get.return_value = mocked_response |
1466 | ||
1391 | 1467 |
def mocked_requests_http_ssl_error(*args, **kwargs): |
1392 | 1468 |
raise requests.exceptions.SSLError('SSL error') |
1469 | ||
1393 | 1470 |
mocked_get.side_effect = mocked_requests_http_ssl_error |
1394 | 1471 |
resp = resp.form.submit(status=200) |
1395 | 1472 |
assert 'Failed to retrieve remote calendar (https://example.com/foo.ics, SSL error).' in resp.text |
1396 | 1473 | |
1474 | ||
1397 | 1475 |
def test_agenda_day_view(app, admin_user, manager_user, api_user): |
1398 | 1476 |
agenda = Agenda.objects.create(label='New Example', kind='meetings') |
1399 | 1477 |
desk = Desk.objects.create(agenda=agenda, label='New Desk') |
... | ... | |
1409 | 1487 |
resp = resp.follow() |
1410 | 1488 |
assert 'No opening hours this day.' in resp.text # no time pediod |
1411 | 1489 | |
1412 |
timeperiod = TimePeriod(desk=desk, weekday=today.weekday(),
|
|
1413 |
start_time=datetime.time(10, 0),
|
|
1414 |
end_time=datetime.time(18, 0))
|
|
1490 |
timeperiod = TimePeriod( |
|
1491 |
desk=desk, weekday=today.weekday(), start_time=datetime.time(10, 0), end_time=datetime.time(18, 0)
|
|
1492 |
) |
|
1415 | 1493 |
timeperiod.save() |
1416 | 1494 |
resp = app.get('/manage/agendas/%s/' % agenda.id, status=302).follow() |
1417 | 1495 |
assert not 'No opening hours this day.' in resp.text |
... | ... | |
1434 | 1512 |
booking_url = resp.json['data'][0]['api']['fillslot_url'] |
1435 | 1513 |
booking_url2 = resp.json['data'][2]['api']['fillslot_url'] |
1436 | 1514 |
resp = app.post(booking_url) |
1437 |
resp = app.post_json(booking_url2, |
|
1438 |
params={'label': 'foo', 'user': 'bar', 'url': 'http://baz/'}) |
|
1515 |
resp = app.post_json(booking_url2, params={'label': 'foo', 'user': 'bar', 'url': 'http://baz/'}) |
|
1439 | 1516 | |
1440 | 1517 |
app.reset() |
1441 | 1518 |
login(app) |
1442 | 1519 |
date = Booking.objects.all()[0].event.start_datetime |
1443 |
resp = app.get('/manage/agendas/%s/%d/%d/%d/' % ( |
|
1444 |
agenda.id, date.year, date.month, date.day)) |
|
1520 |
resp = app.get('/manage/agendas/%s/%d/%d/%d/' % (agenda.id, date.year, date.month, date.day)) |
|
1445 | 1521 |
assert resp.text.count('div class="booking') == 2 |
1446 | 1522 |
assert 'hourspan-2' in resp.text # table CSS class |
1447 | 1523 |
assert 'height: 50%; top: 0%;' in resp.text # booking cells |
... | ... | |
1450 | 1526 |
# (and visually this will give more room for events) |
1451 | 1527 |
meetingtype = MeetingType(agenda=agenda, label='Baz', duration=15) |
1452 | 1528 |
meetingtype.save() |
1453 |
resp = app.get('/manage/agendas/%s/%d/%d/%d/' % ( |
|
1454 |
agenda.id, date.year, date.month, date.day)) |
|
1529 |
resp = app.get('/manage/agendas/%s/%d/%d/%d/' % (agenda.id, date.year, date.month, date.day)) |
|
1455 | 1530 |
assert resp.text.count('div class="booking') == 2 |
1456 | 1531 |
assert 'hourspan-4' in resp.text # table CSS class |
1457 | 1532 | |
... | ... | |
1464 | 1539 | |
1465 | 1540 |
app.reset() |
1466 | 1541 |
login(app) |
1467 |
resp = app.get('/manage/agendas/%s/%d/%d/%d/' % ( |
|
1468 |
agenda.id, date.year, date.month, date.day)) |
|
1542 |
resp = app.get('/manage/agendas/%s/%d/%d/%d/' % (agenda.id, date.year, date.month, date.day)) |
|
1469 | 1543 |
assert resp.text.count('div class="booking') == 1 |
1470 | 1544 | |
1471 | 1545 |
# wrong type |
1472 | 1546 |
agenda2 = Agenda(label=u'Foo bar') |
1473 | 1547 |
agenda2.save() |
1474 |
resp = app.get('/manage/agendas/%s/%d/%d/%d/' % ( |
|
1475 |
agenda2.id, date.year, date.month, date.day), status=404) |
|
1548 |
resp = app.get('/manage/agendas/%s/%d/%d/%d/' % (agenda2.id, date.year, date.month, date.day), status=404) |
|
1476 | 1549 | |
1477 | 1550 |
# not enough permissions |
1478 | 1551 |
agenda2.view_role = manager_user.groups.all()[0] |
1479 | 1552 |
agenda2.save() |
1480 | 1553 |
app.reset() |
1481 | 1554 |
app = login(app, username='manager', password='manager') |
1482 |
resp = app.get('/manage/agendas/%s/%d/%d/%d/' % ( |
|
1483 |
agenda.id, date.year, date.month, date.day), status=403) |
|
1555 |
resp = app.get('/manage/agendas/%s/%d/%d/%d/' % (agenda.id, date.year, date.month, date.day), status=403) |
|
1484 | 1556 | |
1485 | 1557 |
# just enough permissions |
1486 | 1558 |
agenda.view_role = manager_user.groups.all()[0] |
1487 | 1559 |
agenda.save() |
1488 |
resp = app.get('/manage/agendas/%s/%d/%d/%d/' % ( |
|
1489 |
agenda.id, date.year, date.month, date.day), status=200) |
|
1560 |
resp = app.get('/manage/agendas/%s/%d/%d/%d/' % (agenda.id, date.year, date.month, date.day), status=200)
|
|
1561 | ||
1490 | 1562 | |
1491 | 1563 |
def test_agenda_day_view_late_meeting(app, admin_user, manager_user, api_user): |
1492 | 1564 |
agenda = Agenda.objects.create(label='New Example', kind='meetings') |
... | ... | |
1498 | 1570 | |
1499 | 1571 |
today = datetime.date.today() |
1500 | 1572 | |
1501 |
timeperiod = TimePeriod(desk=desk, weekday=today.weekday(),
|
|
1502 |
start_time=datetime.time(10, 0),
|
|
1503 |
end_time=datetime.time(23, 30))
|
|
1573 |
timeperiod = TimePeriod( |
|
1574 |
desk=desk, weekday=today.weekday(), start_time=datetime.time(10, 0), end_time=datetime.time(23, 30)
|
|
1575 |
) |
|
1504 | 1576 |
timeperiod.save() |
1505 | 1577 | |
1506 | 1578 |
login(app) |
... | ... | |
1508 | 1580 |
assert resp.text.count('<tr') == 15 |
1509 | 1581 |
assert '<th class="hour">11 p.m.</th>' in resp.text |
1510 | 1582 | |
1583 | ||
1511 | 1584 |
def test_agenda_invalid_day_view(app, admin_user, manager_user, api_user): |
1512 | 1585 |
agenda = Agenda.objects.create(label='New Example', kind='meetings') |
1513 | 1586 |
desk = Desk.objects.create(agenda=agenda, label='New Desk') |
... | ... | |
1520 | 1593 |
resp = app.get('/manage/agendas/%s/%d/%d/%d/' % (agenda.id, 2018, 11, 31), status=302) |
1521 | 1594 |
assert resp.location.endswith('2018/11/30/') |
1522 | 1595 | |
1596 | ||
1523 | 1597 |
def test_agenda_month_view(app, admin_user, manager_user, api_user): |
1524 | 1598 |
agenda = Agenda.objects.create(label='Passeports', kind='meetings') |
1525 | 1599 |
desk = Desk.objects.create(agenda=agenda, label='Desk A') |
... | ... | |
1535 | 1609 |
today = datetime.date.today() |
1536 | 1610 |
assert resp.request.url.endswith('%s/%s/' % (today.year, today.month)) |
1537 | 1611 | |
1538 |
assert 'Day view' in resp.text # date view link should be present |
|
1612 |
assert 'Day view' in resp.text # date view link should be present
|
|
1539 | 1613 |
assert 'No opening hours this month.' in resp.text |
1540 | 1614 | |
1541 | 1615 |
today = datetime.date(2018, 11, 10) # fixed day |
1542 | 1616 |
timeperiod_weekday = today.weekday() |
1543 |
timeperiod = TimePeriod(desk=desk, weekday=timeperiod_weekday,
|
|
1544 |
start_time=datetime.time(10, 0),
|
|
1545 |
end_time=datetime.time(18, 0))
|
|
1617 |
timeperiod = TimePeriod( |
|
1618 |
desk=desk, weekday=timeperiod_weekday, start_time=datetime.time(10, 0), end_time=datetime.time(18, 0)
|
|
1619 |
) |
|
1546 | 1620 |
timeperiod.save() |
1547 | 1621 |
resp = app.get('/manage/agendas/%s/%s/%s/' % (agenda.id, today.year, today.month)) |
1548 | 1622 |
assert not 'No opening hours this month.' in resp.text |
1549 | 1623 |
assert not '<div class="booking' in resp.text |
1550 | 1624 |
first_month_day = today.replace(day=1) |
1551 |
last_month_day = today.replace(day=1, month=today.month+1) - datetime.timedelta(days=1)
|
|
1625 |
last_month_day = today.replace(day=1, month=today.month + 1) - datetime.timedelta(days=1)
|
|
1552 | 1626 |
start_week_number = first_month_day.isocalendar()[1] |
1553 | 1627 |
end_week_number = last_month_day.isocalendar()[1] |
1554 | 1628 |
weeks_number = end_week_number - start_week_number + 1 |
... | ... | |
1564 | 1638 |
booking_url = resp.json['data'][0]['api']['fillslot_url'] |
1565 | 1639 |
booking_url2 = resp.json['data'][2]['api']['fillslot_url'] |
1566 | 1640 |
booking = app.post(booking_url) |
1567 |
booking_2 = app.post_json(booking_url2, |
|
1568 |
params={'label': 'foo', 'user': 'bar', 'url': 'http://baz/'}) |
|
1641 |
booking_2 = app.post_json(booking_url2, params={'label': 'foo', 'user': 'bar', 'url': 'http://baz/'}) |
|
1569 | 1642 | |
1570 | 1643 |
app.reset() |
1571 | 1644 |
login(app) |
1572 | 1645 |
date = Booking.objects.all()[0].event.start_datetime |
1573 |
resp = app.get('/manage/agendas/%s/%d/%d/' % ( |
|
1574 |
agenda.id, date.year, date.month)) |
|
1575 |
assert resp.text.count('<div class="booking" style="left:1.0%;height:33.0%;') == 2 # booking cells |
|
1646 |
resp = app.get('/manage/agendas/%s/%d/%d/' % (agenda.id, date.year, date.month)) |
|
1647 |
assert resp.text.count('<div class="booking" style="left:1.0%;height:33.0%;') == 2 # booking cells |
|
1576 | 1648 |
desk = Desk.objects.create(agenda=agenda, label='Desk B') |
1577 |
timeperiod = TimePeriod(desk=desk, weekday=timeperiod_weekday,
|
|
1578 |
start_time=datetime.time(10, 0),
|
|
1579 |
end_time=datetime.time(18, 0))
|
|
1649 |
timeperiod = TimePeriod( |
|
1650 |
desk=desk, weekday=timeperiod_weekday, start_time=datetime.time(10, 0), end_time=datetime.time(18, 0)
|
|
1651 |
) |
|
1580 | 1652 |
timeperiod.save() |
1581 | 1653 | |
1582 | 1654 |
app.reset() |
... | ... | |
1613 | 1685 |
resp = app.get('/manage/agendas/%s/%s/%s/' % (agenda.id, today.year, today.month)) |
1614 | 1686 |
assert not 'No opening hours this month.' in resp.text |
1615 | 1687 | |
1688 | ||
1616 | 1689 |
def test_agenda_month_view_dst_change(app, admin_user, manager_user, api_user): |
1617 | 1690 |
agenda = Agenda.objects.create(label='Passeports', kind='meetings') |
1618 | 1691 |
desk = Desk.objects.create(agenda=agenda, label='Desk A') |
... | ... | |
1621 | 1694 |
meetingtype.save() |
1622 | 1695 | |
1623 | 1696 |
for weekday in range(0, 7): # open all mornings |
1624 |
TimePeriod(desk=desk, weekday=weekday,
|
|
1625 |
start_time=datetime.time(9, 0),
|
|
1626 |
end_time=datetime.time(12, 0)).save()
|
|
1697 |
TimePeriod( |
|
1698 |
desk=desk, weekday=weekday, start_time=datetime.time(9, 0), end_time=datetime.time(12, 0)
|
|
1699 |
).save() |
|
1627 | 1700 | |
1628 | 1701 |
login(app) |
1629 | 1702 |
for date in ('2019-10-01', '2019-10-31'): |
... | ... | |
1658 | 1731 |
resp = app.get('/manage/', status=200) |
1659 | 1732 |
resp = app.get('/manage/agendas/import/', status=403) |
1660 | 1733 | |
1734 | ||
1661 | 1735 |
def test_import_agenda(app, admin_user): |
1662 | 1736 |
agenda = Agenda(label=u'Foo bar') |
1663 | 1737 |
agenda.save() |
tests/test_misc.py | ||
---|---|---|
1 | 1 |
from chrono.manager.widgets import DateTimeWidget, TimeWidget |
2 | 2 | |
3 | ||
3 | 4 |
def test_widgets_init(): |
4 | 5 |
DateTimeWidget() |
5 | 6 |
TimeWidget() |
tests/test_sso.py | ||
---|---|---|
1 | 1 |
import pytest |
2 | ||
3 | 2 |
from django.test import override_settings |
4 | 3 | |
5 | 4 |
from chrono.wsgi import application |
6 | 5 | |
7 | 6 |
pytestmark = pytest.mark.django_db |
8 | 7 | |
8 | ||
9 | 9 |
def test_sso(app): |
10 | 10 |
with override_settings(MELLON_IDENTITY_PROVIDERS=[{'METADATA': 'x', 'ENTITY_ID': 'x'}]): |
11 | 11 |
resp = app.get('/login/') |
... | ... | |
14 | 14 |
resp = app.get('/login/?next=/manage/') |
15 | 15 |
assert resp.location.endswith('/accounts/mellon/login/?next=/manage/') |
16 | 16 | |
17 | ||
17 | 18 |
def test_slo(app): |
18 | 19 |
with override_settings(MELLON_IDENTITY_PROVIDERS=[{'METADATA': 'x', 'ENTITY_ID': 'x'}]): |
19 | 20 |
resp = app.get('/logout/') |
tests/test_time_periods.py | ||
---|---|---|
1 | 1 |
# -*- coding: utf-8 -*- |
2 | 2 | |
3 | 3 |
import datetime |
4 |
import pytest |
|
5 | 4 | |
5 |
import pytest |
|
6 | 6 |
from django.test import override_settings |
7 | 7 |
from django.utils.encoding import force_text |
8 | 8 |
from django.utils.timezone import localtime, make_aware |
9 | 9 | |
10 |
from chrono.agendas.models import Agenda, TimePeriod, TimePeriodException, MeetingType, Desk
|
|
10 |
from chrono.agendas.models import Agenda, Desk, MeetingType, TimePeriod, TimePeriodException
|
|
11 | 11 | |
12 | 12 |
pytestmark = pytest.mark.django_db |
13 | 13 | |
... | ... | |
18 | 18 |
desk = Desk.objects.create(label='Desk 1', agenda=agenda) |
19 | 19 |
meeting_type = MeetingType(duration=60, agenda=agenda) |
20 | 20 |
meeting_type.save() |
21 |
timeperiod = TimePeriod(desk=desk, weekday=0,
|
|
22 |
start_time=datetime.time(9, 0),
|
|
23 |
end_time=datetime.time(12, 0))
|
|
21 |
timeperiod = TimePeriod( |
|
22 |
desk=desk, weekday=0, start_time=datetime.time(9, 0), end_time=datetime.time(12, 0)
|
|
23 |
) |
|
24 | 24 |
events = timeperiod.get_time_slots( |
25 |
min_datetime=make_aware(datetime.datetime(2016, 9, 1)), |
|
26 |
max_datetime=make_aware(datetime.datetime(2016, 10, 1)), |
|
27 |
meeting_type=meeting_type) |
|
25 |
min_datetime=make_aware(datetime.datetime(2016, 9, 1)), |
|
26 |
max_datetime=make_aware(datetime.datetime(2016, 10, 1)), |
|
27 |
meeting_type=meeting_type, |
|
28 |
) |
|
28 | 29 |
events = list(sorted(events, key=lambda x: x.start_datetime)) |
29 | 30 |
assert events[0].start_datetime.timetuple()[:5] == (2016, 9, 5, 9, 0) |
30 | 31 |
assert events[1].start_datetime.timetuple()[:5] == (2016, 9, 5, 10, 0) |
... | ... | |
35 | 36 |
assert len(events) == 12 |
36 | 37 | |
37 | 38 |
# another start before the timeperiod |
38 |
timeperiod = TimePeriod(desk=desk, weekday=1,
|
|
39 |
start_time=datetime.time(9, 0),
|
|
40 |
end_time=datetime.time(12, 0))
|
|
39 |
timeperiod = TimePeriod( |
|
40 |
desk=desk, weekday=1, start_time=datetime.time(9, 0), end_time=datetime.time(12, 0)
|
|
41 |
) |
|
41 | 42 |
events = timeperiod.get_time_slots( |
42 |
min_datetime=make_aware(datetime.datetime(2016, 9, 1)), |
|
43 |
max_datetime=make_aware(datetime.datetime(2016, 10, 1)), |
|
44 |
meeting_type=meeting_type) |
|
43 |
min_datetime=make_aware(datetime.datetime(2016, 9, 1)), |
|
44 |
max_datetime=make_aware(datetime.datetime(2016, 10, 1)), |
|
45 |
meeting_type=meeting_type, |
|
46 |
) |
|
45 | 47 |
events = list(sorted(events, key=lambda x: x.start_datetime)) |
46 | 48 |
assert events[0].start_datetime.timetuple()[:5] == (2016, 9, 6, 9, 0) |
47 | 49 |
assert events[-1].start_datetime.timetuple()[:5] == (2016, 9, 27, 11, 0) |
48 | 50 |
assert len(events) == 12 |
49 | 51 | |
50 | 52 |
# a start on the day of the timeperiod |
51 |
timeperiod = TimePeriod(desk=desk, weekday=3,
|
|
52 |
start_time=datetime.time(9, 0),
|
|
53 |
end_time=datetime.time(12, 0))
|
|
53 |
timeperiod = TimePeriod( |
|
54 |
desk=desk, weekday=3, start_time=datetime.time(9, 0), end_time=datetime.time(12, 0)
|
|
55 |
) |
|
54 | 56 |
events = timeperiod.get_time_slots( |
55 |
min_datetime=make_aware(datetime.datetime(2016, 9, 1)), |
|
56 |
max_datetime=make_aware(datetime.datetime(2016, 10, 1)), |
|
57 |
meeting_type=meeting_type) |
|
57 |
min_datetime=make_aware(datetime.datetime(2016, 9, 1)), |
|
58 |
max_datetime=make_aware(datetime.datetime(2016, 10, 1)), |
|
59 |
meeting_type=meeting_type, |
|
60 |
) |
|
58 | 61 |
events = list(sorted(events, key=lambda x: x.start_datetime)) |
59 | 62 |
assert events[0].start_datetime.timetuple()[:5] == (2016, 9, 1, 9, 0) |
60 | 63 |
assert events[-1].start_datetime.timetuple()[:5] == (2016, 9, 29, 11, 0) |
61 | 64 |
assert len(events) == 15 |
62 | 65 | |
63 | 66 |
# a start after the day of the timeperiod |
64 |
timeperiod = TimePeriod(desk=desk, weekday=4,
|
|
65 |
start_time=datetime.time(9, 0),
|
|
66 |
end_time=datetime.time(12, 0))
|
|
67 |
timeperiod = TimePeriod( |
|
68 |
desk=desk, weekday=4, start_time=datetime.time(9, 0), end_time=datetime.time(12, 0)
|
|
69 |
) |
|
67 | 70 |
events = timeperiod.get_time_slots( |
68 |
min_datetime=make_aware(datetime.datetime(2016, 9, 1)), |
|
69 |
max_datetime=make_aware(datetime.datetime(2016, 10, 1)), |
|
70 |
meeting_type=meeting_type) |
|
71 |
min_datetime=make_aware(datetime.datetime(2016, 9, 1)), |
|
72 |
max_datetime=make_aware(datetime.datetime(2016, 10, 1)), |
|
73 |
meeting_type=meeting_type, |
|
74 |
) |
|
71 | 75 |
events = list(sorted(events, key=lambda x: x.start_datetime)) |
72 | 76 |
assert events[0].start_datetime.timetuple()[:5] == (2016, 9, 2, 9, 0) |
73 | 77 |
assert events[-1].start_datetime.timetuple()[:5] == (2016, 9, 30, 11, 0) |
74 | 78 |
assert len(events) == 15 |
75 | 79 | |
76 | 80 |
# another start after the day of the timeperiod |
77 |
timeperiod = TimePeriod(desk=desk, weekday=5,
|
|
78 |
start_time=datetime.time(9, 0),
|
|
79 |
end_time=datetime.time(12, 0))
|
|
81 |
timeperiod = TimePeriod( |
|
82 |
desk=desk, weekday=5, start_time=datetime.time(9, 0), end_time=datetime.time(12, 0)
|
|
83 |
) |
|
80 | 84 |
events = timeperiod.get_time_slots( |
81 |
min_datetime=make_aware(datetime.datetime(2016, 9, 1)), |
|
82 |
max_datetime=make_aware(datetime.datetime(2016, 10, 1)), |
|
83 |
meeting_type=meeting_type) |
|
85 |
min_datetime=make_aware(datetime.datetime(2016, 9, 1)), |
|
86 |
max_datetime=make_aware(datetime.datetime(2016, 10, 1)), |
|
87 |
meeting_type=meeting_type, |
|
88 |
) |
|
84 | 89 |
events = list(sorted(events, key=lambda x: x.start_datetime)) |
85 | 90 |
assert events[0].start_datetime.timetuple()[:5] == (2016, 9, 3, 9, 0) |
86 | 91 |
assert events[-1].start_datetime.timetuple()[:5] == (2016, 9, 24, 11, 0) |
... | ... | |
89 | 94 |
# shorter duration -> double the events |
90 | 95 |
meeting_type.duration = 30 |
91 | 96 |
meeting_type.save() |
92 |
timeperiod = TimePeriod(desk=desk, weekday=5,
|
|
93 |
start_time=datetime.time(9, 0),
|
|
94 |
end_time=datetime.time(12, 0))
|
|
97 |
timeperiod = TimePeriod( |
|
98 |
desk=desk, weekday=5, start_time=datetime.time(9, 0), end_time=datetime.time(12, 0)
|
|
99 |
) |
|
95 | 100 |
events = timeperiod.get_time_slots( |
96 |
min_datetime=make_aware(datetime.datetime(2016, 9, 1)), |
|
97 |
max_datetime=make_aware(datetime.datetime(2016, 10, 1)), |
|
98 |
meeting_type=meeting_type) |
|
101 |
min_datetime=make_aware(datetime.datetime(2016, 9, 1)), |
|
102 |
max_datetime=make_aware(datetime.datetime(2016, 10, 1)), |
|
103 |
meeting_type=meeting_type, |
|
104 |
) |
|
99 | 105 |
events = list(sorted(events, key=lambda x: x.start_datetime)) |
100 | 106 |
assert events[0].start_datetime.timetuple()[:5] == (2016, 9, 3, 9, 0) |
101 | 107 |
assert events[-1].start_datetime.timetuple()[:5] == (2016, 9, 24, 11, 30) |
... | ... | |
105 | 111 |
@override_settings(LANGUAGE_CODE='fr-fr') |
106 | 112 |
def test_time_period_exception_as_string(): |
107 | 113 |
# single day |
108 |
assert force_text(TimePeriodException( |
|
109 |
start_datetime=make_aware(datetime.datetime(2018, 1, 18)), |
|
110 |
end_datetime=make_aware(datetime.datetime(2018, 1, 19))) |
|
111 |
) == u'18 jan. 2018' |
|
114 |
assert ( |
|
115 |
force_text( |
|
116 |
TimePeriodException( |
|
117 |
start_datetime=make_aware(datetime.datetime(2018, 1, 18)), |
|
118 |
end_datetime=make_aware(datetime.datetime(2018, 1, 19)), |
|
119 |
) |
|
120 |
) |
|
121 |
== u'18 jan. 2018' |
|
122 |
) |
|
112 | 123 | |
113 | 124 |
# multiple full days |
114 |
assert force_text(TimePeriodException( |
|
115 |
start_datetime=make_aware(datetime.datetime(2018, 1, 18)), |
|
116 |
end_datetime=make_aware(datetime.datetime(2018, 1, 20))) |
|
117 |
) == u'18 jan. 2018 → 20 jan. 2018' |
|
125 |
assert ( |
|
126 |
force_text( |
|
127 |
TimePeriodException( |
|
128 |
start_datetime=make_aware(datetime.datetime(2018, 1, 18)), |
|
129 |
end_datetime=make_aware(datetime.datetime(2018, 1, 20)), |
|
130 |
) |
|
131 |
) |
|
132 |
== u'18 jan. 2018 → 20 jan. 2018' |
|
133 |
) |
|
118 | 134 | |
119 | 135 |
# a few hours in a day |
120 |
assert force_text(TimePeriodException( |
|
121 |
start_datetime=make_aware(datetime.datetime(2018, 1, 18, 10, 0)), |
|
122 |
end_datetime=make_aware(datetime.datetime(2018, 1, 18, 12, 0))) |
|
123 |
) == u'18 jan. 2018 10:00 → 12:00' |
|
136 |
assert ( |
|
137 |
force_text( |
|
138 |
TimePeriodException( |
|
139 |
start_datetime=make_aware(datetime.datetime(2018, 1, 18, 10, 0)), |
|
140 |
end_datetime=make_aware(datetime.datetime(2018, 1, 18, 12, 0)), |
|
141 |
) |
|
142 |
) |
|
143 |
== u'18 jan. 2018 10:00 → 12:00' |
|
144 |
) |
|
124 | 145 | |
125 | 146 |
# multiple days and different times |
126 |
assert force_text(TimePeriodException( |
|
127 |
start_datetime=make_aware(datetime.datetime(2018, 1, 18, 10, 0)), |
|
128 |
end_datetime=make_aware(datetime.datetime(2018, 1, 20, 12, 0))) |
|
129 |
) == u'18 jan. 2018 10:00 → 20 jan. 2018 12:00' |
|
147 |
assert ( |
|
148 |
force_text( |
|
149 |
TimePeriodException( |
|
150 |
start_datetime=make_aware(datetime.datetime(2018, 1, 18, 10, 0)), |
|
151 |
end_datetime=make_aware(datetime.datetime(2018, 1, 20, 12, 0)), |
|
152 |
) |
|
153 |
) |
|
154 |
== u'18 jan. 2018 10:00 → 20 jan. 2018 12:00' |
|
155 |
) |
|
130 | 156 | |
131 | 157 | |
132 | 158 |
def test_desk_opening_hours(): |
... | ... | |
139 | 165 |
assert len(hours) == 0 |
140 | 166 | |
141 | 167 |
# morning |
142 |
TimePeriod(desk=desk, weekday=0, |
|
143 |
start_time=datetime.time(9, 0), |
|
144 |
end_time=datetime.time(12, 0)).save() |
|
168 |
TimePeriod(desk=desk, weekday=0, start_time=datetime.time(9, 0), end_time=datetime.time(12, 0)).save() |
|
145 | 169 |
hours = list(desk.get_opening_hours(datetime.date(2018, 1, 22))) |
146 | 170 |
assert len(hours) == 1 |
147 | 171 |
assert hours[0].begin.time() == datetime.time(9, 0) |
148 | 172 |
assert hours[0].end.time() == datetime.time(12, 0) |
149 | 173 | |
150 | 174 |
# and afternoon |
151 |
TimePeriod(desk=desk, weekday=0, |
|
152 |
start_time=datetime.time(14, 0), |
|
153 |
end_time=datetime.time(17, 0)).save() |
|
175 |
TimePeriod(desk=desk, weekday=0, start_time=datetime.time(14, 0), end_time=datetime.time(17, 0)).save() |
|
154 | 176 |
hours = list(desk.get_opening_hours(datetime.date(2018, 1, 22))) |
155 | 177 |
assert len(hours) == 2 |
156 | 178 |
assert hours[0].begin.time() == datetime.time(9, 0) |
... | ... | |
161 | 183 | |
162 | 184 |
# full day exception |
163 | 185 |
exception = TimePeriodException( |
164 |
desk=desk, |
|
165 |
start_datetime=make_aware(datetime.datetime(2018, 1, 22)), |
|
166 |
end_datetime=make_aware(datetime.datetime(2018, 1, 23))) |
|
186 |
desk=desk, |
|
187 |
start_datetime=make_aware(datetime.datetime(2018, 1, 22)), |
|
188 |
end_datetime=make_aware(datetime.datetime(2018, 1, 23)), |
|
189 |
) |
|
167 | 190 |
exception.save() |
168 | 191 | |
169 | 192 |
hours = list(desk.get_opening_hours(datetime.date(2018, 1, 22))) |
... | ... | |
200 | 223 |
desk = Desk.objects.create(label='Desk 1', agenda=agenda) |
201 | 224 |
meeting_type = MeetingType(duration=120, agenda=agenda) |
202 | 225 |
meeting_type.save() |
203 |
timeperiod = TimePeriod(desk=desk, weekday=0,
|
|
204 |
start_time=datetime.time(21, 0),
|
|
205 |
end_time=datetime.time(23, 0))
|
|
226 |
timeperiod = TimePeriod( |
|
227 |
desk=desk, weekday=0, start_time=datetime.time(21, 0), end_time=datetime.time(23, 0)
|
|
228 |
) |
|
206 | 229 |
events = timeperiod.get_time_slots( |
207 |
min_datetime=make_aware(datetime.datetime(2016, 9, 1)), |
|
208 |
max_datetime=make_aware(datetime.datetime(2016, 10, 1)), |
|
209 |
meeting_type=meeting_type) |
|
230 |
min_datetime=make_aware(datetime.datetime(2016, 9, 1)), |
|
231 |
max_datetime=make_aware(datetime.datetime(2016, 10, 1)), |
|
232 |
meeting_type=meeting_type, |
|
233 |
) |
|
210 | 234 |
events = list(sorted(events, key=lambda x: x.start_datetime)) |
211 | 235 |
assert events[0].start_datetime.timetuple()[:5] == (2016, 9, 5, 21, 0) |
212 | 236 |
assert events[1].start_datetime.timetuple()[:5] == (2016, 9, 12, 21, 0) |
213 |
- |