Merge pull request #793 from gpodder/remove-advertise
[mygpo.git] / mygpo / web / utils.py
blob599800b108e32b00f589258af210dfaf6b5836ed
1 import re
2 import math
3 import string
4 import collections
5 from datetime import datetime
7 from django.utils.translation import ngettext
8 from django.views.decorators.cache import never_cache
9 from django.utils.html import strip_tags
10 from django.urls 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" + (podcast.description or "")
136 return podcast
139 @never_cache
140 def maintenance(request, *args, **kwargs):
141 resp = render(request, "maintenance.html", {})
142 resp.status_code = 503
143 return resp
146 def get_podcast_link_target(podcast, view_name="podcast", add_args=[]):
147 """Returns the link-target for a Podcast, preferring slugs over Ids"""
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.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 """the link-target for a Podcast group, preferring slugs over Ids"""
164 args = [group.slug]
165 view_name = "%s-slug-id" % view_name
166 return reverse(view_name, args=args + add_args)
169 def get_episode_link_target(episode, podcast, view_name="episode", add_args=[]):
170 """Returns the link-target for an Episode, preferring slugs over Ids"""
172 # prefer slugs
173 if episode.slug:
174 args = [podcast.slug, episode.slug]
175 view_name = "%s-slug" % view_name
177 # fallback: UUIDs
178 else:
179 podcast = podcast or episode.podcast
180 args = [podcast.id, episode.id]
181 view_name = "%s-id" % view_name
183 return strip_tags(reverse(view_name, args=args + add_args))
186 # doesn't include the '@' because it's not stored as part of a twitter handle
187 TWITTER_CHARS = string.ascii_letters + string.digits + "_"
190 def normalize_twitter(s):
191 """normalize user input that is supposed to be a Twitter handle"""
192 return "".join(i for i in s if i in TWITTER_CHARS)
195 CCLICENSE = re.compile(
196 r"https?://(www\.)?creativecommons.org/licenses/([a-z-]+)/([0-9.]+)?/?"
198 CCPUBLICDOMAIN = re.compile(
199 r"https?://(www\.)?creativecommons.org/licenses/publicdomain/?"
201 LicenseInfo = collections.namedtuple("LicenseInfo", "name version url")
204 def license_info(license_url):
205 """Extracts license information from the license URL
207 >>> i = license_info('http://creativecommons.org/licenses/by/3.0/')
208 >>> i.name
209 'CC BY'
210 >>> i.version
211 '3.0'
212 >>> i.url
213 'http://creativecommons.org/licenses/by/3.0/'
215 >>> ihttps = license_info('https://creativecommons.org/licenses/by/3.0/')
216 >>> i.name == ihttps.name and i.version == ihttps.version
217 True
219 >>> iwww = license_info('http://www.creativecommons.org/licenses/by/3.0/')
220 >>> i.name == iwww.name and i.version == iwww.version
221 True
223 >>> iwww = license_info('https://www.creativecommons.org/licenses/by/3.0/')
224 >>> i.name == iwww.name and i.version == iwww.version
225 True
227 >>> i = license_info('http://www.creativecommons.org/licenses/publicdomain')
228 >>> i.name
229 'Public Domain'
230 >>> i.version is None
231 True
233 >>> ihttps = license_info('https://www.creativecommons.org/licenses/publicdomain')
234 >>> i.name == ihttps.name and i.version == ihttps.version
235 True
237 >>> i = license_info('http://example.com/my-own-license')
238 >>> i.name is None
239 True
240 >>> i.version is None
241 True
242 >>> i.url
243 'http://example.com/my-own-license'
245 m = CCLICENSE.match(license_url)
246 if m:
247 _, name, version = m.groups()
248 return LicenseInfo("CC %s" % name.upper(), version, license_url)
250 m = CCPUBLICDOMAIN.match(license_url)
251 if m:
252 return LicenseInfo("Public Domain", None, license_url)
254 return LicenseInfo(None, None, license_url)
257 def check_restrictions(obj):
258 """checks for known restrictions of the object"""
260 restrictions = obj.restrictions.split(",")
261 if "hide" in restrictions:
262 raise Http404
264 if "hide-author" in restrictions:
265 obj.author = None
267 return obj
270 def hours_to_str(hours_total):
271 """returns a human-readable string representation of some hours
273 >>> hours_to_str(1)
274 '1 hour'
276 >>> hours_to_str(5)
277 '5 hours'
279 >>> hours_to_str(100)
280 '4 days, 4 hours'
282 >>> hours_to_str(960)
283 '5 weeks, 5 days'
285 >>> hours_to_str(961)
286 '5 weeks, 5 days, 1 hour'
289 weeks = int(hours_total / 24 / 7)
290 days = int(hours_total / 24) % 7
291 hours = hours_total % 24
293 strs = []
295 if weeks:
296 strs.append(
297 ngettext("%(weeks)d week", "%(weeks)d weeks", weeks) % {"weeks": weeks}
300 if days:
301 strs.append(ngettext("%(days)d day", "%(days)d days", days) % {"days": days})
303 if hours:
304 strs.append(
305 ngettext("%(hours)d hour", "%(hours)d hours", hours) % {"hours": hours}
308 return ", ".join(strs)