0007-api-book-also-resources-in-fillslot-s-endpoint-38942.patch
chrono/api/views.py | ||
---|---|---|
40 | 40 |
from ..interval import IntervalSet |
41 | 41 | |
42 | 42 | |
43 |
class APIError(Exception): |
|
44 |
err = 1 |
|
45 |
http_status = 200 |
|
46 | ||
47 |
def __init__(self, *args, **kwargs): |
|
48 |
self.__dict__.update(kwargs) |
|
49 |
super(APIError, self).__init__(*args) |
|
50 | ||
51 |
def to_response(self): |
|
52 |
data = { |
|
53 |
'err': self.err, |
|
54 |
'err_class': self.err_class, |
|
55 |
'err_desc': force_text(self), |
|
56 |
} |
|
57 |
if hasattr(self, 'errors'): |
|
58 |
data['errors'] = self.errors |
|
59 |
return Response(data, status=self.http_status) |
|
60 | ||
61 | ||
43 | 62 |
def format_response_datetime(dt): |
44 | 63 |
return localtime(dt).strftime('%Y-%m-%d %H:%M:%S') |
45 | 64 | |
... | ... | |
310 | 329 |
return places |
311 | 330 | |
312 | 331 | |
332 |
def get_resources_from_request(request, agenda): |
|
333 |
if agenda.kind != 'meetings' or 'resources' not in request.GET: |
|
334 |
return [] |
|
335 |
resources_slugs = [s for s in request.GET['resources'].split(',') if s] |
|
336 |
resources = list(agenda.resources.filter(slug__in=resources_slugs)) |
|
337 |
if len(resources) != len(resources_slugs): |
|
338 |
unknown_slugs = set(resources_slugs) - set([r.slug for r in resources]) |
|
339 |
unknown_slugs = sorted(list(unknown_slugs)) |
|
340 |
raise APIError( |
|
341 |
_('invalid resource: %s') % ', '.join(unknown_slugs), |
|
342 |
err_class='invalid resource: %s' % ', '.join(unknown_slugs), |
|
343 |
http_status=status.HTTP_400_BAD_REQUEST, |
|
344 |
) |
|
345 |
return resources |
|
346 | ||
347 | ||
313 | 348 |
class Agendas(APIView): |
314 | 349 |
permission_classes = () |
315 | 350 | |
... | ... | |
447 | 482 | |
448 | 483 |
now_datetime = now() |
449 | 484 | |
450 |
resources = None |
|
451 |
if agenda.kind == 'meetings' and 'resources' in request.GET: |
|
452 |
resources_slugs = [s for s in request.GET['resources'].split(',') if s] |
|
453 |
resources = list(agenda.resources.filter(slug__in=resources_slugs)) |
|
454 |
if len(resources) != len(resources_slugs): |
|
455 |
unknown_slugs = set(resources_slugs) - set([r.slug for r in resources]) |
|
456 |
unknown_slugs = sorted(list(unknown_slugs)) |
|
457 |
return Response( |
|
458 |
{ |
|
459 |
'err': 1, |
|
460 |
'err_class': 'invalid resource: %s' % ', '.join(unknown_slugs), |
|
461 |
'err_desc': _('invalid resource: %s') % ', '.join(unknown_slugs), |
|
462 |
}, |
|
463 |
status=status.HTTP_400_BAD_REQUEST, |
|
464 |
) |
|
485 |
try: |
|
486 |
resources = get_resources_from_request(request, agenda) |
|
487 |
except APIError as e: |
|
488 |
return e.to_response() |
|
465 | 489 | |
466 | 490 |
# Generate an unique slot for each possible meeting [start_datetime, |
467 | 491 |
# end_datetime] range. |
... | ... | |
756 | 780 |
) |
757 | 781 |
datetimes.add(make_aware(datetime.datetime.strptime(datetime_str, '%Y-%m-%d-%H%M'))) |
758 | 782 | |
783 |
try: |
|
784 |
resources = get_resources_from_request(request, agenda) |
|
785 |
except APIError as e: |
|
786 |
return e.to_response() |
|
787 | ||
759 | 788 |
# get all free slots and separate them by desk |
760 | 789 |
try: |
761 | 790 |
all_slots = sorted( |
762 |
get_all_slots(agenda, agenda.get_meetingtype(id_=meeting_type_id)), |
|
791 |
get_all_slots(agenda, agenda.get_meetingtype(id_=meeting_type_id), resources=resources),
|
|
763 | 792 |
key=lambda slot: slot.start_datetime, |
764 | 793 |
) |
765 | 794 |
except (MeetingType.DoesNotExist, ValueError): |
... | ... | |
847 | 876 |
# create them now, with data from the slots and the desk we found. |
848 | 877 |
events = [] |
849 | 878 |
for start_datetime in datetimes: |
850 |
events.append( |
|
851 |
Event.objects.create( |
|
852 |
agenda=available_desk.agenda, |
|
853 |
meeting_type_id=meeting_type_id, |
|
854 |
start_datetime=start_datetime, |
|
855 |
full=False, |
|
856 |
places=1, |
|
857 |
desk=available_desk, |
|
858 |
) |
|
879 |
event = Event.objects.create( |
|
880 |
agenda=available_desk.agenda, |
|
881 |
meeting_type_id=meeting_type_id, |
|
882 |
start_datetime=start_datetime, |
|
883 |
full=False, |
|
884 |
places=1, |
|
885 |
desk=available_desk, |
|
859 | 886 |
) |
887 |
if resources: |
|
888 |
event.resources.add(*resources) |
|
889 |
events.append(event) |
|
860 | 890 |
else: |
861 | 891 |
try: |
862 | 892 |
events = Event.objects.filter(id__in=[int(s) for s in slots]).order_by('start_datetime') |
... | ... | |
973 | 1003 |
} |
974 | 1004 |
for x in events |
975 | 1005 |
] |
1006 |
if agenda.kind == 'meetings': |
|
1007 |
response['resources'] = [r.slug for r in resources] |
|
976 | 1008 | |
977 | 1009 |
return Response(response) |
978 | 1010 |
tests/test_api.py | ||
---|---|---|
1080 | 1080 |
assert Booking.objects.count() == 2 |
1081 | 1081 | |
1082 | 1082 | |
1083 |
def test_booking_api_meeting_with_resources(app, user): |
|
1084 |
tomorrow = datetime.date.today() + datetime.timedelta(days=1) |
|
1085 |
tomorrow_str = tomorrow.isoformat() |
|
1086 |
agenda = Agenda.objects.create( |
|
1087 |
label='Agenda', kind='meetings', minimal_booking_delay=0, maximal_booking_delay=10 |
|
1088 |
) |
|
1089 |
resource1 = Resource.objects.create(label='Resource 1') |
|
1090 |
resource2 = Resource.objects.create(label='Resource 2') |
|
1091 |
resource3 = Resource.objects.create(label='Resource 3') |
|
1092 |
agenda.resources.add(resource1, resource2, resource3) |
|
1093 |
desk = Desk.objects.create(agenda=agenda, slug='desk') |
|
1094 |
meeting_type = MeetingType.objects.create(agenda=agenda, slug='foo-bar') |
|
1095 |
TimePeriod.objects.create( |
|
1096 |
weekday=tomorrow.weekday(), start_time=datetime.time(9, 0), end_time=datetime.time(17, 00), desk=desk, |
|
1097 |
) |
|
1098 | ||
1099 |
app.authorization = ('Basic', ('john.doe', 'password')) |
|
1100 | ||
1101 |
# make a booking without resource |
|
1102 |
resp = app.post('/api/agenda/%s/fillslot/%s:%s-1000/' % (agenda.pk, meeting_type.pk, tomorrow_str)) |
|
1103 |
assert resp.json['datetime'] == '%s 10:00:00' % tomorrow_str |
|
1104 |
assert resp.json['end_datetime'] == '%s 10:30:00' % tomorrow_str |
|
1105 |
assert resp.json['duration'] == 30 |
|
1106 |
assert resp.json['resources'] == [] |
|
1107 |
booking = Booking.objects.latest('pk') |
|
1108 |
assert list(booking.event.resources.all()) == [] |
|
1109 | ||
1110 |
# now try to book also a resource - slot not free |
|
1111 |
resp = app.post( |
|
1112 |
'/api/agenda/%s/fillslot/%s:%s-1000/?resources=%s' |
|
1113 |
% (agenda.pk, meeting_type.pk, tomorrow_str, resource1.slug) |
|
1114 |
) |
|
1115 |
assert resp.json['err'] == 1 |
|
1116 |
assert resp.json['reason'] == 'no more desk available' # legacy |
|
1117 |
assert resp.json['err_class'] == 'no more desk available' |
|
1118 |
assert resp.json['err_desc'] == 'no more desk available' |
|
1119 | ||
1120 |
# slot is free |
|
1121 |
resp = app.post( |
|
1122 |
'/api/agenda/%s/fillslot/%s:%s-0900/?resources=%s' |
|
1123 |
% (agenda.pk, meeting_type.pk, tomorrow_str, resource1.slug) |
|
1124 |
) |
|
1125 |
assert resp.json['datetime'] == '%s 09:00:00' % tomorrow_str |
|
1126 |
assert resp.json['end_datetime'] == '%s 09:30:00' % tomorrow_str |
|
1127 |
assert resp.json['duration'] == 30 |
|
1128 |
assert resp.json['resources'] == [resource1.slug] |
|
1129 |
booking = Booking.objects.latest('pk') |
|
1130 |
assert list(booking.event.resources.all()) == [resource1] |
|
1131 |
resp = app.post( |
|
1132 |
'/api/agenda/%s/fillslot/%s:%s-0930/?resources=%s,%s' |
|
1133 |
% (agenda.pk, meeting_type.pk, tomorrow_str, resource1.slug, resource2.slug) |
|
1134 |
) |
|
1135 |
assert resp.json['datetime'] == '%s 09:30:00' % tomorrow_str |
|
1136 |
assert resp.json['end_datetime'] == '%s 10:00:00' % tomorrow_str |
|
1137 |
assert resp.json['duration'] == 30 |
|
1138 |
assert resp.json['resources'] == [resource1.slug, resource2.slug] |
|
1139 |
booking = Booking.objects.latest('pk') |
|
1140 |
assert list(booking.event.resources.all()) == [resource1, resource2] |
|
1141 | ||
1142 |
# resource is unknown or not valid for this agenda |
|
1143 |
resp = app.post( |
|
1144 |
'/api/agenda/%s/fillslot/%s:%s-0900/?resources=foobarblah' |
|
1145 |
% (agenda.pk, meeting_type.pk, tomorrow_str), |
|
1146 |
status=400, |
|
1147 |
) |
|
1148 |
assert resp.json['err'] == 1 |
|
1149 |
assert resp.json['reason'] == 'invalid resource: foobarblah' # legacy |
|
1150 |
assert resp.json['err_class'] == 'invalid resource: foobarblah' |
|
1151 |
assert resp.json['err_desc'] == 'invalid resource: foobarblah' |
|
1152 |
agenda.resources.remove(resource3) |
|
1153 |
resp = app.post( |
|
1154 |
'/api/agenda/%s/fillslot/%s:%s-0900/?resources=%s' |
|
1155 |
% (agenda.pk, meeting_type.pk, tomorrow_str, resource3.slug), |
|
1156 |
status=400, |
|
1157 |
) |
|
1158 |
assert resp.json['err'] == 1 |
|
1159 |
assert resp.json['reason'] == 'invalid resource: resource-3' # legacy |
|
1160 |
assert resp.json['err_class'] == 'invalid resource: resource-3' |
|
1161 |
assert resp.json['err_desc'] == 'invalid resource: resource-3' |
|
1162 |
resp = app.post( |
|
1163 |
'/api/agenda/%s/fillslot/%s:%s-0900/?resources=%s,foobarblah' |
|
1164 |
% (agenda.pk, meeting_type.pk, tomorrow_str, resource3.slug), |
|
1165 |
status=400, |
|
1166 |
) |
|
1167 |
assert resp.json['err'] == 1 |
|
1168 |
assert resp.json['reason'] == 'invalid resource: foobarblah, resource-3' # legacy |
|
1169 |
assert resp.json['err_class'] == 'invalid resource: foobarblah, resource-3' |
|
1170 |
assert resp.json['err_desc'] == 'invalid resource: foobarblah, resource-3' |
|
1171 |
resp = app.post( |
|
1172 |
'/api/agenda/%s/fillslot/%s:%s-0900/?resources=%s,foobarblah' |
|
1173 |
% (agenda.pk, meeting_type.pk, tomorrow_str, resource1.slug), |
|
1174 |
status=400, |
|
1175 |
) |
|
1176 |
assert resp.json['err'] == 1 |
|
1177 |
assert resp.json['reason'] == 'invalid resource: foobarblah' # legacy |
|
1178 |
assert resp.json['err_class'] == 'invalid resource: foobarblah' |
|
1179 |
assert resp.json['err_desc'] == 'invalid resource: foobarblah' |
|
1180 | ||
1181 |
# booking is canceled: slot is free |
|
1182 |
booking.cancel() |
|
1183 |
resp = app.post( |
|
1184 |
'/api/agenda/%s/fillslot/%s:%s-0930/?resources=%s,%s' |
|
1185 |
% (agenda.pk, meeting_type.pk, tomorrow_str, resource1.slug, resource2.slug) |
|
1186 |
) |
|
1187 |
assert resp.json['datetime'] == '%s 09:30:00' % tomorrow_str |
|
1188 |
assert resp.json['end_datetime'] == '%s 10:00:00' % tomorrow_str |
|
1189 |
assert resp.json['duration'] == 30 |
|
1190 |
assert resp.json['resources'] == [resource1.slug, resource2.slug] |
|
1191 |
booking = Booking.objects.latest('pk') |
|
1192 |
assert list(booking.event.resources.all()) == [resource1, resource2] |
|
1193 | ||
1194 | ||
1083 | 1195 |
def test_booking_api_meeting_fillslots(app, meetings_agenda, user): |
1084 | 1196 |
agenda_id = meetings_agenda.slug |
1085 | 1197 |
meeting_type = MeetingType.objects.get(agenda=meetings_agenda) |
1086 |
- |