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