Projet

Général

Profil

0001-start-virtual-agendas-37123.patch

Emmanuel Cazenave, 20 février 2020 16:10

Télécharger (37,6 ko)

Voir les différences:

Subject: [PATCH 1/2] start virtual agendas (#37123)

 .../migrations/0038_start_virtual_agendas.py  |  58 +++
 chrono/agendas/models.py                      |  70 ++-
 chrono/api/views.py                           | 109 +++--
 tests/test_agendas.py                         |  47 ++
 tests/test_api.py                             | 453 +++++++++++++++++-
 5 files changed, 689 insertions(+), 48 deletions(-)
 create mode 100644 chrono/agendas/migrations/0038_start_virtual_agendas.py
chrono/agendas/migrations/0038_start_virtual_agendas.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-02-20 12:15
3
from __future__ import unicode_literals
4

  
5
from django.db import migrations, models
6
import django.db.models.deletion
7

  
8

  
9
class Migration(migrations.Migration):
10

  
11
    dependencies = [
12
        ('agendas', '0037_timeperiodexceptionsource_ics_file'),
13
    ]
14

  
15
    operations = [
16
        migrations.CreateModel(
17
            name='VirtualMember',
18
            fields=[
19
                (
20
                    'id',
21
                    models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
22
                ),
23
            ],
24
        ),
25
        migrations.AlterField(
26
            model_name='agenda',
27
            name='kind',
28
            field=models.CharField(
29
                choices=[('events', 'Events'), ('meetings', 'Meetings'), ('virtual', 'Virtual')],
30
                default='events',
31
                max_length=20,
32
                verbose_name='Kind',
33
            ),
34
        ),
35
        migrations.AddField(
36
            model_name='virtualmember',
37
            name='real_agenda',
38
            field=models.ForeignKey(
39
                on_delete=django.db.models.deletion.CASCADE,
40
                related_name='virtual_members',
41
                to='agendas.Agenda',
42
            ),
43
        ),
44
        migrations.AddField(
45
            model_name='virtualmember',
46
            name='virtual_agenda',
47
            field=models.ForeignKey(
48
                on_delete=django.db.models.deletion.CASCADE, related_name='real_members', to='agendas.Agenda'
49
            ),
50
        ),
51
        migrations.AddField(
52
            model_name='agenda',
53
            name='real_agendas',
54
            field=models.ManyToManyField(
55
                related_name='virtual_agendas', through='agendas.VirtualMember', to='agendas.Agenda'
56
            ),
57
        ),
58
    ]
chrono/agendas/models.py
43 43
AGENDA_KINDS = (
44 44
    ('events', _('Events')),
45 45
    ('meetings', _('Meetings')),
46
    ('virtual', _('Virtual')),
46 47
)
47 48

  
48 49

  
......
77 78
    maximal_booking_delay = models.PositiveIntegerField(
78 79
        _('Maximal booking delay (in days)'), default=56
79 80
    )  # eight weeks
81
    real_agendas = models.ManyToManyField(
82
        'self',
83
        related_name='virtual_agendas',
84
        symmetrical=False,
85
        through='VirtualMember',
86
        through_fields=('virtual_agenda', 'real_agenda'),
87
    )
80 88
    edit_role = models.ForeignKey(
81 89
        Group,
82 90
        blank=True,
......
119 127
        group_ids = [x.id for x in user.groups.all()]
120 128
        return bool(self.view_role_id in group_ids)
121 129

  
130
    def accept_meetings(self):
131
        if self.kind == 'virtual':
132
            return not self.real_agendas.filter(kind='events').exists()
133
        return self.kind == 'meetings'
134

  
135
    def get_real_agendas(self):
136
        if self.kind == 'virtual':
137
            return self.real_agendas.all()
138
        return [self]
139

  
140
    def iter_meetingtypes(self):
141
        """ Expose agenda's meetingtypes.
142
        straighforward on a real agenda
143
        On a virtual agenda we expose transient meeting types based on on the
144
        the real ones shared by every real agendas.
145
        """
146
        if self.kind == 'virtual':
147
            queryset = (
148
                MeetingType.objects.filter(agenda__virtual_agendas__in=[self])
149
                .values('slug', 'duration', 'label')
150
                .annotate(total=Count('*'))
151
                .filter(total=self.real_agendas.count())
152
            )
153
            return [
154
                MeetingType(duration=mt['duration'], label=mt['label'], slug=mt['slug'])
155
                for mt in queryset.order_by('slug')
156
            ]
157

  
158
        return self.meetingtype_set.all().order_by('slug')
159

  
160
    def get_meetingtype(self, id_=None, slug=None):
161
        match = id_ or slug
162
        assert match, 'an identifier or a slug should be specified'
163

  
164
        if self.kind == 'virtual':
165
            match = id_ or slug
166
            meeting_type = None
167
            for mt in self.iter_meetingtypes():
168
                if mt.slug == match:
169
                    meeting_type = mt
170
                    break
171
            if meeting_type is None:
172
                raise MeetingType.DoesNotExist()
173
            return meeting_type
174

  
175
        if id_:
176
            return MeetingType.objects.get(id=id_, agenda=self)
177
        return MeetingType.objects.get(slug=slug, agenda=self)
178

  
122 179
    def get_base_meeting_duration(self):
123
        durations = [x.duration for x in MeetingType.objects.filter(agenda=self)]
180
        durations = [x.duration for x in self.iter_meetingtypes()]
124 181
        if not durations:
125 182
            raise ValueError()
126 183
        gcd = durations[0]
......
129 186
        return gcd
130 187

  
131 188
    def export_json(self):
189
        # TODO VIRTUAL
132 190
        agenda = {
133 191
            'label': self.label,
134 192
            'slug': self.slug,
......
185 243
        return created
186 244

  
187 245

  
246
class VirtualMember(models.Model):
247
    virtual_agenda = models.ForeignKey(Agenda, on_delete=models.CASCADE, related_name='real_members')
248
    real_agenda = models.ForeignKey(Agenda, on_delete=models.CASCADE, related_name='virtual_members')
249

  
250

  
188 251
WEEKDAYS_LIST = sorted(WEEKDAYS.items(), key=lambda x: x[0])
189 252

  
190 253

  
......
193 256
        self.start_datetime = start_datetime
194 257
        self.end_datetime = start_datetime + datetime.timedelta(minutes=meeting_type.duration)
195 258
        self.meeting_type = meeting_type
196
        self.id = '%s:%s' % (self.meeting_type.id, start_datetime.strftime('%Y-%m-%d-%H%M'))
259
        self.id = '%s:%s' % (meeting_type.id or meeting_type.slug, start_datetime.strftime('%Y-%m-%d-%H%M'))
197 260
        self.desk = desk
198 261

  
199 262
    def __str__(self):
......
276 339
        unique_together = ['agenda', 'slug']
277 340

  
278 341
    def save(self, *args, **kwargs):
342
        assert self.agenda.kind != 'virtual', "a meetingtype can't reference a virtual agenda"
279 343
        if not self.slug:
280 344
            self.slug = generate_slug(self, agenda=self.agenda)
281 345
        super(MeetingType, self).save(*args, **kwargs)
......
330 394
        return date_format(localtime(self.start_datetime), format='DATETIME_FORMAT')
331 395

  
332 396
    def save(self, *args, **kwargs):
397
        assert self.agenda.kind != 'virtual', "an event can't reference a virtual agenda"
333 398
        self.check_full()
334 399
        return super(Event, self).save(*args, **kwargs)
335 400

  
......
515 580
        unique_together = ['agenda', 'slug']
516 581

  
517 582
    def save(self, *args, **kwargs):
583
        assert self.agenda.kind != 'virtual', "a desk can't reference a virtual agenda"
518 584
        if not self.slug:
519 585
            self.slug = generate_slug(self, agenda=self.agenda)
520 586
        super(Desk, self).save(*args, **kwargs)
chrono/api/views.py
63 63
    }
64 64

  
65 65
    base_date = now().date()
66
    open_slots_by_desk = defaultdict(lambda: Intervals())
67
    for time_period in TimePeriod.objects.filter(desk__agenda=agenda):
68
        duration = (
69
            datetime.datetime.combine(base_date, time_period.end_time)
70
            - datetime.datetime.combine(base_date, time_period.start_time)
71
        ).seconds / 60
72
        if duration < meeting_type.duration:
73
            # skip time period that can't even hold a single meeting
74
            continue
75
        for slot in time_period.get_time_slots(**time_period_filters):
76
            slot.full = False
77
            open_slots_by_desk[time_period.desk_id].add(slot.start_datetime, slot.end_datetime, slot)
66

  
67
    agendas = agenda.get_real_agendas()
68

  
69
    open_slots = {}
70
    for agenda in agendas:
71
        open_slots[agenda] = defaultdict(lambda: Intervals())
72

  
73
    for agenda in agendas:
74
        for time_period in TimePeriod.objects.filter(desk__agenda=agenda):
75
            duration = (
76
                datetime.datetime.combine(base_date, time_period.end_time)
77
                - datetime.datetime.combine(base_date, time_period.start_time)
78
            ).seconds / 60
79
            if duration < meeting_type.duration:
80
                # skip time period that can't even hold a single meeting
81
                continue
82
            for slot in time_period.get_time_slots(**time_period_filters):
83
                slot.full = False
84
                open_slots[agenda][time_period.desk_id].add(slot.start_datetime, slot.end_datetime, slot)
78 85

  
79 86
    # remove excluded slot
80
    excluded_slot_by_desk = get_exceptions_by_desk(agenda)
81
    for desk, excluded_interval in excluded_slot_by_desk.items():
82
        for interval in excluded_interval:
83
            begin, end = interval
84
            open_slots_by_desk[desk].remove_overlap(localtime(begin), localtime(end))
85

  
86
    for event in (
87
        agenda.event_set.filter(
88
            agenda=agenda,
89
            start_datetime__gte=min_datetime,
90
            start_datetime__lte=max_datetime + datetime.timedelta(meeting_type.duration),
91
        )
92
        .select_related('meeting_type')
93
        .exclude(booking__cancellation_datetime__isnull=False)
94
    ):
95
        for slot in open_slots_by_desk[event.desk_id].search_data(event.start_datetime, event.end_datetime):
96
            slot.full = True
87
    for agenda in agendas:
88
        excluded_slot_by_desk = get_exceptions_by_desk(agenda)
89

  
90
        for desk, excluded_interval in excluded_slot_by_desk.items():
91
            for interval in excluded_interval:
92
                begin, end = interval
93
                open_slots[agenda][desk].remove_overlap(localtime(begin), localtime(end))
94

  
95
    for agenda in agendas:
96
        for event in (
97
            agenda.event_set.filter(
98
                agenda=agenda,
99
                start_datetime__gte=min_datetime,
100
                start_datetime__lte=max_datetime + datetime.timedelta(meeting_type.duration),
101
            )
102
            .select_related('meeting_type')
103
            .exclude(booking__cancellation_datetime__isnull=False)
104
        ):
105
            for slot in open_slots[agenda][event.desk_id].search_data(
106
                event.start_datetime, event.end_datetime
107
            ):
108
                slot.full = True
97 109

  
98 110
    slots = []
99
    for desk in open_slots_by_desk:
100
        slots.extend(open_slots_by_desk[desk].iter_data())
111
    for agenda in agendas:
112
        for desk in open_slots[agenda]:
113
            slots.extend(open_slots[agenda][desk].iter_data())
101 114
    slots.sort(key=lambda slot: slot.start_datetime)
102 115
    return slots
103 116

  
......
118 131
                reverse('api-agenda-datetimes', kwargs={'agenda_identifier': agenda.slug})
119 132
            )
120 133
        }
