byceps.blueprints.admin.news.views   C
last analyzed

Complexity

Total Complexity 56

Size/Duplication

Total Lines 659
Duplicated Lines 1.82 %

Test Coverage

Coverage 72.2%

Importance

Changes 0
Metric Value
eloc 405
dl 12
loc 659
ccs 213
cts 295
cp 0.722
rs 5.5199
c 0
b 0
f 0
wmc 56

29 Functions

Rating   Name   Duplication   Size   Complexity  
A image_update() 0 29 2
B image_create() 0 48 8
A image_create_form() 0 18 2
A image_update_form() 0 14 2
A channel_delete() 0 36 3
A channel_view() 0 24 1
A item_publish_later_form() 0 12 2
A _create_html_diff() 12 12 1
A item_list_versions() 0 22 1
A item_compare_versions() 0 26 2
A channel_index_for_brand() 0 15 1
A item_create_form() 0 16 2
A item_update_form() 0 18 2
A channel_create_form() 0 12 2
A item_publish_now() 0 13 1
A _find_version() 0 7 2
A _get_image_or_404() 0 7 2
A _render_item_version() 0 36 2
A _get_channel_or_404() 0 7 2
A item_create() 0 25 2
A item_update() 0 25 2
A item_view() 0 10 1
A item_view_version() 0 10 1
A image_unset_featured() 0 10 1
A channel_create() 0 24 2
A _get_item_or_404() 0 7 2
A _get_brand_or_404() 0 7 2
A item_publish_later() 0 27 2
A image_set_featured() 0 10 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

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:

