1
|
|
|
""" |
2
|
|
|
byceps.blueprints.admin.board.views |
3
|
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
4
|
|
|
|
5
|
|
|
:Copyright: 2014-2024 Jochen Kupperschmidt |
6
|
|
|
:License: Revised BSD (see `LICENSE` file for details) |
7
|
|
|
""" |
8
|
|
|
|
9
|
1 |
|
from dataclasses import dataclass |
10
|
|
|
from uuid import UUID |
11
|
1 |
|
|
12
|
|
|
from flask import abort, request |
13
|
1 |
|
from flask_babel import gettext |
14
|
1 |
|
|
15
|
|
|
from byceps.services.board import ( |
16
|
1 |
|
board_category_command_service, |
17
|
|
|
board_category_query_service, |
18
|
|
|
board_posting_query_service, |
19
|
|
|
board_service, |
20
|
|
|
board_topic_query_service, |
21
|
|
|
) |
22
|
|
|
from byceps.services.board.models import Board, BoardCategory, BoardCategoryID |
23
|
1 |
|
from byceps.services.brand import brand_service |
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.views import ( |
28
|
1 |
|
permission_required, |
29
|
|
|
redirect_to, |
30
|
|
|
respond_no_content, |
31
|
|
|
) |
32
|
|
|
|
33
|
|
|
from .forms import BoardCreateForm, CategoryCreateForm, CategoryUpdateForm |
34
|
1 |
|
|
35
|
|
|
|
36
|
|
|
blueprint = create_blueprint('board_admin', __name__) |
37
|
1 |
|
|
38
|
|
|
|
39
|
|
|
@dataclass(frozen=True) |
40
|
1 |
|
class BoardStats: |
41
|
1 |
|
category_count: int |
42
|
1 |
|
topic_count: int |
43
|
1 |
|
posting_count: int |
44
|
1 |
|
|
45
|
|
|
|
46
|
|
|
# -------------------------------------------------------------------- # |
47
|
|
|
# boards |
48
|
|
|
|
49
|
|
|
|
50
|
|
|
@blueprint.get('/brands/<brand_id>') |
51
|
1 |
|
@permission_required('board_category.view') |
52
|
1 |
|
@templated |
53
|
1 |
|
def board_index_for_brand(brand_id): |
54
|
1 |
|
"""List categories for that brand.""" |
55
|
|
|
brand = _get_brand_or_404(brand_id) |
56
|
1 |
|
|
57
|
|
|
boards = board_service.get_boards_for_brand(brand.id) |
58
|
1 |
|
board_ids = [board.id for board in boards] |
59
|
1 |
|
|
60
|
|
|
stats_by_board_id = { |
61
|
1 |
|
board_id: BoardStats( |
62
|
|
|
board_category_query_service.count_categories_for_board(board_id), |
63
|
|
|
board_topic_query_service.count_topics_for_board(board_id), |
64
|
|
|
board_posting_query_service.count_postings_for_board(board_id), |
65
|
|
|
) |
66
|
|
|
for board_id in board_ids |
67
|
|
|
} |
68
|
|
|
|
69
|
|
|
return { |
70
|
1 |
|
'boards': boards, |
71
|
|
|
'brand': brand, |
72
|
|
|
'stats_by_board_id': stats_by_board_id, |
73
|
|
|
} |
74
|
|
|
|
75
|
|
|
|
76
|
|
|
@blueprint.get('/boards/<board_id>') |
77
|
1 |
|
@permission_required('board_category.view') |
78
|
1 |
|
@templated |
79
|
1 |
|
def board_view(board_id): |
80
|
1 |
|
"""View the board.""" |
81
|
|
|
board = _get_board_or_404(board_id) |
82
|
1 |
|
|
83
|
|
|
brand = brand_service.find_brand(board.brand_id) |
84
|
1 |
|
|
85
|
|
|
categories = board_category_query_service.get_categories(board.id) |
86
|
1 |
|
|
87
|
|
|
return { |
88
|
1 |
|
'board_id': board.id, |
89
|
|
|
'board_brand_id': board.brand_id, |
90
|
|
|
'brand': brand, |
91
|
|
|
'categories': categories, |
92
|
|
|
} |
93
|
|
|
|
94
|
|
|
|
95
|
|
|
@blueprint.get('/for_brand/<brand_id>/boards/create') |
96
|
1 |
|
@permission_required('board.create') |
97
|
1 |
|
@templated |
98
|
1 |
|
def board_create_form(brand_id, erroneous_form=None): |
99
|
1 |
|
"""Show form to create a board.""" |
100
|
|
|
brand = _get_brand_or_404(brand_id) |
101
|
1 |
|
|
102
|
|
|
form = erroneous_form if erroneous_form else BoardCreateForm() |
103
|
1 |
|
|
104
|
|
|
return { |
105
|
1 |
|
'brand': brand, |
106
|
|
|
'form': form, |
107
|
|
|
} |
108
|
|
|
|
109
|
|
|
|
110
|
|
|
@blueprint.post('/for_brand/<brand_id>/boards') |
111
|
1 |
|
@permission_required('board.create') |
112
|
1 |
|
def board_create(brand_id): |
113
|
1 |
|
"""Create a board.""" |
114
|
|
|
brand = _get_brand_or_404(brand_id) |
115
|
1 |
|
|
116
|
|
|
form = BoardCreateForm(request.form) |
117
|
1 |
|
if not form.validate(): |
118
|
1 |
|
return board_create_form(brand.id, form) |
119
|
|
|
|
120
|
|
|
board_id = form.board_id.data.strip().lower() |
121
|
1 |
|
|
122
|
|
|
board = board_service.create_board(brand, board_id) |
123
|
1 |
|
|
124
|
|
|
flash_success( |
125
|
1 |
|
gettext( |
126
|
|
|
'Board with ID "%(board_id)s" has been created.', |
127
|
|
|
board_id=board.id, |
128
|
|
|
) |
129
|
|
|
) |
130
|
|
|
return redirect_to('.board_view', board_id=board.id) |
131
|
1 |
|
|
132
|
|
|
|
133
|
|
|
# -------------------------------------------------------------------- # |
134
|
|
|
# categories |
135
|
|
|
|
136
|
|
|
|
137
|
|
|
@blueprint.get('/categories/for_board/<board_id>/copy_from/<source_board_id>') |
138
|
1 |
|
@permission_required('board_category.create') |
139
|
1 |
|
@templated |
140
|
1 |
|
def category_copy_from_form(board_id, source_board_id, erroneous_form=None): |
141
|
1 |
|
"""Show form to copy an existing category to this board.""" |
142
|
|
|
board = _get_board_or_404(board_id) |
143
|
|
|
|
144
|
|
|
brand = brand_service.find_brand(board.brand_id) |
145
|
|
|
|
146
|
|
|
source_board = _get_board_or_404(source_board_id) |
147
|
|
|
|
148
|
|
|
categories = board_category_query_service.get_categories(source_board.id) |
149
|
|
|
|
150
|
|
|
return { |
151
|
|
|
'board': board, |
152
|
|
|
'brand': brand, |
153
|
|
|
'categories': categories, |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
|
157
|
|
|
@blueprint.get('/categories/for_board/<board_id>/create') |
158
|
1 |
|
@permission_required('board_category.create') |
159
|
1 |
|
@templated |
160
|
1 |
|
def category_create_form(board_id, erroneous_form=None): |
161
|
1 |
|
"""Show form to create a category.""" |
162
|
|
|
board = _get_board_or_404(board_id) |
163
|
1 |
|
|
164
|
|
|
brand = brand_service.find_brand(board.brand_id) |
165
|
1 |
|
|
166
|
|
|
brand_boards = board_service.get_boards_for_brand(brand.id) |
167
|
1 |
|
|
168
|
|
|
source_category = _get_source_category() |
169
|
1 |
|
|
170
|
|
|
form = ( |
171
|
1 |
|
erroneous_form |
172
|
|
|
if erroneous_form |
173
|
|
|
else CategoryCreateForm(obj=source_category) |
174
|
|
|
) |
175
|
|
|
|
176
|
|
|
return { |
177
|
1 |
|
'board': board, |
178
|
|
|
'brand': brand, |
179
|
|
|
'brand_boards': brand_boards, |
180
|
|
|
'form': form, |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
|
184
|
|
|
def _get_source_category() -> BoardCategory | None: |
185
|
1 |
|
source_category_id = request.args.get('source_category_id') |
186
|
1 |
|
if not source_category_id: |
187
|
1 |
|
return None |
188
|
1 |
|
|
189
|
|
|
return board_category_query_service.find_category_by_id( |
190
|
|
|
BoardCategoryID(UUID(source_category_id)) |
191
|
|
|
) |
192
|
|
|
|
193
|
1 |
|
|
194
|
1 |
|
@blueprint.post('/categories/for_board/<board_id>') |
195
|
1 |
|
@permission_required('board_category.create') |
196
|
|
|
def category_create(board_id): |
197
|
1 |
|
"""Create a category.""" |
198
|
|
|
board = _get_board_or_404(board_id) |
199
|
1 |
|
|
200
|
1 |
|
form = CategoryCreateForm(request.form) |
201
|
|
|
if not form.validate(): |
202
|
|
|
return category_create_form(board.id, form) |
203
|
1 |
|
|
204
|
1 |
|
slug = form.slug.data.strip().lower() |
205
|
1 |
|
title = form.title.data.strip() |
206
|
|
|
description = form.description.data.strip() |
207
|
1 |
|
|
208
|
|
|
category = board_category_command_service.create_category( |
209
|
|
|
board.id, slug, title, description |
210
|
|
|
) |
211
|
1 |
|
|
212
|
|
|
flash_success( |
213
|
|
|
gettext('Category "%(title)s" has been created.', title=category.title) |
214
|
1 |
|
) |
215
|
|
|
return redirect_to('.board_view', board_id=board.id) |
216
|
|
|
|
217
|
1 |
|
|
218
|
1 |
|
@blueprint.get('/categories/<uuid:category_id>/update') |
219
|
1 |
|
@permission_required('board_category.update') |
220
|
1 |
|
@templated |
221
|
|
|
def category_update_form(category_id, erroneous_form=None): |
222
|
1 |
|
"""Show form to update the category.""" |
223
|
|
|
category = _get_category_or_404(category_id) |
224
|
1 |
|
|
225
|
1 |
|
board = board_service.find_board(category.board_id) |
226
|
|
|
brand = brand_service.find_brand(board.brand_id) |
227
|
1 |
|
|
228
|
|
|
form = ( |
229
|
|
|
erroneous_form if erroneous_form else CategoryUpdateForm(obj=category) |
230
|
|
|
) |
231
|
1 |
|
|
232
|
|
|
return { |
233
|
|
|
'category': category, |
234
|
|
|
'brand': brand, |
235
|
|
|
'form': form, |
236
|
|
|
} |
237
|
|
|
|
238
|
1 |
|
|
239
|
1 |
|
@blueprint.post('/categories/<uuid:category_id>') |
240
|
1 |
|
@permission_required('board_category.update') |
241
|
|
|
def category_update(category_id): |
242
|
|
|
"""Update the category.""" |
243
|
|
|
category = _get_category_or_404(category_id) |
244
|
|
|
|
245
|
|
|
form = CategoryUpdateForm(request.form) |
246
|
|
|
if not form.validate(): |
247
|
|
|
return category_update_form(category_id, form) |
248
|
|
|
|
249
|
|
|
slug = form.slug.data |
250
|
|
|
title = form.title.data |
251
|
|
|
description = form.description.data |
252
|
|
|
|
253
|
|
|
category = board_category_command_service.update_category( |
254
|
|
|
category.id, slug, title, description |
255
|
|
|
) |
256
|
|
|
|
257
|
|
|
flash_success( |
258
|
|
|
gettext('Category "%(title)s" has been updated.', title=category.title) |
259
|
|
|
) |
260
|
|
|
return redirect_to('.board_view', board_id=category.board_id) |
261
|
|
|
|
262
|
1 |
|
|
263
|
1 |
|
@blueprint.post('/categories/<uuid:category_id>/flags/hidden') |
264
|
1 |
|
@permission_required('board_category.update') |
265
|
1 |
|
@respond_no_content |
266
|
|
|
def category_hide(category_id): |
267
|
|
|
"""Hide the category.""" |
268
|
|
|
category = _get_category_or_404(category_id) |
269
|
|
|
|
270
|
|
|
board_category_command_service.hide_category(category.id) |
271
|
|
|
|
272
|
|
|
flash_success( |
273
|
|
|
gettext('Category "%(title)s" has been hidden.', title=category.title) |
274
|
|
|
) |
275
|
|
|
|
276
|
1 |
|
|
277
|
1 |
|
@blueprint.delete('/categories/<uuid:category_id>/flags/hidden') |
278
|
1 |
|
@permission_required('board_category.update') |
279
|
1 |
|
@respond_no_content |
280
|
|
|
def category_unhide(category_id): |
281
|
|
|
"""Un-hide the category.""" |
282
|
|
|
category = _get_category_or_404(category_id) |
283
|
|
|
|
284
|
|
|
board_category_command_service.unhide_category(category.id) |
285
|
|
|
|
286
|
|
|
flash_success( |
287
|
|
|
gettext( |
288
|
|
|
'Category "%(title)s" has been made visible.', title=category.title |
289
|
|
|
) |
290
|
|
|
) |
291
|
|
|
|
292
|
1 |
|
|
293
|
1 |
|
@blueprint.post('/categories/<uuid:category_id>/up') |
294
|
1 |
|
@permission_required('board_category.update') |
295
|
1 |
|
@respond_no_content |
296
|
|
|
def category_move_up(category_id): |
297
|
|
|
"""Move the category upwards by one position.""" |
298
|
|
|
category = _get_category_or_404(category_id) |
299
|
|
|
|
300
|
|
|
result = board_category_command_service.move_category_up(category.id) |
301
|
|
|
|
302
|
|
|
if result.is_err(): |
303
|
|
|
flash_error( |
304
|
|
|
gettext( |
305
|
|
|
'Category "%(title)s" is already at the top.', |
306
|
|
|
title=category.title, |
307
|
|
|
) |
308
|
|
|
) |
309
|
|
|
return |
310
|
|
|
|
311
|
|
|
flash_success( |
312
|
|
|
gettext( |
313
|
|
|
'Category "%(title)s" has been moved upwards by one position.', |
314
|
|
|
title=category.title, |
315
|
|
|
) |
316
|
|
|
) |
317
|
|
|
|
318
|
1 |
|
|
319
|
1 |
|
@blueprint.post('/categories/<uuid:category_id>/down') |
320
|
1 |
|
@permission_required('board_category.update') |
321
|
1 |
|
@respond_no_content |
322
|
|
|
def category_move_down(category_id): |
323
|
|
|
"""Move the category downwards by one position.""" |
324
|
|
|
category = _get_category_or_404(category_id) |
325
|
|
|
|
326
|
|
|
result = board_category_command_service.move_category_down(category.id) |
327
|
|
|
|
328
|
|
|
if result.is_err(): |
329
|
|
|
flash_error( |
330
|
|
|
gettext( |
331
|
|
|
'Category "%(title)s" is already at the bottom.', |
332
|
|
|
title=category.title, |
333
|
|
|
) |
334
|
|
|
) |
335
|
|
|
return |
336
|
|
|
|
337
|
|
|
flash_success( |
338
|
|
|
gettext( |
339
|
|
|
'Category "%(title)s" has been moved downwards by one position.', |
340
|
|
|
title=category.title, |
341
|
|
|
) |
342
|
|
|
) |
343
|
|
|
|
344
|
1 |
|
|
345
|
1 |
|
@blueprint.delete('/categories/<uuid:category_id>') |
346
|
1 |
|
@permission_required('board_category.create') |
347
|
1 |
|
@respond_no_content |
348
|
|
|
def category_delete(category_id): |
349
|
|
|
"""Delete a category.""" |
350
|
|
|
category = _get_category_or_404(category_id) |
351
|
|
|
|
352
|
|
|
result = board_category_command_service.delete_category(category.id) |
353
|
|
|
|
354
|
|
|
if result.is_err(): |
355
|
|
|
flash_error( |
356
|
|
|
gettext( |
357
|
|
|
'Category "%(title)s" could not be deleted.', |
358
|
|
|
title=category.title, |
359
|
|
|
) |
360
|
|
|
) |
361
|
|
|
return |
362
|
|
|
|
363
|
|
|
flash_success( |
364
|
|
|
gettext('Category "%(title)s" has been deleted.', title=category.title) |
365
|
|
|
) |
366
|
|
|
|
367
|
|
|
|
368
|
|
|
# -------------------------------------------------------------------- # |
369
|
|
|
# helpers |
370
|
|
|
|
371
|
1 |
|
|
372
|
1 |
|
def _get_brand_or_404(brand_id): |
373
|
|
|
brand = brand_service.find_brand(brand_id) |
374
|
1 |
|
|
375
|
|
|
if brand is None: |
376
|
|
|
abort(404) |
377
|
1 |
|
|
378
|
|
|
return brand |
379
|
|
|
|
380
|
1 |
|
|
381
|
1 |
|
def _get_board_or_404(board_id) -> Board: |
382
|
|
|
board = board_service.find_board(board_id) |
383
|
1 |
|
|
384
|
|
|
if board is None: |
385
|
|
|
abort(404) |
386
|
1 |
|
|
387
|
|
|
return board |
388
|
|
|
|
389
|
1 |
|
|
390
|
1 |
|
def _get_category_or_404(category_id: UUID) -> BoardCategory: |
391
|
|
|
category = board_category_query_service.find_category_by_id( |
392
|
1 |
|
BoardCategoryID(category_id) |
393
|
|
|
) |
394
|
|
|
|
395
|
1 |
|
if category is None: |
396
|
|
|
abort(404) |
397
|
|
|
|
398
|
|
|
return category |
399
|
|
|
|