Projet

Général

Profil

0001-misc-black-29209.patch

Lauréline Guérin, 13 décembre 2019 22:19

Télécharger (279 ko)

Voir les différences:

Subject: [PATCH 1/4] misc: black (#29209)

 .../sync_desks_timeperiod_exceptions.py       |   3 +-
 chrono/agendas/migrations/0001_initial.py     |  14 +-
 chrono/agendas/migrations/0002_event.py       |  11 +-
 chrono/agendas/migrations/0003_booking.py     |  10 +-
 .../0004_booking_cancellation_datetime.py     |   2 +-
 chrono/agendas/migrations/0005_event_label.py |   8 +-
 .../migrations/0006_auto_20160707_1357.py     |  17 +-
 .../migrations/0007_auto_20160722_1135.py     |   4 +-
 .../migrations/0008_auto_20160910_1319.py     |  41 +-
 .../migrations/0010_auto_20160918_1250.py     |  23 +-
 .../migrations/0013_auto_20161028_1603.py     |   4 +-
 .../0014_booking_primary_booking.py           |   7 +-
 .../migrations/0015_auto_20170628_1137.py     |  12 +-
 chrono/agendas/migrations/0016_desk.py        |   9 +-
 .../migrations/0017_timeperiod_desk.py        |   8 +-
 chrono/agendas/migrations/0018_event_desk.py  |   5 +-
 .../migrations/0019_timeperiodexception.py    |  14 +-
 .../migrations/0020_auto_20171102_1021.py     |   7 +-
 .../migrations/0021_auto_20171126_1330.py     |  14 +-
 .../migrations/0022_auto_20171202_1828.py     |   4 +-
 .../migrations/0023_auto_20171202_1835.py     |   4 +-
 .../migrations/0024_auto_20180426_1127.py     |   5 +-
 .../migrations/0025_auto_20181206_1252.py     |  22 +-
 .../migrations/0027_event_description.py      |   4 +-
 chrono/agendas/migrations/0028_event_slug.py  |   4 +-
 .../migrations/0029_auto_20191106_1320.py     |   4 +-
 .../migrations/0030_auto_20191107_1200.py     |   6 +-
 .../migrations/0031_auto_20191107_1225.py     |  10 +-
 .../migrations/0032_auto_20191127_0919.py     |   9 +-
 chrono/agendas/models.py                      | 212 +++---
 chrono/api/urls.py                            |  49 +-
 chrono/api/views.py                           | 410 +++++++-----
 chrono/interval.py                            |   1 +
 chrono/manager/forms.py                       | 107 +--
 .../management/commands/export_site.py        |   4 +-
 .../management/commands/import_site.py        |  25 +-
 chrono/manager/urls.py                        | 153 +++--
 chrono/manager/views.py                       | 304 ++++++---
 chrono/manager/widgets.py                     |  46 +-
 chrono/settings.py                            |  16 +-
 chrono/urls.py                                |   9 +-
 chrono/urls_utils.py                          |   3 +-
 chrono/views.py                               |   6 +-
 chrono/wsgi.py                                |   5 +-
 tests/conftest.py                             |   3 +-
 tests/test_agendas.py                         |  85 ++-
 tests/test_api.py                             | 615 ++++++++++++------
 tests/test_api_utils.py                       |  17 +-
 tests/test_data_migrations.py                 |  44 +-
 tests/test_import_export.py                   |  33 +-
 tests/test_manager.py                         | 346 ++++++----
 tests/test_misc.py                            |   1 +
 tests/test_sso.py                             |   3 +-
 tests/test_time_periods.py                    | 162 +++--
 54 files changed, 1801 insertions(+), 1143 deletions(-)
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
-