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
):
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'])
47 >>> sanitize_language_codes(['de-at', 'en', 'en-gb', '(asdf', 'Deutsch'])
51 ls
= [sanitize_language_code(l
) for l
in ls
if l
and RE_LANG
.match(l
)]
55 def get_language_names(lang
):
57 Takes a list of language codes and returns a list of tuples
64 except UnknownLocaleError
:
67 if locale
.display_name
:
68 res
[l
] = locale
.display_name
73 def get_page_list(start
, total
, cur
, show_max
):
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)
90 if show_max
>= (total
- start
):
91 return range(start
, total
+1)
94 if (cur
- start
) > show_max
/ 2:
95 ps
.extend(range(start
, show_max
/ 4))
97 ps
.extend(range(cur
- show_max
/ 4, cur
))
100 ps
.extend(range(start
, 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
))
109 ps
.extend(range(total
- show_max
/ 4, total
+ 1))
112 ps
.extend(range(cur
+ 1, total
+ 1))
117 def process_lang_params(request
):
119 lang
= request
.GET
.get('lang', 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 '')
135 def maintenance(request
, *args
, **kwargs
):
136 resp
= render(request
, 'maintenance.html', {})
137 resp
.status_code
= 503
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
151 args
= [podcast
.slug
]
152 view_name
= '%s-slug-id' % view_name
154 # as a fallback we use CouchDB-IDs
156 args
= [podcast
.get_id()]
157 view_name
= '%s-slug-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
173 view_name
= '%s-slug-id' % view_name
175 # to keep URLs short, we use use oldids
179 # as a fallback we use CouchDB-IDs
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',
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
198 args
= [podcast
.slug
or podcast
.get_id(), episode
.slug
]
199 view_name
= '%s-slug-id' % view_name
201 # for short URLs, prefer oldids over CouchDB-IDs
203 args
= [episode
.oldid
]
205 # fallback: CouchDB-IDs
208 if isinstance(episode
.podcast
, Podcast
):
209 podcast
= episode
.podcast
210 elif isinstance(episode
.podcast
, basestring
):
211 podcast
= podcast_by_id(episode
.podcast
)
213 args
= [podcast
.slug
or podcast
.get_id(), episode
._id
]
214 view_name
= '%s-slug-id' % view_name
216 return strip_tags(reverse(view_name
, args
=args
+ add_args
))
219 def fetch_episode_data(episodes
, podcasts
={}):
222 podcast_ids
= [episode
.podcast
for episode
in episodes
]
223 podcasts
= podcasts_to_dict(podcast_ids
)
225 def set_podcast(episode
):
226 episode
= proxy_object(episode
)
227 episode
.podcast
= podcasts
.get(episode
.podcast
, None)
230 return map(set_podcast
, episodes
)
233 # doesn't include the '@' because it's not stored as part of a twitter handle
234 TWITTER_CHARS
= string
.ascii_letters
+ string
.digits
+ '_'
237 def normalize_twitter(s
):
238 """ normalize user input that is supposed to be a Twitter handle """
239 return "".join(i
for i
in s
if i
in TWITTER_CHARS
)
242 CCLICENSE
= re
.compile(r
'http://(www\.)?creativecommons.org/licenses/([a-z-]+)/([0-9.]+)?/?')
243 CCPUBLICDOMAIN
= re
.compile(r
'http://(www\.)?creativecommons.org/licenses/publicdomain/?')
244 LicenseInfo
= collections
.namedtuple('LicenseInfo', 'name version url')
246 def license_info(license_url
):
247 """ Extracts license information from the license URL
249 >>> i = license_info('http://creativecommons.org/licenses/by/3.0/')
255 'http://creativecommons.org/licenses/by/3.0/'
257 >>> iwww = license_info('http://www.creativecommons.org/licenses/by/3.0/')
258 >>> i.name == iwww.name and i.version == iwww.version
261 >>> i = license_info('http://www.creativecommons.org/licenses/publicdomain')
264 >>> i.version is None
267 >>> i = license_info('http://example.com/my-own-license')
270 >>> i.version is None
273 'http://example.com/my-own-license'
275 m
= CCLICENSE
.match(license_url
)
277 _
, name
, version
= m
.groups()
278 return LicenseInfo('CC %s' % name
.upper(), version
, license_url
)
280 m
= CCPUBLICDOMAIN
.match(license_url
)
282 return LicenseInfo('Public Domain', None, license_url
)
284 return LicenseInfo(None, None, license_url
)
287 def check_restrictions(obj
):
288 """ checks for known restrictions of the object """
289 if "hide" in obj
.restrictions
:
292 if "hide-author" in obj
.restrictions
:
298 def hours_to_str(hours_total
):
299 """ returns a human-readable string representation of some hours
307 >>> hours_to_str(100)
310 >>> hours_to_str(960)
313 >>> hours_to_str(961)
314 u'5 weeks, 5 days, 1 hour'
317 weeks
= hours_total
/ 24 / 7
318 days
= hours_total
/ 24 % 7
319 hours
= hours_total
% 24
324 strs
.append(ungettext('%(weeks)d week', '%(weeks)d weeks', weeks
) %
328 strs
.append(ungettext('%(days)d day', '%(days)d days', days
) %
332 strs
.append(ungettext('%(hours)d hour', '%(hours)d hours', hours
) %
335 return ', '.join(strs
)