Complexity

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like byceps.blueprints.admin.news.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.news.views
3
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4
5
:Copyright: 2006-2021 Jochen Kupperschmidt
6
:License: Revised BSD (see `LICENSE` file for details)
7
"""
8
9 1
from datetime import date, datetime
10
11 1
from flask import abort, g, request
12 1
from flask_babel import gettext
13
14 1
from ....services.brand import service as brand_service
15 1
from ....services.image import service as image_service
16 1
from ....services.news import channel_service as news_channel_service
17 1
from ....services.news import html_service as news_html_service
18 1
from ....services.news import image_service as news_image_service
19 1
from ....services.news import service as news_item_service
20 1
from ....services.news.transfer.models import Channel
21 1
from ....services.site import service as site_service
22 1
from ....services.text_diff import service as text_diff_service
23 1
from ....services.user import service as user_service
24 1
from ....services.user.service import UserIdRejected
25 1
from ....signals import news as news_signals
26 1
from ....util.datetime.format import format_datetime_short
27 1
from ....util.datetime.timezone import local_tz_to_utc
28 1
from ....util.framework.blueprint import create_blueprint
29 1
from ....util.framework.flash import flash_error, flash_success
30 1
from ....util.framework.templating import templated
31 1
from ....util.iterables import pairwise
32 1
from ....util.views import permission_required, redirect_to, respond_no_content
33
34 1
from .forms import (
35
    ChannelCreateForm,
36
    ImageCreateForm,
37
    ImageUpdateForm,
38
    ItemCreateForm,
39
    ItemPublishLaterForm,
40
    ItemUpdateForm,
41
)
42
43
44 1
blueprint = create_blueprint('news_admin', __name__)
45
46
47
# -------------------------------------------------------------------- #
48
# channels
49
50
51 1
@blueprint.get('/brands/<brand_id>')
52 1
@permission_required('news_item.view')
53 1
@templated
54
def channel_index_for_brand(brand_id):
55
    """List channels for that brand."""
56 1
    brand = _get_brand_or_404(brand_id)
57
58 1
    channels = news_channel_service.get_channels_for_brand(brand.id)
59
60 1
    item_count_by_channel_id = news_item_service.get_item_count_by_channel_id()
61
62 1
    return {
63
        'brand': brand,
64
        'channels': channels,
65
        'item_count_by_channel_id': item_count_by_channel_id,
66
    }
67
68
69 1
@blueprint.get('/for_brand/<brand_id>/channels/create')
70 1
@permission_required('news_channel.create')
71 1
@templated
72 1
def channel_create_form(brand_id, erroneous_form=None):
73
    """Show form to create a channel."""
74 1
    brand = _get_brand_or_404(brand_id)
75
76 1
    form = erroneous_form if erroneous_form else ChannelCreateForm()
77
78 1
    return {
79
        'brand': brand,
80
        'form': form,
81
    }
82
83
84 1
@blueprint.post('/for_brand/<brand_id>/channels')
85 1
@permission_required('news_channel.create')
86
def channel_create(brand_id):
87
    """Create a channel."""
88 1
    brand = _get_brand_or_404(brand_id)
89
90 1
    form = ChannelCreateForm(request.form)
91 1
    if not form.validate():
92
        return channel_create_form(brand.id, form)
93
94 1
    channel_id = form.channel_id.data.strip().lower()
95 1
    url_prefix = form.url_prefix.data.strip()
96
97 1
    channel = news_channel_service.create_channel(
98
        brand.id, channel_id, url_prefix
99
    )
100
101 1
    flash_success(
102
        gettext(
103
            'News channel "%(channel_id)s" has been created.',
104
            channel_id=channel.id,
105
        )
106
    )
107 1
    return redirect_to('.channel_view', channel_id=channel.id)
108
109
110 1
@blueprint.get('/channels/<channel_id>', defaults={'page': 1})
111 1
@blueprint.get('/channels/<channel_id>/pages/<int:page>')
112 1
@permission_required('news_item.view')
113 1
@templated
114
def channel_view(channel_id, page):
115
    """View that channel and list its news items."""
116 1
    channel = _get_channel_or_404(channel_id)
117
118 1
    brand = brand_service.find_brand(channel.brand_id)
119
120 1
    channel_ids = {channel.id}
121 1
    per_page = request.args.get('per_page', type=int, default=15)
122
123 1
    items = news_item_service.get_items_paginated(channel_ids, page, per_page)
124
125 1
    user_ids = {item.current_version.creator_id for item in items.items}
126 1
    users = user_service.get_users(user_ids, include_avatars=True)
127 1
    users_by_id = user_service.index_users_by_id(users)
128
129 1
    return {
130
        'channel': channel,
131
        'brand': brand,
132
        'items': items,
133
        'users_by_id': users_by_id,
134
    }
135
136
137 1
@blueprint.delete('/channels/<channel_id>')
138 1
@permission_required('news_channel.create')
139 1
@respond_no_content
140
def channel_delete(channel_id):
141
    """Delete the channel."""
142
    channel = _get_channel_or_404(channel_id)
143
144
    if news_item_service.has_channel_items(channel.id):
145
        flash_error(
146
            gettext(
147
                'News channel "%(channel_id)s" cannot be deleted because it contains news items.',
148
                channel_id=channel.id,
149
            )
150
        )
151
        return
152
153
    sites_for_brand = site_service.get_sites_for_brand(channel.brand_id)
154
    linked_sites = {
155
        site for site in sites_for_brand if channel.id in site.news_channel_ids
156
    }
157
    if linked_sites:
158
        flash_error(
159
            gettext(
160
                'News channel "%(channel_id)s" cannot be deleted because it is referenced by %(site_count)s site(s).',
161
                channel_id=channel.id,
162
                site_count=len(linked_sites),
163
            )
164
        )
165
        return
166
167
    news_channel_service.delete_channel(channel.id)
168
169
    flash_success(
170
        gettext(
171
            'News channel "%(channel_id)s" has been deleted.',
172
            channel_id=channel_id,
173
        )
174
    )
175
176
177
# -------------------------------------------------------------------- #
178
# images
179
180
181 1
@blueprint.get('/for_item/<item_id>/create')
182 1
@permission_required('news_item.update')
183 1
@templated
184 1
def image_create_form(item_id, erroneous_form=None):
185
    """Show form to create a news image."""
186 1
    item = _get_item_or_404(item_id)
187
188 1
    form = erroneous_form if erroneous_form else ImageCreateForm()
189
190 1
    image_type_names = image_service.get_image_type_names(
191
        news_image_service.ALLOWED_IMAGE_TYPES
192
    )
193
194 1
    return {
195
        'item': item,
196
        'form': form,
197
        'allowed_types': image_type_names,
198
        'maximum_dimensions': news_image_service.MAXIMUM_DIMENSIONS,
199
    }
200
201
202 1
@blueprint.post('/for_item/<item_id>')
203 1
@permission_required('news_item.update')
204
def image_create(item_id):
205
    """Create a news image."""
206
    item = _get_item_or_404(item_id)
207
208
    # Make `InputRequired` work on `FileField`.
209
    form_fields = request.form.copy()
210
    if request.files:
211
        form_fields.update(request.files)
212
213
    form = ImageCreateForm(form_fields)
214
    if not form.validate():
215
        return image_create_form(item.id, form)
216
217
    creator_id = g.user.id
218
    image = request.files.get('image')
219
    alt_text = form.alt_text.data.strip()
220
    caption = form.caption.data.strip()
221
    attribution = form.attribution.data.strip()
222
223
    if not image or not image.filename:
224
        abort(400, 'No file to upload has been specified.')
225
226
    try:
227
        image = news_image_service.create_image(
228
            creator_id,
229
            item.id,
230
            image.stream,
231
            alt_text=alt_text,
232
            caption=caption,
233
            attribution=attribution,
234
        )
235
    except UserIdRejected as e:
236
        abort(400, 'Invalid creator ID')
237
    except image_service.ImageTypeProhibited as e:
238
        abort(400, str(e))
239
    except FileExistsError:
240
        abort(409, 'File already exists, not overwriting.')
241
242
    flash_success(
243
        gettext(
244
            'News image #%(image_number)s has been created.',
245
            image_number=image.number,
246
        )
247
    )
248
249
    return redirect_to('.item_view', item_id=image.item_id)
250
251
252 1
@blueprint.get('/images/<uuid:image_id>/update')
253 1
@permission_required('news_item.update')
254 1
@templated
255 1
def image_update_form(image_id, erroneous_form=None):
256
    """Show form to update a news image."""
257
    image = _get_image_or_404(image_id)
258
    item = news_item_service.find_item(image.item_id)
259
260
    form = erroneous_form if erroneous_form else ImageUpdateForm(obj=image)
261
262
    return {
263
        'image': image,
264
        'item': item,
265
        'form': form,
266
    }
267
268
269 1
@blueprint.post('/images/<uuid:image_id>')
270 1
@permission_required('news_item.update')
271
def image_update(image_id):
272
    """Update a news image."""
273
    image = _get_image_or_404(image_id)
274
275
    form = ImageUpdateForm(request.form)
276
    if not form.validate():
277
        return image_update_form(image.id, form)
278
279
    alt_text = form.alt_text.data.strip()
280
    caption = form.caption.data.strip()
281
    attribution = form.attribution.data.strip()
282
283
    image = news_image_service.update_image(
284
        image.id,
285
        alt_text=alt_text,
286
        caption=caption,
287
        attribution=attribution,
288
    )
289
290
    flash_success(
291
        gettext(
292
            'News image #%(image_number)s has been updated.',
293
            image_number=image.number,
294
        )
295
    )
296
297
    return redirect_to('.item_view', item_id=image.item_id)
298
299
300 1
@blueprint.post('/images/<uuid:image_id>/featured')
301 1
@permission_required('news_item.update')
302 1
@respond_no_content
303
def image_set_featured(image_id):
304
    """Set the image as featured image."""
305
    image = _get_image_or_404(image_id)
306
307
    news_item_service.set_featured_image(image.item_id, image.id)
308
309
    flash_success(gettext('Featured image has been set.'))
310
311
312 1
@blueprint.delete('/items/<uuid:item_id>/featured')
313 1
@permission_required('news_item.update')
314 1
@respond_no_content
315
def image_unset_featured(item_id):
316
    """Unset the item's featured image."""
