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
27 from babel
.localedata
import exists
30 from webob
import Response
, exc
31 from lxml
.html
.clean
import Cleaner
34 from mediagoblin
import mg_globals
35 from mediagoblin
.db
.util
import ObjectId
38 def _activate_testing():
40 Call this to activate testing in util.py
46 def clear_test_buckets():
48 We store some things for testing purposes that should be cleared
49 when we want a "clean slate" of information for our next round of
50 tests. Call this function to wipe all that stuff clean.
52 Also wipes out some other things we might redefine during testing,
55 global SETUP_JINJA_ENVS
58 global EMAIL_TEST_INBOX
59 global EMAIL_TEST_MBOX_INBOX
61 EMAIL_TEST_MBOX_INBOX
= []
63 clear_test_template_context()
66 def get_jinja_loader(user_template_path
=None):
68 Set up the Jinja template loaders, possibly allowing for user
71 (In the future we may have another system for providing theming;
72 for now this is good enough.)
74 if user_template_path
:
75 return jinja2
.ChoiceLoader(
76 [jinja2
.FileSystemLoader(user_template_path
),
77 jinja2
.PackageLoader('mediagoblin', 'templates')])
79 return jinja2
.PackageLoader('mediagoblin', 'templates')
85 def get_jinja_env(template_loader
, locale
):
87 Set up the Jinja environment,
89 (In the future we may have another system for providing theming;
90 for now this is good enough.)
94 # If we have a jinja environment set up with this locale, just
96 if SETUP_JINJA_ENVS
.has_key(locale
):
97 return SETUP_JINJA_ENVS
[locale
]
99 template_env
= jinja2
.Environment(
100 loader
=template_loader
, autoescape
=True,
101 extensions
=['jinja2.ext.i18n', 'jinja2.ext.autoescape'])
103 template_env
.install_gettext_callables(
104 mg_globals
.translations
.gettext
,
105 mg_globals
.translations
.ngettext
)
108 SETUP_JINJA_ENVS
[locale
] = template_env
113 # We'll store context information here when doing unit tests
114 TEMPLATE_TEST_CONTEXT
= {}
117 def render_template(request
, template_path
, context
):
119 Render a template with context.
121 Always inserts the request into the context, so you don't have to.
122 Also stores the context if we're doing unit tests. Helpful!
124 template
= request
.template_env
.get_template(
126 context
['request'] = request
127 rendered
= template
.render(context
)
130 TEMPLATE_TEST_CONTEXT
[template_path
] = context
135 def clear_test_template_context():
136 global TEMPLATE_TEST_CONTEXT
137 TEMPLATE_TEST_CONTEXT
= {}
140 def render_to_response(request
, template
, context
):
141 """Much like Django's shortcut.render()"""
142 return Response(render_template(request
, template
, context
))
145 def redirect(request
, *args
, **kwargs
):
146 """Returns a HTTPFound(), takes a request and then urlgen params"""
147 return exc
.HTTPFound(location
=request
.urlgen(*args
, **kwargs
))
150 def setup_user_in_request(request
):
152 Examine a request and tack on a request.user parameter if that's
155 if not request
.session
.has_key('user_id'):
160 user
= request
.app
.db
.User
.one(
161 {'_id': ObjectId(request
.session
['user_id'])})
164 # Something's wrong... this user doesn't exist? Invalidate
166 request
.session
.invalidate()
171 def import_component(import_string
):
173 Import a module component defined by STRING. Probably a method,
174 class, or global variable.
177 - import_string: a string that defines what to import. Written
178 in the format of "module1.module2:component"
180 module_name
, func_name
= import_string
.split(':', 1)
181 __import__(module_name
)
182 module
= sys
.modules
[module_name
]
183 func
= getattr(module
, func_name
)
186 _punct_re
= re
.compile(r
'[\t !"#$%&\'()*\
-/<=>?
@\
[\\\
]^_`
{|
},.]+')
188 def slugify(text, delim=u'-'):
190 Generates an ASCII-only slug. Taken from http://flask.pocoo.org/snippets/5/
193 for word in _punct_re.split(text.lower()):
194 word = word.encode('translit
/long')
197 return unicode(delim.join(result))
199 ### ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
200 ### Special email test stuff begins HERE
201 ### ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
203 # We have two "test inboxes" here:
207 # If you're writing test views
, you
'll probably want to check this.
208 # It contains a list of MIMEText messages.
210 # EMAIL_TEST_MBOX_INBOX:
211 # ----------------------
212 # This collects the messages from the FakeMhost inbox. It's reslly
213 # just here for testing the send_email method itself.
215 # Anyway this contains:
217 # - to: a list of email recipient addresses
218 # - message: not just the body, but the whole message, including
223 # Before running tests that call functions which send email, you should
224 # always call _clear_test_inboxes() to "wipe" the inboxes clean.
226 EMAIL_TEST_INBOX
= []
227 EMAIL_TEST_MBOX_INBOX
= []
230 class FakeMhost(object):
232 Just a fake mail host so we can capture and test messages
238 def sendmail(self
, from_addr
, to_addrs
, message
):
239 EMAIL_TEST_MBOX_INBOX
.append(
244 def _clear_test_inboxes():
245 global EMAIL_TEST_INBOX
246 global EMAIL_TEST_MBOX_INBOX
247 EMAIL_TEST_INBOX
= []
248 EMAIL_TEST_MBOX_INBOX
= []
250 ### ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
251 ### </Special email test stuff>
252 ### ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
254 def send_email(from_addr
, to_addrs
, subject
, message_body
):
256 Simple email sending wrapper, use this so we can capture messages
257 for unit testing purposes.
260 - from_addr: address you're sending the email from
261 - to_addrs: list of recipient email addresses
262 - subject: subject of the email
263 - message_body: email body text
265 # TODO: make a mock mhost if testing is enabled
266 if TESTS_ENABLED
or mg_globals
.email_debug_mode
:
268 elif not mg_globals
.email_debug_mode
:
269 mhost
= smtplib
.SMTP()
273 message
= MIMEText(message_body
.encode('utf-8'), 'plain', 'utf-8')
274 message
['Subject'] = subject
275 message
['From'] = from_addr
276 message
['To'] = ', '.join(to_addrs
)
279 EMAIL_TEST_INBOX
.append(message
)
281 if getattr(mg_globals
, 'email_debug_mode', False):
282 print u
"===== Email ====="
283 print u
"From address: %s" % message
['From']
284 print u
"To addresses: %s" % message
['To']
285 print u
"Subject: %s" % message
['Subject']
287 print message
.get_payload(decode
=True)
289 return mhost
.sendmail(from_addr
, to_addrs
, message
.as_string())
297 TRANSLATIONS_PATH
= pkg_resources
.resource_filename(
298 'mediagoblin', 'translations')
301 def locale_to_lower_upper(locale
):
303 Take a locale, regardless of style, and format it like "en-us"
306 lang
, country
= locale
.split('-', 1)
307 return '%s_%s' % (lang
.lower(), country
.upper())
309 lang
, country
= locale
.split('_', 1)
310 return '%s_%s' % (lang
.lower(), country
.upper())
312 return locale
.lower()
315 def locale_to_lower_lower(locale
):
317 Take a locale, regardless of style, and format it like "en_US"
320 lang
, country
= locale
.split('_', 1)
321 return '%s-%s' % (lang
.lower(), country
.lower())
323 return locale
.lower()
326 def get_locale_from_request(request
):
328 Figure out what target language is most appropriate based on the
331 request_form
= request
.GET
or request
.POST
333 if request_form
.has_key('lang'):
334 return locale_to_lower_upper(request_form
['lang'])
336 accept_lang_matches
= request
.accept_language
.best_matches()
338 # Your routing can explicitly specify a target language
339 if request
.matchdict
.has_key('locale'):
340 target_lang
= request
.matchdict
['locale']
341 elif request
.session
.has_key('target_lang'):
342 target_lang
= request
.session
['target_lang']
343 # Pull the first acceptable language
344 elif accept_lang_matches
:
345 target_lang
= accept_lang_matches
[0]
346 # Fall back to English
350 return locale_to_lower_upper(target_lang
)
353 # A super strict version of the lxml.html cleaner class
354 HTML_CLEANER
= Cleaner(
361 processing_instructions
=True,
367 'div', 'b', 'i', 'em', 'strong', 'p', 'ul', 'ol', 'li', 'a', 'br'],
368 remove_unknown_tags
=False, # can't be used with allow_tags
369 safe_attrs_only
=True,
370 add_nofollow
=True, # for now
372 whitelist_tags
=set([]))
375 def clean_html(html
):
376 return HTML_CLEANER
.clean_html(html
)
379 MARKDOWN_INSTANCE
= markdown
.Markdown(safe_mode
='escape')
382 def cleaned_markdown_conversion(text
):
384 Take a block of text, run it through MarkDown, and clean its HTML.
386 # Markdown will do nothing with and clean_html can do nothing with
391 return clean_html(MARKDOWN_INSTANCE
.convert(text
))
396 def setup_gettext(locale
):
398 Setup the gettext instance based on this locale
400 # Later on when we have plugins we may want to enable the
401 # multi-translations system they have so we can handle plugin
404 # TODO: fallback nicely on translations from pt_PT to pt if not
406 if SETUP_GETTEXTS
.has_key(locale
):
407 this_gettext
= SETUP_GETTEXTS
[locale
]
409 this_gettext
= gettext
.translation(
410 'mediagoblin', TRANSLATIONS_PATH
, [locale
], fallback
=True)
412 SETUP_GETTEXTS
[locale
] = this_gettext
414 mg_globals
.setup_globals(
415 translations
=this_gettext
)
418 PAGINATION_DEFAULT_PER_PAGE
= 30
420 class Pagination(object):
422 Pagination class for mongodb queries.
424 Initialization through __init__(self, cursor, page=1, per_page=2),
425 get actual data slice through __call__().
428 def __init__(self
, page
, cursor
, per_page
=PAGINATION_DEFAULT_PER_PAGE
):
430 Initializes Pagination
433 - page: requested page
434 - per_page: number of objects per page
438 self
.per_page
= per_page
440 self
.total_count
= self
.cursor
.count()
444 Returns slice of objects for the requested page
446 return self
.cursor
.skip(
447 (self
.page
- 1) * self
.per_page
).limit(self
.per_page
)
451 return int(ceil(self
.total_count
/ float(self
.per_page
)))
459 return self
.page
< self
.pages
461 def iter_pages(self
, left_edge
=2, left_current
=2,
462 right_current
=5, right_edge
=2):
464 for num
in xrange(1, self
.pages
+ 1):
465 if num
<= left_edge
or \
466 (num
> self
.page
- left_current
- 1 and \
467 num
< self
.page
+ right_current
) or \
468 num
> self
.pages
- right_edge
:
474 def get_page_url_explicit(self
, base_url
, get_params
, page_no
):
476 Get a page url by adding a page= parameter to the base url
478 new_get_params
= copy
.copy(get_params
or {})
479 new_get_params
['page'] = page_no
481 base_url
, urllib
.urlencode(new_get_params
))
483 def get_page_url(self
, request
, page_no
):
485 Get a new page url based of the request, and the new page number.
487 This is a nice wrapper around get_page_url_explicit()
489 return self
.get_page_url_explicit(
490 request
.path_info
, request
.GET
, page_no
)