121
    elif agenda.kind == 'meetings':
134
    elif agenda.accept_meetings():
122 135
        agenda_detail['api'] = {
123 136
            'meetings_url': request.build_absolute_uri(
124 137
                reverse('api-agenda-meetings', kwargs={'agenda_identifier': agenda.slug})
......
270 283
            if agenda_identifier is None:
271 284
                # legacy access by meeting id
272 285
                meeting_type = MeetingType.objects.get(id=meeting_identifier)
286
                agenda = meeting_type.agenda
273 287
            else:
274
                meeting_type = MeetingType.objects.get(
275
                    slug=meeting_identifier, agenda__slug=agenda_identifier
276
                )
277
        except (ValueError, MeetingType.DoesNotExist):
278
            raise Http404()
288
                agenda = Agenda.objects.get(slug=agenda_identifier)
289
                meeting_type = agenda.get_meetingtype(slug=meeting_identifier)
279 290

  
280
        agenda = meeting_type.agenda
291
        except (ValueError, MeetingType.DoesNotExist, Agenda.DoesNotExist):
292
            raise Http404()
281 293

  
282 294
        now_datetime = now()
283 295

  
......
328 340
            agenda = Agenda.objects.get(slug=agenda_identifier)
329 341
        except Agenda.DoesNotExist:
330 342
            raise Http404()
331
        if agenda.kind != 'meetings':
332
            raise Http404('agenda found, but it was not a meetings agenda')
343
        if not agenda.accept_meetings():
344
            raise Http404('agenda found, but it does not accept meetings')
333 345

  
334 346
        meeting_types = []
335
        for meeting_type in agenda.meetingtype_set.all():
347
        for meeting_type in agenda.iter_meetingtypes():
336 348
            meeting_types.append(
337 349
                {
338 350
                    'text': meeting_type.label,
......
517 529

  
518 530
        available_desk = None
519 531

  
520
        if agenda.kind == 'meetings':
532
        if agenda.accept_meetings():
521 533
            # slots are actually timeslot ids (meeting_type:start_datetime), not events ids.
522 534
            # split them back to get both parts
523 535
            meeting_type_id = slots[0].split(':')[0]
......
548 560
                datetimes.add(make_aware(datetime.datetime.strptime(datetime_str, '%Y-%m-%d-%H%M')))
549 561

  
550 562
            # get all free slots and separate them by desk
551
            all_slots = get_all_slots(agenda, MeetingType.objects.get(id=meeting_type_id))
563
            all_slots = get_all_slots(agenda, agenda.get_meetingtype(id_=meeting_type_id))
552 564
            all_slots = [slot for slot in all_slots if not slot.full]
553 565
            datetimes_by_desk = defaultdict(set)
554 566
            for slot in all_slots:
555 567
                datetimes_by_desk[slot.desk.id].add(slot.start_datetime)
556 568

  
569
            # TODO: fill policy for virtual agendas
557 570
            # search first desk where all requested slots are free
558 571
            for available_desk_id in sorted(datetimes_by_desk.keys()):
559 572
                if datetimes.issubset(datetimes_by_desk[available_desk_id]):
......
572 585
            datetimes = list(datetimes)
573 586
            datetimes.sort()
574 587

  
588
            # get a real meeting_type for virtual agenda
589
            if agenda.kind == 'virtual':
590
                meeting_type_id = MeetingType.objects.get(
591
                    agenda=available_desk.agenda, slug=meeting_type_id
592
                ).pk
593

  
575 594
            # booking requires real Event objects (not lazy Timeslots);
576 595
            # create them now, with data from the slots and the desk we found.
577 596
            events = []
578 597
            for start_datetime in datetimes:
579 598
                events.append(
580 599
                    Event.objects.create(
581
                        agenda=agenda,
600
                        agenda=available_desk.agenda,
582 601
                        meeting_type_id=meeting_type_id,
583 602
                        start_datetime=start_datetime,
584 603
                        full=False,
......
658 677
            response['api']['accept_url'] = request.build_absolute_uri(
659 678
                reverse('api-accept-booking', kwargs={'booking_pk': primary_booking.id})
660 679
            )
661
        if agenda.kind == 'meetings':
680
        if agenda.accept_meetings():
662 681
            response['end_datetime'] = format_response_datetime(events[-1].end_datetime)
663 682
            response['duration'] = (events[-1].end_datetime - events[-1].start_datetime).seconds // 60
664 683
        if available_desk:
tests/test_agendas.py
18 18
    MeetingType,
19 19
    TimePeriodException,
20 20
    TimePeriodExceptionSource,
21
    VirtualMember,
21 22
)
22 23

  
23 24
pytestmark = pytest.mark.django_db
......
537 538
        elif event.label == 'bar':
538 539
            assert event.booked_places_count == 3
539 540
            assert event.waiting_list_count == 0
541

  
542

  
543
def test_virtual_agenda_init():
544
    agenda1 = Agenda.objects.create(label=u'Agenda 1', kind='meetings')
545
    agenda2 = Agenda.objects.create(label=u'Agenda 2', kind='meetings')
546
    virt_agenda = Agenda.objects.create(label=u'Virtual agenda', kind='virtual')
547
    VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=agenda1)
548
    VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=agenda2)
549
    virt_agenda.save()
550

  
551
    assert virt_agenda.real_agendas.count() == 2
552
    assert virt_agenda.real_agendas.get(pk=agenda1.pk)
553
    assert virt_agenda.real_agendas.get(pk=agenda2.pk)
554

  
555
    for agenda in (agenda1, agenda2):
556
        assert agenda.virtual_agendas.count() == 1
557
        assert agenda.virtual_agendas.get() == virt_agenda
558

  
559

  
560
def test_virtual_agenda_base_meeting_duration():
561
    virt_agenda = Agenda.objects.create(label=u'Virtual agenda', kind='virtual')
562

  
563
    with pytest.raises(ValueError):
564
        virt_agenda.get_base_meeting_duration()
565

  
566
    agenda1 = Agenda.objects.create(label='Agenda 1', kind='meetings')
567
    VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=agenda1)
568

  
569
    with pytest.raises(ValueError):
570
        virt_agenda.get_base_meeting_duration()
571

  
572
    meeting_type = MeetingType(agenda=agenda1, label='Foo', duration=30)
573
    meeting_type.save()
574
    assert virt_agenda.get_base_meeting_duration() == 30
575

  
576
    meeting_type = MeetingType(agenda=agenda1, label='Bar', duration=60)
577
    meeting_type.save()
578
    assert virt_agenda.get_base_meeting_duration() == 30
579

  
580
    agenda2 = Agenda.objects.create(label='Agenda 2', kind='meetings')
581
    VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=agenda2)
582
    virt_agenda.save()
583

  
584
    meeting_type = MeetingType(agenda=agenda2, label='Bar', duration=60)
585
    meeting_type.save()
586
    assert virt_agenda.get_base_meeting_duration() == 60
tests/test_api.py
11 11
from django.test.utils import CaptureQueriesContext
12 12
from django.utils.timezone import now, make_aware, localtime
13 13

  
14
from chrono.agendas.models import Agenda, Event, Booking, MeetingType, TimePeriod, Desk, TimePeriodException
14
from chrono.agendas.models import (
15
    Agenda,
16
    Event,
17
    Booking,
18
    MeetingType,
19
    TimePeriod,
20
    Desk,
21
    TimePeriodException,
22
    VirtualMember,
23
)
15 24
import chrono.api.views
16 25

  
17 26

  
......
38 47
    settings.TIME_ZONE = request.param
39 48

  
40 49

  
50
# 2017-05-20 -> saturday
41 51
@pytest.fixture(
42 52
    params=[
43 53
        datetime.datetime(2017, 5, 20, 1, 12),
......
109 119
    return agenda
110 120

  
111 121

  
122
@pytest.fixture
123
def virtual_meetings_agenda(meetings_agenda):
124
    agenda = Agenda.objects.create(label='Virtual Agenda', kind='virtual')
125
    VirtualMember.objects.create(virtual_agenda=agenda, real_agenda=meetings_agenda)
126
    return agenda
127

  
128

  
112 129
def test_agendas_api(app, some_data, meetings_agenda):
113 130
    agenda1 = Agenda.objects.filter(label=u'Foo bar')[0]
114 131
    agenda2 = Agenda.objects.filter(label=u'Foo bar2')[0]
132
    virtual_agenda = Agenda.objects.create(
133
        label='Virtual Agenda', kind='virtual', minimal_booking_delay=1, maximal_booking_delay=56
134
    )
115 135
    resp = app.get('/api/agenda/')
116 136
    assert resp.json == {
117 137
        'data': [
......
152 172
                    'fillslots_url': 'http://testserver/api/agenda/%s/fillslots/' % agenda2.slug,
153 173
                },
154 174
            },
175
            {
176
                'text': 'Virtual Agenda',
177
                'id': 'virtual-agenda',
178
                'slug': 'virtual-agenda',
179
                'minimal_booking_delay': 1,
180
                'maximal_booking_delay': 56,
181
                'kind': 'virtual',
182
                'api': {
183
                    'meetings_url': 'http://testserver/api/agenda/%s/meetings/' % virtual_agenda.slug,
184
                    'desks_url': 'http://testserver/api/agenda/%s/desks/' % virtual_agenda.slug,
185
                    'fillslots_url': 'http://testserver/api/agenda/%s/fillslots/' % virtual_agenda.slug,
186
                },
187
            },
155 188
        ]
156 189
    }
157 190

  
......
2138 2171
    api_url = '/api/agenda/%s/' % agenda.slug
2139 2172
    resp = app.get(api_url)
2140 2173
    assert type(resp.json['data']) is dict
2174

  
2175

  
2176
def test_virtual_agenda_detail(app, virtual_meetings_agenda):
2177
    resp = app.get('/api/agenda/%s/' % virtual_meetings_agenda.slug)
2178
    assert resp.json == {
2179
        'data': {
2180
            'text': 'Virtual Agenda',
2181
            'id': 'virtual-agenda',
2182
            'slug': 'virtual-agenda',
2183
            'minimal_booking_delay': 1,
2184
            'maximal_booking_delay': 56,
2185
            'kind': 'virtual',
2186
            'api': {
2187
                'meetings_url': 'http://testserver/api/agenda/%s/meetings/' % virtual_meetings_agenda.slug,
2188
                'desks_url': 'http://testserver/api/agenda/%s/desks/' % virtual_meetings_agenda.slug,
2189
                'fillslots_url': 'http://testserver/api/agenda/%s/fillslots/' % virtual_meetings_agenda.slug,
2190
            },
2191
        },
2192
    }
2193

  
2194

  
2195
def test_virtual_agendas_meetingtypes_api(app):
2196
    virt_agenda = Agenda.objects.create(label=u'Virtual agenda', kind='virtual')
2197

  
2198
    # No meetings because no real agenda
2199
    resp = app.get('/api/agenda/%s/meetings/' % virt_agenda.slug)
2200
    assert resp.json == {'data': []}
2201

  
2202
    # One real agenda : every meetings exposed
2203
    foo_agenda = Agenda.objects.create(label=u'Foo', kind='meetings')
2204
    MeetingType.objects.create(agenda=foo_agenda, label='Meeting1', duration=30)
2205
    MeetingType.objects.create(agenda=foo_agenda, label='Meeting2', duration=15)
2206
    VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=foo_agenda)
