Passed
Push — main ( 7fb18a...baf70b )
by Jochen
04:45
created

byceps.blueprints.admin.news.views.image_update()   A

Complexity

Conditions 2

Size

Total Lines 29
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 4.3145

Importance

Changes 0
Metric Value
cc 2
eloc 20
nop 1
dl 0
loc 29
ccs 2
cts 12
cp 0.1666
crap 4.3145
rs 9.4
c 0
b 0
f 0
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.service import UserIdRejected
24 1
from ....signals import news as news_signals
25 1
from ....util.authorization import register_permission_enum
26 1
from ....util.datetime.format import format_datetime_short
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.templatefilters import local_tz_to_utc
32 1
from ....util.views import permission_required, redirect_to, respond_no_content
33
34 1
from .authorization import NewsChannelPermission, NewsItemPermission
35 1
from .forms import (
36
    ChannelCreateForm,
37
    ImageCreateForm,
38
    ImageUpdateForm,
39
    ItemCreateForm,
40
    ItemPublishLaterForm,
41
    ItemUpdateForm,
42
)
43
44
45 1
blueprint = create_blueprint('news_admin', __name__)
46
47
48 1
register_permission_enum(NewsChannelPermission)
49 1
register_permission_enum(NewsItemPermission)
50
51
52
# -------------------------------------------------------------------- #
53
# channels
54
55
56 1
@blueprint.get('/brands/<brand_id>')
57 1
@permission_required(NewsItemPermission.view)
58 1
@templated
59
def channel_index_for_brand(brand_id):
60
    """List channels for that brand."""
61 1
    brand = _get_brand_or_404(brand_id)
62
63 1
    channels = news_channel_service.get_channels_for_brand(brand.id)
64
65 1
    item_count_by_channel_id = news_item_service.get_item_count_by_channel_id()
66
67 1
    return {
68
        'brand': brand,
69
        'channels': channels,
70
        'item_count_by_channel_id': item_count_by_channel_id,
71
    }
72
73
74 1
@blueprint.get('/for_brand/<brand_id>/channels/create')
75 1
@permission_required(NewsChannelPermission.create)
76 1
@templated
77 1
def channel_create_form(brand_id, erroneous_form=None):
78
    """Show form to create a channel."""
79 1
    brand = _get_brand_or_404(brand_id)
80
81 1
    form = erroneous_form if erroneous_form else ChannelCreateForm()
82
83 1
    return {
84
        'brand': brand,
85
        'form': form,
86
    }
87
88
89 1
@blueprint.post('/for_brand/<brand_id>/channels')
90 1
@permission_required(NewsChannelPermission.create)
91
def channel_create(brand_id):
92
    """Create a channel."""
93 1
    brand = _get_brand_or_404(brand_id)
94
95 1
    form = ChannelCreateForm(request.form)
96 1
    if not form.validate():
97
        return channel_create_form(brand.id, form)
98
99 1
    channel_id = form.channel_id.data.strip().lower()
100 1
    url_prefix = form.url_prefix.data.strip()
101
102 1
    channel = news_channel_service.create_channel(
103
        brand.id, channel_id, url_prefix
104
    )
105
106 1
    flash_success(
107
        gettext(
108
            'News channel "%(channel_id)s" has been created.',
109
            channel_id=channel.id,
110
        )
111
    )
112 1
    return redirect_to('.channel_view', channel_id=channel.id)
113
114
115 1
@blueprint.get('/channels/<channel_id>', defaults={'page': 1})
116 1
@blueprint.get('/channels/<channel_id>/pages/<int:page>')
117 1
@permission_required(NewsItemPermission.view)
118 1
@templated
119
def channel_view(channel_id, page):
120
    """View that channel and list its news items."""
121 1
    channel = _get_channel_or_404(channel_id)
122
123 1
    brand = brand_service.find_brand(channel.brand_id)
124
125 1
    channel_ids = {channel.id}
126 1
    per_page = request.args.get('per_page', type=int, default=15)
127
128 1
    items = news_item_service.get_items_paginated(channel_ids, page, per_page)
129
130 1
    return {
131
        'channel': channel,
132
        'brand': brand,
133
        'items': items,
134
    }
