topic_update_form()   B
last analyzed

Complexity

Conditions 6

Size

Total Lines 35
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 20.3996

Importance

Changes 0
Metric Value
cc 6
eloc 26
nop 2
dl 0
loc 35
ccs 5
cts 19
cp 0.2632
crap 20.3996
rs 8.3226
c 0
b 0
f 0
1
"""
2
byceps.blueprints.site.board.views_topic
3
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4
5
:Copyright: 2014-2024 Jochen Kupperschmidt
6
:License: Revised BSD (see `LICENSE` file for details)
7
"""
8
9 1
import dataclasses
10
from datetime import datetime
11 1
12 1
from flask import abort, g, redirect, request
13
from flask_babel import gettext
14 1
15 1
from byceps.blueprints.site.site.navigation import subnavigation_for_view
16
from byceps.services.authn.session.models import CurrentUser
17 1
from byceps.services.board import (
18 1
    board_category_query_service,
19 1
    board_last_view_service,
20
    board_posting_query_service,
21
    board_topic_command_service,
22
    board_topic_query_service,
23
)
24
from byceps.services.board.models import TopicID
25
from byceps.services.orga_team import orga_team_service
26 1
from byceps.services.text_markup import text_markup_service
27 1
from byceps.services.user import user_service
28 1
from byceps.signals import board as board_signals
29 1
from byceps.util.framework.flash import flash_error, flash_success
30 1
from byceps.util.framework.templating import templated
31 1
from byceps.util.views import (
32 1
    permission_required,
33 1
    respond_no_content_with_location,
34
)
35
36
from . import _helpers as h, service
37
from .blueprint import blueprint
38 1
from .forms import PostingCreateForm, TopicCreateForm, TopicUpdateForm
39 1
40 1
41
REACTION_KINDS_IN_ORDER = [
42
    'thumbsup',
43 1
    'thumbsdown',
44 1
    'heart',
45 1
]
46 1
47 1
REACTION_KINDS_TO_SYMBOLS = {
48
    'heart': '❤️',
49 1
    'thumbsdown': '👎',
50 1
    'thumbsup': '👍',
51
}
52 1
53
54 1
@blueprint.get('/topics', defaults={'page': 1})
55 1
@blueprint.get('/topics/pages/<int:page>')
56
@templated
57 1
@subnavigation_for_view('board')
58
def topic_index(page):
59
    """List latest topics in all categories."""
60
    board_id = h.get_board_id()
61 1
    user = g.user
62 1
63
    h.require_board_access(board_id, user.id)
64 1
65
    include_hidden = service.may_current_user_view_hidden()
66
    topics_per_page = service.get_topics_per_page_value()
67
68
    topics = board_topic_query_service.paginate_topics(
69 1
        board_id, page, topics_per_page, include_hidden=include_hidden
70 1
    )
71 1
72 1
    service.add_topic_creators(topics.items)
73 1
    service.add_topic_unseen_flag(topics.items, user)
74
75 1
    return {
76
        'topics': topics,
77 1
    }
78
79 1
80
@blueprint.get('/topics/<uuid:topic_id>', defaults={'page': 0})
81
@blueprint.get('/topics/<uuid:topic_id>/pages/<int:page>')
82
@templated
83 1
@subnavigation_for_view('board')
84
def topic_view(topic_id, page):
85
    """List postings for the topic."""
86 1
    user = g.user
87
88 1
    include_hidden = service.may_current_user_view_hidden()
89
90
    topic = board_topic_query_service.find_topic_visible_for_user(
91 1
        topic_id, include_hidden=include_hidden
92
    )
93
94
    if topic is None:
95 1
        abort(404)
96
97
    board_id = h.get_board_id()
98
99 1
    if topic.category.hidden or (topic.category.board_id != board_id):
100 1
        abort(404)
101 1
102
    h.require_board_access(board_id, user.id)
103
104
    # Copy last view timestamp for later use to compare postings
105 1
    # against it.
