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 __future__
import division
19 from email
.MIMEText
import MIMEText
26 from math
import ceil
, floor
30 from babel
.localedata
import exists
31 from babel
.support
import LazyProxy
34 from webob
import Response
, exc
35 from lxml
.html
.clean
import Cleaner
37 from wtforms
.form
import Form
39 from mediagoblin
import mg_globals
40 from mediagoblin
import messages
41 from mediagoblin
.db
.util
import ObjectId
43 from itertools
import izip
, count
45 DISPLAY_IMAGE_FETCHING_ORDER
= [u
'medium', u
'original', u
'thumb']
48 def _activate_testing():
50 Call this to activate testing in util.py
56 def clear_test_buckets():
58 We store some things for testing purposes that should be cleared
59 when we want a "clean slate" of information for our next round of
60 tests. Call this function to wipe all that stuff clean.
62 Also wipes out some other things we might redefine during testing,
65 global SETUP_JINJA_ENVS
68 global EMAIL_TEST_INBOX
69 global EMAIL_TEST_MBOX_INBOX
71 EMAIL_TEST_MBOX_INBOX
= []
73 clear_test_template_context()
79 def get_jinja_env(template_loader
, locale
):
81 Set up the Jinja environment,
83 (In the future we may have another system for providing theming;
84 for now this is good enough.)
88 # If we have a jinja environment set up with this locale, just
90 if SETUP_JINJA_ENVS
.has_key(locale
):
91 return SETUP_JINJA_ENVS
[locale
]
93 template_env
= jinja2
.Environment(
94 loader
=template_loader
, autoescape
=True,
95 extensions
=['jinja2.ext.i18n', 'jinja2.ext.autoescape'])
97 template_env
.install_gettext_callables(
98 mg_globals
.translations
.ugettext
,
99 mg_globals
.translations
.ungettext
)
101 # All templates will know how to ...
102 # ... fetch all waiting messages and remove them from the queue
103 # ... construct a grid of thumbnails or other media
104 template_env
.globals['fetch_messages'] = messages
.fetch_messages
105 template_env
.globals['gridify_list'] = gridify_list
106 template_env
.globals['gridify_cursor'] = gridify_cursor
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
, status
=200):
142 """Much like Django's shortcut.render()"""
144 render_template(request
, template
, context
),
148 def redirect(request
, *args
, **kwargs
):
149 """Returns a HTTPFound(), takes a request and then urlgen params"""
152 if kwargs
.get('querystring'):
153 querystring
= kwargs
.get('querystring')
154 del kwargs
['querystring']
156 return exc
.HTTPFound(
158 request
.urlgen(*args
, **kwargs
),
159 querystring
if querystring
else '']))
162 def setup_user_in_request(request
):
164 Examine a request and tack on a request.user parameter if that's
167 if not request
.session
.has_key('user_id'):
172 user
= request
.app
.db
.User
.one(
173 {'_id': ObjectId(request
.session
['user_id'])})
176 # Something's wrong... this user doesn't exist? Invalidate
178 request
.session
.invalidate()
183 def import_component(import_string
):
185 Import a module component defined by STRING. Probably a method,
186 class, or global variable.
189 - import_string: a string that defines what to import. Written
190 in the format of "module1.module2:component"
192 module_name
, func_name
= import_string
.split(':', 1)
193 __import__(module_name
)
194 module
= sys
.modules
[module_name
]
195 func
= getattr(module
, func_name
)
198 _punct_re
= re
.compile(r
'[\t !"#$%&\'()*\
-/<=>?
@\
[\\\
]^_`
{|
},.]+')
200 def slugify(text, delim=u'-'):
202 Generates an ASCII-only slug. Taken from http://flask.pocoo.org/snippets/5/
205 for word in _punct_re.split(text.lower()):
206 word = word.encode('translit
/long')
209 return unicode(delim.join(result))
211 ### ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
212 ### Special email test stuff begins HERE
213 ### ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
215 # We have two "test inboxes" here:
219 # If you're writing test views
, you
'll probably want to check this.
220 # It contains a list of MIMEText messages.
222 # EMAIL_TEST_MBOX_INBOX:
223 # ----------------------
224 # This collects the messages from the FakeMhost inbox. It's reslly
225 # just here for testing the send_email method itself.
227 # Anyway this contains:
229 # - to: a list of email recipient addresses
230 # - message: not just the body, but the whole message, including
235 # Before running tests that call functions which send email, you should
236 # always call _clear_test_inboxes() to "wipe" the inboxes clean.
238 EMAIL_TEST_INBOX
= []
239 EMAIL_TEST_MBOX_INBOX
= []
242 class FakeMhost(object):
244 Just a fake mail host so we can capture and test messages
247 def login(self
, *args
, **kwargs
):
250 def sendmail(self
, from_addr
, to_addrs
, message
):
251 EMAIL_TEST_MBOX_INBOX
.append(
256 def _clear_test_inboxes():
257 global EMAIL_TEST_INBOX
258 global EMAIL_TEST_MBOX_INBOX
259 EMAIL_TEST_INBOX
= []
260 EMAIL_TEST_MBOX_INBOX
= []
262 ### ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
263 ### </Special email test stuff>
264 ### ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
266 def send_email(from_addr
, to_addrs
, subject
, message_body
):
268 Simple email sending wrapper, use this so we can capture messages
269 for unit testing purposes.
272 - from_addr: address you're sending the email from
273 - to_addrs: list of recipient email addresses
274 - subject: subject of the email
275 - message_body: email body text
277 if TESTS_ENABLED
or mg_globals
.app_config
['email_debug_mode']:
279 elif not mg_globals
.app_config
['email_debug_mode']:
280 mhost
= smtplib
.SMTP(
281 mg_globals
.app_config
['email_smtp_host'],
282 mg_globals
.app_config
['email_smtp_port'])
284 # SMTP.__init__ Issues SMTP.connect implicitly if host
285 if not mg_globals
.app_config
['email_smtp_host']: # e.g. host = ''
286 mhost
.connect() # We SMTP.connect explicitly
288 if mg_globals
.app_config
['email_smtp_user'] \
289 or mg_globals
.app_config
['email_smtp_pass']:
291 mg_globals
.app_config
['email_smtp_user'],
292 mg_globals
.app_config
['email_smtp_pass'])
294 message
= MIMEText(message_body
.encode('utf-8'), 'plain', 'utf-8')
295 message
['Subject'] = subject
296 message
['From'] = from_addr
297 message
['To'] = ', '.join(to_addrs
)
300 EMAIL_TEST_INBOX
.append(message
)
302 if mg_globals
.app_config
['email_debug_mode']:
303 print u
"===== Email ====="
304 print u
"From address: %s" % message
['From']
305 print u
"To addresses: %s" % message
['To']
306 print u
"Subject: %s" % message
['Subject']
308 print message
.get_payload(decode
=True)
310 return mhost
.sendmail(from_addr
, to_addrs
, message
.as_string())
318 TRANSLATIONS_PATH
= pkg_resources
.resource_filename(
319 'mediagoblin', 'i18n')
322 def locale_to_lower_upper(locale
):
324 Take a locale, regardless of style, and format it like "en-us"
327 lang
, country
= locale
.split('-', 1)
328 return '%s_%s' % (lang
.lower(), country
.upper())
330 lang
, country
= locale
.split('_', 1)
331 return '%s_%s' % (lang
.lower(), country
.upper())
333 return locale
.lower()
336 def locale_to_lower_lower(locale
):
338 Take a locale, regardless of style, and format it like "en_US"
341 lang
, country
= locale
.split('_', 1)
342 return '%s-%s' % (lang
.lower(), country
.lower())
344 return locale
.lower()
347 def get_locale_from_request(request
):
349 Figure out what target language is most appropriate based on the
352 request_form
= request
.GET
or request
.POST
354 if request_form
.has_key('lang'):
355 return locale_to_lower_upper(request_form
['lang'])
357 accept_lang_matches
= request
.accept_language
.best_matches()
359 # Your routing can explicitly specify a target language
360 matchdict
= request
.matchdict
or {}
362 if matchdict
.has_key('locale'):
363 target_lang
= matchdict
['locale']
364 elif request
.session
.has_key('target_lang'):
365 target_lang
= request
.session
['target_lang']
366 # Pull the first acceptable language
367 elif accept_lang_matches
:
368 target_lang
= accept_lang_matches
[0]
369 # Fall back to English
373 return locale_to_lower_upper(target_lang
)
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 # clean_html barfs on an empty string
403 return HTML_CLEANER
.clean_html(html
)
406 def convert_to_tag_list_of_dicts(tag_string
):
408 Filter input from incoming string containing user tags,
410 Strips trailing, leading, and internal whitespace, and also converts
411 the "tags" text into an array of tags
416 # Strip out internal, trailing, and leading whitespace
417 stripped_tag_string
= u
' '.join(tag_string
.strip().split())
419 # Split the tag string into a list of tags
420 for tag
in stripped_tag_string
.split(
421 mg_globals
.app_config
['tags_delimiter']):
423 # Ignore empty or duplicate tags
424 if tag
.strip() and tag
.strip() not in [t
['name'] for t
in taglist
]:
426 taglist
.append({'name': tag
.strip(),
427 'slug': slugify(tag
.strip())})
431 def media_tags_as_string(media_entry_tags
):
433 Generate a string from a media item's tags, stored as a list of dicts
435 This is the opposite of convert_to_tag_list_of_dicts
437 media_tag_string
= ''
439 media_tag_string
= mg_globals
.app_config
['tags_delimiter'].join(
440 [tag
['name'] for tag
in media_entry_tags
])
441 return media_tag_string
443 TOO_LONG_TAG_WARNING
= \
444 u
'Tags must be shorter than %s characters. Tags that are too long: %s'
446 def tag_length_validator(form
, field
):
448 Make sure tags do not exceed the maximum tag length.
450 tags
= convert_to_tag_list_of_dicts(field
.data
)
452 tag
['name'] for tag
in tags
453 if len(tag
['name']) > mg_globals
.app_config
['tags_max_length']]
456 raise wtforms
.ValidationError(
457 TOO_LONG_TAG_WARNING
% (mg_globals
.app_config
['tags_max_length'], \
458 ', '.join(too_long_tags
)))
461 MARKDOWN_INSTANCE
= markdown
.Markdown(safe_mode
='escape')
463 def cleaned_markdown_conversion(text
):
465 Take a block of text, run it through MarkDown, and clean its HTML.
467 # Markdown will do nothing with and clean_html can do nothing with
472 return clean_html(MARKDOWN_INSTANCE
.convert(text
))
477 def setup_gettext(locale
):
479 Setup the gettext instance based on this locale
481 # Later on when we have plugins we may want to enable the
482 # multi-translations system they have so we can handle plugin
485 # TODO: fallback nicely on translations from pt_PT to pt if not
487 if SETUP_GETTEXTS
.has_key(locale
):
488 this_gettext
= SETUP_GETTEXTS
[locale
]
490 this_gettext
= gettext
.translation(
491 'mediagoblin', TRANSLATIONS_PATH
, [locale
], fallback
=True)
493 SETUP_GETTEXTS
[locale
] = this_gettext
495 mg_globals
.setup_globals(
496 translations
=this_gettext
)
499 # Force en to be setup before anything else so that
500 # mg_globals.translations is never None
504 def pass_to_ugettext(*args
, **kwargs
):
506 Pass a translation on to the appropriate ugettext method.
508 The reason we can't have a global ugettext method is because
509 mg_globals gets swapped out by the application per-request.
511 return mg_globals
.translations
.ugettext(
515 def lazy_pass_to_ugettext(*args
, **kwargs
):
517 Lazily pass to ugettext.
519 This is useful if you have to define a translation on a module
520 level but you need it to not translate until the time that it's
523 return LazyProxy(pass_to_ugettext
, *args
, **kwargs
)
526 def pass_to_ngettext(*args
, **kwargs
):
528 Pass a translation on to the appropriate ngettext method.
530 The reason we can't have a global ngettext method is because
531 mg_globals gets swapped out by the application per-request.
533 return mg_globals
.translations
.ngettext(
537 def lazy_pass_to_ngettext(*args
, **kwargs
):
539 Lazily pass to ngettext.
541 This is useful if you have to define a translation on a module
542 level but you need it to not translate until the time that it's
545 return LazyProxy(pass_to_ngettext
, *args
, **kwargs
)
548 def fake_ugettext_passthrough(string
):
550 Fake a ugettext call for extraction's sake ;)
552 In wtforms there's a separate way to define a method to translate
553 things... so we just need to mark up the text so that it can be
554 extracted, not so that it's actually run through gettext.
559 PAGINATION_DEFAULT_PER_PAGE
= 30
561 class Pagination(object):
563 Pagination class for mongodb queries.
565 Initialization through __init__(self, cursor, page=1, per_page=2),
566 get actual data slice through __call__().
569 def __init__(self
, page
, cursor
, per_page
=PAGINATION_DEFAULT_PER_PAGE
,
572 Initializes Pagination
575 - page: requested page
576 - per_page: number of objects per page
578 - jump_to_id: ObjectId, sets the page to the page containing the object
579 with _id == jump_to_id.
582 self
.per_page
= per_page
584 self
.total_count
= self
.cursor
.count()
585 self
.active_id
= None
588 cursor
= copy
.copy(self
.cursor
)
590 for (doc
, increment
) in izip(cursor
, count(0)):
591 if doc
['_id'] == jump_to_id
:
592 self
.page
= 1 + int(floor(increment
/ self
.per_page
))
594 self
.active_id
= jump_to_id
600 Returns slice of objects for the requested page
602 return self
.cursor
.skip(
603 (self
.page
- 1) * self
.per_page
).limit(self
.per_page
)
607 return int(ceil(self
.total_count
/ float(self
.per_page
)))
615 return self
.page
< self
.pages
617 def iter_pages(self
, left_edge
=2, left_current
=2,
618 right_current
=5, right_edge
=2):
620 for num
in xrange(1, self
.pages
+ 1):
621 if num
<= left_edge
or \
622 (num
> self
.page
- left_current
- 1 and \
623 num
< self
.page
+ right_current
) or \
624 num
> self
.pages
- right_edge
:
630 def get_page_url_explicit(self
, base_url
, get_params
, page_no
):
632 Get a page url by adding a page= parameter to the base url
634 new_get_params
= copy
.copy(get_params
or {})
635 new_get_params
['page'] = page_no
637 base_url
, urllib
.urlencode(new_get_params
))
639 def get_page_url(self
, request
, page_no
):
641 Get a new page url based of the request, and the new page number.
643 This is a nice wrapper around get_page_url_explicit()
645 return self
.get_page_url_explicit(
646 request
.path_info
, request
.GET
, page_no
)
649 def gridify_list(this_list
, num_cols
=5):
651 Generates a list of lists where each sub-list's length depends on
652 the number of columns in the list
656 # Figure out how many rows we should have
657 num_rows
= int(ceil(float(len(this_list
)) / num_cols
))
659 for row_num
in range(num_rows
):
660 slice_min
= row_num
* num_cols
661 slice_max
= (row_num
+ 1) * num_cols
663 row
= this_list
[slice_min
:slice_max
]
670 def gridify_cursor(this_cursor
, num_cols
=5):
672 Generates a list of lists where each sub-list's length depends on
673 the number of columns in the list
675 return gridify_list(list(this_cursor
), num_cols
)
678 def render_404(request
):
682 return render_to_response(
683 request
, 'mediagoblin/404.html', {}, status
=400)