135
136
137 1
@blueprint.delete('/channels/<channel_id>')
138 1
@permission_required(NewsChannelPermission.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(NewsItemPermission.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(NewsItemPermission.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(NewsItemPermission.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(NewsItemPermission.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
# -------------------------------------------------------------------- #
301
# items
302
303
304 1
@blueprint.get('/items/<uuid:item_id>')
305 1
@permission_required(NewsItemPermission.view)
306 1
@templated('admin/news/item_view_version')
307
def item_view(item_id):
308
    """Show the current version of the news item."""
309 1
    item = _get_item_or_404(item_id)
310
311 1
    version = news_item_service.get_current_item_version(item.id)
312
313 1
    return _render_item_version(version, item)
314
315
316 1
@blueprint.get('/versions/<uuid:version_id>')
317 1
@permission_required(NewsItemPermission.view)
318 1
@templated
319
def item_view_version(version_id):
320
    """Show the news item with the given version."""
321 1
    version = _find_version(version_id)
322
323 1
    item = news_item_service.find_item(version.item_id)
324
325 1
    return _render_item_version(version, item)
326
327
328 1
def _render_item_version(version, item):
329
    """Render the news item version."""
330 1
    channel = item.channel
331 1
    brand = brand_service.find_brand(channel.brand_id)
332
333 1
    current_version = news_item_service.get_current_item_version(item.id)
334 1
    is_current_version = version.id == current_version.id
335
336 1
    context = {
337
        'version': version,
338
        'brand': brand,
339
        'is_current_version': is_current_version,
340
    }
341
342 1
    try:
343 1
        rendered_body = news_html_service.render_body(
344
            version.body, channel.id, item.images
345
        )
346
347 1
        context.update(
348
            {
349
                'rendered_body': rendered_body,
350
                'error_occurred': False,
351
            }
352
        )
353
    except Exception as e:
354
        context.update(
355
            {
356
                'error_occurred': True,
357
                'error_message': str(e),
358
            }
359
        )
360
361 1
    return context
362
363
364 1
@blueprint.get('/items/<uuid:item_id>/versions')
365 1
@permission_required(NewsItemPermission.view)
366 1
@templated
367
def item_list_versions(item_id):
368
    """List news item's versions."""
369 1
    item = _get_item_or_404(item_id)
370
371 1
    channel = item.channel
372 1
    brand = brand_service.find_brand(channel.brand_id)
373
374 1
    versions = news_item_service.get_item_versions(item.id)
375 1
    versions_pairwise = list(pairwise(versions + [None]))
376
377 1
    return {
378
        'item': item,
379
        'brand': brand,
380
        'versions_pairwise': versions_pairwise,
381
    }
382
383
384 1
@blueprint.get('/items/<uuid:from_version_id>/compare_to/<uuid:to_version_id>')
385 1
@permission_required(NewsItemPermission.view)
386 1
@templated
387
def item_compare_versions(from_version_id, to_version_id):
388
    """Show the difference between two news item versions."""
389 1
    from_version = _find_version(from_version_id)
390 1
    to_version = _find_version(to_version_id)
391
392 1
    if from_version.item_id != to_version.item_id:
393
        abort(400, 'The versions do not belong to the same item.')
394
395 1
    item = news_item_service.find_item(from_version.item_id)
396 1
    channel = item.channel
397 1
    brand = brand_service.find_brand(channel.brand_id)
398
399 1
    html_diff_title = _create_html_diff(from_version, to_version, 'title')
400 1
    html_diff_body = _create_html_diff(from_version, to_version, 'body')
401 1
    html_diff_image_url_path = _create_html_diff(
402
        from_version, to_version, 'image_url_path'
403
    )
404
405 1
    return {
406
        'brand': brand,
407
        'diff_title': html_diff_title,
408
        'diff_body': html_diff_body,
409
        'diff_image_url_path': html_diff_image_url_path,
410
    }
411
412
413 1
@blueprint.get('/for_channel/<channel_id>/create')
414 1
@permission_required(NewsItemPermission.create)
415 1
@templated
416 1
def item_create_form(channel_id, erroneous_form=None):
417
    """Show form to create a news item."""
418 1
    channel = _get_channel_or_404(channel_id)
419
420 1
    if erroneous_form:
421
        form = erroneous_form
422
    else:
423 1
        slug_prefix = date.today().strftime('%Y-%m-%d-')
424 1
        form = ItemCreateForm(slug=slug_prefix)
425
426 1
    return {
427
        'channel': channel,
428
        'form': form,
429
    }
430
431
432 1
@blueprint.post('/for_channel/<channel_id>')
433 1
@permission_required(NewsItemPermission.create)
434
def item_create(channel_id):
435
    """Create a news item."""
436 1
    channel = _get_channel_or_404(channel_id)
437
438 1
    form = ItemCreateForm(request.form)
439 1
    if not form.validate():
440
        return item_create_form(channel.id, form)
441
442 1
    slug = form.slug.data.strip().lower()
443 1
    creator = g.user
444 1
    title = form.title.data.strip()
445 1
    body = form.body.data.strip()
446 1
    image_url_path = form.image_url_path.data.strip()
447
448 1
    item = news_item_service.create_item(
449
        channel.id, slug, creator.id, title, body, image_url_path=image_url_path
450
    )
451
452 1
    flash_success(
453
        gettext('News item "%(title)s" has been created.', title=item.title)
454
    )
455
456 1
    return redirect_to('.item_view', item_id=item.id)
457
458
459 1
@blueprint.get('/items/<uuid:item_id>/update')
460 1
@permission_required(NewsItemPermission.update)
461 1
@templated
462 1
def item_update_form(item_id, erroneous_form=None):
463
    """Show form to update a news item."""
464 1
    item = _get_item_or_404(item_id)
465
466 1
    current_version = news_item_service.get_current_item_version(item.id)
467
468 1
    form = (
469
        erroneous_form
470
        if erroneous_form
471
        else ItemUpdateForm(obj=current_version, slug=item.slug)
472
    )
473
474 1
    return {
475
        'item': item,
476
        'form': form,
477
    }
478
479
480 1
@blueprint.post('/items/<uuid:item_id>')
481 1
@permission_required(NewsItemPermission.update)
482
def item_update(item_id):
483
    """Update a news item."""
484
    item = _get_item_or_404(item_id)
485
486
    form = ItemUpdateForm(request.form)
487
    if not form.validate():
488
        return item_update_form(item.id, form)
489
490
    creator = g.user
491
    slug = form.slug.data.strip().lower()
492
    title = form.title.data.strip()
493
    body = form.body.data.strip()
494
    image_url_path = form.image_url_path.data.strip()
495
496
    item = news_item_service.update_item(
497
        item.id, slug, creator.id, title, body, image_url_path=image_url_path
498
    )
499
500
    flash_success(
501
        gettext('News item "%(title)s" has been updated.', title=item.title)
502
    )
503
504
    return redirect_to('.item_view', item_id=item.id)
505
506
507 1
@blueprint.get('/items/<uuid:item_id>/publish_later')
508 1
@permission_required(NewsItemPermission.publish)
509 1
@templated
510 1
def item_publish_later_form(item_id, erroneous_form=None):
511
    """Show form to publish a news item at a time in the future."""
512 1
    item = _get_item_or_404(item_id)
513
514 1
    form = erroneous_form if erroneous_form else ItemPublishLaterForm()
515
516 1
    return {
517
        'item': item,
518
        'form': form,
519
    }
520
521
522 1
@blueprint.post('/items/<uuid:item_id>/publish_later')
523 1
@permission_required(NewsItemPermission.publish)
524
def item_publish_later(item_id):
525
    """Publish a news item at a time in the future."""
526 1
    item = _get_item_or_404(item_id)
527
528 1
    form = ItemPublishLaterForm(request.form)
529 1
    if not form.validate():
530
        return item_publish_later_form(item.id, form)
531
532 1
    publish_at = local_tz_to_utc(
533
        datetime.combine(form.publish_on.data, form.publish_at.data)
534
    )
535
536 1
    event = news_item_service.publish_item(
537
        item.id, publish_at=publish_at, initiator_id=g.user.id
538
    )
539
540 1
    news_signals.item_published.send(None, event=event)
541
542 1
    flash_success(
543
        gettext(
544
            'News item "%(title)s" will be published later.', title=item.title
545
        )
546
    )
547
548 1
    return redirect_to('.item_view', item_id=item.id)
549
550
551 1
@blueprint.post('/items/<uuid:item_id>/publish_now')
552 1
@permission_required(NewsItemPermission.publish)
553 1
@respond_no_content
554
def item_publish_now(item_id):
555
    """Publish a news item now."""
556 1
    item = _get_item_or_404(item_id)
557
558 1
    event = news_item_service.publish_item(item.id, initiator_id=g.user.id)
559
560 1
    news_signals.item_published.send(None, event=event)
561
562 1
    flash_success(
563
        gettext('News item "%(title)s" has been published.', title=item.title)
564
    )
565
566
567
# -------------------------------------------------------------------- #
568
# helpers
569
570
571 1
def _get_brand_or_404(brand_id):
572 1
    brand = brand_service.find_brand(brand_id)
573
574 1
    if brand is None:
575
        abort(404)
576
577 1
    return brand
578
579
580 1
def _get_channel_or_404(channel_id) -> Channel:
581 1
    channel = news_channel_service.find_channel(channel_id)
582
583 1
    if channel is None:
584
        abort(404)
585
586 1
    return channel
587
588
589 1
def _get_item_or_404(item_id):
590 1
    item = news_item_service.find_item(item_id)
591
592 1
    if item is None:
593
        abort(404)
594
595 1
    return item
596
597
598 1
def _get_image_or_404(image_id):
599
    image = news_image_service.find_image(image_id)
600
601
    if image is None:
602
        abort(404)
603
604
    return image
605
606
607 1
def _find_version(version_id):
608 1
    version = news_item_service.find_item_version(version_id)
609
610 1
    if version is None:
611
        abort(404)
612
613 1
    return version
614
615
616 1 View Code Duplication
def _create_html_diff(from_version, to_version, attribute_name):
617
    """Create an HTML diff between the named attribute's value of each
618
    of the two versions.
619
    """
620 1
    from_description = format_datetime_short(from_version.created_at)
621 1
    to_description = format_datetime_short(to_version.created_at)
622
623 1
    from_text = getattr(from_version, attribute_name)
624 1
    to_text = getattr(to_version, attribute_name)
625
626 1
    return text_diff_service.create_html_diff(
627
        from_text, to_text, from_description, to_description
628
    )
629