appoint_user_manager_form()   A
last analyzed

Complexity

Conditions 3

Size

Total Lines 21
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 9.3211

Importance

Changes 0
Metric Value
eloc 14
dl 0
loc 21
rs 9.7
c 0
b 0
f 0
cc 3
nop 2
ccs 1
cts 9
cp 0.1111
crap 9.3211
1
"""
2
byceps.blueprints.site.ticketing.views
3
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4
5
:Copyright: 2014-2024 Jochen Kupperschmidt
6
:License: Revised BSD (see `LICENSE` file for details)
7
"""
8
9 1
from typing import Any
10
11 1
from flask import abort, g, request
12 1
from flask_babel import gettext
13
14 1
from byceps.services.orga_team import orga_team_service
15 1
from byceps.services.party import party_service
16 1
from byceps.services.shop.order import order_service
17 1
from byceps.services.ticketing import (
18
    barcode_service,
19
    ticket_category_service,
20
    ticket_seat_management_service,
21
    ticket_service,
22
    ticket_user_management_service,
23
)
24 1
from byceps.util.framework.blueprint import create_blueprint
25 1
from byceps.util.framework.flash import flash_error, flash_success
26 1
from byceps.util.framework.templating import templated
27 1
from byceps.util.iterables import find
28 1
from byceps.util.views import login_required, redirect_to, respond_no_content
29
30 1
from . import (
31
    intranet_login_as_checkin,  # Load to connect to signal.  # noqa: F401
32
    notification_service,
33
)
34 1
from .forms import SpecifyUserForm
35
36
37 1
blueprint = create_blueprint('ticketing', __name__)
38
39
40 1
@blueprint.app_context_processor
41 1
def inject_ticket_sale_stats() -> dict[str, Any]:
42 1
    if not g.party_id:
43 1
        return {}
44
45 1
    if g.site.is_intranet:
46
        return {}
47
48 1
    ticket_sale_stats = ticket_service.get_ticket_sale_stats(g.party)
49
50 1
    return {
51
        'ticket_sale_stats': ticket_sale_stats,
52
    }
53
54
55 1
@blueprint.get('/mine')
56 1
@login_required
57 1
@templated
58 1
def index_mine():
59
    """List tickets related to the current user."""
60 1
    if g.party is None:
61
        # No party is configured for the current site.
62
        abort(404)
63
64 1
    party = g.party
65
66 1
    user = g.user
67
68 1
    tickets = ticket_service.get_tickets_related_to_user_for_party(
69
        user.id, party.id
70
    )
71
72 1
    tickets = [ticket for ticket in tickets if not ticket.revoked]
73
74 1
    order_numbers = {
75
        ticket.order_number
76
        for ticket in tickets
77
        if ticket.owned_by_id == user.id
78
    }
79 1
    order_ids_by_order_number = order_service.get_order_ids_for_order_numbers(
80
        order_numbers
81
    )
82
83 1
    ticket_user_ids = {ticket.used_by_id for ticket in tickets}
84 1
    orga_ids = orga_team_service.select_orgas_for_party(
85
        ticket_user_ids, g.party_id
86
    )
87
88 1
    current_user_uses_any_ticket = find(
89
        tickets, lambda t: t.used_by_id == user.id
90
    )
91
92 1
    return {
93
        'party_title': party.title,
94
        'tickets': tickets,
95
        'order_ids_by_order_number': order_ids_by_order_number,
96
        'orga_ids': orga_ids,
97
        'current_user_uses_any_ticket': current_user_uses_any_ticket,
98
        'is_user_allowed_to_print_ticket': _is_user_allowed_to_print_ticket,
99
        'ticket_management_enabled': _is_ticket_management_enabled(),
100
    }
101
102
103 1
@blueprint.get('/tickets/<uuid:ticket_id>/printable.html')
104 1
@login_required
105 1
@templated
106 1
def view_printable_html(ticket_id):
107
    """Show a form to select a user to appoint for the ticket."""
108
    ticket = _get_ticket_or_404(ticket_id)
109
110
    if not _is_user_allowed_to_print_ticket(ticket, g.user.id):
111
        # Hide ticket ID validity rather than openly denying access.
