Projet

Général

Profil

0001-start-virtual-agendas-37123.patch

Emmanuel Cazenave, 12 février 2020 15:21

Télécharger (35 ko)

Voir les différences:

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

 .../migrations/0037_auto_20200210_1620.py     |  30 ++
 chrono/agendas/models.py                      |  59 ++-
 chrono/api/views.py                           | 109 +++--
 tests/test_agendas.py                         |  46 ++
 tests/test_api.py                             | 441 ++++++++++++++++++
 5 files changed, 638 insertions(+), 47 deletions(-)
 create mode 100644 chrono/agendas/migrations/0037_auto_20200210_1620.py
chrono/agendas/migrations/0037_auto_20200210_1620.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-02-10 15:20
3
from __future__ import unicode_literals
4

  
5
from django.db import migrations, models
6

  
7

  
8
class Migration(migrations.Migration):
9

  
10
    dependencies = [
11
        ('agendas', '0036_auto_20191223_1758'),
12
    ]
13

  
14
    operations = [
15
        migrations.AddField(
16
            model_name='agenda',
17
            name='real_agendas',
18
            field=models.ManyToManyField(related_name='virtual_agendas', to='agendas.Agenda'),
19
        ),
20
        migrations.AlterField(
21
            model_name='agenda',
22
            name='kind',
23
            field=models.CharField(
24
                choices=[('events', 'Events'), ('meetings', 'Meetings'), ('virtual', 'Virtual')],
25
                default='events',
26
                max_length=20,
27
                verbose_name='Kind',
28
            ),
29
        ),
30
    ]
chrono/agendas/models.py
42 42
AGENDA_KINDS = (
43 43
    ('events', _('Events')),
44 44
    ('meetings', _('Meetings')),
45
    ('virtual', _('Virtual')),
45 46
)
46 47

  
47 48

  
......
76 77
    maximal_booking_delay = models.PositiveIntegerField(
77 78
        _('Maximal booking delay (in days)'), default=56
78 79
    )  # eight weeks
80
    real_agendas = models.ManyToManyField('self', related_name='virtual_agendas', symmetrical=False)
