1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011 Free Software Foundation, Inc
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.
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.
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/>.
17 from email
.MIMEText
import MIMEText
28 from babel
.localedata
import exists
31 from paste
.deploy
.loadwsgi
import NicerConfigParser
32 from webob
import Response
, exc
33 from lxml
.html
.clean
import Cleaner
35 from mediagoblin
import mg_globals
36 from mediagoblin
.db
.util
import ObjectId
39 def _activate_testing():
41 Call this to activate testing in util.py
47 def clear_test_buckets():
49 We store some things for testing purposes that should be cleared
50 when we want a "clean slate" of information for our next round of
51 tests. Call this function to wipe all that stuff clean.
53 Also wipes out some other things we might redefine during testing,
56 global SETUP_JINJA_ENVS
59 global EMAIL_TEST_INBOX
60 global EMAIL_TEST_MBOX_INBOX
62 EMAIL_TEST_MBOX_INBOX
= []
64 clear_test_template_context()
67 def get_jinja_loader(user_template_path
=None):
69 Set up the Jinja template loaders, possibly allowing for user
72 (In the future we may have another system for providing theming;
73 for now this is good enough.)
75 if user_template_path
:
76 return jinja2
.ChoiceLoader(
77 [jinja2
.FileSystemLoader(user_template_path
),
78 jinja2
.PackageLoader('mediagoblin', 'templates')])
80 return jinja2
.PackageLoader('mediagoblin', 'templates')
86 def get_jinja_env(template_loader
, locale
):
88 Set up the Jinja environment,
90 (In the future we may have another system for providing theming;
91 for now this is good enough.)
95 # If we have a jinja environment set up with this locale, just
97 if SETUP_JINJA_ENVS
.has_key(locale
):
98 return SETUP_JINJA_ENVS
[locale
]
100 template_env
= jinja2
.Environment(
101 loader
=template_loader
, autoescape
=True,
102 extensions
=['jinja2.ext.i18n', 'jinja2.ext.autoescape'])
104 template_env
.install_gettext_callables(
105 mg_globals
.translations
.gettext
,
106 mg_globals
.translations
.ngettext
)
109 SETUP_JINJA_ENVS
[locale
] = template_env
114 # We'll store context information here when doing unit tests
115 TEMPLATE_TEST_CONTEXT
= {}
118 def render_template(request
, template_path
, context
):
120 Render a template with context.
122 Always inserts the request into the context, so you don't have to.
123 Also stores the context if we're doing unit tests. Helpful!
125 template
= request
.template_env
.get_template(
127 context
['request'] = request
128 rendered
= template
.render(context
)
131 TEMPLATE_TEST_CONTEXT
[template_path
] = context
136 def clear_test_template_context():
137 global TEMPLATE_TEST_CONTEXT
138 TEMPLATE_TEST_CONTEXT
= {}
141 def render_to_response(request
, template
, context
):
142 """Much like Django's shortcut.render()"""
143 return Response(render_template(request
, template
, context
))
146 def redirect(request
, *args
, **kwargs
):
147 """Returns a HTTPFound(), takes a request and then urlgen params"""
148 return exc
.HTTPFound(location
=request
.urlgen(*args
, **kwargs
))
151 def setup_user_in_request(request
):
153 Examine a request and tack on a request.user parameter if that's
156 if not request
.session
.has_key('user_id'):
161 user
= request
.app
.db
.User
.one(
162 {'_id': ObjectId(request
.session
['user_id'])})
165 # Something's wrong... this user doesn't exist? Invalidate
167 request
.session
.invalidate()
172 def import_component(import_string
):
174 Import a module component defined by STRING. Probably a method,
175 class, or global variable.
178 - import_string: a string that defines what to import. Written
179 in the format of "module1.module2:component"
181 module_name
, func_name
= import_string
.split(':', 1)
182 __import__(module_name
)
183 module
= sys
.modules
[module_name
]
184 func
= getattr(module
, func_name
)
187 _punct_re
= re
.compile(r
'[\t !"#$%&\'()*\
-/<=>?
@\
[\\\
]^_`
{|
},.]+')
189 def slugify(text, delim=u'-'):
191 Generates an ASCII-only slug. Taken from http://flask.pocoo.org/snippets/5/
194 for word in _punct_re.split(text.lower()):
195 word = word.encode('translit
/long')
198 return unicode(delim.join(result))
200 ### ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
201 ### Special email test stuff begins HERE
202 ### ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
204 # We have two "test inboxes" here:
208 # If you're writing test views
, you
'll probably want to check this.
209 # It contains a list of MIMEText messages.
211 # EMAIL_TEST_MBOX_INBOX:
212 # ----------------------
213 # This collects the messages from the FakeMhost inbox. It's reslly
214 # just here for testing the send_email method itself.
216 # Anyway this contains:
218 # - to: a list of email recipient addresses
219 # - message: not just the body, but the whole message, including
224 # Before running tests that call functions which send email, you should
225 # always call _clear_test_inboxes() to "wipe" the inboxes clean.
227 EMAIL_TEST_INBOX
= []
228 EMAIL_TEST_MBOX_INBOX
= []
231 class FakeMhost(object):
233 Just a fake mail host so we can capture and test messages
239 def sendmail(self
, from_addr
, to_addrs
, message
):
240 EMAIL_TEST_MBOX_INBOX
.append(
245 def _clear_test_inboxes():
246 global EMAIL_TEST_INBOX
247 global EMAIL_TEST_MBOX_INBOX
248 EMAIL_TEST_INBOX
= []
249 EMAIL_TEST_MBOX_INBOX
= []
251 ### ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
252 ### </Special email test stuff>
253 ### ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
255 def send_email(from_addr
, to_addrs
, subject
, message_body
):
257 Simple email sending wrapper, use this so we can capture messages
258 for unit testing purposes.
261 - from_addr: address you're sending the email from
262 - to_addrs: list of recipient email addresses
263 - subject: subject of the email
264 - message_body: email body text
266 # TODO: make a mock mhost if testing is enabled
267 if TESTS_ENABLED
or mg_globals
.email_debug_mode
:
269 elif not mg_globals
.email_debug_mode
:
270 mhost
= smtplib
.SMTP()
274 message
= MIMEText(message_body
.encode('utf-8'), 'plain', 'utf-8')
275 message
['Subject'] = subject
276 message
['From'] = from_addr
277 message
['To'] = ', '.join(to_addrs
)
280 EMAIL_TEST_INBOX
.append(message
)
282 if getattr(mg_globals
, 'email_debug_mode', False):
283 print u
"===== Email ====="
284 print u
"From address: %s" % message
['From']
285 print u
"To addresses: %s" % message
['To']
286 print u
"Subject: %s" % message
['Subject']
288 print message
.get_payload(decode
=True)
290 return mhost
.sendmail(from_addr
, to_addrs
, message
.as_string())
298 TRANSLATIONS_PATH
= pkg_resources
.resource_filename(
299 'mediagoblin', 'translations')
302 def locale_to_lower_upper(locale
):
304 Take a locale, regardless of style, and format it like "en-us"
307 lang
, country
= locale
.split('-', 1)
308 return '%s_%s' % (lang
.lower(), country
.upper())
310 lang
, country
= locale
.split('_', 1)
311 return '%s_%s' % (lang
.lower(), country
.upper())
313 return locale
.lower()
316 def locale_to_lower_lower(locale
):
318 Take a locale, regardless of style, and format it like "en_US"
321 lang
, country
= locale
.split('_', 1)
322 return '%s-%s' % (lang
.lower(), country
.lower())
324 return locale
.lower()
327 def get_locale_from_request(request
):
329 Figure out what target language is most appropriate based on the
332 request_form
= request
.GET
or request
.POST
334 if request_form
.has_key('lang'):
335 return locale_to_lower_upper(request_form
['lang'])
337 accept_lang_matches
= request
.accept_language
.best_matches()
339 # Your routing can explicitly specify a target language
340 if request
.matchdict
.has_key('locale'):
341 target_lang
= request
.matchdict
['locale']
342 elif request
.session
.has_key('target_lang'):
343 target_lang
= request
.session
['target_lang']
344 # Pull the first acceptable language
345 elif accept_lang_matches
:
346 target_lang
= accept_lang_matches
[0]
347 # Fall back to English
351 return locale_to_lower_upper(target_lang
)
354 def read_config_file(conf_file
):
356 Read a paste deploy style config file and process it.
358 if not os
.path
.exists(conf_file
):
360 "MEDIAGOBLIN_CONFIG not set or file does not exist")
362 parser
= NicerConfigParser(conf_file
)
363 parser
.read(conf_file
)
364 parser
._defaults
.setdefault(
365 'here', os
.path
.dirname(os
.path
.abspath(conf_file
)))
366 parser
._defaults
.setdefault(
367 '__file__', os
.path
.abspath(conf_file
))
370 [(section_name
, dict(parser
.items(section_name
)))
371 for section_name
in parser
.sections()])
376 # A super strict version of the lxml.html cleaner class
377 HTML_CLEANER
= Cleaner(
384 processing_instructions
=True,
390 'div', 'b', 'i', 'em', 'strong', 'p', 'ul', 'ol', 'li', 'a', 'br'],
391 remove_unknown_tags
=False, # can't be used with allow_tags
392 safe_attrs_only
=True,
393 add_nofollow
=True, # for now
395 whitelist_tags
=set([]))
398 def clean_html(html
):
399 return HTML_CLEANER
.clean_html(html
)
404 def setup_gettext(locale
):
406 Setup the gettext instance based on this locale
408 # Later on when we have plugins we may want to enable the
409 # multi-translations system they have so we can handle plugin
412 # TODO: fallback nicely on translations from pt_PT to pt if not
414 if SETUP_GETTEXTS
.has_key(locale
):
415 this_gettext
= SETUP_GETTEXTS
[locale
]
417 this_gettext
= gettext
.translation(
418 'mediagoblin', TRANSLATIONS_PATH
, [locale
], fallback
=True)
420 SETUP_GETTEXTS
[locale
] = this_gettext
422 mg_globals
.setup_globals(
423 translations
=this_gettext
)
426 PAGINATION_DEFAULT_PER_PAGE
= 30
428 class Pagination(object):
430 Pagination class for mongodb queries.
432 Initialization through __init__(self, cursor, page=1, per_page=2),
433 get actual data slice through __call__().
436 def __init__(self
, page
, cursor
, per_page
=PAGINATION_DEFAULT_PER_PAGE
):
438 Initializes Pagination
441 - page: requested page
442 - per_page: number of objects per page
446 self
.per_page
= per_page
448 self
.total_count
= self
.cursor
.count()
452 Returns slice of objects for the requested page
454 return self
.cursor
.skip(
455 (self
.page
- 1) * self
.per_page
).limit(self
.per_page
)
459 return int(ceil(self
.total_count
/ float(self
.per_page
)))
467 return self
.page
< self
.pages
469 def iter_pages(self
, left_edge
=2, left_current
=2,
470 right_current
=5, right_edge
=2):
472 for num
in xrange(1, self
.pages
+ 1):
473 if num
<= left_edge
or \
474 (num
> self
.page
- left_current
- 1 and \
475 num
< self
.page
+ right_current
) or \
476 num
> self
.pages
- right_edge
:
482 def get_page_url_explicit(self
, base_url
, get_params
, page_no
):
484 Get a page url by adding a page= parameter to the base url
486 new_get_params
= copy
.copy(get_params
or {})
487 new_get_params
['page'] = page_no
489 base_url
, urllib
.urlencode(new_get_params
))
491 def get_page_url(self
, request
, page_no
):
493 Get a new page url based of the request, and the new page number.
495 This is a nice wrapper around get_page_url_explicit()
497 return self
.get_page_url_explicit(
498 request
.path_info
, request
.GET
, page_no
)