106
    last_viewed_at = board_last_view_service.find_topic_last_viewed_at(
107
        topic.id, user.id
108
    )
109 1
110
    postings_per_page = service.get_postings_per_page_value()
111 1
    if page == 0:
112
        posting_url_to_redirect_to = _find_posting_url_to_redirect_to(
113
            topic.id, user, include_hidden, last_viewed_at
114 1
        )
115
116 1
        if posting_url_to_redirect_to is not None:
117
            # Jump to a specific post. This requires a redirect.
118
            return redirect(posting_url_to_redirect_to, code=307)
119
120 1
        page = 1
121
122 1
    if user.authenticated:
123
        # Mark as viewed before aborting so a user can itself remove the
124 1
        # 'new' tag from a locked topic.
125
        board_last_view_service.mark_topic_as_just_viewed(topic.id, user.id)
126 1
127
    postings = board_posting_query_service.paginate_postings(
128
        topic.id, include_hidden, page, postings_per_page
129
    )
130
131 1
    service.add_unseen_flag_to_postings(postings.items, last_viewed_at)
132
133
    is_last_page = not postings.has_next
134
135
    service.enrich_creators(postings.items, g.brand_id, g.party_id)
136
137 1
    is_current_user_orga = (
138
        user.authenticated
139
        and orga_team_service.is_orga_for_party(g.user.id, g.party_id)
140
    )
141
142
    context = {
143 1
        'topic': topic,
144
        'postings': postings,
145
        'is_last_page': is_last_page,
146
        'may_topic_be_updated_by_current_user': service.may_topic_be_updated_by_current_user,
147
        'may_posting_be_updated_by_current_user': service.may_posting_be_updated_by_current_user,
148
        'is_current_user_orga': is_current_user_orga,
149
        'reaction_kinds_in_order': REACTION_KINDS_IN_ORDER,
150
        'reaction_kinds_to_symbols': REACTION_KINDS_TO_SYMBOLS,
151
    }
152
153
    if is_last_page:
154 1
        context.update(
155 1
            {
156
                'form': PostingCreateForm(),
157
                'smileys': text_markup_service.get_smileys(),
158
            }
159
        )
160
161
    return context
162 1
163
164
def _find_posting_url_to_redirect_to(
165 1
    topic_id: TopicID,
166
    user: CurrentUser,
167
    include_hidden: bool,
168
    last_viewed_at: datetime | None,
169
) -> str | None:
170
    if not user.authenticated:
171 1
        # All postings are potentially new to a guest, so start on
172
        # the first page.
173
        return None
174 1
175
    if last_viewed_at is None:
176 1
        # This topic is completely new to the current user, so
177
        # start on the first page.
178
        return None
179 1
180
    posting = board_topic_query_service.find_default_posting_to_jump_to(
181
        topic_id, last_viewed_at, include_hidden=include_hidden
182
    )
183
184
    if posting is None:
185
        return None
186
187
    page = service.calculate_posting_page_number(posting)
188
189
    return h.build_url_for_posting_in_topic_view(posting, page)
190
191
192
@blueprint.get('/categories/<category_id>/create')
193 1
@permission_required('board_topic.create')
194 1
@templated
195 1
@subnavigation_for_view('board')
196 1
def topic_create_form(category_id, erroneous_form=None):
197 1
    """Show a form to create a topic in the category."""
198
    category = h.get_category_or_404(category_id)
199
200
    form = erroneous_form if erroneous_form else TopicCreateForm()
201
202
    return {
203
        'category': category,
204
        'form': form,
205
        'smileys': text_markup_service.get_smileys(),
206
    }
207
208
209
@blueprint.post('/categories/<category_id>/create')
210 1
@permission_required('board_topic.create')
211 1
def topic_create(category_id):
212 1
    """Create a topic in the category."""
213
    category = h.get_category_or_404(category_id)
214
215
    form = TopicCreateForm(request.form)
216
    if not form.validate():
217
        return topic_create_form(category.id, form)
218
219
    creator = g.user