79 81
    edit_role = models.ForeignKey(
80 82
        Group,
81 83
        blank=True,
......
118 120
        group_ids = [x.id for x in user.groups.all()]
119 121
        return bool(self.view_role_id in group_ids)
120 122

  
123
    def accept_meetings(self):
124
        if self.kind == 'virtual':
125
            return not self.real_agendas.filter(kind='events').exists()
126
        return self.kind == 'meetings'
127

  
128
    def get_real_agendas(self):
129
        if self.kind == 'virtual':
130
            return self.real_agendas.all()
131
        return [self]
132

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

  
151
        return self.meetingtype_set.all().order_by('slug')
152

  
153
    def get_meetingtype(self, id_=None, slug=None):
154
        match = id_ or slug
155
        assert match, 'an identifier or a slug should be specified'
156

  
157
        if self.kind == 'virtual':
158
            match = id_ or slug
159
            meeting_type = None
160
            for mt in self.iter_meetingtypes():
161
                if mt.slug == match:
162
                    meeting_type = mt
163
                    break
164
            if meeting_type is None:
165
                raise MeetingType.DoesNotExist()
166
            return meeting_type
167

  
168
        if id_:
169
            return MeetingType.objects.get(id=id_, agenda=self)
170
        return MeetingType.objects.get(slug=slug, agenda=self)
171

  
121 172
    def get_base_meeting_duration(self):
122
        durations = [x.duration for x in MeetingType.objects.filter(agenda=self)]
173
        durations = [x.duration for x in self.iter_meetingtypes()]
123 174
        if not durations:
124 175
            raise ValueError()
125 176
        gcd = durations[0]
......
128 179
        return gcd
129 180

  
130 181
    def export_json(self):
182
        # TODO VIRTUAL
131 183
        agenda = {
132 184
            'label': self.label,
133 185
            'slug': self.slug,
......
192 244
        self.start_datetime = start_datetime
193 245
        self.end_datetime = start_datetime + datetime.timedelta(minutes=meeting_type.duration)
194 246
        self.meeting_type = meeting_type
195
        self.id = '%s:%s' % (self.meeting_type.id, start_datetime.strftime('%Y-%m-%d-%H%M'))
247
        self.id = '%s:%s' % (meeting_type.id or meeting_type.slug, start_datetime.strftime('%Y-%m-%d-%H%M'))
196 248
        self.desk = desk
197 249

  
198 250
    def __str__(self):
......
275 327
        unique_together = ['agenda', 'slug']
276 328

  
277 329
    def save(self, *args, **kwargs):
330
        assert self.agenda.kind != 'virtual', "a meetingtype can't reference a virtual agenda"
278 331
        if not self.slug:
279 332
            self.slug = generate_slug(self, agenda=self.agenda)
280 333
        super(MeetingType, self).save(*args, **kwargs)
......
329 382
        return date_format(localtime(self.start_datetime), format='DATETIME_FORMAT')
330 383

  
331 384
    def save(self, *args, **kwargs):
385
        assert self.agenda.kind != 'virtual', "an event can't reference a virtual agenda"
332 386
        self.check_full()
333 387
        return super(Event, self).save(*args, **kwargs)
334 388

  
......
514 568
        unique_together = ['agenda', 'slug']
515 569

  
516 570
    def save(self, *args, **kwargs):
571
        assert self.agenda.kind != 'virtual', "a desk can't reference a virtual agenda"
517 572
        if not self.slug:
518 573
            self.slug = generate_slug(self, agenda=self.agenda)
519 574
        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
498 498
        elif event.label == 'bar':
499 499
            assert event.booked_places_count == 3
500 500
            assert event.waiting_list_count == 0
501

  
502

  
503
def test_virtual_agenda_init():
504
    agenda1 = Agenda.objects.create(label=u'Agenda 1', kind='meetings')
505
    agenda2 = Agenda.objects.create(label=u'Agenda 2', kind='meetings')
506
    virt_agenda = Agenda.objects.create(label=u'Virtual agenda', kind='virtual')
507
    virt_agenda.real_agendas.add(agenda1, agenda2)
508
    virt_agenda.save()
509

  
510
    assert virt_agenda.real_agendas.count() == 2
511
    assert virt_agenda.real_agendas.get(pk=agenda1.pk)
512
    assert virt_agenda.real_agendas.get(pk=agenda2.pk)
513

  
514
    for agenda in (agenda1, agenda2):
515
        assert agenda.virtual_agendas.count() == 1
516
        assert agenda.virtual_agendas.get() == virt_agenda
517

  
518

  
519
def test_virtual_agenda_base_meeting_duration():
520
    virt_agenda = Agenda.objects.create(label=u'Virtual agenda', kind='virtual')
521

  
522
    with pytest.raises(ValueError):
523
        virt_agenda.get_base_meeting_duration()
524

  
525
    agenda1 = Agenda.objects.create(label='Agenda 1', kind='meetings')
526
    virt_agenda.real_agendas.add(agenda1)
527
    virt_agenda.save()
528

  
529
    with pytest.raises(ValueError):
530
        virt_agenda.get_base_meeting_duration()
531

  
532
    meeting_type = MeetingType(agenda=agenda1, label='Foo', duration=30)
533
    meeting_type.save()
534
    assert virt_agenda.get_base_meeting_duration() == 30
535

  
536
    meeting_type = MeetingType(agenda=agenda1, label='Bar', duration=60)
537
    meeting_type.save()
538
    assert virt_agenda.get_base_meeting_duration() == 30
539

  
540
    agenda2 = Agenda.objects.create(label='Agenda 2', kind='meetings')
541
    virt_agenda.real_agendas.add(agenda2)
542
    virt_agenda.save()
543

  
544
    meeting_type = MeetingType(agenda=agenda2, label='Bar', duration=60)
545
    meeting_type.save()
546
    assert virt_agenda.get_base_meeting_duration() == 60
tests/test_api.py
38 38
    settings.TIME_ZONE = request.param
39 39

  
40 40

  
41
# 2017-05-20 -> saturday
41 42
@pytest.fixture(
42 43
    params=[
43 44
        datetime.datetime(2017, 5, 20, 1, 12),
......
109 110
    return agenda
110 111

  
111 112

  
113
@pytest.fixture
114
def virtual_meetings_agenda(meetings_agenda):
115
    agenda = Agenda.objects.create(label='Virtual Agenda', kind='virtual')
116
    agenda.real_agendas.add(meetings_agenda)
117
    return agenda
118

  
119

  
112 120
def test_agendas_api(app, some_data, meetings_agenda):
113 121
    agenda1 = Agenda.objects.filter(label=u'Foo bar')[0]
114 122
    agenda2 = Agenda.objects.filter(label=u'Foo bar2')[0]
123
    virtual_agenda = Agenda.objects.create(
124
        label='Virtual Agenda', kind='virtual', minimal_booking_delay=1, maximal_booking_delay=56
125
    )
115 126
    resp = app.get('/api/agenda/')
116 127
    assert resp.json == {
117 128
        'data': [
......
152 163
                    'fillslots_url': 'http://testserver/api/agenda/%s/fillslots/' % agenda2.slug,
153 164
                },
154 165
            },
166
            {
167
                'text': 'Virtual Agenda',
168
                'id': 'virtual-agenda',
169
                'slug': 'virtual-agenda',
170
                'minimal_booking_delay': 1,
171
                'maximal_booking_delay': 56,
172
                'kind': 'virtual',
173
                'api': {
174
                    'meetings_url': 'http://testserver/api/agenda/%s/meetings/' % virtual_agenda.slug,
175
                    'desks_url': 'http://testserver/api/agenda/%s/desks/' % virtual_agenda.slug,
176
                    'fillslots_url': 'http://testserver/api/agenda/%s/fillslots/' % virtual_agenda.slug,
177
                },
178
            },
155 179
        ]
156 180
    }
157 181

  
......
2138 2162
    api_url = '/api/agenda/%s/' % agenda.slug
2139 2163
    resp = app.get(api_url)
2140 2164
    assert type(resp.json['data']) is dict
2165

  
2166

  
2167
def test_virtual_agenda_detail(app, virtual_meetings_agenda):
2168
    resp = app.get('/api/agenda/%s/' % virtual_meetings_agenda.slug)
2169
    assert resp.json == {
2170
        'data': {
2171
            'text': 'Virtual Agenda',
2172
            'id': 'virtual-agenda',
2173
            'slug': 'virtual-agenda',
2174
            'minimal_booking_delay': 1,
2175
            'maximal_booking_delay': 56,
2176
            'kind': 'virtual',
2177
            'api': {
2178
                'meetings_url': 'http://testserver/api/agenda/%s/meetings/' % virtual_meetings_agenda.slug,
2179
                'desks_url': 'http://testserver/api/agenda/%s/desks/' % virtual_meetings_agenda.slug,
2180
                'fillslots_url': 'http://testserver/api/agenda/%s/fillslots/' % virtual_meetings_agenda.slug,
2181
            },
2182
        },
2183
    }
2184

  
2185

  
2186
def test_virtual_agendas_meetingtypes_api(app):
2187
    virt_agenda = Agenda.objects.create(label=u'Virtual agenda', kind='virtual')
2188

  
2189
    # No meetings because no real agenda
2190
    resp = app.get('/api/agenda/%s/meetings/' % virt_agenda.slug)
2191
    assert resp.json == {'data': []}
2192

  
2193
    # One real agenda : every meetings exposed
2194
    foo_agenda = Agenda.objects.create(label=u'Foo', kind='meetings')
2195
    MeetingType.objects.create(agenda=foo_agenda, label='Meeting1', duration=30)
2196
    MeetingType.objects.create(agenda=foo_agenda, label='Meeting2', duration=15)
2197
    virt_agenda.real_agendas.add(foo_agenda)
2198
    resp = app.get('/api/agenda/%s/meetings/' % virt_agenda.slug)
2199
    assert resp.json == {
2200
        'data': [
2201
            {
2202
                'text': 'Meeting1',
2203
                'id': 'meeting1',
2204
                'duration': 30,
2205
                'api': {
2206
                    'datetimes_url': 'http://testserver/api/agenda/virtual-agenda/meetings/meeting1/datetimes/',
2207
                },
2208
            },
2209
            {
2210
                'text': 'Meeting2',
2211
                'id': 'meeting2',
2212
                'duration': 15,
2213
                'api': {
2214
                    'datetimes_url': 'http://testserver/api/agenda/virtual-agenda/meetings/meeting2/datetimes/',
2215
                },
2216
            },
2217
        ]
2218
    }
2219

  
2220
    # Several real agendas
2221

  
2222
    bar_agenda = Agenda.objects.create(label=u'Bar', kind='meetings')
2223
    MeetingType.objects.create(agenda=bar_agenda, label='Meeting Bar', duration=30)
2224
    virt_agenda.real_agendas.add(bar_agenda)
2225

  
2226
    # Bar agenda has no meeting type: no meetings exposed
2227
    resp = app.get('/api/agenda/%s/meetings/' % virt_agenda.slug)
2228
    assert resp.json == {'data': []}
2229

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

  
2235
    # Bar agenda has a meetings wih same label, but different slug and duration: no meetings exposed
2236
    mt.label = 'Meeting1'
2237
    mt.save()
2238
    resp = app.get('/api/agenda/%s/meetings/' % virt_agenda.slug)
2239
    assert resp.json == {'data': []}
2240

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

  
2247
    # Bar agenda has a meetings wih same label, slug and duration: only this meeting exposed
2248
    mt.duration = 30
2249
    mt.save()
2250
    resp = app.get('/api/agenda/%s/meetings/' % virt_agenda.slug)
2251
    assert resp.json == {
2252
        'data': [
2253
            {
2254
                'text': 'Meeting1',
2255
                'id': 'meeting1',
2256
                'duration': 30,
2257
                'api': {
2258
                    'datetimes_url': 'http://testserver/api/agenda/virtual-agenda/meetings/meeting1/datetimes/',
2259
                },
2260
            },
2261
        ]
2262
    }
2263

  
2264

  
2265
def test_virtual_agendas_meetings_datetimes_api(app, virtual_meetings_agenda):
2266
    real_agenda = virtual_meetings_agenda.real_agendas.first()
2267
    meeting_type = real_agenda.meetingtype_set.first()
2268
    default_desk = real_agenda.desk_set.first()
2269
    # Unkown meeting
2270
    app.get('/api/agenda/%s/meetings/xxx/datetimes/' % virtual_meetings_agenda.slug, status=404)
2271

  
2272
    virt_meeting_type = virtual_meetings_agenda.iter_meetingtypes()[0]
2273
    api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virtual_meetings_agenda.slug, virt_meeting_type.slug)
2274
    resp = app.get(api_url)
2275
    assert len(resp.json['data']) == 144
2276

  
2277
    virtual_meetings_agenda.minimal_booking_delay = 7
2278
    virtual_meetings_agenda.maximal_booking_delay = 28
2279
    virtual_meetings_agenda.save()
2280
    resp = app.get(api_url)
2281
    assert len(resp.json['data']) == 54
2282

  
2283
    virtual_meetings_agenda.minimal_booking_delay = 1
2284
    virtual_meetings_agenda.maximal_booking_delay = 56
2285
    virtual_meetings_agenda.save()
2286
    resp = app.get(api_url)
2287
    assert len(resp.json['data']) == 144
2288

  
2289
    resp = app.get(api_url)
2290
    dt = datetime.datetime.strptime(resp.json['data'][2]['id'].split(':')[1], '%Y-%m-%d-%H%M')
2291
    ev = Event(
2292
        agenda=real_agenda,
2293
        meeting_type=meeting_type,
2294
        places=1,
2295
        full=False,
2296
        start_datetime=make_aware(dt),
2297
        desk=default_desk,
2298
    )
2299
    ev.save()
2300
    booking = Booking(event=ev)
2301
    booking.save()
2302
    resp2 = app.get(api_url)
2303
    assert len(resp2.json['data']) == 144
2304
    assert resp.json['data'][0] == resp2.json['data'][0]
2305
    assert resp.json['data'][1] == resp2.json['data'][1]
2306
    assert resp.json['data'][2] != resp2.json['data'][2]
2307
    assert resp.json['data'][2]['disabled'] is False
2308
    assert resp2.json['data'][2]['disabled'] is True
2309
    assert resp.json['data'][3] == resp2.json['data'][3]
2310

  
2311
    # test with a timeperiod overlapping current moment, it should get one
2312
    # datetime for the current timeperiod + two from the next week.
2313
    if now().time().hour == 23:
2314
        # skip this part of the test as it would require support for events
2315
        # crossing midnight
2316
        return
2317

  
2318
    TimePeriod.objects.filter(desk=default_desk).delete()
2319
    start_time = localtime(now()) - datetime.timedelta(minutes=10)
2320
    time_period = TimePeriod(
2321
        weekday=localtime(now()).weekday(),
2322
        start_time=start_time,
2323
        end_time=start_time + datetime.timedelta(hours=1),
2324
        desk=default_desk,
2325
    )
2326
    time_period.save()
2327
    virtual_meetings_agenda.minimal_booking_delay = 0
2328
    virtual_meetings_agenda.maximal_booking_delay = 10
2329
    virtual_meetings_agenda.save()
2330
    resp = app.get(api_url)
2331
    assert len(resp.json['data']) == 3
2332

  
2333

  
2334
def test_virtual_agendas_meetings_exception(app, user, virtual_meetings_agenda):
2335
    app.authorization = ('Basic', ('john.doe', 'password'))
2336
    real_agenda = virtual_meetings_agenda.real_agendas.first()
2337
    desk = real_agenda.desk_set.first()
2338
    virt_meeting_type = virtual_meetings_agenda.iter_meetingtypes()[0]
2339
    datetimes_url = '/api/agenda/%s/meetings/%s/datetimes/' % (
2340
        virtual_meetings_agenda.slug,
2341
        virt_meeting_type.slug,
2342
    )
2343
    resp = app.get(datetimes_url)
2344

  
2345
    # test exception at the lowest limit
2346
    excp1 = TimePeriodException.objects.create(
2347
        desk=desk,
2348
        start_datetime=make_aware(datetime.datetime(2017, 5, 22, 10, 0)),
2349
        end_datetime=make_aware(datetime.datetime(2017, 5, 22, 12, 0)),
2350
    )
2351
    resp2 = app.get(datetimes_url)
2352
    assert len(resp.json['data']) == len(resp2.json['data']) + 4
2353

  
2354
    # test exception at the highest limit
2355
    excp1.end_datetime = make_aware(datetime.datetime(2017, 5, 22, 11, 0))
2356
    excp1.save()
2357
    resp2 = app.get(datetimes_url)
2358
    assert len(resp.json['data']) == len(resp2.json['data']) + 2
2359

  
2360
    # add an exception with an end datetime less than excp1 end datetime
2361
    # and make sure that excp1 end datetime preveil
2362
    excp1.end_datetime = make_aware(datetime.datetime(2017, 5, 23, 11, 0))
2363
    excp1.save()
2364

  
2365
    TimePeriodException.objects.create(
2366
        desk=excp1.desk,
2367
        start_datetime=make_aware(datetime.datetime(2017, 5, 22, 15, 0)),
2368
        end_datetime=make_aware(datetime.datetime(2017, 5, 23, 9, 0)),
2369
    )
2370

  
2371
    resp2 = app.get(datetimes_url)
2372
    assert len(resp.json['data']) == len(resp2.json['data']) + 6
2373

  
2374
    # with a second desk
2375
    desk2 = Desk.objects.create(label='Desk 2', agenda=real_agenda)
2376
    time_period = desk.timeperiod_set.first()
2377
    TimePeriod.objects.create(
2378
        desk=desk2,
2379
        start_time=time_period.start_time,
2380
        end_time=time_period.end_time,
2381
        weekday=time_period.weekday,
2382
    )
2383
    resp3 = app.get(datetimes_url)
2384
    assert len(resp.json['data']) == len(resp3.json['data']) + 2  # +2 because excp1 changed
2385

  
2386

  
2387
def test_virtual_agendas_meetings_datetimes_multiple_agendas(app, time_zone, mock_now):
2388
    foo_agenda = Agenda.objects.create(
2389
        label='Foo Meeting', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=5
2390
    )
2391
    foo_meeting_type = MeetingType.objects.create(agenda=foo_agenda, label='Meeting Type', duration=30)
2392
    foo_desk_1 = Desk.objects.create(agenda=foo_agenda, label='Foo desk 1')
2393

  
2394
    test_1st_weekday = (localtime(now()).weekday() + 2) % 7
2395
    test_2nd_weekday = (localtime(now()).weekday() + 3) % 7
2396
    test_3rd_weekday = (localtime(now()).weekday() + 4) % 7
2397
    test_4th_weekday = (localtime(now()).weekday() + 5) % 7
2398

  
2399
    def create_time_perdiods(desk, end=12):
2400
        TimePeriod.objects.create(
2401
            weekday=test_1st_weekday,
2402
            start_time=datetime.time(10, 0),
2403
            end_time=datetime.time(end, 0),
2404
            desk=desk,
2405
        )
2406
        TimePeriod.objects.create(
2407
            weekday=test_2nd_weekday,
2408
            start_time=datetime.time(10, 0),
2409
            end_time=datetime.time(end, 0),
2410
            desk=desk,
2411
        )
2412

  
2413
    create_time_perdiods(foo_desk_1)
2414
    virt_agenda = Agenda.objects.create(
2415
        label='Virtual Agenda', kind='virtual', minimal_booking_delay=1, maximal_booking_delay=5
2416
    )
2417
    virt_agenda.real_agendas.add(foo_agenda)
2418
    virt_meeting_type = virt_agenda.iter_meetingtypes()[0]
2419

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

  
2426
    virt_agenda.maximal_booking_delay = 9  # another monday comes in
2427
    virt_agenda.save()
2428
    resp = app.get(api_url)
2429
    assert len(resp.json['data']) == 12
2430

  
2431
    # Back to next monday and tuesday restriction
2432
    virt_agenda.maximal_booking_delay = 5
2433
    virt_agenda.save()
2434

  
2435
    # Add another agenda
2436
    bar_agenda = Agenda.objects.create(
2437
        label='Bar Meeting', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=5
2438
    )
2439
    bar_meeting_type = MeetingType.objects.create(agenda=bar_agenda, label='Meeting Type', duration=30)
2440
    bar_desk_1 = Desk.objects.create(agenda=bar_agenda, label='Bar desk 1')
2441
    create_time_perdiods(bar_desk_1, end=13)  # bar_agenda has two more slots each day
2442
    virt_agenda.real_agendas.add(bar_agenda)
2443
    resp = app.get(api_url)
2444
    assert len(resp.json['data']) == 12
2445

  
2446
    # simulate booking
2447
    dt = datetime.datetime.strptime(resp.json['data'][2]['id'].split(':')[1], '%Y-%m-%d-%H%M')
2448
    ev = Event.objects.create(
2449
        agenda=foo_agenda,
2450
        meeting_type=foo_meeting_type,
2451
        places=1,
2452
        full=False,
2453
        start_datetime=make_aware(dt),
2454
        desk=foo_desk_1,
2455
    )
2456
    booking1 = Booking.objects.create(event=ev)
2457

  
2458
    resp = app.get(api_url)
2459
    assert len(resp.json['data']) == 12
2460
    # No disabled slot, because the booked slot is still available in second agenda
2461
    for slot in resp.json['data']:
2462
        assert slot['disabled'] is False
2463

  
2464
    ev = Event.objects.create(
2465
        agenda=bar_agenda,
2466
        meeting_type=bar_meeting_type,
2467
        places=1,
2468
        full=False,
2469
        start_datetime=make_aware(dt),
2470
        desk=bar_desk_1,
2471
    )
2472
    booking2 = Booking.objects.create(event=ev)
2473

  
2474
    resp = app.get(api_url)
2475
    assert len(resp.json['data']) == 12
2476
    # now one slot is disabled
2477
    for i, slot in enumerate(resp.json['data']):
2478
        if i == 2:
2479
            assert slot['disabled']
2480
        else:
2481
            assert slot['disabled'] is False
2482

  
2483
    # Cancel booking, every slot available
2484
    booking1.cancel()
2485
    booking2.cancel()
2486
    resp = app.get(api_url)
2487
    assert len(resp.json['data']) == 12
2488
    for slot in resp.json['data']:
2489
        assert slot['disabled'] is False
2490

  
2491
    # Add new desk on foo_agenda, open on wednesday
2492
    foo_desk_2 = Desk.objects.create(agenda=foo_agenda, label='Foo desk 2')
2493
    TimePeriod.objects.create(
2494
        weekday=test_3rd_weekday,
2495
        start_time=datetime.time(10, 0),
2496
        end_time=datetime.time(12, 0),
2497
        desk=foo_desk_2,
2498
    )
2499
    resp = app.get(api_url)
2500
    assert len(resp.json['data']) == 16
2501

  
2502
    # Add new desk on bar_agenda, open on thursday
2503
    bar_desk_2 = Desk.objects.create(agenda=bar_agenda, label='Bar desk 2')
2504
    TimePeriod.objects.create(
2505
        weekday=test_4th_weekday,
2506
        start_time=datetime.time(10, 0),
2507
        end_time=datetime.time(12, 0),
2508
        desk=bar_desk_2,
2509
    )
2510
    resp = app.get(api_url)
2511
    assert len(resp.json['data']) == 20
2512

  
2513

  
2514
def test_virtual_agendas_meetings_booking(app, mock_now, user):
2515
    foo_agenda = Agenda.objects.create(
2516
        label='Foo Meeting', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=5
2517
    )
2518
    MeetingType.objects.create(agenda=foo_agenda, label='Meeting Type', duration=30)
2519
    foo_desk_1 = Desk.objects.create(agenda=foo_agenda, label='Foo desk 1')
2520

  
2521
    TimePeriod.objects.create(
2522
        weekday=0, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=foo_desk_1,
2523
    )
2524
    TimePeriod.objects.create(
2525
        weekday=1, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=foo_desk_1,
2526
    )
2527
    bar_agenda = Agenda.objects.create(
2528
        label='Bar Meeting', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=5
2529
    )
2530
    MeetingType.objects.create(agenda=bar_agenda, label='Meeting Type', duration=30)
2531
    bar_desk_1 = Desk.objects.create(agenda=foo_agenda, label='Bar desk 1')
2532

  
2533
    TimePeriod.objects.create(
2534
        weekday=0, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=bar_desk_1,
2535
    )
2536
    TimePeriod.objects.create(
2537
        weekday=1, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=bar_desk_1,
2538
    )
2539

  
2540
    virt_agenda = Agenda.objects.create(
2541
        label='Virtual Agenda', kind='virtual', minimal_booking_delay=1, maximal_booking_delay=5
2542
    )
2543
    virt_agenda.real_agendas.add(foo_agenda, bar_agenda)
2544
    virt_meeting_type = virt_agenda.iter_meetingtypes()[0]
2545
    # We are saturday and we can book for next monday and tuesday, 4 slots available each day
2546
    api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virt_agenda.slug, virt_meeting_type.slug)