2207
    resp = app.get('/api/agenda/%s/meetings/' % virt_agenda.slug)
2208
    assert resp.json == {
2209
        'data': [
2210
            {
2211
                'text': 'Meeting1',
2212
                'id': 'meeting1',
2213
                'duration': 30,
2214
                'api': {
2215
                    'datetimes_url': 'http://testserver/api/agenda/virtual-agenda/meetings/meeting1/datetimes/',
2216
                },
2217
            },
2218
            {
2219
                'text': 'Meeting2',
2220
                'id': 'meeting2',
2221
                'duration': 15,
2222
                'api': {
2223
                    'datetimes_url': 'http://testserver/api/agenda/virtual-agenda/meetings/meeting2/datetimes/',
2224
                },
2225
            },
2226
        ]
2227
    }
2228

  
2229
    # Several real agendas
2230

  
2231
    bar_agenda = Agenda.objects.create(label=u'Bar', kind='meetings')
2232
    MeetingType.objects.create(agenda=bar_agenda, label='Meeting Bar', duration=30)
2233
    VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=bar_agenda)
2234

  
2235
    # Bar agenda has no meeting type: no meetings exposed
2236
    resp = app.get('/api/agenda/%s/meetings/' % virt_agenda.slug)
2237
    assert resp.json == {'data': []}
