[Migration] Get Podcasts / Episodes from PostgreSQL
[mygpo.git] / mygpo / web / utils.py
blob233fbeca8eefbbab74e79bb02f5c56790900cf28
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.core.models import Podcast
16 from mygpo.core.proxy import proxy_object
17 from mygpo.db.couchdb.podcast import podcast_by_id, podcasts_to_dict
20 def get_accepted_lang(request):
21 """ returns a list of language codes accepted by the HTTP request """
23 lang_str = request.META.get('HTTP_ACCEPT_LANGUAGE', '')
24 lang_str = filter(lambda c: c in string.letters+',', lang_str)
25 langs = lang_str.split(',')
26 langs = [s[:2] for s in langs]
27 langs = map(str.strip, langs)
28 langs = filter(None, langs)
29 return list(set(langs))
32 RE_LANG = re.compile('^[a-zA-Z]{2}[-_]?.*$')
35 def sanitize_language_code(lang):
36 return lang[:2].lower()
39 def sanitize_language_codes(ls):
40 """
41 expects a list of language codes and returns a unique lost of the first
42 part of all items. obviously invalid entries are skipped
44 >>> sanitize_language_codes(['de-at', 'de-ch'])
45 ['de']
47 >>> sanitize_language_codes(['de-at', 'en', 'en-gb', '(asdf', 'Deutsch'])
48 ['de', 'en']
49 """
51 ls = [sanitize_language_code(l) for l in ls if l and RE_LANG.match(l)]
52 return list(set(ls))
55 def get_language_names(lang):
56 """
57 Takes a list of language codes and returns a list of tuples
58 with (code, name)
59 """
60 res = {}
61 for l in lang:
62 try:
63 locale = Locale(l)
64 except UnknownLocaleError:
65 continue
67 if locale.display_name:
68 res[l] = locale.display_name
70 return res
73 def get_page_list(start, total, cur, show_max):
74 """
75 returns a list of pages to be linked for navigation in a paginated view
77 >>> get_page_list(1, 100, 1, 10)
78 [1, 2, 3, 4, 5, 6, '...', 98, 99, 100]
80 >>> get_page_list(1, 100, 50, 10)
81 [1, '...', 48, 49, 50, 51, '...', 98, 99, 100]
83 >>> get_page_list(1, 100, 99, 10)
84 [1, '...', 97, 98, 99, 100]
86 >>> get_page_list(1, 3, 2, 10)
87 [1, 2, 3]
88 """
90 if show_max >= (total - start):
91 return range(start, total+1)
93 ps = []
94 if (cur - start) > show_max / 2:
95 ps.extend(range(start, show_max / 4))
96 ps.append('...')
97 ps.extend(range(cur - show_max / 4, cur))
99 else:
100 ps.extend(range(start, cur))
102 ps.append(cur)
104 if (total - cur) > show_max / 2:
105 # for the first pages, show more pages at the beginning
106 add = show_max / 2 - len(ps)
107 ps.extend(range(cur + 1, cur + show_max / 4 + add))
108 ps.append('...')
109 ps.extend(range(total - show_max / 4, total + 1))
111 else:
112 ps.extend(range(cur + 1, total + 1))
114 return ps
117 def process_lang_params(request):
119 lang = request.GET.get('lang', None)
121 if lang is None:
122 langs = get_accepted_lang(request)
123 lang = next(iter(langs), '')
125 return sanitize_language_code(lang)
128 def symbian_opml_changes(podcast):
129 podcast.description = (podcast.title or '') + '\n' + \
130 (podcast.description or '')
131 return podcast
134 @never_cache
135 def maintenance(request, *args, **kwargs):
136 resp = render(request, 'maintenance.html', {})
137 resp.status_code = 503
138 return resp
141 def get_podcast_link_target(podcast, view_name='podcast', add_args=[]):
142 """ Returns the link-target for a Podcast, preferring slugs over Ids
144 automatically distringuishes between relational Podcast objects and
145 CouchDB-based Podcasts """
147 from mygpo.core.models import Podcast
149 # we prefer slugs
150 if podcast.slug:
151 args = [podcast.slug]
152 view_name = '%s-slug' % view_name
154 # as a fallback we use UUIDs
155 else:
156 args = [podcast.get_id()]
157 view_name = '%s-id' % view_name
159 return reverse(view_name, args=args + add_args)
162 def get_podcast_group_link_target(group, view_name, add_args=[]):
163 """ Returns the link-target for a Podcast group, preferring slugs over Ids
165 automatically distringuishes between relational Podcast objects and
166 CouchDB-based Podcasts """
168 from mygpo.core.models import PodcastGroup
170 # we prefer slugs
171 if group.slug:
172 args = [group.slug]
173 view_name = '%s-slug-id' % view_name
175 # to keep URLs short, we use use oldids
176 elif group.oldid:
177 args = [group.oldid]
179 # as a fallback we use CouchDB-IDs
180 else:
181 args = [group._id]
182 view_name = '%s-slug-id' % view_name
184 return reverse(view_name, args=args + add_args)
187 def get_episode_link_target(episode, podcast, view_name='episode',
188 add_args=[]):
189 """ Returns the link-target for an Episode, preferring slugs over Ids
191 automatically distringuishes between relational Episode objects and
192 CouchDB-based Episodes """
194 from mygpo.core.models import Podcast
196 # prefer slugs
197 if episode.slug:
198 args = [podcast.slug, episode.slug]
199 view_name = '%s-slug' % view_name
201 # fallback: UUIDs
202 else:
203 podcast = podcast or episode.podcast
204 args = [podcast.get_id(), episode.get_id()]
205 view_name = '%s-id' % view_name
207 return strip_tags(reverse(view_name, args=args + add_args))
210 def fetch_episode_data(episodes, podcasts={}):
212 if not podcasts:
213 podcast_ids = [episode.podcast for episode in episodes]
214 podcasts = podcasts_to_dict(podcast_ids)
216 def set_podcast(episode):
217 episode = proxy_object(episode)
218 episode.podcast = podcasts.get(episode.podcast, None)
219 return episode
221 return map(set_podcast, episodes)
224 # doesn't include the '@' because it's not stored as part of a twitter handle
225 TWITTER_CHARS = string.ascii_letters + string.digits + '_'
228 def normalize_twitter(s):
229 """ normalize user input that is supposed to be a Twitter handle """
230 return "".join(i for i in s if i in TWITTER_CHARS)
233 CCLICENSE = re.compile(r'http://(www\.)?creativecommons.org/licenses/([a-z-]+)/([0-9.]+)?/?')
234 CCPUBLICDOMAIN = re.compile(r'http://(www\.)?creativecommons.org/licenses/publicdomain/?')
235 LicenseInfo = collections.namedtuple('LicenseInfo', 'name version url')
237 def license_info(license_url):
238 """ Extracts license information from the license URL
240 >>> i = license_info('http://creativecommons.org/licenses/by/3.0/')
241 >>> i.name
242 'CC BY'
243 >>> i.version
244 '3.0'
245 >>> i.url
246 'http://creativecommons.org/licenses/by/3.0/'
248 >>> iwww = license_info('http://www.creativecommons.org/licenses/by/3.0/')
249 >>> i.name == iwww.name and i.version == iwww.version
250 True
252 >>> i = license_info('http://www.creativecommons.org/licenses/publicdomain')
253 >>> i.name
254 'Public Domain'
255 >>> i.version is None
256 True
258 >>> i = license_info('http://example.com/my-own-license')
259 >>> i.name is None
260 True
261 >>> i.version is None
262 True
263 >>> i.url
264 'http://example.com/my-own-license'
266 m = CCLICENSE.match(license_url)
267 if m:
268 _, name, version = m.groups()
269 return LicenseInfo('CC %s' % name.upper(), version, license_url)
271 m = CCPUBLICDOMAIN.match(license_url)
272 if m:
273 return LicenseInfo('Public Domain', None, license_url)
275 return LicenseInfo(None, None, license_url)
278 def check_restrictions(obj):
279 """ checks for known restrictions of the object """
281 restrictions = obj.restrictions.split(',')
282 if "hide" in restrictions:
283 raise Http404
285 if "hide-author" in restrictions:
286 obj.author = None
288 return obj
291 def hours_to_str(hours_total):
292 """ returns a human-readable string representation of some hours
294 >>> hours_to_str(1)
295 u'1 hour'
297 >>> hours_to_str(5)
298 u'5 hours'
300 >>> hours_to_str(100)
301 u'4 days, 4 hours'
303 >>> hours_to_str(960)
304 u'5 weeks, 5 days'
306 >>> hours_to_str(961)
307 u'5 weeks, 5 days, 1 hour'
310 weeks = hours_total / 24 / 7
311 days = hours_total / 24 % 7
312 hours = hours_total % 24
314 strs = []
316 if weeks:
317 strs.append(ungettext('%(weeks)d week', '%(weeks)d weeks', weeks) %
318 { 'weeks': weeks})
320 if days:
321 strs.append(ungettext('%(days)d day', '%(days)d days', days) %
322 { 'days': days})
324 if hours:
325 strs.append(ungettext('%(hours)d hour', '%(hours)d hours', hours) %
326 { 'hours': hours})
328 return ', '.join(strs)