2547
    resp = app.get(api_url)
2548
    assert len(resp.json['data']) == 8
2549

  
2550
    # make a booking
2551
    fillslot_url = resp.json['data'][0]['api']['fillslot_url']
2552
    app.authorization = ('Basic', ('john.doe', 'password'))
2553
    resp_booking = app.post(fillslot_url)
2554
    assert Booking.objects.count() == 1
2555
    booking = Booking.objects.get(pk=resp_booking.json['booking_id'])
2556
    assert (
2557
        resp_booking.json['datetime']
2558
        == localtime(booking.event.start_datetime).strftime('%Y-%m-%d %H:%M:%S')
2559
        == resp.json['data'][0]['datetime']
2560
    )
2561

  
2562
    assert resp_booking.json['end_datetime'] == localtime(
2563
        Booking.objects.all()[0].event.end_datetime
2564
    ).strftime('%Y-%m-%d %H:%M:%S')
2565
    assert resp_booking.json['duration'] == 30
2566

  
2567
    # second booking on the same slot (available on the second real agenda)
2568
    resp_booking = app.post(fillslot_url)
2569
    assert Booking.objects.count() == 2
2570
    booking = Booking.objects.get(pk=resp_booking.json['booking_id'])
2571
    assert (
2572
        resp_booking.json['datetime']
2573
        == localtime(booking.event.start_datetime).strftime('%Y-%m-%d %H:%M:%S')
2574
        == resp.json['data'][0]['datetime']
2575
    )
2576

  
2577
    # try booking the same timeslot a third time: full
2578
    resp_booking = app.post(fillslot_url)
2579
    assert resp_booking.json['err'] == 1
2580
    assert resp_booking.json['err_class'] == 'no more desk available'
2581
    assert resp_booking.json['err_desc'] == 'no more desk available'
2141
-