220
    title = form.title.data.strip()
221
    body = form.body.data.strip()
222
223
    topic, event = board_topic_command_service.create_topic(
224
        category.id, creator, title, body
225
    )
226
    topic_url = h.build_external_url_for_topic(topic.id)
227
228
    flash_success(
229
        gettext('Topic "%(title)s" has been created.', title=topic.title)
230
    )
231
232
    event = dataclasses.replace(event, url=topic_url)
233
    board_signals.topic_created.send(None, event=event)
234
235
    return redirect(topic_url)
236
237
238
@blueprint.get('/topics/<uuid:topic_id>/update')
239 1
@permission_required('board_topic.update')
240 1
@templated
241 1
@subnavigation_for_view('board')
242 1
def topic_update_form(topic_id, erroneous_form=None):
243 1
    """Show form to update a topic."""
244
    db_topic = h.get_db_topic_or_404(topic_id)
245
    url = h.build_url_for_topic(db_topic.id)
246
247
    user_may_update = service.may_topic_be_updated_by_current_user(db_topic)
248
249
    if db_topic.locked and not user_may_update:
250
        flash_error(
251
            gettext('The topic must not be updated because it is locked.')
252
        )
253
        return redirect(url)
254
255
    if db_topic.hidden:
256
        flash_error(gettext('The topic must not be updated.'))
257
        return redirect(url)
258
259
    if not user_may_update:
260
        flash_error(gettext('You are not allowed to update this topic.'))
261
        return redirect(url)
262
263
    form = (
264
        erroneous_form
265
        if erroneous_form
266
        else TopicUpdateForm(obj=db_topic, body=db_topic.initial_posting.body)
267
    )
268
269
    return {
270
        'form': form,
271
        'topic': db_topic,
272
        'smileys': text_markup_service.get_smileys(),
273
    }
274
275
276
@blueprint.post('/topics/<uuid:topic_id>')
277 1
@permission_required('board_topic.update')
278 1
def topic_update(topic_id):
279 1
    """Update a topic."""
280
    db_topic = h.get_db_topic_or_404(topic_id)
281
    url = h.build_url_for_topic(db_topic.id)
282
283
    user_may_update = service.may_topic_be_updated_by_current_user(db_topic)
284
285
    if db_topic.locked and not user_may_update:
286
        flash_error(
287
            gettext('The topic must not be updated because it is locked.')
288
        )
289
        return redirect(url)
290
291
    if db_topic.hidden:
292
        flash_error(gettext('The topic must not be updated.'))
293
        return redirect(url)
294
295
    if not user_may_update:
296
        flash_error(gettext('You are not allowed to update this topic.'))
297
        return redirect(url)
298
299
    form = TopicUpdateForm(request.form)
300
    if not form.validate():
301
        return topic_update_form(topic_id, form)
302
303
    board_topic_command_service.update_topic(
304
        db_topic.id, g.user, form.title.data, form.body.data
305
    )
306
307
    flash_success(
308
        gettext('Topic "%(title)s" has been updated.', title=db_topic.title)
309
    )
310
    return redirect(url)
311
312
313
@blueprint.get('/topics/<uuid:topic_id>/moderate')
314 1
@permission_required('board.hide')
315 1
@templated
316 1
@subnavigation_for_view('board')
317 1
def topic_moderate_form(topic_id):
318 1
    """Show a form to moderate the topic."""
319
    board_id = h.get_board_id()
320
    db_topic = h.get_db_topic_or_404(topic_id)
321
322
    db_topic.creator = user_service.get_user(db_topic.creator_id)
323
324
    categories = board_category_query_service.get_categories_excluding(
325
        board_id, db_topic.category_id
326
    )
327
328
    return {
329
        'topic': db_topic,
330
        'categories': categories,
331
    }
332
333
334
@blueprint.post('/topics/<uuid:topic_id>/flags/hidden')
335 1
@permission_required('board.hide')
336 1
@respond_no_content_with_location
337 1
def topic_hide(topic_id):
338 1
    """Hide a topic."""
