Projet

Général

Profil

0007-api-add-date-time-period-support-in-datetimes-and-fi.patch

Valentin Deniaud, 25 octobre 2022 10:53

Télécharger (17,2 ko)

Voir les différences:

Subject: [PATCH 7/7] api: add date time period support in datetimes and
 fillslot (#70185)

 chrono/agendas/models.py             | 88 ++++++++++++++++++++--------
 chrono/api/views.py                  |  2 +-
 tests/api/datetimes/test_meetings.py | 88 ++++++++++++++++++++++++++++
 tests/api/fillslot/test_all.py       | 36 ++++++++++++
 tests/test_time_periods.py           | 25 ++++++++
 5 files changed, 212 insertions(+), 27 deletions(-)
chrono/agendas/models.py
541 541
            self.reminder_settings.duplicate(agenda_target=new_agenda)
542 542
        return new_agenda
543 543

  
544
    def get_effective_time_periods(self):
544
    def get_effective_time_periods(self, min_datetime=None, max_datetime=None):
545 545
        """Regroup timeperiods by desks.
546 546

  
547 547
        List all timeperiods, timeperiods having the same begin_time and
548 548
        end_time are regrouped in a SharedTimePeriod object, which has a
549 549
        list of desks instead of only one desk.
550 550
        """
551
        min_date = min_datetime.date() if min_datetime else None
552
        max_date = max_datetime.date() if max_datetime else None
551 553
        if self.kind == 'virtual':
552
            return self.get_effective_time_periods_virtual()
554
            return self.get_effective_time_periods_virtual(min_date, max_date)
553 555
        elif self.kind == 'meetings':
554
            return self.get_effective_time_periods_meetings()
556
            return self.get_effective_time_periods_meetings(min_date, max_date)
555 557
        else:
556 558
            raise ValueError('does not work with kind %r' % self.kind)
557 559

  
558
    def get_effective_time_periods_meetings(self):
560
    def get_effective_time_periods_meetings(self, min_date, max_date):
559 561
        """List timeperiod instances for all desks of the agenda, convert them
560 562
        into an Interval of WeekTime which can be compared and regrouped using
561 563
        itertools.groupby.
562 564
        """
565
        time_periods = TimePeriod.objects.filter(desk__agenda=self)
566
        if min_date:
567
            time_periods.filter(Q(date__isnull=True) | Q(date__gte=min_date))
568
        if max_date:
569
            time_periods.filter(Q(date__isnull=True) | Q(date__lte=max_date))
570

  
563 571
        yield from (
564 572
            SharedTimePeriod.from_weektime_interval(
565 573
                weektime_interval,
566 574
                desks=[time_period.desk for time_period in time_periods],
567 575
            )
568 576
            for weektime_interval, time_periods in itertools.groupby(
569
                TimePeriod.objects.filter(desk__agenda=self)
570
                .prefetch_related('desk')
571
                .order_by('weekday', 'start_time', 'end_time'),
577
                time_periods.prefetch_related('desk').order_by('weekday', 'start_time', 'end_time'),
572 578
                key=TimePeriod.as_weektime_interval,
573 579
            )
574 580
        )
575 581

  
576
    def get_effective_time_periods_virtual(self):
582
    def get_effective_time_periods_virtual(self, min_date, max_date):
577 583
        """List timeperiod instances for all desks of all real agendas of this
578 584
        virtual agenda, convert them into an Interval of WeekTime which can be
579 585
        compared and regrouped using itertools.groupby.
580 586
        """
587
        time_periods = TimePeriod.objects.filter(desk__agenda__virtual_agendas=self)
588
        if min_date:
589
            time_periods.filter(Q(date__isnull=True) | Q(date__gte=min_date))
590
        if max_date:
591
            time_periods.filter(Q(date__isnull=True) | Q(date__lte=max_date))
592

  
581 593
        closed_hours_by_days = IntervalSet.from_ordered(
582 594
            [
583 595
                time_period.as_weektime_interval()
......
585 597
            ]
586 598
        )
587 599
        for time_period_interval, time_periods in itertools.groupby(
588
            TimePeriod.objects.filter(desk__agenda__virtual_agendas=self)
589
            .order_by('weekday', 'start_time', 'end_time')
590
            .prefetch_related('desk'),
600
            time_periods.order_by('weekday', 'start_time', 'end_time').prefetch_related('desk'),
591 601
            key=lambda tp: tp.as_weektime_interval(),
592 602
        ):
593 603
            time_periods = list(time_periods)
......
1090 1100
class WeekTime(collections.namedtuple('WeekTime', ['weekday', 'time'])):
1091 1101
    """Representation of a time point in a weekday, ex.: Monday at 5 o'clock."""
1092 1102

  
1093
    def __new__(cls, weekday, weekday_indexes, time):
1103
    def __new__(cls, weekday, weekday_indexes, date, time):
1104
        if date:
1105
            weekday = date.weekday()
1094 1106
        self = super().__new__(cls, weekday, time)
1095 1107
        self.weekday_indexes = weekday_indexes
1108
        self.date = date
1096 1109
        return self
1097 1110

  
1098 1111
    def __repr__(self):
1099 1112
        return '%s / %s' % (
1100
            force_str(WEEKDAYS[self.weekday]),
1113
            self.date or force_str(WEEKDAYS[self.weekday]),
1101 1114
            date_format(self.time, 'TIME_FORMAT'),
1102 1115
        )
1103 1116

  
......
1138 1151
        ]