2238

  
2239
    # Bar agenda has a meetings wih different label, slug, duration: no meetings exposed
2240
    mt = MeetingType.objects.create(agenda=bar_agenda, label='Meeting Type Bar', duration=15)
2241
    resp = app.get('/api/agenda/%s/meetings/' % virt_agenda.slug)
2242
    assert resp.json == {'data': []}
2243

  
2244
    # Bar agenda has a meetings wih same label, but different slug and duration: no meetings exposed
2245
    mt.label = 'Meeting1'
2246
    mt.save()
2247
    resp = app.get('/api/agenda/%s/meetings/' % virt_agenda.slug)
2248
    assert resp.json == {'data': []}
2249

  
2250
    # Bar agenda has a meetings wih same label and slug, but different duration: no meetings exposed
2251
    mt.slug = 'meeting1'
2252
    mt.save()
2253
    resp = app.get('/api/agenda/%s/meetings/' % virt_agenda.slug)
2254
    assert resp.json == {'data': []}
2255

  
2256
    # Bar agenda has a meetings wih same label, slug and duration: only this meeting exposed
2257
    mt.duration = 30
2258
    mt.save()
2259
    resp = app.get('/api/agenda/%s/meetings/' % virt_agenda.slug)
2260
    assert resp.json == {
2261
        'data': [
2262
            {
2263
                'text': 'Meeting1',
2264
                'id': 'meeting1',
2265
                'duration': 30,
2266
                'api': {
2267
                    'datetimes_url': 'http://testserver/api/agenda/virtual-agenda/meetings/meeting1/datetimes/',
2268
                },
2269
            },
2270
        ]
2271
    }