339
    db_topic = h.get_db_topic_or_404(topic_id)
340 1
    moderator = g.user
341 1
342
    event = board_topic_command_service.hide_topic(db_topic.id, moderator)
343 1
344
    flash_success(
345 1
        gettext('Topic "%(title)s" has been hidden.', title=db_topic.title),
346
        icon='hidden',
347
    )
348
349
    event = dataclasses.replace(
350 1
        event, url=h.build_external_url_for_topic(db_topic.id)
351
    )
352
    board_signals.topic_hidden.send(None, event=event)
353 1
354
    return h.build_url_for_topic_in_category_view(db_topic)
355 1
356
357
@blueprint.delete('/topics/<uuid:topic_id>/flags/hidden')
358 1
@permission_required('board.hide')
359 1
@respond_no_content_with_location
360 1
def topic_unhide(topic_id):
361 1
    """Un-hide a topic."""
362
    db_topic = h.get_db_topic_or_404(topic_id)
363 1
    moderator = g.user
364 1
365
    event = board_topic_command_service.unhide_topic(db_topic.id, moderator)
366 1
367
    flash_success(
368 1
        gettext(
369
            'Topic "%(title)s" has been made visible again.',
370
            title=db_topic.title,
371
        ),
372
        icon='view',
373
    )
374
375
    event = dataclasses.replace(
376 1
        event, url=h.build_external_url_for_topic(db_topic.id)
377
    )
378
    board_signals.topic_unhidden.send(None, event=event)
379 1
380
    return h.build_url_for_topic_in_category_view(db_topic)
381 1
382
383
@blueprint.post('/topics/<uuid:topic_id>/flags/locked')
384 1
@permission_required('board_topic.lock')
385 1
@respond_no_content_with_location
386 1
def topic_lock(topic_id):
387 1
    """Lock a topic."""
388
    db_topic = h.get_db_topic_or_404(topic_id)
389 1
    moderator = g.user
390 1
391
    event = board_topic_command_service.lock_topic(db_topic.id, moderator)
392 1
393
    flash_success(
394 1
        gettext('Topic "%(title)s" has been locked.', title=db_topic.title),
395
        icon='lock',
396
    )
397
398
    event = dataclasses.replace(
399 1
        event, url=h.build_external_url_for_topic(db_topic.id)
400
    )
401
    board_signals.topic_locked.send(None, event=event)
402 1
403
    return h.build_url_for_topic_in_category_view(db_topic)
404 1
405
406
@blueprint.delete('/topics/<uuid:topic_id>/flags/locked')
407 1
@permission_required('board_topic.lock')
408 1
@respond_no_content_with_location
409 1
def topic_unlock(topic_id):
410 1
    """Unlock a topic."""
411
    db_topic = h.get_db_topic_or_404(topic_id)
412 1
    moderator = g.user
413 1
414
    event = board_topic_command_service.unlock_topic(db_topic.id, moderator)
415 1
416
    flash_success(
417 1
        gettext('Topic "%(title)s" has been unlocked.', title=db_topic.title),
418
        icon='unlock',
419
    )
420
421
    event = dataclasses.replace(
422 1
        event, url=h.build_external_url_for_topic(db_topic.id)
423
    )
424
    board_signals.topic_unlocked.send(None, event=event)
425 1
426
    return h.build_url_for_topic_in_category_view(db_topic)
427 1
428
429
@blueprint.post('/topics/<uuid:topic_id>/flags/pinned')
430 1
@permission_required('board_topic.pin')
431 1
@respond_no_content_with_location
432 1
def topic_pin(topic_id):
433 1
    """Pin a topic."""
434
    db_topic = h.get_db_topic_or_404(topic_id)
435 1
    moderator = g.user
436 1
437
    event = board_topic_command_service.pin_topic(db_topic.id, moderator)
438 1
439
    flash_success(
440 1
        gettext('Topic "%(title)s" has been pinned.', title=db_topic.title),
441
        icon='pin',
442
    )
