attribute values should be quoted :)
[mediagoblin.git] / mediagoblin / util.py
CommitLineData
8e1e744d 1# GNU MediaGoblin -- federated, autonomous media hosting
e5572c60
ML
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
4d4f6050 17from email.MIMEText import MIMEText
b77eec65
CAW
18import gettext
19import pkg_resources
4d4f6050 20import smtplib
cb8ea0fe 21import sys
0546833c 22import re
31a8ff42 23import jinja2
254bc431 24from mediagoblin.db.util import ObjectId
0546833c 25import translitcodec
31a8ff42 26
29f3fb70
CAW
27from mediagoblin import globals as mgoblin_globals
28
ae85ed0f 29import urllib
ae85ed0f 30from math import ceil
44e3e917 31import copy
4d4f6050
CAW
32
33TESTS_ENABLED = False
34def _activate_testing():
35 """
36 Call this to activate testing in util.py
37 """
38 global TESTS_ENABLED
39 TESTS_ENABLED = True
40
41
0e0e3d9a 42def get_jinja_loader(user_template_path=None):
904f61c2 43 """
0e0e3d9a 44 Set up the Jinja template loaders, possibly allowing for user
904f61c2
CAW
45 overridden templates.
46
47 (In the future we may have another system for providing theming;
48 for now this is good enough.)
49 """
31a8ff42 50 if user_template_path:
0e0e3d9a 51 return jinja2.ChoiceLoader(
31a8ff42
CAW
52 [jinja2.FileSystemLoader(user_template_path),
53 jinja2.PackageLoader('mediagoblin', 'templates')])
54 else:
0e0e3d9a 55 return jinja2.PackageLoader('mediagoblin', 'templates')
31a8ff42 56
0e0e3d9a
CAW
57
58def 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 """
b77eec65
CAW
65 setup_gettext(locale)
66
67 template_env = jinja2.Environment(
0e0e3d9a 68 loader=template_loader, autoescape=True,
20c834ff 69 extensions=['jinja2.ext.i18n'])
58dec5ef 70
b77eec65
CAW
71 template_env.install_gettext_callables(
72 mgoblin_globals.translations.gettext,
73 mgoblin_globals.translations.ngettext)
74
75 return template_env
76
58dec5ef
CAW
77
78def 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'):
59dd5c7e 84 request.user = None
58dec5ef
CAW
85 return
86
5d6840a0 87 user = None
6648c52b 88 user = request.app.db.User.one(
254bc431 89 {'_id': ObjectId(request.session['user_id'])})
5d6840a0 90
c74e1462
CAW
91 if not user:
92 # Something's wrong... this user doesn't exist? Invalidate
93 # this session.
58dec5ef 94 request.session.invalidate()
5d6840a0
CAW
95
96 request.user = user
cb8ea0fe
CAW
97
98
99def 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
4d4f6050 113
0546833c
AW
114_punct_re = re.compile(r'[\t !"#$%&\'()*\-/<=>?@\[\\\]^_`{|},.]+')
115
116def 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))
4d4f6050
CAW
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
154EMAIL_TEST_INBOX = []
155EMAIL_TEST_MBOX_INBOX = []
156
157
158class 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
172def _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
182def send_email(from_addr, to_addrs, subject, message_body):
61ec968b
CAW
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 """
4d4f6050 193 # TODO: make a mock mhost if testing is enabled
29f3fb70 194 if TESTS_ENABLED or mgoblin_globals.email_debug_mode:
4d4f6050 195 mhost = FakeMhost()
29f3fb70 196 elif not mgoblin_globals.email_debug_mode:
4d4f6050
CAW
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
21919313 209 if getattr(mgoblin_globals, 'email_debug_mode', False):
29f3fb70
CAW
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
21919313 217 return mhost.sendmail(from_addr, to_addrs, message.as_string())
20c834ff 218
8b28bee4
CAW
219
220###################
221# Translation tools
222###################
223
224
b77eec65
CAW
225TRANSLATIONS_PATH = pkg_resources.resource_filename(
226 'mediagoblin', 'translations')
227
228
8b28bee4
CAW
229def 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
243def 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
254def 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
376e6ef2
CAW
267 if request.matchdict.has_key('locale'):
268 target_lang = request.matchdict['locale']
8b28bee4
CAW
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
0e0e3d9a 278 return locale_to_lower_upper(target_lang)
b77eec65
CAW
279
280
281def 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)
ae85ed0f
BK
296
297
b9e9610b
CAW
298PAGINATION_DEFAULT_PER_PAGE = 30
299
ae85ed0f
BK
300class Pagination(object):
301 """
dffa0b09
CAW
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__().
ae85ed0f 306 """
ca3ca51c 307
b9e9610b 308 def __init__(self, page, cursor, per_page=PAGINATION_DEFAULT_PER_PAGE):
44e3e917 309 """
a98d5254
CAW
310 Initializes Pagination
311
312 Args:
313 - page: requested page
314 - per_page: number of objects per page
315 - cursor: db cursor
44e3e917
BK
316 """
317 self.page = page
ca3ca51c
BK
318 self.per_page = per_page
319 self.cursor = cursor
ca3ca51c
BK
320 self.total_count = self.cursor.count()
321
322 def __call__(self):
44e3e917 323 """
a98d5254 324 Returns slice of objects for the requested page
44e3e917 325 """
140e2102
CAW
326 return self.cursor.skip(
327 (self.page - 1) * self.per_page).limit(self.per_page)
ae85ed0f
BK
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
44e3e917 353
50c880ac 354 def get_page_url_explicit(self, base_url, get_params, page_no):
44e3e917 355 """
50c880ac 356 Get a page url by adding a page= parameter to the base url
44e3e917
BK
357 """
358 new_get_params = copy.copy(get_params or {})
359 new_get_params['page'] = page_no
360 return "%s?%s" % (
50c880ac
CAW
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)