2272

  
2273

  
2274
def test_virtual_agendas_meetings_datetimes_api(app, virtual_meetings_agenda):
2275
    real_agenda = virtual_meetings_agenda.real_agendas.first()
2276
    meeting_type = real_agenda.meetingtype_set.first()
2277
    default_desk = real_agenda.desk_set.first()
2278
    # Unkown meeting
2279
    app.get('/api/agenda/%s/meetings/xxx/datetimes/' % virtual_meetings_agenda.slug, status=404)
2280

  
2281
    virt_meeting_type = virtual_meetings_agenda.iter_meetingtypes()[0]
2282
    api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virtual_meetings_agenda.slug, virt_meeting_type.slug)
2283
    resp = app.get(api_url)
2284
    assert len(resp.json['data']) == 144
2285

  
2286
    virtual_meetings_agenda.minimal_booking_delay = 7
2287
    virtual_meetings_agenda.maximal_booking_delay = 28
2288
    virtual_meetings_agenda.save()
2289
    resp = app.get(api_url)
2290
    assert len(resp.json['data']) == 54
2291

  
2292
    virtual_meetings_agenda.minimal_booking_delay = 1
2293
    virtual_meetings_agenda.maximal_booking_delay = 56
2294
    virtual_meetings_agenda.save()
2295
    resp = app.get(api_url)