1139 1152

  
1140 1153
    def __str__(self):
1141
        label = force_str(WEEKDAYS[self.weekday])
1142
        if self.weekday_indexes:
1143
            label = _('%(weekday)s (%(ordinals)s of the month)') % {
1144
                'weekday': label,
1145
                'ordinals': ', '.join(ordinal(i) for i in self.weekday_indexes),
1146
            }
1154
        if self.date:
1155
            label = date_format(self.date, 'l d F Y')
1156
        else:
1157
            label = force_str(WEEKDAYS[self.weekday])
1158
            if self.weekday_indexes:
1159
                label = _('%(weekday)s (%(ordinals)s of the month)') % {
1160
                    'weekday': label,
1161
                    'ordinals': ', '.join(ordinal(i) for i in self.weekday_indexes),
1162
                }
1147 1163

  
1148 1164
        label = '%s / %s → %s' % (
1149 1165
            label,
......
1189 1205

  
1190 1206
    def as_weektime_interval(self):
1191 1207
        return Interval(
1192
            WeekTime(self.weekday, self.weekday_indexes, self.start_time),
1193
            WeekTime(self.weekday, self.weekday_indexes, self.end_time),
1208
            WeekTime(self.weekday, self.weekday_indexes, self.date, self.start_time),
1209
            WeekTime(self.weekday, self.weekday_indexes, self.date, self.end_time),
1194 1210
        )
1195 1211

  
1196 1212
    def as_shared_timeperiods(self):
......
1199 1215
            weekday_indexes=self.weekday_indexes,
1200 1216
            start_time=self.start_time,
1201 1217
            end_time=self.end_time,
1218
            date=self.date,
1202 1219
            desks=[self.desk],
1203 1220
        )
1204 1221

  
......
1224 1241
    of get_all_slots() for details).