317
    item = _get_item_or_404(item_id)
318
319
    news_item_service.set_featured_image(item.id, image_id=None)
320
321
    flash_success(gettext('Featured image has been unset.'))
322
323
324
# -------------------------------------------------------------------- #
325
# items
326
327
328 1
@blueprint.get('/items/<uuid:item_id>')
329 1
@permission_required('news_item.view')
330 1
@templated('admin/news/item_view_version')
331
def item_view(item_id):
332
    """Show the current version of the news item."""
333 1
    item = _get_item_or_404(item_id)
334
335 1
    version = news_item_service.get_current_item_version(item.id)
336
337 1
    return _render_item_version(version, item)
338
339
340 1
@blueprint.get('/versions/<uuid:version_id>')
341 1
@permission_required('news_item.view')
342 1
@templated
343
def item_view_version(version_id):
344
    """Show the news item with the given version."""
345 1
    version = _find_version(version_id)
346
347 1
    item = news_item_service.find_item(version.item_id)
348
349 1
    return _render_item_version(version, item)
350
351
352 1
def _render_item_version(version, item):
353
    """Render the news item version."""
354 1
    channel = item.channel
355 1
    brand = brand_service.find_brand(channel.brand_id)
356
357 1
    creator = user_service.get_user(version.creator_id, include_avatar=True)
358
359 1
    current_version = news_item_service.get_current_item_version(item.id)
360 1
    is_current_version = version.id == current_version.id
361
362 1
    context = {
363
        'item': item,
364
        'version': version,
365
        'brand': brand,
366
        'creator': creator,
367
        'is_current_version': is_current_version,
368
    }
369
370 1
    try:
371 1
        rendered_body = news_html_service.render_body(item, version.body)