2296
    assert len(resp.json['data']) == 144
2297

  
2298
    resp = app.get(api_url)
2299
    dt = datetime.datetime.strptime(resp.json['data'][2]['id'].split(':')[1], '%Y-%m-%d-%H%M')
2300
    ev = Event(
2301
        agenda=real_agenda,
2302
        meeting_type=meeting_type,
2303
        places=1,
2304
        full=False,
2305
        start_datetime=make_aware(dt),
2306
        desk=default_desk,
2307
    )
2308
    ev.save()
2309
    booking = Booking(event=ev)
2310
    booking.save()
2311
    resp2 = app.get(api_url)
2312
    assert len(resp2.json['data']) == 144
2313
    assert resp.json['data'][0] == resp2.json['data'][0]
2314
    assert resp.json['data'][1] == resp2.json['data'][1]
2315
    assert resp.json['data'][2] != resp2.json['data'][2]
2316
    assert resp.json['data'][2]['disabled'] is False
2317
    assert resp2.json['data'][2]['disabled'] is True
2318
    assert resp.json['data'][3] == resp2.json['data'][3]
2319

  
2320
    # test with a timeperiod overlapping current moment, it should get one
2321
    # datetime for the current timeperiod + two from the next week.
2322
    if now().time().hour == 23:
2323
        # skip this part of the test as it would require support for events
2324
        # crossing midnight
2325
        return
2326

  
2327
    TimePeriod.objects.filter(desk=default_desk).delete()
2328
    start_time = localtime(now()) - datetime.timedelta(minutes=10)
2329
    time_period = TimePeriod(
2330
        weekday=localtime(now()).weekday(),
2331
        start_time=start_time,
2332
        end_time=start_time + datetime.timedelta(hours=1),
2333
        desk=default_desk,
2334
    )
2335
    time_period.save()
2336
    virtual_meetings_agenda.minimal_booking_delay = 0
2337
    virtual_meetings_agenda.maximal_booking_delay = 10
2338
    virtual_meetings_agenda.save()
2339
    resp = app.get(api_url)
2340
    assert len(resp.json['data']) == 3
2341

  
2342

  
2343
def test_virtual_agendas_meetings_exception(app, user, virtual_meetings_agenda):
2344
    app.authorization = ('Basic', ('john.doe', 'password'))
2345
    real_agenda = virtual_meetings_agenda.real_agendas.first()
2346
    desk = real_agenda.desk_set.first()
2347
    virt_meeting_type = virtual_meetings_agenda.iter_meetingtypes()[0]
2348
    datetimes_url = '/api/agenda/%s/meetings/%s/datetimes/' % (
2349
        virtual_meetings_agenda.slug,
2350
        virt_meeting_type.slug,
2351
    )
2352
    resp = app.get(datetimes_url)
2353

  
2354
    # test exception at the lowest limit
2355
    excp1 = TimePeriodException.objects.create(
2356
        desk=desk,
2357
        start_datetime=make_aware(datetime.datetime(2017, 5, 22, 10, 0)),
2358
        end_datetime=make_aware(datetime.datetime(2017, 5, 22, 12, 0)),
2359
    )
2360
    resp2 = app.get(datetimes_url)
2361
    assert len(resp.json['data']) == len(resp2.json['data']) + 4
2362

  
2363
    # test exception at the highest limit
2364
    excp1.end_datetime = make_aware(datetime.datetime(2017, 5, 22, 11, 0))
2365
    excp1.save()
2366
    resp2 = app.get(datetimes_url)
2367
    assert len(resp.json['data']) == len(resp2.json['data']) + 2
2368

  
2369
    # add an exception with an end datetime less than excp1 end datetime
2370
    # and make sure that excp1 end datetime preveil
2371
    excp1.end_datetime = make_aware(datetime.datetime(2017, 5, 23, 11, 0))
