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
):
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'])
46 >>> set(sanitize_language_codes(['de-at', 'en', 'en-gb', '(asdf', 'Deutsch'])) == {'de', 'en'}
50 ls
= [sanitize_language_code(l
) for l
in ls
if l
and RE_LANG
.match(l
)]
54 def get_language_names(lang
):
56 Takes a list of language codes and returns a list of tuples
63 except UnknownLocaleError
:
66 if locale
.display_name
:
67 res
[l
] = locale
.display_name
72 def get_page_list(start
, total
, cur
, show_max
):
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)
92 # if we get "total" as a float (eg from total_entries / entries_per_page)
94 total
= math
.ceil(total
)
96 if show_max
>= (total
- start
):
97 return list(range(start
, total
+ 1))
100 if (cur
- start
) > show_max
/ 2:
101 ps
.extend(list(range(start
, int(show_max
/ 4))))
103 ps
.extend(list(range(cur
- int(show_max
/ 4), cur
)))
106 ps
.extend(list(range(start
, 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
)))
115 ps
.extend(list(range(total
- int(show_max
/ 4), total
+ 1)))
118 ps
.extend(list(range(cur
+ 1, total
+ 1)))
123 def process_lang_params(request
):
125 lang
= request
.GET
.get("lang", 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 "")
140 def maintenance(request
, *args
, **kwargs
):
141 resp
= render(request
, "maintenance.html", {})
142 resp
.status_code
= 503
146 def get_podcast_link_target(podcast
, view_name
="podcast", add_args
=[]):
147 """Returns the link-target for a Podcast, preferring slugs over Ids"""
151 args
= [podcast
.slug
]
152 view_name
= "%s-slug" % view_name
154 # as a fallback we use UUIDs
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"""
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"""
174 args
= [podcast
.slug
, episode
.slug
]
175 view_name
= "%s-slug" % view_name
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/')
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
219 >>> iwww = license_info('http://www.creativecommons.org/licenses/by/3.0/')
220 >>> i.name == iwww.name and i.version == iwww.version
223 >>> iwww = license_info('https://www.creativecommons.org/licenses/by/3.0/')
224 >>> i.name == iwww.name and i.version == iwww.version
227 >>> i = license_info('http://www.creativecommons.org/licenses/publicdomain')
230 >>> i.version is None
233 >>> ihttps = license_info('https://www.creativecommons.org/licenses/publicdomain')
234 >>> i.name == ihttps.name and i.version == ihttps.version
237 >>> i = license_info('http://example.com/my-own-license')
240 >>> i.version is None
243 'http://example.com/my-own-license'
245 m
= CCLICENSE
.match(license_url
)
247 _
, name
, version
= m
.groups()
248 return LicenseInfo("CC %s" % name
.upper(), version
, license_url
)
250 m
= CCPUBLICDOMAIN
.match(license_url
)
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
:
264 if "hide-author" in restrictions
:
270 def hours_to_str(hours_total
):
271 """returns a human-readable string representation of some hours
279 >>> hours_to_str(100)
282 >>> hours_to_str(960)
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
297 ngettext("%(weeks)d week", "%(weeks)d weeks", weeks
) % {"weeks": weeks
}
301 strs
.append(ngettext("%(days)d day", "%(days)d days", days
) % {"days": days
})
305 ngettext("%(hours)d hour", "%(hours)d hours", hours
) % {"hours": hours
}
308 return ", ".join(strs
)