112
        abort(404)
113
114
    ticket_category = ticket_category_service.get_category(ticket.category_id)
115
    party = party_service.get_party(ticket_category.party_id)
116
117
    barcode_svg = barcode_service.render_svg(ticket.code)
118
119
    # Encode SVG to be used inline as part of a data URI.
120
    # Replacements are not complete, but sufficient for this case.
121
    #
122
    # See https://codepen.io/tigt/post/optimizing-svgs-in-data-uris
123
    # for details.
124
    # fmt: off
125
    barcode_svg_inline = (
126
        barcode_svg
127
        .replace('\n', '%0A')
128
        .replace('#', '%23')
129
        .replace('<', '%3C')
130
        .replace('>', '%3E')
131
        .replace('"', "'")
132
    )
133
    # fmt: on
134
135
    return {
136
        'party_title': party.title,
137
        'ticket_code': ticket.code,
138
        'ticket_category_title': ticket_category.title,
139
        'ticket_owner': ticket.owned_by,
140
        'ticket_user': ticket.used_by,
141
        'occupied_seat': ticket.occupied_seat,
142
        'barcode_svg_inline': barcode_svg_inline,
143
    }
144
145
146 1
# -------------------------------------------------------------------- #
147 1
# user
148 1
149 1
150
@blueprint.get('/tickets/<uuid:ticket_id>/appoint_user')
151
@login_required
152
@templated
153
def appoint_user_form(ticket_id, erroneous_form=None):
154
    """Show a form to select a user to appoint for the ticket."""
155
    _abort_if_ticket_management_disabled()
156
157
    ticket = _get_ticket_or_404(ticket_id)
158
159
    _abort_if_ticket_user_checked_in(ticket)
160
161
    manager = g.user
162
163
    if not ticket.is_user_managed_by(manager.id):
164
        abort(403)
165
166
    form = erroneous_form if erroneous_form else SpecifyUserForm()
167
168
    return {
169
        'ticket': ticket,
170 1
        'form': form,
171 1
    }
172
173
174
@blueprint.post('/tickets/<uuid:ticket_id>/user')
175
def appoint_user(ticket_id):
176
    """Appoint a user for the ticket."""
177
    _abort_if_ticket_management_disabled()
178
179
    ticket = _get_ticket_or_404(ticket_id)
180
181
    _abort_if_ticket_user_checked_in(ticket)
182
183
    form = SpecifyUserForm(request.form)
184
    if not form.validate():
185
        return appoint_user_form(ticket_id, form)
186
187
    manager = g.user
188
189
    if not ticket.is_user_managed_by(manager.id):
190
        abort(403)
191
192
    previous_user = ticket.used_by if ticket.used_by_id != g.user.id else None
193
    new_user = form.user.data
194
195
    result = ticket_user_management_service.appoint_user(
196
        ticket.id, new_user.id, manager.id
197
    )
198
199
    if result.is_err():
200
        flash_error(result.unwrap_err().message)
201
        return redirect_to('.index_mine')
202
203
    flash_success(
204
        gettext(
205
            '%(screen_name)s has been assigned as user of ticket %(ticket_code)s.',
206
            screen_name=new_user.screen_name,
207
            ticket_code=ticket.code,
208
        )
209
    )
210
211
    if previous_user:
212
        notification_service.notify_withdrawn_user(
213
            ticket, previous_user, manager
214
        )
215
216
    notification_service.notify_appointed_user(ticket, new_user, manager)
217 1
218 1
    return redirect_to('.index_mine')
219 1
220
221
@blueprint.delete('/tickets/<uuid:ticket_id>/user')
222
@respond_no_content
223
def withdraw_user(ticket_id):
224
    """Withdraw the ticket's user and appoint its owner instead."""
225
    _abort_if_ticket_management_disabled()
226
227
    ticket = _get_ticket_or_404(ticket_id)
228
229
    _abort_if_ticket_user_checked_in(ticket)
230
231
    manager = g.user
232
233
    if not ticket.is_user_managed_by(manager.id):
234
        abort(403)
235
236
    previous_user = ticket.used_by if ticket.used_by_id != g.user.id else None