2372
    excp1.save()
2373

  
2374
    TimePeriodException.objects.create(
2375
        desk=excp1.desk,
2376
        start_datetime=make_aware(datetime.datetime(2017, 5, 22, 15, 0)),
2377
        end_datetime=make_aware(datetime.datetime(2017, 5, 23, 9, 0)),
2378
    )
2379

  
2380
    resp2 = app.get(datetimes_url)
2381
    assert len(resp.json['data']) == len(resp2.json['data']) + 6
2382

  
2383
    # with a second desk
2384
    desk2 = Desk.objects.create(label='Desk 2', agenda=real_agenda)
2385
    time_period = desk.timeperiod_set.first()
2386
    TimePeriod.objects.create(
2387
        desk=desk2,
2388
        start_time=time_period.start_time,
2389
        end_time=time_period.end_time,
2390
        weekday=time_period.weekday,
2391
    )
2392
    resp3 = app.get(datetimes_url)
2393
    assert len(resp.json['data']) == len(resp3.json['data']) + 2  # +2 because excp1 changed
2394

  
2395

  
2396
def test_virtual_agendas_meetings_datetimes_multiple_agendas(app, time_zone, mock_now):
2397
    foo_agenda = Agenda.objects.create(
2398
        label='Foo Meeting', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=5
2399
    )
2400
    foo_meeting_type = MeetingType.objects.create(agenda=foo_agenda, label='Meeting Type', duration=30)
2401
    foo_desk_1 = Desk.objects.create(agenda=foo_agenda, label='Foo desk 1')
2402

  
2403
    test_1st_weekday = (localtime(now()).weekday() + 2) % 7
2404
    test_2nd_weekday = (localtime(now()).weekday() + 3) % 7
2405
    test_3rd_weekday = (localtime(now()).weekday() + 4) % 7
2406
    test_4th_weekday = (localtime(now()).weekday() + 5) % 7
2407

  
2408
    def create_time_perdiods(desk, end=12):
2409
        TimePeriod.objects.create(
2410
            weekday=test_1st_weekday,
2411
            start_time=datetime.time(10, 0),
2412
            end_time=datetime.time(end, 0),
2413
            desk=desk,
2414
        )
2415
        TimePeriod.objects.create(
2416
            weekday=test_2nd_weekday,
2417
            start_time=datetime.time(10, 0),
2418
            end_time=datetime.time(end, 0),
2419
            desk=desk,
2420
        )
2421

  
2422
    create_time_perdiods(foo_desk_1)
2423
    virt_agenda = Agenda.objects.create(
2424
        label='Virtual Agenda', kind='virtual', minimal_booking_delay=1, maximal_booking_delay=5
2425
    )
2426
    VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=foo_agenda)
2427
    virt_meeting_type = virt_agenda.iter_meetingtypes()[0]
2428

  
2429
    # We are saturday and we can book for next monday and tuesday, 4 slots available each day
2430
    api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virt_agenda.slug, virt_meeting_type.slug)
2431
    resp = app.get(api_url)
2432
    assert len(resp.json['data']) == 8
2433
    assert resp.json['data'][0]['id'] == 'meeting-type:2017-05-22-1000'
2434

  
2435
    virt_agenda.maximal_booking_delay = 9  # another monday comes in
2436
    virt_agenda.save()
2437
    resp = app.get(api_url)
2438
    assert len(resp.json['data']) == 12
2439

  
2440
    # Back to next monday and tuesday restriction
2441
    virt_agenda.maximal_booking_delay = 5
2442
    virt_agenda.save()
2443

  
2444
    # Add another agenda
2445
    bar_agenda = Agenda.objects.create(
2446
        label='Bar Meeting', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=5
2447
    )
2448
    bar_meeting_type = MeetingType.objects.create(agenda=bar_agenda, label='Meeting Type', duration=30)
2449
    bar_desk_1 = Desk.objects.create(agenda=bar_agenda, label='Bar desk 1')
2450
    create_time_perdiods(bar_desk_1, end=13)  # bar_agenda has two more slots each day
2451
    VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=bar_agenda)
2452
    resp = app.get(api_url)
2453
    assert len(resp.json['data']) == 12
2454

  
2455
    # simulate booking
2456
    dt = datetime.datetime.strptime(resp.json['data'][2]['id'].split(':')[1], '%Y-%m-%d-%H%M')
2457
    ev = Event.objects.create(
2458
        agenda=foo_agenda,
2459
        meeting_type=foo_meeting_type,
2460
        places=1,
2461
        full=False,
2462
        start_datetime=make_aware(dt),
2463
        desk=foo_desk_1,
2464
    )
2465
    booking1 = Booking.objects.create(event=ev)
2466

  
2467
    resp = app.get(api_url)
2468
    assert len(resp.json['data']) == 12
2469
    # No disabled slot, because the booked slot is still available in second agenda
2470
    for slot in resp.json['data']:
2471
        assert slot['disabled'] is False
2472

  
2473
    ev = Event.objects.create(
2474
        agenda=bar_agenda,
2475
        meeting_type=bar_meeting_type,
2476
        places=1,
2477
        full=False,
2478
        start_datetime=make_aware(dt),
2479
        desk=bar_desk_1,
2480
    )
