First changes to media gallery view
[mediagoblin.git] / mediagoblin / util.py
1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011 Free Software Foundation, Inc
3 #
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU Affero General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17 from email.MIMEText import MIMEText
18 import gettext
19 import pkg_resources
20 import smtplib
21 import sys
22 import re
23 import jinja2
24 from mediagoblin.db.util import ObjectId
25 import translitcodec
26
27 from mediagoblin import globals as mgoblin_globals
28
29 import urllib
30 from math import ceil
31 import copy
32
33 TESTS_ENABLED = False
34 def _activate_testing():
35 """
36 Call this to activate testing in util.py
37 """
38 global TESTS_ENABLED
39 TESTS_ENABLED = True
40
41
42 def get_jinja_loader(user_template_path=None):
43 """
44 Set up the Jinja template loaders, possibly allowing for user
45 overridden templates.
46
47 (In the future we may have another system for providing theming;
48 for now this is good enough.)
49 """
50 if user_template_path:
51 return jinja2.ChoiceLoader(
52 [jinja2.FileSystemLoader(user_template_path),
53 jinja2.PackageLoader('mediagoblin', 'templates')])
54 else:
55 return jinja2.PackageLoader('mediagoblin', 'templates')
56
57
58 def get_jinja_env(template_loader, locale):
59 """
60 Set up the Jinja environment,
61
62 (In the future we may have another system for providing theming;
63 for now this is good enough.)
64 """
65 setup_gettext(locale)
66
67 template_env = jinja2.Environment(
68 loader=template_loader, autoescape=True,
69 extensions=['jinja2.ext.i18n'])
70
71 template_env.install_gettext_callables(
72 mgoblin_globals.translations.gettext,
73 mgoblin_globals.translations.ngettext)
74
75 return template_env
76
77
78 def setup_user_in_request(request):
79 """
80 Examine a request and tack on a request.user parameter if that's
81 appropriate.
82 """
83 if not request.session.has_key('user_id'):
84 request.user = None
85 return
86
87 user = None
88 user = request.app.db.User.one(
89 {'_id': ObjectId(request.session['user_id'])})
90
91 if not user:
92 # Something's wrong... this user doesn't exist? Invalidate
93 # this session.
94 request.session.invalidate()
95
96 request.user = user
97
98
99 def import_component(import_string):
100 """
101 Import a module component defined by STRING. Probably a method,
102 class, or global variable.
103
104 Args:
105 - import_string: a string that defines what to import. Written
106 in the format of "module1.module2:component"
107 """
108 module_name, func_name = import_string.split(':', 1)
109 __import__(module_name)
110 module = sys.modules[module_name]
111 func = getattr(module, func_name)
112 return func
113
114 _punct_re = re.compile(r'[\t !"#$%&\'()*\-/<=>?@\[\\\]^_`{|},.]+')
115
116 def slugify(text, delim=u'-'):
117 """
118 Generates an ASCII-only slug. Taken from http://flask.pocoo.org/snippets/5/
119 """
120 result = []
121 for word in _punct_re.split(text.lower()):
122 word = word.encode('translit/long')
123 if word:
124 result.append(word)
125 return unicode(delim.join(result))
126
127 ### ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
128 ### Special email test stuff begins HERE
129 ### ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
130
131 # We have two "test inboxes" here:
132 #
133 # EMAIL_TEST_INBOX:
134 # ----------------
135 # If you're writing test views, you'll probably want to check this.
136 # It contains a list of MIMEText messages.
137 #
138 # EMAIL_TEST_MBOX_INBOX:
139 # ----------------------
140 # This collects the messages from the FakeMhost inbox. It's reslly
141 # just here for testing the send_email method itself.
142 #
143 # Anyway this contains:
144 # - from
145 # - to: a list of email recipient addresses
146 # - message: not just the body, but the whole message, including
147 # headers, etc.
148 #
149 # ***IMPORTANT!***
150 # ----------------
151 # Before running tests that call functions which send email, you should
152 # always call _clear_test_inboxes() to "wipe" the inboxes clean.
153
154 EMAIL_TEST_INBOX = []
155 EMAIL_TEST_MBOX_INBOX = []
156
157
158 class FakeMhost(object):
159 """
160 Just a fake mail host so we can capture and test messages
161 from send_email
162 """
163 def connect(self):
164 pass
165
166 def sendmail(self, from_addr, to_addrs, message):
167 EMAIL_TEST_MBOX_INBOX.append(
168 {'from': from_addr,
169 'to': to_addrs,
170 'message': message})
171
172 def _clear_test_inboxes():
173 global EMAIL_TEST_INBOX
174 global EMAIL_TEST_MBOX_INBOX
175 EMAIL_TEST_INBOX = []
176 EMAIL_TEST_MBOX_INBOX = []
177
178 ### ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
179 ### </Special email test stuff>
180 ### ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
181
182 def send_email(from_addr, to_addrs, subject, message_body):
183 """
184 Simple email sending wrapper, use this so we can capture messages
185 for unit testing purposes.
186
187 Args:
188 - from_addr: address you're sending the email from
189 - to_addrs: list of recipient email addresses
190 - subject: subject of the email
191 - message_body: email body text
192 """
193 # TODO: make a mock mhost if testing is enabled
194 if TESTS_ENABLED or mgoblin_globals.email_debug_mode:
195 mhost = FakeMhost()
196 elif not mgoblin_globals.email_debug_mode:
197 mhost = smtplib.SMTP()
198
199 mhost.connect()
200
201 message = MIMEText(message_body.encode('utf-8'), 'plain', 'utf-8')
202 message['Subject'] = subject
203 message['From'] = from_addr
204 message['To'] = ', '.join(to_addrs)
205
206 if TESTS_ENABLED:
207 EMAIL_TEST_INBOX.append(message)
208
209 if getattr(mgoblin_globals, 'email_debug_mode', False):
210 print u"===== Email ====="
211 print u"From address: %s" % message['From']
212 print u"To addresses: %s" % message['To']
213 print u"Subject: %s" % message['Subject']
214 print u"-- Body: --"
215 print message.get_payload(decode=True)
216
217 return mhost.sendmail(from_addr, to_addrs, message.as_string())
218
219
220 ###################
221 # Translation tools
222 ###################
223
224
225 TRANSLATIONS_PATH = pkg_resources.resource_filename(
226 'mediagoblin', 'translations')
227
228
229 def locale_to_lower_upper(locale):
230 """
231 Take a locale, regardless of style, and format it like "en-us"
232 """
233 if '-' in locale:
234 lang, country = locale.split('-', 1)
235 return '%s_%s' % (lang.lower(), country.upper())
236 elif '_' in locale:
237 lang, country = locale.split('_', 1)
238 return '%s_%s' % (lang.lower(), country.upper())
239 else:
240 return locale.lower()
241
242
243 def locale_to_lower_lower(locale):
244 """
245 Take a locale, regardless of style, and format it like "en_US"
246 """
247 if '_' in locale:
248 lang, country = locale.split('_', 1)
249 return '%s-%s' % (lang.lower(), country.lower())
250 else:
251 return locale.lower()
252
253
254 def get_locale_from_request(request):
255 """
256 Figure out what target language is most appropriate based on the
257 request
258 """
259 request_form = request.GET or request.POST
260
261 if request_form.has_key('lang'):
262 return locale_to_lower_upper(request_form['lang'])
263
264 accept_lang_matches = request.accept_language.best_matches()
265
266 # Your routing can explicitly specify a target language
267 if request.matchdict.has_key('locale'):
268 target_lang = request.matchdict['locale']
269 elif request.session.has_key('target_lang'):
270 target_lang = request.session['target_lang']
271 # Pull the first acceptable language
272 elif accept_lang_matches:
273 target_lang = accept_lang_matches[0]
274 # Fall back to English
275 else:
276 target_lang = 'en'
277
278 return locale_to_lower_upper(target_lang)
279
280
281 def setup_gettext(locale):
282 """
283 Setup the gettext instance based on this locale
284 """
285 # Later on when we have plugins we may want to enable the
286 # multi-translations system they have so we can handle plugin
287 # translations too
288
289 # TODO: fallback nicely on translations from pt_PT to pt if not
290 # available, etc.
291 this_gettext = gettext.translation(
292 'mediagoblin', TRANSLATIONS_PATH, [locale], fallback=True)
293
294 mgoblin_globals.setup_globals(
295 translations=this_gettext)
296
297
298 PAGINATION_DEFAULT_PER_PAGE = 30
299
300 class Pagination(object):
301 """
302 Pagination class for mongodb queries.
303
304 Initialization through __init__(self, cursor, page=1, per_page=2),
305 get actual data slice through __call__().
306 """
307
308 def __init__(self, page, cursor, per_page=PAGINATION_DEFAULT_PER_PAGE):
309 """
310 Initializes Pagination
311
312 Args:
313 - page: requested page
314 - per_page: number of objects per page
315 - cursor: db cursor
316 """
317 self.page = page
318 self.per_page = per_page
319 self.cursor = cursor
320 self.total_count = self.cursor.count()
321
322 def __call__(self):
323 """
324 Returns slice of objects for the requested page
325 """
326 return self.cursor.skip(
327 (self.page - 1) * self.per_page).limit(self.per_page)
328
329 @property
330 def pages(self):
331 return int(ceil(self.total_count / float(self.per_page)))
332
333 @property
334 def has_prev(self):
335 return self.page > 1
336
337 @property
338 def has_next(self):
339 return self.page < self.pages
340
341 def iter_pages(self, left_edge=2, left_current=2,
342 right_current=5, right_edge=2):
343 last = 0
344 for num in xrange(1, self.pages + 1):
345 if num <= left_edge or \
346 (num > self.page - left_current - 1 and \
347 num < self.page + right_current) or \
348 num > self.pages - right_edge:
349 if last + 1 != num:
350 yield None
351 yield num
352 last = num
353
354 def get_page_url_explicit(self, base_url, get_params, page_no):
355 """
356 Get a page url by adding a page= parameter to the base url
357 """
358 new_get_params = copy.copy(get_params or {})
359 new_get_params['page'] = page_no
360 return "%s?%s" % (
361 base_url, urllib.urlencode(new_get_params))
362
363 def get_page_url(self, request, page_no):
364 """
365 Get a new page url based of the request, and the new page number.
366
367 This is a nice wrapper around get_page_url_explicit()
368 """
369 return self.get_page_url_explicit(
370 request.path_info, request.GET, page_no)