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

Complexity

Total Complexity 66

Size/Duplication

Total Lines 762
Duplicated Lines 0 %

Test Coverage

Coverage 69.83%

Importance

Changes 0
Metric Value
eloc 475
dl 0
loc 762
ccs 243
cts 348
cp 0.6983
rs 3.12
c 0
b 0
f 0
wmc 66

32 Functions

Rating   Name   Duplication   Size   Complexity  
A _find_version() 0 7 2
A channel_update() 0 24 2
A _get_image_or_404() 0 7 2
A channel_view() 0 30 2
A item_publish_later_form() 0 12 2
A _create_html_diff() 0 12 1
A item_list_versions() 0 22 1
A _get_channel_or_404() 0 7 2
A image_update() 0 29 2
A item_create() 0 32 2
B item_compare_versions() 0 34 6
A channel_index_for_brand() 0 23 1
A item_create_form() 0 16 2
B image_create() 0 48 8
A item_update_form() 0 21 2
A channel_update_form() 0 16 2
A item_update() 0 32 2
A image_create_form() 0 18 2
A channel_create_form() 0 13 2
A image_update_form() 0 14 2
A item_view() 0 9 1
A channel_delete() 0 36 3
A item_view_version() 0 23 1
A image_unset_featured() 0 10 1
A item_publish_now() 0 13 1
A channel_create() 0 27 2
A _get_item_or_404() 0 7 2
A item_view_version_preview() 0 23 2
A item_unpublish() 0 11 1
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   Complexity   

Complexity

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