1225 1242
    """
1226 1243

  
1227
    __slots__ = ['weekday', 'weekday_indexes', 'start_time', 'end_time', 'desks']
1244
    __slots__ = ['weekday', 'weekday_indexes', 'start_time', 'end_time', 'date', 'desks']
1228 1245

  
1229
    def __init__(self, weekday, weekday_indexes, start_time, end_time, desks):
1246
    def __init__(self, weekday, weekday_indexes, start_time, end_time, date, desks):
1230 1247
        self.weekday = weekday
1231 1248
        self.weekday_indexes = weekday_indexes
1232 1249
        self.start_time = start_time
1233 1250
        self.end_time = end_time
1251
        self.date = date
1234 1252
        self.desks = set(desks)
1235 1253

  
1236 1254
    def __str__(self):
......
1241 1259
        )
1242 1260

  
1243 1261
    def __eq__(self, other):
1244
        return (self.weekday, self.start_time, self.end_time) == (
1262
        return (self.weekday, self.start_time, self.end_time, self.date) == (
1245 1263
            other.weekday,
1246 1264
            other.start_time,
1247 1265
            other.end_time,
1266
            other.date,
1248 1267
        )
1249 1268

  
1250 1269
    def __lt__(self, other):
1251
        return (self.weekday, self.start_time, self.end_time) < (
1270
        return (self.weekday, self.start_time, self.end_time, self.date) < (
1252 1271
            other.weekday,
1253 1272
            other.start_time,
1254 1273
            other.end_time,
1274
            other.date,
1255 1275
        )
1256 1276

  
1257 1277
    def get_time_slots(self, min_datetime, max_datetime, meeting_duration, base_duration):
......
1276 1296
        Generated start_datetime MUST be in the local timezone, and the local
1277 1297
        timezone must not change, as the API needs it to generate stable ids.
1278 1298
        """
1299
        if self.date and not (min_datetime.date() <= self.date <= max_datetime.date()):
1300
            return
1301

  
1279 1302
        meeting_duration = datetime.timedelta(minutes=meeting_duration)
1280 1303
        duration = datetime.timedelta(minutes=base_duration)
1281 1304

  
1282
        real_min_datetime = min_datetime + datetime.timedelta(days=self.weekday - min_datetime.weekday())
1305
        real_min_datetime = (
1306
            min_datetime + datetime.timedelta(days=self.weekday - min_datetime.weekday())
1307
            if not self.date
1308
            else min_datetime
1309
        )
1283 1310
        if real_min_datetime < min_datetime:
1284 1311
            real_min_datetime += datetime.timedelta(days=7)
1285 1312

  
......
1291 1318
        event_datetime = make_aware(make_naive(real_min_datetime)).replace(
1292 1319
            hour=self.start_time.hour, minute=self.start_time.minute, second=0, microsecond=0
1293 1320
        )
1321
        if self.date:
1322
            event_datetime = event_datetime.replace(
1323
                day=self.date.day, month=self.date.month, year=self.date.year
1324
            )
1294 1325
        # don't start before min_datetime
1295 1326
        event_datetime = max(event_datetime, min_datetime)
1296 1327

  
......
1303 1334
                or event_datetime.date() != next_time.date()
1304 1335
                or (self.weekday_indexes and get_weekday_index(event_datetime) not in self.weekday_indexes)
1305 1336
            ):
1337
                # if time slot is not repeating, end now
1338
                if self.date:
1339
                    break
1340

  
1306 1341
                # switch to naive time for day/week changes
1307 1342
                event_datetime = make_naive(event_datetime)
1308 1343
                # back to morning
......
1333 1368
            weekday_indexes=begin.weekday_indexes or end.weekday_indexes,
1334 1369
            start_time=begin.time,
1335 1370
            end_time=end.time,
1371
            date=begin.date or end.date,
1336 1372
            desks=desks,
1337 1373
        )
1338 1374

  
chrono/api/views.py
285 285
        )
286 286

  
287 287
    unique_booked = {}
288
    for time_period in base_agenda.get_effective_time_periods():
288
    for time_period in base_agenda.get_effective_time_periods(base_min_datetime, base_max_datetime):