237
238
    # Intentionally assign the ticket user manager as the new
239
    # ticket user instead of removing the ticket user.
240
    result = ticket_user_management_service.appoint_user(
241
        ticket.id, manager.id, manager.id
242
    )
243
244
    if result.is_err():
245
        flash_error(result.unwrap_err().message)
246
        return
247
248
    flash_success(
249
        gettext(
250
            'You have been assigned as user of ticket %(ticket_code)s.',
251
            ticket_code=ticket.code,
252
        )
253
    )
254
255
    if previous_user:
256
        notification_service.notify_withdrawn_user(
257
            ticket, previous_user, manager
258
        )
259
260
261 1
# -------------------------------------------------------------------- #
262 1
# user manager
263 1
264 1
265
@blueprint.get('/tickets/<uuid:ticket_id>/appoint_user_manager')
266
@login_required
267
@templated
268
def appoint_user_manager_form(ticket_id, erroneous_form=None):
269
    """Show a form to select a user to appoint as user manager for the ticket."""
270
    _abort_if_ticket_management_disabled()
271
272
    ticket = _get_ticket_or_404(ticket_id)
273
274
    _abort_if_ticket_user_checked_in(ticket)
275
276
    manager = g.user
277
278
    if not ticket.is_owned_by(manager.id):
279
        abort(403)
280
281
    form = erroneous_form if erroneous_form else SpecifyUserForm()
282
283
    return {
284
        'ticket': ticket,
285 1
        'form': form,
286 1
    }
287
288
289 View Code Duplication
@blueprint.post('/tickets/<uuid:ticket_id>/user_manager')
290
def appoint_user_manager(ticket_id):
291
    """Appoint a user manager for the ticket."""
292
    _abort_if_ticket_management_disabled()
293
294
    ticket = _get_ticket_or_404(ticket_id)
295
296
    _abort_if_ticket_user_checked_in(ticket)
297
298
    form = SpecifyUserForm(request.form)
299
    if not form.validate():
300
        return appoint_user_manager_form(ticket_id, form)
301
302
    manager = g.user
303
304
    if not ticket.is_owned_by(manager.id):
305
        abort(403)
306
307
    user = form.user.data
308
309
    result = ticket_user_management_service.appoint_user_manager(
310
        ticket.id, user.id, manager.id
311
    )
312
313
    if result.is_err():
314
        flash_error(result.unwrap_err().message)
315
        return redirect_to('.index_mine')
316
317
    flash_success(
318
        gettext(
319
            '%(screen_name)s has been assigned as user manager '
320
            'of ticket %(ticket_code)s.',
321
            screen_name=user.screen_name,
322
            ticket_code=ticket.code,
323
        )
324
    )
325
326
    notification_service.notify_appointed_user_manager(ticket, user, manager)
327 1
328 1
    return redirect_to('.index_mine')
329 1
330
331
@blueprint.delete('/tickets/<uuid:ticket_id>/user_manager')
332
@respond_no_content
333
def withdraw_user_manager(ticket_id):
334
    """Withdraw the ticket's user manager."""
335
    _abort_if_ticket_management_disabled()
336
337
    ticket = _get_ticket_or_404(ticket_id)
338
339
    _abort_if_ticket_user_checked_in(ticket)
340
341
    manager = g.user
342
343
    if not ticket.is_owned_by(manager.id):
344
        abort(403)
345
346
    user = ticket.user_managed_by
347
348
    result = ticket_user_management_service.withdraw_user_manager(
349
        ticket.id, manager.id
350
    )
351
352
    if result.is_err():
353
        flash_error(result.unwrap_err().message)
354
        return
355
356
    flash_success(
357
        gettext(
358
            'User manager of ticket %(ticket_code)s has been removed.',
359
            ticket_code=ticket.code,
360
        )
361
    )
362
363
    notification_service.notify_withdrawn_user_manager(ticket, user, manager)
364
365
366 1
# -------------------------------------------------------------------- #
367 1
# seat manager
368 1
369 1
370
@blueprint.get('/tickets/<uuid:ticket_id>/appoint_seat_manager')
371
@login_required
372
@templated
373
def appoint_seat_manager_form(ticket_id, erroneous_form=None):
374
    """Show a form to select a user to appoint as seat manager for the ticket."""
