[Migration] handle Episode.listeners = None in episode toplist
[mygpo.git] / mygpo / web / utils.py
blob9d0f1fa731f1b943fdb9da5befd8e201f7da0c16
1 import re
2 import string
3 import collections
4 from datetime import datetime
6 from django.utils.translation import ungettext
7 from django.views.decorators.cache import never_cache
8 from django.utils.html import strip_tags
9 from django.core.urlresolvers import reverse
10 from django.shortcuts import render
11 from django.http import Http404
13 from babel import Locale, UnknownLocaleError
15 from mygpo.podcasts.models import Podcast
18 def get_accepted_lang(request):
19 """ returns a list of language codes accepted by the HTTP request """
21 lang_str = request.META.get('HTTP_ACCEPT_LANGUAGE', '')
22 lang_str = filter(lambda c: c in string.letters+',', lang_str)
23 langs = lang_str.split(',')
24 langs = [s[:2] for s in langs]
25 langs = map(str.strip, langs)
26 langs = filter(None, langs)
27 return list(set(langs))
30 RE_LANG = re.compile('^[a-zA-Z]{2}[-_]?.*$')
33 def sanitize_language_code(lang):
34 return lang[:2].lower()
37 def sanitize_language_codes(ls):
38 """
39 expects a list of language codes and returns a unique lost of the first
40 part of all items. obviously invalid entries are skipped
42 >>> sanitize_language_codes(['de-at', 'de-ch'])
43 ['de']
45 >>> sanitize_language_codes(['de-at', 'en', 'en-gb', '(asdf', 'Deutsch'])
46 ['de', 'en']
47 """
49 ls = [sanitize_language_code(l) for l in ls if l and RE_LANG.match(l)]
50 return list(set(ls))
53 def get_language_names(lang):
54 """
55 Takes a list of language codes and returns a list of tuples
56 with (code, name)
57 """
58 res = {}
59 for l in lang:
60 try:
61 locale = Locale(l)
62 except UnknownLocaleError:
63 continue
65 if locale.display_name:
66 res[l] = locale.display_name
68 return res
71 def get_page_list(start, total, cur, show_max):
72 """
73 returns a list of pages to be linked for navigation in a paginated view
75 >>> get_page_list(1, 100, 1, 10)
76 [1, 2, 3, 4, 5, 6, '...', 98, 99, 100]
78 >>> get_page_list(1, 100, 50, 10)
79 [1, '...', 48, 49, 50, 51, '...', 98, 99, 100]
81 >>> get_page_list(1, 100, 99, 10)
82 [1, '...', 97, 98, 99, 100]
84 >>> get_page_list(1, 3, 2, 10)
85 [1, 2, 3]
86 """
88 if show_max >= (total - start):
89 return range(start, total+1)
91 ps = []
92 if (cur - start) > show_max / 2:
93 ps.extend(range(start, show_max / 4))
94 ps.append('...')
95 ps.extend(range(cur - show_max / 4, cur))
97 else:
98 ps.extend(range(start, cur))
100 ps.append(cur)
102 if (total - cur) > show_max / 2:
103 # for the first pages, show more pages at the beginning
104 add = show_max / 2 - len(ps)
105 ps.extend(range(cur + 1, cur + show_max / 4 + add))
106 ps.append('...')
107 ps.extend(range(total - show_max / 4, total + 1))
109 else:
110 ps.extend(range(cur + 1, total + 1))
112 return ps
115 def process_lang_params(request):
117 lang = request.GET.get('lang', None)
119 if lang is None:
120 langs = get_accepted_lang(request)
121 lang = next(iter(langs), '')
123 return sanitize_language_code(lang)
126 def symbian_opml_changes(podcast):
127 podcast.description = (podcast.title or '') + '\n' + \
128 (podcast.description or '')
129 return podcast
132 @never_cache
133 def maintenance(request, *args, **kwargs):
134 resp = render(request, 'maintenance.html', {})
135 resp.status_code = 503
136 return resp
139 def get_podcast_link_target(podcast, view_name='podcast', add_args=[]):
140 """ Returns the link-target for a Podcast, preferring slugs over Ids
142 automatically distringuishes between relational Podcast objects and
143 CouchDB-based Podcasts """
145 # we prefer slugs
146 if podcast.slug:
147 args = [podcast.slug]
148 view_name = '%s-slug' % view_name
150 # as a fallback we use UUIDs
151 else:
152 args = [podcast.get_id()]
153 view_name = '%s-id' % view_name
155 return reverse(view_name, args=args + add_args)
158 def get_podcast_group_link_target(group, view_name, add_args=[]):
159 """ Returns the link-target for a Podcast group, preferring slugs over Ids
161 automatically distringuishes between relational Podcast objects and
162 CouchDB-based Podcasts """
164 # we prefer slugs
165 if group.slug:
166 args = [group.slug]
167 view_name = '%s-slug-id' % view_name
169 # as a fallback we use CouchDB-IDs
170 else:
171 args = [group._id]
172 view_name = '%s-slug-id' % view_name
174 return reverse(view_name, args=args + add_args)
177 def get_episode_link_target(episode, podcast, view_name='episode',
178 add_args=[]):
179 """ Returns the link-target for an Episode, preferring slugs over Ids
181 automatically distringuishes between relational Episode objects and
182 CouchDB-based Episodes """
184 # prefer slugs
185 if episode.slug:
186 args = [podcast.slug, episode.slug]
187 view_name = '%s-slug' % view_name
189 # fallback: UUIDs
190 else:
191 podcast = podcast or episode.podcast
192 args = [podcast.get_id(), episode.get_id()]
193 view_name = '%s-id' % view_name
195 return strip_tags(reverse(view_name, args=args + add_args))
198 # doesn't include the '@' because it's not stored as part of a twitter handle
199 TWITTER_CHARS = string.ascii_letters + string.digits + '_'
202 def normalize_twitter(s):
203 """ normalize user input that is supposed to be a Twitter handle """
204 return "".join(i for i in s if i in TWITTER_CHARS)
207 CCLICENSE = re.compile(r'http://(www\.)?creativecommons.org/licenses/([a-z-]+)/([0-9.]+)?/?')
208 CCPUBLICDOMAIN = re.compile(r'http://(www\.)?creativecommons.org/licenses/publicdomain/?')
209 LicenseInfo = collections.namedtuple('LicenseInfo', 'name version url')
211 def license_info(license_url):
212 """ Extracts license information from the license URL
214 >>> i = license_info('http://creativecommons.org/licenses/by/3.0/')
215 >>> i.name
216 'CC BY'
217 >>> i.version
218 '3.0'
219 >>> i.url
220 'http://creativecommons.org/licenses/by/3.0/'
222 >>> iwww = license_info('http://www.creativecommons.org/licenses/by/3.0/')
223 >>> i.name == iwww.name and i.version == iwww.version
224 True
226 >>> i = license_info('http://www.creativecommons.org/licenses/publicdomain')
227 >>> i.name
228 'Public Domain'
229 >>> i.version is None
230 True
232 >>> i = license_info('http://example.com/my-own-license')
233 >>> i.name is None
234 True
235 >>> i.version is None
236 True
237 >>> i.url
238 'http://example.com/my-own-license'
240 m = CCLICENSE.match(license_url)
241 if m:
242 _, name, version = m.groups()
243 return LicenseInfo('CC %s' % name.upper(), version, license_url)
245 m = CCPUBLICDOMAIN.match(license_url)
246 if m:
247 return LicenseInfo('Public Domain', None, license_url)
249 return LicenseInfo(None, None, license_url)
252 def check_restrictions(obj):
253 """ checks for known restrictions of the object """
255 restrictions = obj.restrictions.split(',')
256 if "hide" in restrictions:
257 raise Http404
259 if "hide-author" in restrictions:
260 obj.author = None
262 return obj
265 def hours_to_str(hours_total):
266 """ returns a human-readable string representation of some hours
268 >>> hours_to_str(1)
269 u'1 hour'
271 >>> hours_to_str(5)
272 u'5 hours'
274 >>> hours_to_str(100)
275 u'4 days, 4 hours'
277 >>> hours_to_str(960)
278 u'5 weeks, 5 days'
280 >>> hours_to_str(961)
281 u'5 weeks, 5 days, 1 hour'
284 weeks = hours_total / 24 / 7
285 days = hours_total / 24 % 7
286 hours = hours_total % 24
288 strs = []
290 if weeks:
291 strs.append(ungettext('%(weeks)d week', '%(weeks)d weeks', weeks) %
292 { 'weeks': weeks})
294 if days:
295 strs.append(ungettext('%(days)d day', '%(days)d days', days) %
296 { 'days': days})
298 if hours:
299 strs.append(ungettext('%(hours)d hour', '%(hours)d hours', hours) %
300 { 'hours': hours})
302 return ', '.join(strs)