372
373 1
        context.update(
374
            {
375
                'rendered_body': rendered_body,
376
                'error_occurred': False,
377
            }
378
        )
379
    except Exception as e:
380
        context.update(
381
            {
382
                'error_occurred': True,
383
                'error_message': str(e),
384
            }
385
        )
386
387 1
    return context
388
389
390 1
@blueprint.get('/items/<uuid:item_id>/versions')
391 1
@permission_required('news_item.view')
392 1
@templated
393
def item_list_versions(item_id):
394
    """List news item's versions."""
395 1
    item = _get_item_or_404(item_id)
396
397 1
    channel = item.channel
398 1
    brand = brand_service.find_brand(channel.brand_id)
399
400 1
    versions = news_item_service.get_item_versions(item.id)
401 1
    versions_pairwise = list(pairwise(versions + [None]))
402
403 1
    user_ids = {version.creator_id for version in versions}
404 1
    users = user_service.get_users(user_ids, include_avatars=True)
405 1
    users_by_id = user_service.index_users_by_id(users)
406
407 1
    return {
408
        'item': item,
409
        'brand': brand,
410
        'versions_pairwise': versions_pairwise,
411
        'users_by_id': users_by_id,
412
    }
413
414
415 1
@blueprint.get('/items/<uuid:from_version_id>/compare_to/<uuid:to_version_id>')
416 1
@permission_required('news_item.view')
417 1
@templated
418
def item_compare_versions(from_version_id, to_version_id):
419
    """Show the difference between two news item versions."""
420 1
    from_version = _find_version(from_version_id)
421 1
    to_version = _find_version(to_version_id)
422
423 1
    if from_version.item_id != to_version.item_id:
424
        abort(400, 'The versions do not belong to the same item.')
425
426 1
    item = news_item_service.find_item(from_version.item_id)
427 1
    channel = item.channel
428 1
    brand = brand_service.find_brand(channel.brand_id)
429
430 1
    html_diff_title = _create_html_diff(from_version, to_version, 'title')
431 1
    html_diff_body = _create_html_diff(from_version, to_version, 'body')
432 1
    html_diff_image_url_path = _create_html_diff(
433
        from_version, to_version, 'image_url_path'
434
    )
435
436 1
    return {
437
        'brand': brand,
438
        'diff_title': html_diff_title,
439
        'diff_body': html_diff_body,
440
        'diff_image_url_path': html_diff_image_url_path,
441
    }
442
443
444 1
@blueprint.get('/for_channel/<channel_id>/create')
445 1
@permission_required('news_item.create')
446 1
@templated
447 1
def item_create_form(channel_id, erroneous_form=None):
448
    """Show form to create a news item."""
449 1
    channel = _get_channel_or_404(channel_id)
450
451 1
    if erroneous_form:
452
        form = erroneous_form
453
    else:
454 1
        slug_prefix = date.today().strftime('%Y-%m-%d-')
455 1
        form = ItemCreateForm(slug=slug_prefix)
456
457 1
    return {
458
        'channel': channel,
459
        'form': form,
460
    }
461
462
463 1
@blueprint.post('/for_channel/<channel_id>')
464 1
@permission_required('news_item.create')
465
def item_create(channel_id):
466
    """Create a news item."""
467 1
    channel = _get_channel_or_404(channel_id)
468
469 1
    form = ItemCreateForm(request.form)
470 1
    if not form.validate():
471
        return item_create_form(channel.id, form)
472
473 1
    slug = form.slug.data.strip().lower()
474 1
    creator = g.user
475 1
    title = form.title.data.strip()
476 1
    body = form.body.data.strip()
477 1
    image_url_path = form.image_url_path.data.strip()
478
479 1
    item = news_item_service.create_item(
480
        channel.id, slug, creator.id, title, body, image_url_path=image_url_path
481
    )
482
483 1
    flash_success(
484
        gettext('News item "%(title)s" has been created.', title=item.title)
485
    )
486
487 1
    return redirect_to('.item_view', item_id=item.id)
488
489
490 1
@blueprint.get('/items/<uuid:item_id>/update')
491 1
@permission_required('news_item.update')
492 1
@templated
493 1
def item_update_form(item_id, erroneous_form=None):
494
    """Show form to update a news item."""
495 1
    item = _get_item_or_404(item_id)
496
497 1
    current_version = news_item_service.get_current_item_version(item.id)
498
499 1
    form = (
500
        erroneous_form
501
        if erroneous_form
502
        else ItemUpdateForm(obj=current_version, slug=item.slug)
503
    )
504
505 1
    return {
506
        'item': item,
507
        'form': form,
508
    }
