]> git.0d.be Git - panikweb.git/blob - panikweb/paniktags/templatetags/paniktags.py
prepare support for multiple players
[panikweb.git] / panikweb / paniktags / templatetags / paniktags.py
1 import datetime
2 import email.utils
3 import json
4 import re
5 import time
6 import urllib.parse
7 import uuid
8 from datetime import datetime, timedelta
9
10 from django import template
11 from django.conf import settings
12 from django.db.models.query import QuerySet
13 from django.urls import reverse
14 from django.utils.encoding import force_str
15 from django.utils.html import strip_tags
16 from django.utils.http import quote
17 from django.utils.safestring import mark_safe
18 from django.utils.text import Truncator
19 from emissions.app_settings import app_settings as emissions_app_settings
20 from emissions.models import Emission, Episode, Focus, NewsItem, SoundFile
21 from emissions.utils import period_program
22 from taggit.models import Tag
23
24 from panikweb import search, utils
25
26 register = template.Library()
27
28
29 @register.filter(name='zip')
30 def zip_lists(a, b):
31     return zip(a, b)
32
33
34 @register.inclusion_tag('includes/audio.html', takes_context=True)
35 def audio(context, sound=None, embed=False, display_fragment_name=False):
36     return {
37         'episode': context.get('episode'),
38         'sound': sound,
39         'display_fragment_name': display_fragment_name,
40         'embed': embed,
41         'has_offsite_media': bool(settings.OFFSITE_MEDIA_SOUNDS),
42     }
43
44
45 @register.inclusion_tag('listen/nav.html', takes_context=True)
46 def listen_nav(context, date=None, klass=None):
47     return {
48         'class': klass,
49         'categories': context.get('categories'),
50     }
51
52
53 @register.inclusion_tag('news/nav.html', takes_context=True)
54 def news_nav(context, date=None, klass=None):
55     return {
56         'class': klass,
57         'newsitem': context.get('newsitem'),
58         'categories': context.get('categories'),
59         'news': context.get('news'),
60         'search_query': context.get('search_query'),
61     }
62
63
64 @register.inclusion_tag('emissions/nav.html', takes_context=True)
65 def emission_nav(context, date=None, klass=None):
66     return {
67         'class': klass,
68         'categories': context.get('categories'),
69         'episodes': context.get('episodes'),
70         'emission': context.get('emission'),
71         'episode': context.get('episode'),
72         'search_query': context.get('search_query'),
73     }
74
75
76 @register.inclusion_tag('episodes/inline.html', takes_context=True)
77 def episode_inline(context, date=None, model=None, klass=None):
78     return {
79         'class': klass,
80         'episode': context.get('episode'),
81         'date': date,
82     }
83
84
85 @register.inclusion_tag('episodes/resume.html', takes_context=True)
86 def episode_resume(context, date=None, model=None, klass=None):
87     return {
88         'model': model,
89         'class': klass,
90         'episode': context.get('episode'),
91         'date': date,
92     }
93
94
95 @register.inclusion_tag('episodes/detail.html', takes_context=True)
96 def episode_detail(context, date=None):
97     soundfiles = SoundFile.objects.select_related().filter(
98         fragment=True, podcastable=True, episode=context.get('episode')
99     )
100     return {
101         'episode': context.get('episode'),
102         'emission': context.get('emission'),
103         'diffusions': context.get('diffusions'),
104         'soundfiles': soundfiles,
105         'date': date,
106         'topik_pages': context.get('topik_pages'),
107     }
108
109
110 @register.inclusion_tag('emissions/detail.html', takes_context=True)
111 def emission_detail(context, date=None):
112     return {
113         'emission': context.get('emission'),
114         'schedules': context.get('schedules'),
115     }
116
117
118 @register.inclusion_tag('emissions/resume.html', takes_context=True)
119 def emission_resume(context, date=None):
120     return {
121         'emission': context.get('emission'),
122         'schedules': context.get('schedules'),
123     }
124
125
126 @register.inclusion_tag('emissions/inline.html', takes_context=True)
127 def emission_inline(context, date=None):
128     return {
129         'emission': context.get('emission'),
130         'schedules': context.get('schedules'),
131     }
132
133
134 @register.inclusion_tag('soundfiles/resume.html')
135 def soundfile_resume(soundfile, date=None):
136     return {'soundfile': soundfile, 'date': date}
137
138
139 @register.inclusion_tag('includes/player.html', takes_context=True)
140 def player(context, player_id='main', display_emission_subtitle=False, display_episode_subtitle=False):
141     return {
142         'unique': uuid.uuid4(),
143         'radio_stream_urls': [
144             x for x in settings.RADIO_STREAM_URLS if x.get('player_id', 'main') == player_id
145         ],
146         'soundfiles': context.get('soundfiles'),
147         'player_id': player_id,
148         'prefix': '' if player_id == 'main' else f'{player_id}-',
149         'display_emission_subtitle': display_emission_subtitle,
150         'display_episode_subtitle': display_episode_subtitle,
151     }
152
153
154 @register.inclusion_tag('includes/metaNav.html', takes_context=True)
155 def metanav(context):
156     return {'LANGUAGE_CODE': context.get('LANGUAGE_CODE')}
157
158
159 @register.inclusion_tag('includes/week.html')
160 def weekview(year=None, week=None, weekday=None, include_nonstop=True):
161     year = year if year else datetime.today().isocalendar()[0]
162     week = week if week else datetime.today().isocalendar()[1]
163     weekday = weekday if weekday is not None else datetime.today().weekday()
164
165     date = utils.tofirstdayinisoweek(year, week)
166     date = datetime(
167         *date.timetuple()[:3]
168         + (emissions_app_settings.DAY_HOUR_START, emissions_app_settings.DAY_MINUTE_START)
169     )
170
171     program = period_program(date, date + timedelta(days=7), include_nonstop=include_nonstop)
172     days = []
173     for day in range(7):
174         days.append(
175             {
176                 'cells': [x for x in program if x.is_on_weekday(day + 1)],
177                 'datetime': date + timedelta(days=day),
178             }
179         )
180
181     return {
182         'days': days,
183         'week': week,
184         'year': year,
185         'weekday': weekday,
186         'now': datetime.now(),
187         'current_week': bool(datetime.now() >= date and datetime.now() < date + timedelta(days=7)),
188     }
189
190
191 @register.inclusion_tag('includes/week-nav.html')
192 def weeknav(year=None, week=None, weekday=None):
193     year = year if year else datetime.today().isocalendar()[0]
194     week = week if week else datetime.today().isocalendar()[1]
195     weekday = weekday if weekday is not None else datetime.today().weekday()
196
197     date = utils.tofirstdayinisoweek(year, week)
198     date = datetime(
199         *date.timetuple()[:3]
200         + (emissions_app_settings.DAY_HOUR_START, emissions_app_settings.DAY_MINUTE_START)
201     )
202
203     days = []
204     for day in range(7):
205         days.append({'datetime': date + timedelta(days=day)})
206
207     previous_week = date - timedelta(days=7)
208     previous_week_year, previous_week_no = previous_week.isocalendar()[:2]
209
210     next_week = date + timedelta(days=7)
211     next_week_year, next_week_no = next_week.isocalendar()[:2]
212
213     return {
214         'days': days,
215         'weekday': weekday,
216         'week': week,
217         'year': year,
218         'previous_week_year': previous_week_year,
219         'previous_week_no': previous_week_no,
220         'next_week_year': next_week_year,
221         'next_week_no': next_week_no,
222     }
223
224
225 @register.inclusion_tag('news/inline.html', takes_context=True)
226 def news_inline(context, klass=None, logo=None):
227     return {'content': context.get('content'), 'class': klass, 'logo': logo}
228
229
230 @register.inclusion_tag('news/roll.html')
231 def newsroll():
232     return {
233         'news': Focus.objects.filter(current=True)
234         .select_related('emission', 'newsitem', 'soundfile', 'episode', 'newsitem__category')
235         .order_by('?')[: settings.HOME_FOCUS_COUNT]
236     }
237
238
239 @register.filter
240 def jsonify(object):
241     if isinstance(object, QuerySet):
242         return serialize('json', object)
243     return json.dumps(object)
244
245
246 @register.filter
247 def strreplace(string, args):
248     find = args.split(',')[0]
249     replace = args.split(',')[1]
250     return string.replace(find, replace)
251
252
253 @register.filter
254 def replace(string, args):
255     search = args.split(args[0])[1]
256     replace = args.split(args[0])[2]
257
258     return re.sub(search, replace, string)
259
260
261 def remove_facet(facet_id, url, facet):
262     scheme, netloc, path, query, fragment = list(urllib.parse.urlsplit(str(url)))
263     facet = '%s_exact:%s' % (facet_id, facet)
264     query_string = urllib.parse.parse_qsl(query)
265     query_string = [x for x in query_string if not (x[0] == 'selected_facets' and x[1] == facet)]
266     query = '&'.join(['%s=%s' % x for x in query_string])
267     url = urllib.parse.urlunsplit([scheme, netloc, path, query, None])
268     return re.sub(r'&page=\d+', '', url)
269
270
271 @register.filter
272 def remove_tag_facet(url, facet):
273     return remove_facet('tags', url, facet)
274
275
276 @register.filter
277 def remove_category_facet(url, facet):
278     return remove_facet('categories', url, facet)
279
280
281 @register.filter
282 def remove_news_category_facet(url, facet):
283     return remove_facet('news_categories', url, facet)
284
285
286 def with_facet(url, facet_id, facet):
287     scheme, netloc, path, query, fragment = list(urllib.parse.urlsplit(str(url)))
288     criteria = '%s_exact:' % facet_id
289     query_string = urllib.parse.parse_qsl(query)
290     query_string = [x for x in query_string if not (x[0] == 'selected_facets' and x[1].startswith(criteria))]
291     if facet is not None:
292         query_string.append(('selected_facets', criteria + facet))
293     query = '&'.join(['%s=%s' % x for x in query_string])
294     url = urllib.parse.urlunsplit([scheme, netloc, path, query, None])
295     return re.sub(r'&page=\d+', '', url)
296
297
298 @register.filter
299 def with_news_category_facet(url, facet):
300     return with_facet(url, 'news_categories', facet)
301
302
303 @register.filter
304 def without_news_category_facet(url):
305     return with_facet(url, 'news_categories', facet=None)
306
307
308 @register.filter
309 def with_format_facet(url, facet):
310     return with_facet(url, 'format', facet)
311
312
313 @register.filter
314 def without_format_facet(url):
315     return with_facet(url, 'format', facet=None)
316
317
318 @register.filter
319 def remove_format_facet(url, facet):
320     return remove_facet('format', url, facet)
321
322
323 def append_facet(facet_id, url, facet):
324     facet = quote(facet, safe='')
325     if not '?' in url:
326         url = url + '?'
327     return re.sub(r'&page=\d+', '', url + '&selected_facets=%s_exact:%s' % (facet_id, facet))
328
329
330 @register.filter
331 def append_tag_facet(url, facet):
332     return append_facet('tags', url, facet)
333
334
335 @register.filter
336 def append_category_facet(url, facet):
337     return append_facet('categories', url, facet)
338
339
340 @register.filter
341 def append_news_category_facet(url, facet):
342     return append_facet('news_categories', url, facet)
343
344
345 @register.filter
346 def append_format_facet(url, facet):
347     return append_facet('format', url, facet)
348
349
350 @register.tag
351 def search_result_template(parser, token):
352     try:
353         tag_name, result_str = token.split_contents()
354     except ValueError:
355         raise template.TemplateSyntaxError("%r tag requires exactly one argument" % token.contents.split()[0])
356     return FormatSearchResultNode(result_str)
357
358
359 class FormatSearchResultNode(template.Node):
360     def __init__(self, result_str):
361         self.result_var = template.Variable(result_str)
362
363     def render(self, context):
364         result = self.result_var.resolve(context)
365         dir_mapping = {'newsitem': 'news', 'emission': 'emissions', 'episode': 'episodes'}
366         t = template.loader.get_template('%s/search_result.html' % dir_mapping.get(result.model_name))
367         return t.render({'result': result})
368
369
370 @register.inclusion_tag('includes/piwik.html')
371 def piwik():
372     return {'enabled': settings.ENABLE_PIWIK}
373
374
375 @register.filter
376 def rfc822(datetime):
377     if datetime is None:
378         return ''
379     return email.utils.formatdate(time.mktime(datetime.timetuple()))
380
381
382 @register.inclusion_tag('includes/related.html', takes_context=False)
383 def related_objects(object):
384     sqs = search.MoreLikeThisSearchQuerySet().models(Emission, Episode, NewsItem)
385     return {'more_like_this': sqs.more_like_this(object)[:12]}
386
387
388 @register.simple_tag
389 def random_emissions(count=6):
390     return Emission.objects.filter(archived=False).order_by('?')[:count]
391
392
393 @register.simple_tag
394 def get_tags():
395     return Tag.objects.exclude(taggit_taggeditem_items__isnull=True)
396
397
398 @register.inclusion_tag('includes/topik.html', takes_context=True)
399 def topik(context, topik):
400     return {'page': topik}
401
402
403 @register.filter
404 def get_focus_url(object):
405     if object.newsitem:
406         return reverse('newsitem-view', kwargs={'slug': object.newsitem.slug})
407     if object.emission:
408         return reverse('emission-view', kwargs={'slug': object.emission.slug})
409     if object.episode:
410         return reverse(
411             'episode-view',
412             kwargs={'slug': object.episode.slug, 'emission_slug': object.episode.emission.slug},
413         )
414     if object.soundfile:
415         return reverse(
416             'episode-view',
417             kwargs={
418                 'slug': object.soundfile.episode.slug,
419                 'emission_slug': object.soundfile.episode.emission.slug,
420             },
421         )
422     if object.page:
423         return object.page.get_online_url()
424     return ''
425
426
427 @register.filter
428 def facet_tag(tag):
429     return tag.name.lower()
430
431
432 @register.filter
433 def set_absolute_urls(text):
434     text = text.replace('src="/', 'src="%s' % settings.WEBSITE_BASE_URL)
435     text = text.replace('href="/', 'href="%s' % settings.WEBSITE_BASE_URL)
436     return text
437
438
439 @register.filter
440 def as_absolute_url(url):
441     if url.startswith('/'):
442         url = settings.WEBSITE_BASE_URL + url.lstrip('/')
443     return url
444
445
446 @register.filter
447 def xml_illegal_fix(text):
448     # django.utils.xmlutils.UnserializableContentError: Control characters are not supported in XML 1.0
449     for i in range(0x20):  # remove control characters
450         char = chr(i)
451         if char in ('\t', '\r', '\n'):
452             # only allow tab, carriage return and line feed.
453             continue
454         text = text.replace(char, '')
455     # fffe and ffff are also invalid characters
456     return text.replace('\ufffe', '').replace('\uffff', '')
457
458
459 @register.filter
460 def hr_split(text):
461     return re.split(r'<hr\s*/?>', text)
462
463
464 @register.filter
465 def as_itpc(url):
466     return url.replace('https://', 'itpc://').replace('http://', 'itpc://')
467
468
469 @register.filter
470 def plain_short_description(text):
471     return mark_safe(Truncator(mark_safe(strip_tags(text))).words(75, truncate="…").replace('"', '"'))