289 289
        duration = (
290 290
            datetime.datetime.combine(base_date, time_period.end_time)
291 291
            - datetime.datetime.combine(base_date, time_period.start_time)
tests/api/datetimes/test_meetings.py
2426 2426
        '2022-03-07 11:30:00',
2427 2427
        '2022-03-14 11:30:00',
2428 2428
    ]
2429

  
2430

  
2431
@pytest.mark.freeze_time('2022-10-24 10:00')
2432
def test_datetimes_api_meetings_agenda_date_time_period(app):
2433
    agenda = Agenda.objects.create(
2434
        label='Foo bar', kind='meetings', minimal_booking_delay=0, maximal_booking_delay=8
2435
    )
2436
    meeting_type = MeetingType.objects.create(agenda=agenda, label='Plop', duration=30)
2437
    desk = Desk.objects.create(agenda=agenda, label='desk')
2438

  
2439
    TimePeriod.objects.create(
2440
        date=datetime.date(2022, 10, 24),
2441
        start_time=datetime.time(12, 0),
2442
        end_time=datetime.time(14, 0),
2443
        desk=desk,
2444
    )
2445
    api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (agenda.slug, meeting_type.slug)
2446

  
2447
    resp = app.get(api_url)
2448
    assert [x['datetime'] for x in resp.json['data']] == [
2449
        '2022-10-24 12:00:00',
2450
        '2022-10-24 12:30:00',
2451
        '2022-10-24 13:00:00',
2452
        '2022-10-24 13:30:00',
2453
    ]
2454

  
2455
    resp = app.get(api_url, params={'date_start': '2022-10-25'})
2456
    assert resp.json['data'] == []
2457

  
2458
    # mix with repeating period
2459
    TimePeriod.objects.create(
2460
        weekday=0,
2461
        start_time=datetime.time(13, 0),
2462
        end_time=datetime.time(15, 0),
2463
        desk=desk,
2464
    )
2465

  
2466
    resp = app.get(api_url)
2467
    assert [x['datetime'] for x in resp.json['data']] == [
2468
        '2022-10-24 12:00:00',
2469
        '2022-10-24 12:30:00',
2470
        '2022-10-24 13:00:00',
2471
        '2022-10-24 13:30:00',
2472
        '2022-10-24 14:00:00',
2473
        '2022-10-24 14:30:00',
2474
        '2022-10-31 13:00:00',
2475
        '2022-10-31 13:30:00',
2476
        '2022-10-31 14:00:00',
2477
        '2022-10-31 14:30:00',
2478
    ]
2479

  
2480

  
2481
@pytest.mark.freeze_time('2022-10-24 10:00')
2482
def test_datetimes_api_meetings_virtual_agenda_date_time_period(app):
2483
    agenda = Agenda.objects.create(
2484
        label='Foo bar', kind='meetings', minimal_booking_delay=0, maximal_booking_delay=8
2485
    )
2486
    desk = Desk.objects.create(agenda=agenda, label='desk')
2487
    meeting_type = MeetingType.objects.create(agenda=agenda, label='Plop', duration=30)
2488
    virtual_agenda = Agenda.objects.create(label='Foo bar Meeting', kind='virtual')
2489
    virtual_agenda.real_agendas.add(agenda)
2490

  
2491
    TimePeriod.objects.create(
2492
        date=datetime.date(2022, 10, 24),
2493
        start_time=datetime.time(12, 0),
2494
        end_time=datetime.time(14, 0),
2495
        desk=desk,
2496
    )
2497

  
2498
    api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virtual_agenda.slug, meeting_type.slug)
2499
    resp = app.get(api_url)
2500
    assert [x['datetime'] for x in resp.json['data']] == [
2501
        '2022-10-24 12:00:00',
2502
        '2022-10-24 12:30:00',
2503
        '2022-10-24 13:00:00',
2504
        '2022-10-24 13:30:00',
2505
    ]
2506

  
2507
    # add exclusion period on virtual agenda