2481
    booking2 = Booking.objects.create(event=ev)
2482

  
2483
    resp = app.get(api_url)
2484
    assert len(resp.json['data']) == 12
2485
    # now one slot is disabled
2486
    for i, slot in enumerate(resp.json['data']):
2487
        if i == 2:
2488
            assert slot['disabled']
2489
        else:
2490
            assert slot['disabled'] is False
2491

  
2492
    # Cancel booking, every slot available
2493
    booking1.cancel()
2494
    booking2.cancel()
2495
    resp = app.get(api_url)
2496
    assert len(resp.json['data']) == 12
2497
    for slot in resp.json['data']:
2498
        assert slot['disabled'] is False
2499

  
2500
    # Add new desk on foo_agenda, open on wednesday
2501
    foo_desk_2 = Desk.objects.create(agenda=foo_agenda, label='Foo desk 2')
2502
    TimePeriod.objects.create(
2503
        weekday=test_3rd_weekday,
2504
        start_time=datetime.time(10, 0),
2505
        end_time=datetime.time(12, 0),
2506
        desk=foo_desk_2,
2507
    )
2508
    resp = app.get(api_url)
2509
    assert len(resp.json['data']) == 16
2510

  
2511
    # Add new desk on bar_agenda, open on thursday
2512
    bar_desk_2 = Desk.objects.create(agenda=bar_agenda, label='Bar desk 2')
2513
    TimePeriod.objects.create(
2514
        weekday=test_4th_weekday,
2515
        start_time=datetime.time(10, 0),
2516
        end_time=datetime.time(12, 0),
2517
        desk=bar_desk_2,
2518
    )
2519
    resp = app.get(api_url)
2520
    assert len(resp.json['data']) == 20
2521

  
2522

  
2523
def test_virtual_agendas_meetings_booking(app, mock_now, user):
2524
    foo_agenda = Agenda.objects.create(
2525
        label='Foo Meeting', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=5
2526
    )
2527
    MeetingType.objects.create(agenda=foo_agenda, label='Meeting Type', duration=30)
2528
    foo_desk_1 = Desk.objects.create(agenda=foo_agenda, label='Foo desk 1')
2529

  
2530
    TimePeriod.objects.create(
2531
        weekday=0, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=foo_desk_1,
2532
    )
2533
    TimePeriod.objects.create(
2534
        weekday=1, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=foo_desk_1,
2535
    )
2536
    bar_agenda = Agenda.objects.create(
2537
        label='Bar Meeting', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=5
2538
    )
2539
    MeetingType.objects.create(agenda=bar_agenda, label='Meeting Type', duration=30)
2540
    bar_desk_1 = Desk.objects.create(agenda=foo_agenda, label='Bar desk 1')
2541

  
2542
    TimePeriod.objects.create(
2543
        weekday=0, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=bar_desk_1,
2544
    )
2545
    TimePeriod.objects.create(
2546
        weekday=1, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=bar_desk_1,
2547
    )
2548

  
2549
    virt_agenda = Agenda.objects.create(
2550
        label='Virtual Agenda', kind='virtual', minimal_booking_delay=1, maximal_booking_delay=5
2551
    )
2552
    VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=foo_agenda)
2553
    VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=bar_agenda)
2554
    virt_meeting_type = virt_agenda.iter_meetingtypes()[0]
2555
    # We are saturday and we can book for next monday and tuesday, 4 slots available each day
2556
    api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virt_agenda.slug, virt_meeting_type.slug)
2557
    resp = app.get(api_url)
2558
    assert len(resp.json['data']) == 8
2559

  
2560
    # make a booking
2561
    fillslot_url = resp.json['data'][0]['api']['fillslot_url']
2562
    app.authorization = ('Basic', ('john.doe', 'password'))
2563
    resp_booking = app.post(fillslot_url)
2564
    assert Booking.objects.count() == 1
2565
    booking = Booking.objects.get(pk=resp_booking.json['booking_id'])
2566
    assert (
2567
        resp_booking.json['datetime']
2568
        == localtime(booking.event.start_datetime).strftime('%Y-%m-%d %H:%M:%S')
2569
        == resp.json['data'][0]['datetime']
2570
    )
2571

  
2572
    assert resp_booking.json['end_datetime'] == localtime(
2573
        Booking.objects.all()[0].event.end_datetime
2574
    ).strftime('%Y-%m-%d %H:%M:%S')
2575
    assert resp_booking.json['duration'] == 30
2576

  
2577
    # second booking on the same slot (available on the second real agenda)
2578
    resp_booking = app.post(fillslot_url)
2579
    assert Booking.objects.count() == 2
2580
    booking = Booking.objects.get(pk=resp_booking.json['booking_id'])
2581
    assert (
2582
        resp_booking.json['datetime']
2583
        == localtime(booking.event.start_datetime).strftime('%Y-%m-%d %H:%M:%S')
2584
        == resp.json['data'][0]['datetime']
2585
    )
2586

  
2587
    # try booking the same timeslot a third time: full
2588
    resp_booking = app.post(fillslot_url)
2589
    assert resp_booking.json['err'] == 1
2590
    assert resp_booking.json['err_class'] == 'no more desk available'
2591
    assert resp_booking.json['err_desc'] == 'no more desk available'
2141
-