509
510
511 1
@blueprint.post('/items/<uuid:item_id>')
512 1
@permission_required('news_item.update')
513
def item_update(item_id):
514
    """Update a news item."""
515
    item = _get_item_or_404(item_id)
516
517
    form = ItemUpdateForm(request.form)
518
    if not form.validate():
519
        return item_update_form(item.id, form)
520
521
    creator = g.user
522
    slug = form.slug.data.strip().lower()
523
    title = form.title.data.strip()
524
    body = form.body.data.strip()
525
    image_url_path = form.image_url_path.data.strip()
526
527
    item = news_item_service.update_item(
528
        item.id, slug, creator.id, title, body, image_url_path=image_url_path
529
    )
530
531
    flash_success(
532
        gettext('News item "%(title)s" has been updated.', title=item.title)
533
    )
534
535
    return redirect_to('.item_view', item_id=item.id)
536
537
538 1
@blueprint.get('/items/<uuid:item_id>/publish_later')
539 1
@permission_required('news_item.publish')
540 1
@templated
541 1
def item_publish_later_form(item_id, erroneous_form=None):
542
    """Show form to publish a news item at a time in the future."""
543 1
    item = _get_item_or_404(item_id)
544
545 1
    form = erroneous_form if erroneous_form else ItemPublishLaterForm()
546
547 1
    return {
548
        'item': item,
549
        'form': form,
550
    }
551
552
553 1
@blueprint.post('/items/<uuid:item_id>/publish_later')
554 1
@permission_required('news_item.publish')
555
def item_publish_later(item_id):
556
    """Publish a news item at a time in the future."""
557 1
    item = _get_item_or_404(item_id)
558
559 1
    form = ItemPublishLaterForm(request.form)
560 1
    if not form.validate():
561
        return item_publish_later_form(item.id, form)
562
563 1
    publish_at = local_tz_to_utc(
564
        datetime.combine(form.publish_on.data, form.publish_at.data)
565
    )
566
567 1
    event = news_item_service.publish_item(
568
        item.id, publish_at=publish_at, initiator_id=g.user.id
569
    )
570
571 1
    news_signals.item_published.send(None, event=event)
572
573 1
    flash_success(
574
        gettext(
575
            'News item "%(title)s" will be published later.', title=item.title
576
        )
577
    )
578
579 1
    return redirect_to('.item_view', item_id=item.id)
580
581
582 1
@blueprint.post('/items/<uuid:item_id>/publish_now')
583 1
@permission_required('news_item.publish')
584 1
@respond_no_content
585
def item_publish_now(item_id):
586
    """Publish a news item now."""
587 1
    item = _get_item_or_404(item_id)
588
589 1
    event = news_item_service.publish_item(item.id, initiator_id=g.user.id)
590
591 1
    news_signals.item_published.send(None, event=event)
592
593 1
    flash_success(
594
        gettext('News item "%(title)s" has been published.', title=item.title)
595
    )
596
597
598
# -------------------------------------------------------------------- #
599
# helpers
600
601
602 1
def _get_brand_or_404(brand_id):
603 1
    brand = brand_service.find_brand(brand_id)
604
605 1
    if brand is None:
606
        abort(404)
607
608 1
    return brand
609
610
611 1
def _get_channel_or_404(channel_id) -> Channel:
612 1
    channel = news_channel_service.find_channel(channel_id)
613
614 1
    if channel is None:
615
        abort(404)
616
617 1
    return channel
618
619
620 1
def _get_item_or_404(item_id):
621 1
    item = news_item_service.find_item(item_id)
622
623 1
    if item is None:
624
        abort(404)
625
626 1
    return item
627
628
629 1
def _get_image_or_404(image_id):
630
    image = news_image_service.find_image(image_id)
631
632
    if image is None:
633
        abort(404)
634
635
    return image
636
637
638 1
def _find_version(version_id):
639 1
    version = news_item_service.find_item_version(version_id)
640
641 1
    if version is None:
642
        abort(404)
643
644 1
    return version
645
646
647 1 View Code Duplication
def _create_html_diff(from_version, to_version, attribute_name):
648
    """Create an HTML diff between the named attribute's value of each
649
    of the two versions.
650
    """
651 1
    from_description = format_datetime_short(from_version.created_at)
652 1
    to_description = format_datetime_short(to_version.created_at)
653
654 1
    from_text = getattr(from_version, attribute_name)
655 1
    to_text = getattr(to_version, attribute_name)
656
657 1
    return text_diff_service.create_html_diff(
658
        from_text, to_text, from_description, to_description
659
    )
660