[Clients] handle Unicode decode error when uploading OPML
[mygpo.git] / mygpo / web / utils.py
blobcec9581784987d7df882fb7b27076bdf5ee4cf68
1 import re
2 import math
3 import string
4 import collections
5 from datetime import datetime
7 from django.utils.translation import ungettext
8 from django.views.decorators.cache import never_cache
9 from django.utils.html import strip_tags
10 from django.core.urlresolvers import reverse
11 from django.shortcuts import render
12 from django.http import Http404
14 from babel import Locale, UnknownLocaleError
16 from mygpo.podcasts.models import Podcast
19 def get_accepted_lang(request):
20 """ returns a list of language codes accepted by the HTTP request """
22 lang_str = request.META.get('HTTP_ACCEPT_LANGUAGE', '')
23 lang_str = ''.join([c for c in lang_str if c in string.ascii_letters+','])
24 langs = lang_str.split(',')
25 langs = [s[:2] for s in langs]
26 langs = list(map(str.strip, langs))
27 langs = [_f for _f in langs if _f]
28 return list(set(langs))
31 RE_LANG = re.compile('^[a-zA-Z]{2}[-_]?.*$')
34 def sanitize_language_code(lang):
35 return lang[:2].lower()
38 def sanitize_language_codes(ls):
39 """
40 expects a list of language codes and returns a unique lost of the first
41 part of all items. obviously invalid entries are skipped
43 >>> sanitize_language_codes(['de-at', 'de-ch'])
44 ['de']
46 >>> set(sanitize_language_codes(['de-at', 'en', 'en-gb', '(asdf', 'Deutsch'])) == {'de', 'en'}
47 True
48 """
50 ls = [sanitize_language_code(l) for l in ls if l and RE_LANG.match(l)]
51 return list(set(ls))
54 def get_language_names(lang):
55 """
56 Takes a list of language codes and returns a list of tuples
57 with (code, name)
58 """
59 res = {}
60 for l in lang:
61 try:
62 locale = Locale(l)
63 except UnknownLocaleError:
64 continue
66 if locale.display_name:
67 res[l] = locale.display_name
69 return res
72 def get_page_list(start, total, cur, show_max):
73 """
74 returns a list of pages to be linked for navigation in a paginated view
76 >>> get_page_list(1, 100, 1, 10)
77 [1, 2, 3, 4, 5, 6, '...', 98, 99, 100]
79 >>> get_page_list(1, 995/10, 1, 10)
80 [1, 2, 3, 4, 5, 6, '...', 98, 99, 100]
82 >>> get_page_list(1, 100, 50, 10)
83 [1, '...', 48, 49, 50, 51, '...', 98, 99, 100]
85 >>> get_page_list(1, 100, 99, 10)
86 [1, '...', 97, 98, 99, 100]
88 >>> get_page_list(1, 3, 2, 10)
89 [1, 2, 3]
90 """
92 # if we get "total" as a float (eg from total_entries / entries_per_page)
93 # we round up
94 total = math.ceil(total)
96 if show_max >= (total - start):
97 return list(range(start, total+1))
99 ps = []
100 if (cur - start) > show_max / 2:
101 ps.extend(list(range(start, int(show_max / 4))))
102 ps.append('...')
103 ps.extend(list(range(cur - int(show_max / 4), cur)))
105 else:
106 ps.extend(list(range(start, cur)))
108 ps.append(cur)
110 if (total - cur) > show_max / 2:
111 # for the first pages, show more pages at the beginning
112 add = math.ceil(show_max / 2 - len(ps))
113 ps.extend(list(range(cur + 1, cur + int(show_max / 4) + add)))
114 ps.append('...')
115 ps.extend(list(range(total - int(show_max / 4), total + 1)))
117 else:
118 ps.extend(list(range(cur + 1, total + 1)))
120 return ps
123 def process_lang_params(request):
125 lang = request.GET.get('lang', None)
127 if lang is None:
128 langs = get_accepted_lang(request)
129 lang = next(iter(langs), '')
131 return sanitize_language_code(lang)
134 def symbian_opml_changes(podcast):
135 podcast.description = podcast.display_title + '\n' + \
136 (podcast.description or '')
137 return podcast
140 @never_cache
141 def maintenance(request, *args, **kwargs):
142 resp = render(request, 'maintenance.html', {})
143 resp.status_code = 503
144 return resp
147 def get_podcast_link_target(podcast, view_name='podcast', add_args=[]):
148 """ Returns the link-target for a Podcast, preferring slugs over Ids """
150 # we prefer slugs
151 if podcast.slug:
152 args = [podcast.slug]
153 view_name = '%s-slug' % view_name
155 # as a fallback we use UUIDs
156 else:
157 args = [podcast.get_id()]
158 view_name = '%s-id' % view_name
160 return reverse(view_name, args=args + add_args)
163 def get_podcast_group_link_target(group, view_name, add_args=[]):
164 """ the link-target for a Podcast group, preferring slugs over Ids """
165 args = [group.slug]
166 view_name = '%s-slug-id' % view_name
167 return reverse(view_name, args=args + add_args)
170 def get_episode_link_target(episode, podcast, view_name='episode',
171 add_args=[]):
172 """ Returns the link-target for an Episode, preferring slugs over Ids """
174 # prefer slugs
175 if episode.slug:
176 args = [podcast.slug, episode.slug]
177 view_name = '%s-slug' % view_name
179 # fallback: UUIDs
180 else:
181 podcast = podcast or episode.podcast
182 args = [podcast.get_id(), episode.get_id()]
183 view_name = '%s-id' % view_name
185 return strip_tags(reverse(view_name, args=args + add_args))
188 # doesn't include the '@' because it's not stored as part of a twitter handle
189 TWITTER_CHARS = string.ascii_letters + string.digits + '_'
192 def normalize_twitter(s):
193 """ normalize user input that is supposed to be a Twitter handle """
194 return "".join(i for i in s if i in TWITTER_CHARS)
197 CCLICENSE = re.compile(r'http://(www\.)?creativecommons.org/licenses/([a-z-]+)/([0-9.]+)?/?')
198 CCPUBLICDOMAIN = re.compile(r'http://(www\.)?creativecommons.org/licenses/publicdomain/?')
199 LicenseInfo = collections.namedtuple('LicenseInfo', 'name version url')
201 def license_info(license_url):
202 """ Extracts license information from the license URL
204 >>> i = license_info('http://creativecommons.org/licenses/by/3.0/')
205 >>> i.name
206 'CC BY'
207 >>> i.version
208 '3.0'
209 >>> i.url
210 'http://creativecommons.org/licenses/by/3.0/'
212 >>> iwww = license_info('http://www.creativecommons.org/licenses/by/3.0/')
213 >>> i.name == iwww.name and i.version == iwww.version
214 True
216 >>> i = license_info('http://www.creativecommons.org/licenses/publicdomain')
217 >>> i.name
218 'Public Domain'
219 >>> i.version is None
220 True
222 >>> i = license_info('http://example.com/my-own-license')
223 >>> i.name is None
224 True
225 >>> i.version is None
226 True
227 >>> i.url
228 'http://example.com/my-own-license'
230 m = CCLICENSE.match(license_url)
231 if m:
232 _, name, version = m.groups()
233 return LicenseInfo('CC %s' % name.upper(), version, license_url)
235 m = CCPUBLICDOMAIN.match(license_url)
236 if m:
237 return LicenseInfo('Public Domain', None, license_url)
239 return LicenseInfo(None, None, license_url)
242 def check_restrictions(obj):
243 """ checks for known restrictions of the object """
245 restrictions = obj.restrictions.split(',')
246 if "hide" in restrictions:
247 raise Http404
249 if "hide-author" in restrictions:
250 obj.author = None
252 return obj
255 def hours_to_str(hours_total):
256 """ returns a human-readable string representation of some hours
258 >>> hours_to_str(1)
259 '1 hour'
261 >>> hours_to_str(5)
262 '5 hours'
264 >>> hours_to_str(100)
265 '4 days, 4 hours'
267 >>> hours_to_str(960)
268 '5 weeks, 5 days'
270 >>> hours_to_str(961)
271 '5 weeks, 5 days, 1 hour'
274 weeks = int(hours_total / 24 / 7)
275 days = int(hours_total / 24) % 7
276 hours = hours_total % 24
278 strs = []
280 if weeks:
281 strs.append(ungettext('%(weeks)d week', '%(weeks)d weeks', weeks) %
282 { 'weeks': weeks})
284 if days:
285 strs.append(ungettext('%(days)d day', '%(days)d days', days) %
286 { 'days': days})
288 if hours:
289 strs.append(ungettext('%(hours)d hour', '%(hours)d hours', hours) %
290 { 'hours': hours})
292 return ', '.join(strs)