1
|
|
|
""" |
2
|
|
|
byceps.services.seating.seat_group_service |
3
|
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
4
|
|
|
|
5
|
|
|
:Copyright: 2014-2022 Jochen Kupperschmidt |
6
|
|
|
:License: Revised BSD (see `LICENSE` file for details) |
7
|
|
|
""" |
8
|
|
|
|
9
|
1 |
|
from typing import Optional, Sequence |
10
|
|
|
|
11
|
1 |
|
from sqlalchemy import select |
12
|
|
|
|
13
|
1 |
|
from ...database import db |
14
|
1 |
|
from ...typing import PartyID |
15
|
|
|
|
16
|
1 |
|
from ..ticketing.dbmodels.ticket import Ticket as DbTicket |
17
|
1 |
|
from ..ticketing.dbmodels.ticket_bundle import TicketBundle as DbTicketBundle |
18
|
1 |
|
from ..ticketing.transfer.models import TicketBundleID, TicketCategoryID |
19
|
|
|
|
20
|
1 |
|
from .dbmodels.seat import Seat as DbSeat |
21
|
1 |
|
from .dbmodels.seat_group import ( |
22
|
|
|
Occupancy as DbSeatGroupOccupancy, |
23
|
|
|
SeatGroup as DbSeatGroup, |
24
|
|
|
SeatGroupAssignment as DbSeatGroupAssignment, |
25
|
|
|
) |
26
|
1 |
|
from .transfer.models import SeatID, SeatGroupID |
27
|
|
|
|
28
|
|
|
|
29
|
1 |
|
def create_seat_group( |
30
|
|
|
party_id: PartyID, |
31
|
|
|
ticket_category_id: TicketCategoryID, |
32
|
|
|
title: str, |
33
|
|
|
seats: Sequence[DbSeat], |
34
|
|
|
*, |
35
|
|
|
commit: bool = True, |
36
|
|
|
) -> DbSeatGroup: |
37
|
|
|
"""Create a seat group and assign the given seats.""" |
38
|
|
|
seat_quantity = len(seats) |
39
|
|
|
if seat_quantity == 0: |
40
|
|
|
raise ValueError("No seats specified.") |
41
|
|
|
|
42
|
|
|
ticket_category_ids = {seat.category_id for seat in seats} |
43
|
|
|
if len(ticket_category_ids) != 1 or ( |
44
|
|
|
ticket_category_id not in ticket_category_ids |
45
|
|
|
): |
46
|
|
|
raise ValueError("Seats' ticket category IDs do not match the group's.") |
47
|
|
|
|
48
|
|
|
group = DbSeatGroup(party_id, ticket_category_id, seat_quantity, title) |
49
|
|
|
db.session.add(group) |
50
|
|
|
|
51
|
|
|
for seat in seats: |
52
|
|
|
assignment = DbSeatGroupAssignment(group, seat) |
53
|
|
|
db.session.add(assignment) |
54
|
|
|
|
55
|
|
|
if commit: |
56
|
|
|
db.session.commit() |
57
|
|
|
|
58
|
|
|
return group |
59
|
|
|
|
60
|
|
|
|
61
|
1 |
|
def occupy_seat_group( |
62
|
|
|
seat_group: DbSeatGroup, ticket_bundle: DbTicketBundle |
63
|
|
|
) -> DbSeatGroupOccupancy: |
64
|
|
|
"""Occupy the seat group with that ticket bundle.""" |
65
|
|
|
seats = seat_group.seats |
66
|
|
|
tickets = ticket_bundle.tickets |
67
|
|
|
|
68
|
|
|
_ensure_group_is_available(seat_group) |
69
|
|
|
_ensure_categories_match(seat_group, ticket_bundle) |
70
|
|
|
_ensure_quantities_match(seat_group, ticket_bundle) |
71
|
|
|
_ensure_actual_quantities_match(seats, tickets) |
72
|
|
|
|
73
|
|
|
occupancy = DbSeatGroupOccupancy(seat_group.id, ticket_bundle.id) |
74
|
|
|
db.session.add(occupancy) |
75
|
|
|
|
76
|
|
|
_occupy_seats(seats, tickets) |
77
|
|
|
|
78
|
|
|
db.session.commit() |
79
|
|
|
|
80
|
|
|
return occupancy |
81
|
|
|
|
82
|
|
|
|
83
|
1 |
|
def switch_seat_group( |
84
|
|
|
occupancy: DbSeatGroupOccupancy, to_group: DbSeatGroup |
85
|
|
|
) -> None: |
86
|
|
|
"""Switch ticket bundle to another seat group.""" |
87
|
|
|
ticket_bundle = occupancy.ticket_bundle |
88
|
|
|
tickets = ticket_bundle.tickets |
89
|
|
|
seats = to_group.seats |
90
|
|
|
|
91
|
|
|
_ensure_group_is_available(to_group) |
92
|
|
|
_ensure_categories_match(to_group, ticket_bundle) |
93
|
|
|
_ensure_quantities_match(to_group, ticket_bundle) |
94
|
|
|
_ensure_actual_quantities_match(seats, tickets) |
95
|
|
|
|
96
|
|
|
occupancy.seat_group_id = to_group.id |
97
|
|
|
|
98
|
|
|
_occupy_seats(seats, tickets) |
99
|
|
|
|
100
|
|
|
db.session.commit() |
101
|
|
|
|
102
|
|
|
|
103
|
1 |
|
def _ensure_group_is_available(seat_group: DbSeatGroup) -> None: |
104
|
|
|
"""Raise an error if the seat group is occupied.""" |
105
|
|
|
occupancy = find_occupancy_for_seat_group(seat_group.id) |
106
|
|
|
if occupancy is not None: |
107
|
|
|
raise ValueError('Seat group is already occupied.') |
108
|
|
|
|
109
|
|
|
|
110
|
1 |
|
def _ensure_categories_match( |
111
|
|
|
seat_group: DbSeatGroup, ticket_bundle: DbTicketBundle |
112
|
|
|
) -> None: |
113
|
|
|
"""Raise an error if the seat group's and the ticket bundle's |
114
|
|
|
categories don't match. |
115
|
|
|
""" |
116
|
|
|
if seat_group.ticket_category_id != ticket_bundle.ticket_category_id: |
117
|
|
|
raise ValueError('Seat and ticket categories do not match.') |
118
|
|
|
|
119
|
|
|
|
120
|
1 |
|
def _ensure_quantities_match( |
121
|
|
|
seat_group: DbSeatGroup, ticket_bundle: DbTicketBundle |
122
|
|
|
) -> None: |
123
|
|
|
"""Raise an error if the seat group's and the ticket bundle's |
124
|
|
|
quantities don't match. |
125
|
|
|
""" |
126
|
|
|
if seat_group.seat_quantity != ticket_bundle.ticket_quantity: |
127
|
|
|
raise ValueError('Seat and ticket quantities do not match.') |
128
|
|
|
|
129
|
|
|
|
130
|
1 |
|
def _ensure_actual_quantities_match( |
131
|
|
|
seats: Sequence[DbSeat], tickets: Sequence[DbTicket] |
132
|
|
|
) -> None: |
133
|
|
|
"""Raise an error if the totals of seats and tickets don't match.""" |
134
|
|
|
if len(seats) != len(tickets): |
135
|
|
|
raise ValueError( |
136
|
|
|
'The actual quantities of seats and tickets ' 'do not match.' |
137
|
|
|
) |
138
|
|
|
|
139
|
|
|
|
140
|
1 |
|
def _occupy_seats(seats: Sequence[DbSeat], tickets: Sequence[DbTicket]) -> None: |
141
|
|
|
"""Occupy all seats in the group with all tickets from the bundle.""" |
142
|
|
|
seats = _sort_seats(seats) |
143
|
|
|
tickets = _sort_tickets(tickets) |
144
|
|
|
|
145
|
|
|
for seat, ticket in zip(seats, tickets): |
146
|
|
|
ticket.occupied_seat = seat |
147
|
|
|
|
148
|
|
|
|
149
|
1 |
|
def _sort_seats(seats: Sequence[DbSeat]) -> Sequence[DbSeat]: |
150
|
|
|
"""Create a list of the seats sorted by their respective coordinates.""" |
151
|
|
|
return list(sorted(seats, key=lambda s: (s.coord_x, s.coord_y))) |
152
|
|
|
|
153
|
|
|
|
154
|
1 |
|
def _sort_tickets(tickets: Sequence[DbTicket]) -> Sequence[DbTicket]: |
155
|
|
|
"""Create a list of the tickets sorted by creation time (ascending).""" |
156
|
|
|
return list(sorted(tickets, key=lambda t: t.created_at)) |
157
|
|
|
|
158
|
|
|
|
159
|
1 |
|
def release_seat_group(seat_group_id: SeatGroupID) -> None: |
160
|
|
|
"""Release a seat group so it becomes available again.""" |
161
|
|
|
occupancy = find_occupancy_for_seat_group(seat_group_id) |
162
|
|
|
if occupancy is None: |
163
|
|
|
raise ValueError('Seat group is not occupied.') |
164
|
|
|
|
165
|
|
|
for ticket in occupancy.ticket_bundle.tickets: |
166
|
|
|
ticket.occupied_seat = None |
167
|
|
|
|
168
|
|
|
db.session.delete(occupancy) |
169
|
|
|
|
170
|
|
|
db.session.commit() |
171
|
|
|
|
172
|
|
|
|
173
|
1 |
|
def count_seat_groups_for_party(party_id: PartyID) -> int: |
174
|
|
|
"""Return the number of seat groups for that party.""" |
175
|
1 |
|
return db.session \ |
176
|
|
|
.query(DbSeatGroup) \ |
177
|
|
|
.filter_by(party_id=party_id) \ |
178
|
|
|
.count() |
179
|
|
|
|
180
|
|
|
|
181
|
1 |
|
def find_seat_group(seat_group_id: SeatGroupID) -> Optional[DbSeatGroup]: |
182
|
|
|
"""Return the seat group with that id, or `None` if not found.""" |
183
|
|
|
return db.session.get(DbSeatGroup, seat_group_id) |
184
|
|
|
|
185
|
|
|
|
186
|
1 |
|
def find_seat_group_occupied_by_ticket_bundle( |
187
|
|
|
ticket_bundle_id: TicketBundleID, |
188
|
|
|
) -> Optional[SeatGroupID]: |
189
|
|
|
"""Return the ID of the seat group occupied by that ticket bundle, |
190
|
|
|
or `None` if not found. |
191
|
|
|
""" |
192
|
1 |
|
return db.session.execute( |
193
|
|
|
select(DbSeatGroupOccupancy.seat_group_id) |
194
|
|
|
.filter_by(ticket_bundle_id=ticket_bundle_id) |
195
|
|
|
).one_or_none() |
196
|
|
|
|
197
|
|
|
|
198
|
1 |
|
def find_occupancy_for_seat_group( |
199
|
|
|
seat_group_id: SeatGroupID, |
200
|
|
|
) -> Optional[DbSeatGroupOccupancy]: |
201
|
|
|
"""Return the occupancy for that seat group, or `None` if not found.""" |
202
|
|
|
return db.session.execute( |
203
|
|
|
select(DbSeatGroupOccupancy) |
204
|
|
|
.filter_by(seat_group_id=seat_group_id) |
205
|
|
|
).one_or_none() |
206
|
|
|
|
207
|
|
|
|
208
|
1 |
|
def get_all_seat_groups_for_party(party_id: PartyID) -> Sequence[DbSeatGroup]: |
209
|
|
|
"""Return all seat groups for that party.""" |
210
|
1 |
|
return db.session \ |
211
|
|
|
.query(DbSeatGroup) \ |
212
|
|
|
.filter_by(party_id=party_id) \ |
213
|
|
|
.all() |
214
|
|
|
|
215
|
|
|
|
216
|
1 |
|
def is_seat_part_of_a_group(seat_id: SeatID) -> bool: |
217
|
|
|
"""Return whether or not the seat is part of a seat group.""" |
218
|
1 |
|
return db.session.execute( |
219
|
|
|
select( |
220
|
|
|
select(DbSeatGroupAssignment).filter_by(seat_id=seat_id).exists() |
221
|
|
|
) |
222
|
|
|
).scalar_one() |
223
|
|
|
|