Total Complexity | 66 |
Total Lines | 916 |
Duplicated Lines | 26.75 % |
Coverage | 29.29% |
Changes | 0 |
Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like byceps.blueprints.admin.shop.article.views often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
1 | """ |
||
2 | byceps.blueprints.admin.shop.article.views |
||
3 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
||
4 | |||
5 | :Copyright: 2014-2023 Jochen Kupperschmidt |
||
6 | :License: Revised BSD (see `LICENSE` file for details) |
||
7 | """ |
||
8 | |||
9 | 1 | from __future__ import annotations |
|
10 | |||
11 | 1 | import dataclasses |
|
12 | 1 | from datetime import date, datetime, time |
|
13 | 1 | from decimal import Decimal |
|
14 | |||
15 | 1 | from flask import abort, request |
|
16 | 1 | from flask_babel import gettext, to_user_timezone, to_utc |
|
17 | 1 | from moneyed import Money |
|
18 | |||
19 | 1 | from byceps.services.brand import brand_service |
|
20 | 1 | from byceps.services.party import party_service |
|
21 | 1 | from byceps.services.shop.article import ( |
|
22 | article_sequence_service, |
||
23 | article_service, |
||
24 | ) |
||
25 | 1 | from byceps.services.shop.article.models import ( |
|
26 | Article, |
||
27 | ArticleNumberSequence, |
||
28 | ArticleType, |
||
29 | get_article_type_label, |
||
30 | ) |
||
31 | 1 | from byceps.services.shop.order import ( |
|
32 | order_action_registry_service, |
||
33 | order_action_service, |
||
34 | ordered_articles_service, |
||
35 | ) |
||
36 | 1 | from byceps.services.shop.order.models.order import Order, PaymentState |
|
37 | 1 | from byceps.services.shop.shop import shop_service |
|
38 | 1 | from byceps.services.shop.shop.models import ShopID |
|
39 | 1 | from byceps.services.ticketing import ticket_category_service |
|
40 | 1 | from byceps.services.user_badge import user_badge_service |
|
41 | 1 | from byceps.util.framework.blueprint import create_blueprint |
|
42 | 1 | from byceps.util.framework.flash import flash_error, flash_success |
|
43 | 1 | from byceps.util.framework.templating import templated |
|
44 | 1 | from byceps.util.views import ( |
|
45 | permission_required, |
||
46 | redirect_to, |
||
47 | respond_no_content, |
||
48 | ) |
||
49 | |||
50 | 1 | from .forms import ( |
|
51 | ArticleAttachmentCreateForm, |
||
52 | ArticleCreateForm, |
||
53 | ArticleNumberSequenceCreateForm, |
||
54 | ArticleUpdateForm, |
||
55 | RegisterBadgeAwardingActionForm, |
||
56 | RegisterTicketBundlesCreationActionForm, |
||
57 | RegisterTicketsCreationActionForm, |
||
58 | TicketArticleCreateForm, |
||
59 | TicketBundleArticleCreateForm, |
||
60 | ) |
||
61 | |||
62 | |||
63 | 1 | blueprint = create_blueprint('shop_article_admin', __name__) |
|
64 | |||
65 | |||
66 | 1 | TAX_RATE_DISPLAY_FACTOR = Decimal('100') |
|
67 | |||
68 | |||
69 | 1 | @blueprint.get('/for_shop/<shop_id>', defaults={'page': 1}) |
|
70 | 1 | @blueprint.get('/for_shop/<shop_id>/pages/<int:page>') |
|
71 | 1 | @permission_required('shop_article.view') |
|
72 | 1 | @templated |
|
73 | 1 | def index_for_shop(shop_id, page): |
|
74 | """List articles for that shop.""" |
||
75 | shop = _get_shop_or_404(shop_id) |
||
76 | |||
77 | brand = brand_service.get_brand(shop.brand_id) |
||
78 | |||
79 | per_page = request.args.get('per_page', type=int, default=15) |
||
80 | |||
81 | search_term = request.args.get('search_term', default='').strip() |
||
82 | |||
83 | articles = article_service.get_articles_for_shop_paginated( |
||
84 | shop.id, |
||
85 | page, |
||
86 | per_page, |
||
87 | search_term=search_term, |
||
88 | ) |
||
89 | |||
90 | # Inherit order of enum members. |
||
91 | article_type_labels_by_type = { |
||
92 | type_: get_article_type_label(type_) for type_ in ArticleType |
||
93 | } |
||
94 | |||
95 | totals_by_article_number = { |
||
96 | article.item_number: ordered_articles_service.count_ordered_articles( |
||
97 | article.id |
||
98 | ) |
||
99 | for article in articles.items |
||
100 | } |
||
101 | |||
102 | return { |
||
103 | 'shop': shop, |
||
104 | 'brand': brand, |
||
105 | 'articles': articles, |
||
106 | 'article_type_labels_by_type': article_type_labels_by_type, |
||
107 | 'totals_by_article_number': totals_by_article_number, |
||
108 | 'PaymentState': PaymentState, |
||
109 | 'per_page': per_page, |
||
110 | 'search_term': search_term, |
||
111 | } |
||
112 | |||
113 | |||
114 | 1 | @blueprint.get('/<uuid:article_id>') |
|
115 | 1 | @permission_required('shop_article.view') |
|
116 | 1 | @templated |
|
117 | 1 | def view(article_id): |
|
118 | """Show a single article.""" |
||
119 | article = article_service.find_article_with_details(article_id) |
||
120 | if article is None: |
||
121 | abort(404) |
||
122 | |||
123 | shop = shop_service.get_shop(article.shop_id) |
||
124 | |||
125 | brand = brand_service.get_brand(shop.brand_id) |
||
126 | |||
127 | type_label = get_article_type_label(article.type_) |
||
128 | |||
129 | if article.type_ in (ArticleType.ticket, ArticleType.ticket_bundle): |
||
130 | ticket_category = ticket_category_service.find_category( |
||
131 | article.type_params['ticket_category_id'] |
||
132 | ) |
||
133 | if ticket_category is not None: |
||
134 | ticket_party = party_service.get_party(ticket_category.party_id) |
||
135 | else: |
||
136 | ticket_party = None |
||
137 | else: |
||
138 | ticket_party = None |
||
139 | ticket_category = None |
||
140 | |||
141 | totals = ordered_articles_service.count_ordered_articles(article.id) |
||
142 | |||
143 | actions = order_action_service.get_actions_for_article(article.id) |
||
144 | actions.sort(key=lambda a: a.payment_state.name, reverse=True) |
||
145 | |||
146 | return { |
||
147 | 'article': article, |
||
148 | 'shop': shop, |
||
149 | 'brand': brand, |
||
150 | 'type_label': type_label, |
||
151 | 'ticket_category': ticket_category, |
||
152 | 'ticket_party': ticket_party, |
||
153 | 'totals': totals, |
||
154 | 'PaymentState': PaymentState, |
||
155 | 'actions': actions, |
||
156 | } |
||
157 | |||
158 | |||
159 | 1 | View Code Duplication | @blueprint.get('/<uuid:article_id>/orders') |
160 | 1 | @permission_required('shop_article.view') |
|
161 | 1 | @templated |
|
162 | 1 | def view_orders(article_id): |
|
163 | """List the orders for this article, and the corresponding quantities.""" |
||
164 | article = _get_article_or_404(article_id) |
||
165 | |||
166 | shop = shop_service.get_shop(article.shop_id) |
||
167 | brand = brand_service.get_brand(shop.brand_id) |
||
168 | |||
169 | orders = ordered_articles_service.get_orders_including_article(article.id) |
||
170 | |||
171 | def transform(order: Order) -> tuple[Order, int]: |
||
172 | quantity = sum( |
||
173 | line_item.quantity |
||
174 | for line_item in order.line_items |
||
175 | if line_item.article_id == article.id |
||
176 | ) |
||
177 | |||
178 | return order, quantity |
||
179 | |||
180 | orders_with_quantities = list(map(transform, orders)) |
||
181 | |||
182 | quantity_total = sum(quantity for _, quantity in orders_with_quantities) |
||
183 | |||
184 | return { |
||
185 | 'article': article, |
||
186 | 'shop': shop, |
||
187 | 'brand': brand, |
||
188 | 'quantity_total': quantity_total, |
||
189 | 'orders_with_quantities': orders_with_quantities, |
||
190 | 'now': datetime.utcnow(), |
||
191 | } |
||
192 | |||
193 | |||
194 | 1 | View Code Duplication | @blueprint.get('/<uuid:article_id>/purchases') |
195 | 1 | @permission_required('shop_article.view') |
|
196 | 1 | @templated |
|
197 | 1 | def view_purchases(article_id): |
|
198 | """List the purchases for this article, and the corresponding quantities.""" |
||
199 | article = _get_article_or_404(article_id) |
||
200 | |||
201 | shop = shop_service.get_shop(article.shop_id) |
||
202 | brand = brand_service.get_brand(shop.brand_id) |
||
203 | |||
204 | orders = ordered_articles_service.get_orders_including_article( |
||
205 | article.id, only_payment_state=PaymentState.paid |
||
206 | ) |
||
207 | |||
208 | def transform(order: Order) -> tuple[Order, int]: |
||
209 | quantity = sum( |
||
210 | line_item.quantity |
||
211 | for line_item in order.line_items |
||
212 | if line_item.article_id == article.id |
||
213 | ) |
||
214 | |||
215 | return order, quantity |
||
216 | |||
217 | orders_with_quantities = list(map(transform, orders)) |
||
218 | |||
219 | quantity_total = sum(quantity for _, quantity in orders_with_quantities) |
||
220 | |||
221 | return { |
||
222 | 'article': article, |
||
223 | 'shop': shop, |
||
224 | 'brand': brand, |
||
225 | 'quantity_total': quantity_total, |
||
226 | 'orders_with_quantities': orders_with_quantities, |
||
227 | 'now': datetime.utcnow(), |
||
228 | } |
||
229 | |||
230 | |||
231 | # -------------------------------------------------------------------- # |
||
232 | # create |
||
233 | |||
234 | |||
235 | 1 | View Code Duplication | @blueprint.get('/for_shop/<shop_id>/create/<type>') |
236 | 1 | @permission_required('shop_article.create') |
|
237 | 1 | @templated |
|
238 | 1 | def create_form(shop_id, type, erroneous_form=None): |
|
239 | """Show form to create an article.""" |
||
240 | shop = _get_shop_or_404(shop_id) |
||
241 | type_ = _get_article_type_or_400(type) |
||
242 | |||
243 | brand = brand_service.get_brand(shop.brand_id) |
||
244 | |||
245 | article_number_sequences = _get_active_article_number_sequences_for_shop( |
||
246 | shop.id |
||
247 | ) |
||
248 | article_number_sequence_available = bool(article_number_sequences) |
||
249 | |||
250 | form = ( |
||
251 | erroneous_form |
||
252 | if erroneous_form |
||
253 | else ArticleCreateForm( |
||
254 | price_amount=Decimal('0.0'), tax_rate=Decimal('19.0') |
||
255 | ) |
||
256 | ) |
||
257 | form.set_article_number_sequence_choices(article_number_sequences) |
||
258 | |||
259 | return { |
||
260 | 'shop': shop, |
||
261 | 'brand': brand, |
||
262 | 'article_type_name': type_.name, |
||
263 | 'article_type_label': get_article_type_label(type_), |
||
264 | 'article_number_sequence_available': article_number_sequence_available, |
||
265 | 'form': form, |
||
266 | } |
||
267 | |||
268 | |||
269 | 1 | View Code Duplication | @blueprint.get('/for_shop/<shop_id>/create/ticket') |
270 | 1 | @permission_required('shop_article.create') |
|
271 | 1 | @templated |
|
272 | 1 | def create_ticket_form(shop_id, erroneous_form=None): |
|
273 | """Show form to create a ticket article.""" |
||
274 | shop = _get_shop_or_404(shop_id) |
||
275 | type_ = ArticleType.ticket |
||
276 | |||
277 | brand = brand_service.get_brand(shop.brand_id) |
||
278 | |||
279 | article_number_sequences = _get_active_article_number_sequences_for_shop( |
||
280 | shop.id |
||
281 | ) |
||
282 | article_number_sequence_available = bool(article_number_sequences) |
||
283 | |||
284 | form = ( |
||
285 | erroneous_form |
||
286 | if erroneous_form |
||
287 | else TicketArticleCreateForm( |
||
288 | price_amount=Decimal('0.0'), tax_rate=Decimal('19.0') |
||
289 | ) |
||
290 | ) |
||
291 | form.set_article_number_sequence_choices(article_number_sequences) |
||
292 | form.set_ticket_category_choices(brand.id) |
||
293 | |||
294 | return { |
||
295 | 'shop': shop, |
||
296 | 'brand': brand, |
||
297 | 'article_type_name': type_.name, |
||
298 | 'article_type_label': get_article_type_label(type_), |
||
299 | 'article_number_sequence_available': article_number_sequence_available, |
||
300 | 'form': form, |
||
301 | } |
||
302 | |||
303 | |||
304 | 1 | View Code Duplication | @blueprint.get('/for_shop/<shop_id>/create/ticket_bundle') |
305 | 1 | @permission_required('shop_article.create') |
|
306 | 1 | @templated |
|
307 | 1 | def create_ticket_bundle_form(shop_id, erroneous_form=None): |
|
308 | """Show form to create a ticket bundle article.""" |
||
309 | shop = _get_shop_or_404(shop_id) |
||
310 | type_ = ArticleType.ticket_bundle |
||
311 | |||
312 | brand = brand_service.get_brand(shop.brand_id) |
||
313 | |||
314 | article_number_sequences = _get_active_article_number_sequences_for_shop( |
||
315 | shop.id |
||
316 | ) |
||
317 | article_number_sequence_available = bool(article_number_sequences) |
||
318 | |||
319 | form = ( |
||
320 | erroneous_form |
||
321 | if erroneous_form |
||
322 | else TicketBundleArticleCreateForm( |
||
323 | price_amount=Decimal('0.0'), tax_rate=Decimal('19.0') |
||
324 | ) |
||
325 | ) |
||
326 | form.set_article_number_sequence_choices(article_number_sequences) |
||
327 | form.set_ticket_category_choices(brand.id) |
||
328 | |||
329 | return { |
||
330 | 'shop': shop, |
||
331 | 'brand': brand, |
||
332 | 'article_type_name': type_.name, |
||
333 | 'article_type_label': get_article_type_label(type_), |
||
334 | 'article_number_sequence_available': article_number_sequence_available, |
||
335 | 'form': form, |
||
336 | } |
||
337 | |||
338 | |||
339 | 1 | @blueprint.post('/for_shop/<shop_id>/<type>') |
|
340 | 1 | @permission_required('shop_article.create') |
|
341 | 1 | def create(shop_id, type): |
|
342 | """Create an article.""" |
||
343 | shop = _get_shop_or_404(shop_id) |
||
344 | type_ = _get_article_type_or_400(type) |
||
345 | |||
346 | if type_ == ArticleType.ticket: |
||
347 | form = TicketArticleCreateForm(request.form) |
||
348 | elif type_ == ArticleType.ticket_bundle: |
||
349 | form = TicketBundleArticleCreateForm(request.form) |
||
350 | else: |
||
351 | form = ArticleCreateForm(request.form) |
||
352 | |||
353 | article_number_sequences = _get_active_article_number_sequences_for_shop( |
||
354 | shop.id |
||
355 | ) |
||
356 | if not article_number_sequences: |
||
357 | flash_error( |
||
358 | gettext('No article number sequences are defined for this shop.') |
||
359 | ) |
||
360 | return create_form(shop_id, type_, form) |
||
361 | |||
362 | form.set_article_number_sequence_choices(article_number_sequences) |
||
363 | if type_ in (ArticleType.ticket, ArticleType.ticket_bundle): |
||
364 | form.set_ticket_category_choices(shop.brand_id) |
||
365 | |||
366 | if not form.validate(): |
||
367 | return create_form(shop_id, type_, form) |
||
368 | |||
369 | article_number_sequence_id = form.article_number_sequence_id.data |
||
370 | if not article_number_sequence_id: |
||
371 | flash_error(gettext('No valid article number sequence was specified.')) |
||
372 | return create_form(shop_id, type_, form) |
||
373 | |||
374 | article_number_sequence = ( |
||
375 | article_sequence_service.get_article_number_sequence( |
||
376 | article_number_sequence_id |
||
377 | ) |
||
378 | ) |
||
379 | if article_number_sequence.shop_id != shop.id: |
||
380 | flash_error(gettext('No valid article number sequence was specified.')) |
||
381 | return create_form(shop_id, type_, form) |
||
382 | |||
383 | article_number_generation_result = ( |
||
384 | article_sequence_service.generate_article_number( |
||
385 | article_number_sequence.id |
||
386 | ) |
||
387 | ) |
||
388 | if article_number_generation_result.is_err(): |
||
389 | abort(500, article_number_generation_result.unwrap_err()) |
||
390 | |||
391 | item_number = article_number_generation_result.unwrap() |
||
392 | |||
393 | description = form.description.data.strip() |
||
394 | price = Money(form.price_amount.data, shop.currency) |
||
395 | tax_rate = form.tax_rate.data / TAX_RATE_DISPLAY_FACTOR |
||
396 | available_from_utc = _assemble_datetime_utc( |
||
397 | form.available_from_date.data, form.available_from_time.data |
||
398 | ) |
||
399 | available_until_utc = _assemble_datetime_utc( |
||
400 | form.available_until_date.data, form.available_until_time.data |
||
401 | ) |
||
402 | total_quantity = form.total_quantity.data |
||
403 | max_quantity_per_order = form.max_quantity_per_order.data |
||
404 | not_directly_orderable = form.not_directly_orderable.data |
||
405 | separate_order_required = form.separate_order_required.data |
||
406 | |||
407 | if type_ == ArticleType.ticket: |
||
408 | article = article_service.create_ticket_article( |
||
409 | shop.id, |
||
410 | item_number, |
||
411 | description, |
||
412 | price, |
||
413 | tax_rate, |
||
414 | total_quantity, |
||
415 | max_quantity_per_order, |
||
416 | form.ticket_category_id.data, |
||
417 | available_from=available_from_utc, |
||
418 | available_until=available_until_utc, |
||
419 | not_directly_orderable=not_directly_orderable, |
||
420 | separate_order_required=separate_order_required, |
||
421 | ) |
||
422 | elif type_ == ArticleType.ticket_bundle: |
||
423 | article = article_service.create_ticket_bundle_article( |
||
424 | shop.id, |
||
425 | item_number, |
||
426 | description, |
||
427 | price, |
||
428 | tax_rate, |
||
429 | total_quantity, |
||
430 | max_quantity_per_order, |
||
431 | form.ticket_category_id.data, |
||
432 | form.ticket_quantity.data, |
||
433 | available_from=available_from_utc, |
||
434 | available_until=available_until_utc, |
||
435 | not_directly_orderable=not_directly_orderable, |
||
436 | separate_order_required=separate_order_required, |
||
437 | ) |
||
438 | else: |
||
439 | processing_required = type_ == ArticleType.physical |
||
440 | |||
441 | article = article_service.create_article( |
||
442 | shop.id, |
||
443 | item_number, |
||
444 | type_, |
||
445 | description, |
||
446 | price, |
||
447 | tax_rate, |
||
448 | total_quantity, |
||
449 | max_quantity_per_order, |
||
450 | processing_required, |
||
451 | available_from=available_from_utc, |
||
452 | available_until=available_until_utc, |
||
453 | not_directly_orderable=not_directly_orderable, |
||
454 | separate_order_required=separate_order_required, |
||
455 | ) |
||
456 | |||
457 | flash_success( |
||
458 | gettext( |
||
459 | 'Article "%(item_number)s" has been created.', |
||
460 | item_number=article.item_number, |
||
461 | ) |
||
462 | ) |
||
463 | return redirect_to('.view', article_id=article.id) |
||
464 | |||
465 | |||
466 | # -------------------------------------------------------------------- # |
||
467 | # update |
||
468 | |||
469 | |||
470 | 1 | @blueprint.get('/<uuid:article_id>/update') |
|
471 | 1 | @permission_required('shop_article.update') |
|
472 | 1 | @templated |
|
473 | 1 | def update_form(article_id, erroneous_form=None): |
|
474 | """Show form to update an article.""" |
||
475 | article = _get_article_or_404(article_id) |
||
476 | |||
477 | shop = shop_service.get_shop(article.shop_id) |
||
478 | |||
479 | brand = brand_service.get_brand(shop.brand_id) |
||
480 | |||
481 | data = dataclasses.asdict(article) |
||
482 | data['price_amount'] = article.price.amount |
||
483 | if article.available_from: |
||
484 | available_from_local = to_user_timezone(article.available_from) |
||
485 | data['available_from_date'] = available_from_local.date() |
||
486 | data['available_from_time'] = available_from_local.time() |
||
487 | if article.available_until: |
||
488 | available_until_local = to_user_timezone(article.available_until) |
||
489 | data['available_until_date'] = available_until_local.date() |
||
490 | data['available_until_time'] = available_until_local.time() |
||
491 | |||
492 | form = erroneous_form if erroneous_form else ArticleUpdateForm(data=data) |
||
493 | form.tax_rate.data = article.tax_rate * TAX_RATE_DISPLAY_FACTOR |
||
494 | |||
495 | return { |
||
496 | 'article': article, |
||
497 | 'shop': shop, |
||
498 | 'brand': brand, |
||
499 | 'form': form, |
||
500 | } |
||
501 | |||
502 | |||
503 | 1 | @blueprint.post('/<uuid:article_id>') |
|
504 | 1 | @permission_required('shop_article.update') |
|
505 | 1 | def update(article_id): |
|
506 | """Update an article.""" |
||
507 | article = _get_article_or_404(article_id) |
||
508 | |||
509 | shop = shop_service.get_shop(article.shop_id) |
||
510 | |||
511 | form = ArticleUpdateForm(request.form) |
||
512 | if not form.validate(): |
||
513 | return update_form(article_id, form) |
||
514 | |||
515 | description = form.description.data.strip() |
||
516 | price = Money(form.price_amount.data, shop.currency) |
||
517 | tax_rate = form.tax_rate.data / TAX_RATE_DISPLAY_FACTOR |
||
518 | available_from_utc = _assemble_datetime_utc( |
||
519 | form.available_from_date.data, form.available_from_time.data |
||
520 | ) |
||
521 | available_until_utc = _assemble_datetime_utc( |
||
522 | form.available_until_date.data, form.available_until_time.data |
||
523 | ) |
||
524 | total_quantity = form.total_quantity.data |
||
525 | max_quantity_per_order = form.max_quantity_per_order.data |
||
526 | not_directly_orderable = form.not_directly_orderable.data |
||
527 | separate_order_required = form.separate_order_required.data |
||
528 | |||
529 | article = article_service.update_article( |
||
530 | article.id, |
||
531 | description, |
||
532 | price, |
||
533 | tax_rate, |
||
534 | available_from_utc, |
||
535 | available_until_utc, |
||
536 | total_quantity, |
||
537 | max_quantity_per_order, |
||
538 | not_directly_orderable, |
||
539 | separate_order_required, |
||
540 | ) |
||
541 | |||
542 | flash_success( |
||
543 | gettext( |
||
544 | 'Article "%(description)s" has been updated.', |
||
545 | description=article.description, |
||
546 | ) |
||
547 | ) |
||
548 | return redirect_to('.view', article_id=article.id) |
||
549 | |||
550 | |||
551 | # -------------------------------------------------------------------- # |
||
552 | # article attachments |
||
553 | |||
554 | |||
555 | 1 | @blueprint.get('/<uuid:article_id>/attachments/create') |
|
556 | 1 | @permission_required('shop_article.update') |
|
557 | 1 | @templated |
|
558 | 1 | def attachment_create_form(article_id, erroneous_form=None): |
|
559 | """Show form to attach an article to another article.""" |
||
560 | article = _get_article_or_404(article_id) |
||
561 | |||
562 | shop = shop_service.get_shop(article.shop_id) |
||
563 | |||
564 | brand = brand_service.get_brand(shop.brand_id) |
||
565 | |||
566 | attachable_articles = article_service.get_attachable_articles(article.id) |
||
567 | |||
568 | form = ( |
||
569 | erroneous_form |
||
570 | if erroneous_form |
||
571 | else ArticleAttachmentCreateForm(quantity=0) |
||
572 | ) |
||
573 | form.set_article_to_attach_choices(attachable_articles) |
||
574 | |||
575 | return { |
||
576 | 'article': article, |
||
577 | 'shop': shop, |
||
578 | 'brand': brand, |
||
579 | 'form': form, |
||
580 | } |
||
581 | |||
582 | |||
583 | 1 | @blueprint.post('/<uuid:article_id>/attachments') |
|
584 | 1 | @permission_required('shop_article.update') |
|
585 | 1 | def attachment_create(article_id): |
|
586 | """Attach an article to another article.""" |
||
587 | article = _get_article_or_404(article_id) |
||
588 | |||
589 | attachable_articles = article_service.get_attachable_articles(article.id) |
||
590 | |||
591 | form = ArticleAttachmentCreateForm(request.form) |
||
592 | form.set_article_to_attach_choices(attachable_articles) |
||
593 | |||
594 | if not form.validate(): |
||
595 | return attachment_create_form(article_id, form) |
||
596 | |||
597 | article_to_attach_id = form.article_to_attach_id.data |
||
598 | article_to_attach = article_service.get_article(article_to_attach_id) |
||
599 | quantity = form.quantity.data |
||
600 | |||
601 | article_service.attach_article(article_to_attach.id, quantity, article.id) |
||
602 | |||
603 | flash_success( |
||
604 | gettext( |
||
605 | 'Article "%(article_to_attach_item_number)s" has been attached %(quantity)s times to article "%(article_item_number)s".', |
||
606 | article_to_attach_item_number=article_to_attach.item_number, |
||
607 | quantity=quantity, |
||
608 | article_item_number=article.item_number, |
||
609 | ) |
||
610 | ) |
||
611 | return redirect_to('.view', article_id=article.id) |
||
612 | |||
613 | |||
614 | 1 | @blueprint.delete('/attachments/<uuid:article_id>') |
|
615 | 1 | @permission_required('shop_article.update') |
|
616 | 1 | @respond_no_content |
|
617 | 1 | def attachment_remove(article_id): |
|
618 | """Remove the attachment link from one article to another.""" |
||
619 | attached_article = article_service.find_attached_article(article_id) |
||
620 | |||
621 | if attached_article is None: |
||
622 | abort(404) |
||
623 | |||
624 | article = attached_article.article |
||
625 | attached_to_article = attached_article.attached_to_article |
||
626 | |||
627 | article_service.unattach_article(attached_article.id) |
||
628 | |||
629 | flash_success( |
||
630 | gettext( |
||
631 | 'Article "%(article_item_number)s" is no longer attached to article "%(attached_to_article_item_number)s".', |
||
632 | article_item_number=article.item_number, |
||
633 | attached_to_article_item_number=attached_to_article.item_number, |
||
634 | ) |
||
635 | ) |
||
636 | |||
637 | |||
638 | # -------------------------------------------------------------------- # |
||
639 | # actions |
||
640 | |||
641 | |||
642 | 1 | @blueprint.get('/<uuid:article_id>/actions/badge_awarding/create') |
|
643 | 1 | @permission_required('shop_article.update') |
|
644 | 1 | @templated |
|
645 | 1 | def action_create_form_for_badge_awarding(article_id, erroneous_form=None): |
|
646 | """Show form to register a badge awarding action for the article.""" |
||
647 | article = _get_article_or_404(article_id) |
||
648 | |||
649 | shop = shop_service.get_shop(article.shop_id) |
||
650 | brand = brand_service.get_brand(shop.brand_id) |
||
651 | |||
652 | badges = user_badge_service.get_all_badges() |
||
653 | |||
654 | form = ( |
||
655 | erroneous_form if erroneous_form else RegisterBadgeAwardingActionForm() |
||
656 | ) |
||
657 | form.set_badge_choices(badges) |
||
658 | |||
659 | return { |
||
660 | 'article': article, |
||
661 | 'shop': shop, |
||
662 | 'brand': brand, |
||
663 | 'form': form, |
||
664 | } |
||
665 | |||
666 | |||
667 | 1 | @blueprint.post('/<uuid:article_id>/actions/badge_awarding') |
|
668 | 1 | @permission_required('shop_article.update') |
|
669 | 1 | def action_create_for_badge_awarding(article_id): |
|
670 | """Register a badge awarding action for the article.""" |
||
671 | article = _get_article_or_404(article_id) |
||
672 | |||
673 | badges = user_badge_service.get_all_badges() |
||
674 | |||
675 | form = RegisterBadgeAwardingActionForm(request.form) |
||
676 | form.set_badge_choices(badges) |
||
677 | |||
678 | if not form.validate(): |
||
679 | return action_create_form_for_badge_awarding(article_id, form) |
||
680 | |||
681 | badge_id = form.badge_id.data |
||
682 | badge = user_badge_service.get_badge(badge_id) |
||
683 | |||
684 | order_action_registry_service.register_badge_awarding(article.id, badge.id) |
||
685 | |||
686 | flash_success(gettext('Action has been added.')) |
||
687 | |||
688 | return redirect_to('.view', article_id=article.id) |
||
689 | |||
690 | |||
691 | 1 | @blueprint.get('/<uuid:article_id>/actions/tickets_creation/create') |
|
692 | 1 | @permission_required('shop_article.update') |
|
693 | 1 | @templated |
|
694 | 1 | def action_create_form_for_tickets_creation(article_id, erroneous_form=None): |
|
695 | """Show form to register a tickets creation action for the article.""" |
||
696 | article = _get_article_or_404(article_id) |
||
697 | |||
698 | shop = shop_service.get_shop(article.shop_id) |
||
699 | brand = brand_service.get_brand(shop.brand_id) |
||
700 | |||
701 | form = ( |
||
702 | erroneous_form |
||
703 | if erroneous_form |
||
704 | else RegisterTicketsCreationActionForm() |
||
705 | ) |
||
706 | form.set_category_choices(brand.id) |
||
707 | |||
708 | return { |
||
709 | 'article': article, |
||
710 | 'shop': shop, |
||
711 | 'brand': brand, |
||
712 | 'form': form, |
||
713 | } |
||
714 | |||
715 | |||
716 | 1 | View Code Duplication | @blueprint.post('/<uuid:article_id>/actions/tickets_creation') |
717 | 1 | @permission_required('shop_article.update') |
|
718 | 1 | def action_create_for_tickets_creation(article_id): |
|
719 | """Register a tickets creation action for the article.""" |
||
720 | article = _get_article_or_404(article_id) |
||
721 | |||
722 | shop = shop_service.get_shop(article.shop_id) |
||
723 | brand = brand_service.get_brand(shop.brand_id) |
||
724 | |||
725 | form = RegisterTicketsCreationActionForm(request.form) |
||
726 | form.set_category_choices(brand.id) |
||
727 | |||
728 | if not form.validate(): |
||
729 | return action_create_form_for_tickets_creation(article_id, form) |
||
730 | |||
731 | category_id = form.category_id.data |
||
732 | category = ticket_category_service.get_category(category_id) |
||
733 | |||
734 | order_action_registry_service.register_tickets_creation( |
||
735 | article.id, category.id |
||
736 | ) |
||
737 | |||
738 | flash_success(gettext('Action has been added.')) |
||
739 | |||
740 | return redirect_to('.view', article_id=article.id) |
||
741 | |||
742 | |||
743 | 1 | @blueprint.get('/<uuid:article_id>/actions/ticket_bundles_creation/create') |
|
744 | 1 | @permission_required('shop_article.update') |
|
745 | 1 | @templated |
|
746 | 1 | def action_create_form_for_ticket_bundles_creation( |
|
747 | article_id, erroneous_form=None |
||
748 | ): |
||
749 | """Show form to register a ticket bundles creation action for the article.""" |
||
750 | article = _get_article_or_404(article_id) |
||
751 | |||
752 | shop = shop_service.get_shop(article.shop_id) |
||
753 | brand = brand_service.get_brand(shop.brand_id) |
||
754 | |||
755 | form = ( |
||
756 | erroneous_form |
||
757 | if erroneous_form |
||
758 | else RegisterTicketBundlesCreationActionForm() |
||
759 | ) |
||
760 | form.set_category_choices(brand.id) |
||
761 | |||
762 | return { |
||
763 | 'article': article, |
||
764 | 'shop': shop, |
||
765 | 'brand': brand, |
||
766 | 'form': form, |
||
767 | } |
||
768 | |||
769 | |||
770 | 1 | View Code Duplication | @blueprint.post('/<uuid:article_id>/actions/ticket_bundles_creation') |
771 | 1 | @permission_required('shop_article.update') |
|
772 | 1 | def action_create_for_ticket_bundles_creation(article_id): |
|
773 | """Register a ticket bundles creation action for the article.""" |
||
774 | article = _get_article_or_404(article_id) |
||
775 | |||
776 | shop = shop_service.get_shop(article.shop_id) |
||
777 | brand = brand_service.get_brand(shop.brand_id) |
||
778 | |||
779 | form = RegisterTicketBundlesCreationActionForm(request.form) |
||
780 | form.set_category_choices(brand.id) |
||
781 | |||
782 | if not form.validate(): |
||
783 | return action_create_form_for_ticket_bundles_creation(article_id, form) |
||
784 | |||
785 | category_id = form.category_id.data |
||
786 | category = ticket_category_service.get_category(category_id) |
||
787 | |||
788 | ticket_quantity = form.ticket_quantity.data |
||
789 | |||
790 | order_action_registry_service.register_ticket_bundles_creation( |
||
791 | article.id, category.id, ticket_quantity |
||
792 | ) |
||
793 | |||
794 | flash_success(gettext('Action has been added.')) |
||
795 | |||
796 | return redirect_to('.view', article_id=article.id) |
||
797 | |||
798 | |||
799 | 1 | @blueprint.delete('/actions/<uuid:action_id>') |
|
800 | 1 | @permission_required('shop_article.update') |
|
801 | 1 | @respond_no_content |
|
802 | 1 | def action_remove(action_id): |
|
803 | """Remove the action from the article.""" |
||
804 | action = order_action_service.find_action(action_id) |
||
805 | |||
806 | if action is None: |
||
807 | abort(404) |
||
808 | |||
809 | order_action_service.delete_action(action.id) |
||
810 | |||
811 | flash_success(gettext('Action has been removed.')) |
||
812 | |||
813 | |||
814 | # -------------------------------------------------------------------- # |
||
815 | # article number sequences |
||
816 | |||
817 | |||
818 | 1 | @blueprint.get('/number_sequences/for_shop/<shop_id>/create') |
|
819 | 1 | @permission_required('shop_article.create') |
|
820 | 1 | @templated |
|
821 | 1 | def create_number_sequence_form(shop_id, erroneous_form=None): |
|
822 | """Show form to create an article number sequence.""" |
||
823 | shop = _get_shop_or_404(shop_id) |
||
824 | |||
825 | brand = brand_service.get_brand(shop.brand_id) |
||
826 | |||
827 | form = ( |
||
828 | erroneous_form if erroneous_form else ArticleNumberSequenceCreateForm() |
||
829 | ) |
||
830 | |||
831 | return { |
||
832 | 'shop': shop, |
||
833 | 'brand': brand, |
||
834 | 'form': form, |
||
835 | } |
||
836 | |||
837 | |||
838 | 1 | View Code Duplication | @blueprint.post('/number_sequences/for_shop/<shop_id>') |
839 | 1 | @permission_required('shop_article.create') |
|
840 | 1 | def create_number_sequence(shop_id): |
|
841 | """Create an article number sequence.""" |
||
842 | shop = _get_shop_or_404(shop_id) |
||
843 | |||
844 | form = ArticleNumberSequenceCreateForm(request.form) |
||
845 | if not form.validate(): |
||
846 | return create_number_sequence_form(shop_id, form) |
||
847 | |||
848 | prefix = form.prefix.data.strip() |
||
849 | |||
850 | creation_result = article_sequence_service.create_article_number_sequence( |
||
851 | shop.id, prefix |
||
852 | ) |
||
853 | if creation_result.is_err(): |
||
854 | flash_error( |
||
855 | gettext( |
||
856 | 'Article number sequence could not be created. ' |
||
857 | 'Is prefix "%(prefix)s" already defined?', |
||
858 | prefix=prefix, |
||
859 | ) |
||
860 | ) |
||
861 | return create_number_sequence_form(shop.id, form) |
||
862 | |||
863 | flash_success( |
||
864 | gettext( |
||
865 | 'Article number sequence with prefix "%(prefix)s" has been created.', |
||
866 | prefix=prefix, |
||
867 | ) |
||
868 | ) |
||
869 | return redirect_to('.index_for_shop', shop_id=shop.id) |
||
870 | |||
871 | |||
872 | # -------------------------------------------------------------------- # |
||
873 | # helpers |
||
874 | |||
875 | |||
876 | 1 | def _get_shop_or_404(shop_id): |
|
877 | shop = shop_service.find_shop(shop_id) |
||
878 | |||
879 | if shop is None: |
||
880 | abort(404) |
||
881 | |||
882 | return shop |
||
883 | |||
884 | |||
885 | 1 | def _get_article_or_404(article_id) -> Article: |
|
886 | article = article_service.find_article(article_id) |
||
887 | |||
888 | if article is None: |
||
889 | abort(404) |
||
890 | |||
891 | return article |
||
892 | |||
893 | |||
894 | 1 | def _get_article_type_or_400(value: str) -> ArticleType: |
|
895 | try: |
||
896 | return ArticleType[value] |
||
897 | except KeyError: |
||
898 | abort(400, 'Unknown article type') |
||
899 | |||
900 | |||
901 | 1 | def _get_active_article_number_sequences_for_shop( |
|
902 | shop_id: ShopID, |
||
903 | ) -> list[ArticleNumberSequence]: |
||
904 | sequences = article_sequence_service.get_article_number_sequences_for_shop( |
||
905 | shop_id |
||
906 | ) |
||
907 | return [sequence for sequence in sequences if not sequence.archived] |
||
908 | |||
909 | |||
910 | 1 | def _assemble_datetime_utc(d: date, t: time) -> datetime | None: |
|
911 | if not d or not t: |
||
912 | return None |
||
913 | |||
914 | local_dt = datetime.combine(d, t) |
||
915 | return to_utc(local_dt) |
||
916 |