443
444
    event = dataclasses.replace(
445 1
        event, url=h.build_external_url_for_topic(db_topic.id)
446
    )
447
    board_signals.topic_pinned.send(None, event=event)
448 1
449
    return h.build_url_for_topic_in_category_view(db_topic)
450 1
451
452
@blueprint.delete('/topics/<uuid:topic_id>/flags/pinned')
453 1
@permission_required('board_topic.pin')
454 1
@respond_no_content_with_location
455 1
def topic_unpin(topic_id):
456 1
    """Unpin a topic."""
457
    db_topic = h.get_db_topic_or_404(topic_id)
458 1
    moderator = g.user
459 1
460
    event = board_topic_command_service.unpin_topic(db_topic.id, moderator)
461 1
462
    flash_success(
463 1
        gettext('Topic "%(title)s" has been unpinned.', title=db_topic.title)
464
    )
465
466
    event = dataclasses.replace(
467 1
        event, url=h.build_external_url_for_topic(db_topic.id)
468
    )
469
    board_signals.topic_unpinned.send(None, event=event)
470 1
471
    return h.build_url_for_topic_in_category_view(db_topic)
472 1
473
474
@blueprint.post('/topics/<uuid:topic_id>/move')
475 1
@permission_required('board_topic.move')
476 1
def topic_move(topic_id):
477 1
    """Move a topic from one category to another."""
478
    db_topic = h.get_db_topic_or_404(topic_id)
479 1
    moderator = g.user
480 1
481
    new_category_id = request.form.get('category_id')
482 1
    if not new_category_id:
483 1
        abort(400, 'No target category ID given.')
484
485
    new_category = h.get_category_or_404(new_category_id)
486 1
487
    old_category = db_topic.category
488 1
489
    event = board_topic_command_service.move_topic(
490 1
        db_topic.id, new_category.id, moderator
491
    )
492
493
    flash_success(
494 1
        gettext(
495
            'Topic "%(topic_title)s" has been moved from category '
496
            '"%(old_category_title)s" to category "%(new_category_title)s".',
497
            topic_title=db_topic.title,
498
            old_category_title=old_category.title,
499
            new_category_title=new_category.title,
500
        ),
501
        icon='move',
502
    )
503
504
    event = dataclasses.replace(
505 1
        event, url=h.build_external_url_for_topic(db_topic.id)
506
    )
507
    board_signals.topic_moved.send(None, event=event)
508 1
509
    return redirect(h.build_url_for_topic_in_category_view(db_topic))
510 1
511
512
@blueprint.post('/topics/<uuid:topic_id>/flags/announcements')
513 1
@permission_required('board.announce')
514 1
@respond_no_content_with_location
515 1
def topic_limit_to_announcements(topic_id):
516 1
    """Limit post in the topic to moderators."""
517
    db_topic = h.get_db_topic_or_404(topic_id)
518
519
    board_topic_command_service.limit_topic_to_announcements(db_topic.id)
520
521
    flash_success(
522
        gettext(
523
            'Topic "%(title)s" has been limited to announcements.',
524
            title=db_topic.title,
525
        ),
526
        icon='announce',
527
    )
528
529
    return h.build_url_for_topic_in_category_view(db_topic)
530
531
532
@blueprint.delete('/topics/<uuid:topic_id>/flags/announcements')
533 1
@permission_required('board.announce')
534 1
@respond_no_content_with_location
535 1
def topic_remove_limit_to_announcements(topic_id):
536 1
    """Allow non-moderators to post in the topic again."""
537
    db_topic = h.get_db_topic_or_404(topic_id)
538
539
    board_topic_command_service.remove_limit_of_topic_to_announcements(
540
        db_topic.id
541
    )
542
543
    flash_success(
544
        gettext(
545
            'Topic "%(title)s" has been reopened for regular replies.',
546
            title=db_topic.title,
547
        )
548
    )
549
550
    return h.build_url_for_topic_in_category_view(db_topic)
551