2508
    TimePeriod.objects.create(
2509
        weekday=0, start_time=datetime.time(12, 00), end_time=datetime.time(13, 00), agenda=virtual_agenda
2510
    )
2511
    resp = app.get(api_url)
2512
    resp = app.get(api_url)
2513
    assert [x['datetime'] for x in resp.json['data']] == [
2514
        '2022-10-24 13:00:00',
2515
        '2022-10-24 13:30:00',
2516
    ]
tests/api/fillslot/test_all.py
995 995
    assert Booking.objects.all()[0].extra_data == {'hello': 'world'}
996 996

  
997 997

  
998
@pytest.mark.freeze_time('2022-10-24 10:00')
999
def test_booking_api_meeting_date_time_period(app, user):
1000
    agenda = Agenda.objects.create(
1001
        label='Foo bar', kind='meetings', minimal_booking_delay=0, maximal_booking_delay=8
1002
    )
1003
    meeting_type = MeetingType.objects.create(agenda=agenda, label='Plop', duration=30)
1004
    desk = Desk.objects.create(agenda=agenda, label='desk')
1005

  
1006
    TimePeriod.objects.create(
1007
        date=datetime.date(2022, 10, 24),
1008
        start_time=datetime.time(12, 0),
1009
        end_time=datetime.time(14, 0),
1010
        desk=desk,
1011
    )
1012
    datetimes_resp = app.get('/api/agenda/%s/meetings/%s/datetimes/' % (agenda.slug, meeting_type.slug))
1013
    slot = datetimes_resp.json['data'][0]['id']
1014
    assert slot == 'plop:2022-10-24-1200'
1015

  
1016
    app.authorization = ('Basic', ('john.doe', 'password'))
1017

  
1018
    # single booking
1019
    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, slot))
1020
    assert Booking.objects.count() == 1
1021
    assert resp.json['duration'] == 30
1022

  
1023
    # multiple slots
1024
    slots = [datetimes_resp.json['data'][1]['id'], datetimes_resp.json['data'][2]['id']]
1025
    assert slots == ['plop:2022-10-24-1230', 'plop:2022-10-24-1300']
1026
    resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots})
1027
    assert Booking.objects.count() == 3
1028

  
1029
    resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, slot))
1030
    assert resp.json['err'] == 1
1031
    assert resp.json['err_desc'] == 'no more desk available'
1032

  
1033

  
998 1034
def test_booking_api_available(app, user):
999 1035
    agenda = Agenda.objects.create(label='Foo bar', kind='events', minimal_booking_delay=0)
1000 1036
    for i in range(0, 10):
tests/test_time_periods.py
491 491
                start_time=datetime.time(hour=1, minute=0),
492 492
                end_time=datetime.time(hour=2, minute=0),
493 493
            )
494

  
495

  
496
def test_timeperiod_date_time_slots():
497
    agenda = Agenda(label='Foo bar', slug='bar')
498
    agenda.save()
499
    desk = Desk.objects.create(label='Desk 1', agenda=agenda)
500
    meeting_type = MeetingType(duration=60, agenda=agenda)
501
    meeting_type.save()
502
    timeperiod = TimePeriod(
503
        desk=desk,
504
        date=datetime.date(2022, 10, 24),
505
        start_time=datetime.time(9, 0),
506
        end_time=datetime.time(12, 0),
507
    )
508
    events = timeperiod.as_shared_timeperiods().get_time_slots(
509
        min_datetime=make_aware(datetime.datetime(2022, 10, 1)),
510
        max_datetime=make_aware(datetime.datetime(2022, 11, 1)),
511
        meeting_duration=meeting_type.duration,
512
        base_duration=agenda.get_base_meeting_duration(),
513
    )
514
    assert [x.timetuple()[:5] for x in sorted(events)] == [
515
        (2022, 10, 24, 9, 0),
516
        (2022, 10, 24, 10, 0),
517
        (2022, 10, 24, 11, 0),
518
    ]
494
-