375
    _abort_if_ticket_management_disabled()
376
377
    ticket = _get_ticket_or_404(ticket_id)
378
379
    manager = g.user
380
381
    if not ticket.is_owned_by(manager.id):
382
        abort(403)
383
384
    form = erroneous_form if erroneous_form else SpecifyUserForm()
385
386
    return {
387
        'ticket': ticket,
388 1
        'form': form,
389 1
    }
390
391
392 View Code Duplication
@blueprint.post('/tickets/<uuid:ticket_id>/seat_manager')
393
def appoint_seat_manager(ticket_id):
394
    """Appoint a seat manager for the ticket."""
395
    _abort_if_ticket_management_disabled()
396
397
    form = SpecifyUserForm(request.form)
398
    if not form.validate():
399
        return appoint_seat_manager_form(ticket_id, form)
400
401
    ticket = _get_ticket_or_404(ticket_id)
402
403
    manager = g.user
404
405
    if not ticket.is_owned_by(manager.id):
406
        abort(403)
407
408
    user = form.user.data
409
410
    result = ticket_seat_management_service.appoint_seat_manager(
411
        ticket.id, user.id, manager.id
412
    )
413
414
    if result.is_err():
415
        flash_error(result.unwrap_err().message)
416
        return redirect_to('.index_mine')
417
418
    flash_success(
419
        gettext(
420
            '%(screen_name)s has been assigned as seat manager '
421
            'of ticket %(ticket_code)s.',
422
            screen_name=user.screen_name,
423
            ticket_code=ticket.code,
424
        )
425
    )
426
427
    notification_service.notify_appointed_seat_manager(ticket, user, manager)
428 1
429 1
    return redirect_to('.index_mine')
430 1
431
432
@blueprint.delete('/tickets/<uuid:ticket_id>/seat_manager')
433
@respond_no_content
434
def withdraw_seat_manager(ticket_id):
435
    """Withdraw the ticket's seat manager."""
436
    _abort_if_ticket_management_disabled()
437
438
    ticket = _get_ticket_or_404(ticket_id)
439
440
    manager = g.user
441
442
    if not ticket.is_owned_by(manager.id):
443
        abort(403)
444
445
    user = ticket.seat_managed_by
446
447
    result = ticket_seat_management_service.withdraw_seat_manager(
448
        ticket.id, manager.id
449
    )
450
451
    if result.is_err():
452
        flash_error(result.unwrap_err().message)
453
        return
454
455
    flash_success(
456
        gettext(
457
            'Seat manager of ticket %(ticket_code)s has been removed.',
458
            ticket_code=ticket.code,
459
        )
460
    )
461
462
    notification_service.notify_withdrawn_seat_manager(ticket, user, manager)
463
464 1
465
# -------------------------------------------------------------------- #
466
467
468
def _abort_if_ticket_management_disabled():
469
    if not _is_ticket_management_enabled():
470 1
        flash_error(gettext('Tickets cannot be updated at this time.'))
471 1
        abort(403)
472
473
474 1
def _is_ticket_management_enabled():
475
    if not g.user.authenticated:
476
        return False
477 1
478
    if g.party is None:
479
        return False
480 1
481
    return g.party.ticket_management_enabled
482
483
484
def _get_ticket_or_404(ticket_id):
485
    ticket = ticket_service.find_ticket(ticket_id)
486
487
    if (ticket is None) or ticket.revoked:
488
        abort(404)
489 1
490
    return ticket
491
492
493
def _abort_if_ticket_user_checked_in(ticket):
494
    if ticket.user_checked_in:
495
        flash_error(
496
            gettext('Somebody has already been checked in with this ticket.')
497 1
        )
498
        abort(403)
499
500
501
def _is_user_allowed_to_print_ticket(ticket, user_id):
502
    """Return `True` only if the user is allowed to print the ticket."""
503
    return (
504
        ticket.is_owned_by(user_id)
505
        or ticket.is_managed_by(user_id)
506
        or ticket.used_by_id == user_id
507
    )
508