Moved common, translation, template, and url code out of util.py and into tools/...
[mediagoblin.git] / mediagoblin / util.py
CommitLineData
8e1e744d 1# GNU MediaGoblin -- federated, autonomous media hosting
12a100e4 2# Copyright (C) 2011 MediaGoblin contributors. See AUTHORS.
e5572c60
ML
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
af2fcba5
JW
17from __future__ import division
18
4d4f6050 19from email.MIMEText import MIMEText
ae3bc7fa
AW
20#import gettext
21#import pkg_resources
4d4f6050 22import smtplib
cb8ea0fe 23import sys
ae3bc7fa
AW
24#import re
25#import translitcodec
c5678c1a 26import urllib
af2fcba5 27from math import ceil, floor
c5678c1a 28import copy
909371cd 29import wtforms
c5678c1a 30
ae3bc7fa
AW
31#from babel.localedata import exists
32#from babel.support import LazyProxy
33#import jinja2
9150244a 34from webob import Response, exc
a68ee555 35from lxml.html.clean import Cleaner
4bf8e888 36import markdown
1c266dc3 37from wtforms.form import Form
31a8ff42 38
6e7ce8d1 39from mediagoblin import mg_globals
ae3bc7fa 40#from mediagoblin import messages
c5678c1a 41from mediagoblin.db.util import ObjectId
ae3bc7fa
AW
42from mediagoblin.tools import url
43from mediagoblin.tools import common
44from mediagoblin.tools.template import TEMPLATE_TEST_CONTEXT, render_template
29f3fb70 45
af2fcba5
JW
46from itertools import izip, count
47
2c9e635a
JW
48DISPLAY_IMAGE_FETCHING_ORDER = [u'medium', u'original', u'thumb']
49
4d4f6050
CAW
50def _activate_testing():
51 """
52 Call this to activate testing in util.py
53 """
4d4f6050 54
ae3bc7fa 55 common.TESTS_ENABLED = True
4d4f6050 56
66471f0e
CAW
57def clear_test_buckets():
58 """
59 We store some things for testing purposes that should be cleared
60 when we want a "clean slate" of information for our next round of
61 tests. Call this function to wipe all that stuff clean.
62
63 Also wipes out some other things we might redefine during testing,
64 like the jinja envs.
65 """
66 global SETUP_JINJA_ENVS
67 SETUP_JINJA_ENVS = {}
68
69 global EMAIL_TEST_INBOX
70 global EMAIL_TEST_MBOX_INBOX
71 EMAIL_TEST_INBOX = []
72 EMAIL_TEST_MBOX_INBOX = []
73
74 clear_test_template_context()
75
76
ae3bc7fa 77# SETUP_JINJA_ENVS = {}
f99f61c6
CAW
78
79
ae3bc7fa
AW
80# def get_jinja_env(template_loader, locale):
81# """
82# Set up the Jinja environment,
0e0e3d9a 83
ae3bc7fa
AW
84# (In the future we may have another system for providing theming;
85# for now this is good enough.)
86# """
87# setup_gettext(locale)
b77eec65 88
ae3bc7fa
AW
89# # If we have a jinja environment set up with this locale, just
90# # return that one.
91# if SETUP_JINJA_ENVS.has_key(locale):
92# return SETUP_JINJA_ENVS[locale]
f99f61c6 93
ae3bc7fa
AW
94# template_env = jinja2.Environment(
95# loader=template_loader, autoescape=True,
96# extensions=['jinja2.ext.i18n', 'jinja2.ext.autoescape'])
58dec5ef 97
ae3bc7fa
AW
98# template_env.install_gettext_callables(
99# mg_globals.translations.ugettext,
100# mg_globals.translations.ungettext)
b77eec65 101
ae3bc7fa
AW
102# # All templates will know how to ...
103# # ... fetch all waiting messages and remove them from the queue
104# # ... construct a grid of thumbnails or other media
105# template_env.globals['fetch_messages'] = messages.fetch_messages
106# template_env.globals['gridify_list'] = gridify_list
107# template_env.globals['gridify_cursor'] = gridify_cursor
22646703 108
ae3bc7fa
AW
109# if exists(locale):
110# SETUP_JINJA_ENVS[locale] = template_env
f99f61c6 111
ae3bc7fa 112# return template_env
b77eec65 113
58dec5ef 114
ae3bc7fa
AW
115# # We'll store context information here when doing unit tests
116# TEMPLATE_TEST_CONTEXT = {}
e9279f21
CAW
117
118
ae3bc7fa
AW
119# def render_template(request, template_path, context):
120# """
121# Render a template with context.
e9279f21 122
ae3bc7fa
AW
123# Always inserts the request into the context, so you don't have to.
124# Also stores the context if we're doing unit tests. Helpful!
125# """
126# template = request.template_env.get_template(
127# template_path)
128# context['request'] = request
129# rendered = template.render(context)
e9279f21 130
ae3bc7fa
AW
131# if TESTS_ENABLED:
132# TEMPLATE_TEST_CONTEXT[template_path] = context
e9279f21 133
ae3bc7fa 134# return rendered
e9279f21
CAW
135
136
137def clear_test_template_context():
138 global TEMPLATE_TEST_CONTEXT
139 TEMPLATE_TEST_CONTEXT = {}
140
141
a7c641d1 142def render_to_response(request, template, context, status=200):
1c63ad5d 143 """Much like Django's shortcut.render()"""
a7c641d1
CAW
144 return Response(
145 render_template(request, template, context),
146 status=status)
1c63ad5d
E
147
148
9150244a
E
149def redirect(request, *args, **kwargs):
150 """Returns a HTTPFound(), takes a request and then urlgen params"""
af2fcba5
JW
151
152 querystring = None
153 if kwargs.get('querystring'):
154 querystring = kwargs.get('querystring')
155 del kwargs['querystring']
156
157 return exc.HTTPFound(
158 location=''.join([
159 request.urlgen(*args, **kwargs),
160 querystring if querystring else '']))
9150244a
E
161
162
58dec5ef
CAW
163def setup_user_in_request(request):
164 """
165 Examine a request and tack on a request.user parameter if that's
166 appropriate.
167 """
168 if not request.session.has_key('user_id'):
59dd5c7e 169 request.user = None
58dec5ef
CAW
170 return
171
5d6840a0 172 user = None
6648c52b 173 user = request.app.db.User.one(
254bc431 174 {'_id': ObjectId(request.session['user_id'])})
5d6840a0 175
c74e1462
CAW
176 if not user:
177 # Something's wrong... this user doesn't exist? Invalidate
178 # this session.
58dec5ef 179 request.session.invalidate()
5d6840a0
CAW
180
181 request.user = user
cb8ea0fe
CAW
182
183
184def import_component(import_string):
185 """
186 Import a module component defined by STRING. Probably a method,
187 class, or global variable.
188
189 Args:
190 - import_string: a string that defines what to import. Written
191 in the format of "module1.module2:component"
192 """
193 module_name, func_name = import_string.split(':', 1)
194 __import__(module_name)
195 module = sys.modules[module_name]
196 func = getattr(module, func_name)
197 return func
4d4f6050 198
ae3bc7fa 199# _punct_re = re.compile(r'[\t !"#$%&\'()*\-/<=>?@\[\\\]^_`{|},.]+')
0546833c 200
ae3bc7fa
AW
201# def slugify(text, delim=u'-'):
202# """
203# Generates an ASCII-only slug. Taken from http://flask.pocoo.org/snippets/5/
204# """
205# result = []
206# for word in _punct_re.split(text.lower()):
207# word = word.encode('translit/long')
208# if word:
209# result.append(word)
210# return unicode(delim.join(result))
4d4f6050
CAW
211
212### ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
213### Special email test stuff begins HERE
214### ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
215
216# We have two "test inboxes" here:
217#
218# EMAIL_TEST_INBOX:
219# ----------------
220# If you're writing test views, you'll probably want to check this.
221# It contains a list of MIMEText messages.
222#
223# EMAIL_TEST_MBOX_INBOX:
224# ----------------------
225# This collects the messages from the FakeMhost inbox. It's reslly
226# just here for testing the send_email method itself.
227#
228# Anyway this contains:
229# - from
230# - to: a list of email recipient addresses
231# - message: not just the body, but the whole message, including
232# headers, etc.
233#
234# ***IMPORTANT!***
235# ----------------
236# Before running tests that call functions which send email, you should
237# always call _clear_test_inboxes() to "wipe" the inboxes clean.
238
239EMAIL_TEST_INBOX = []
240EMAIL_TEST_MBOX_INBOX = []
241
242
243class FakeMhost(object):
244 """
245 Just a fake mail host so we can capture and test messages
246 from send_email
247 """
d71170ad 248 def login(self, *args, **kwargs):
4d4f6050
CAW
249 pass
250
251 def sendmail(self, from_addr, to_addrs, message):
252 EMAIL_TEST_MBOX_INBOX.append(
253 {'from': from_addr,
254 'to': to_addrs,
255 'message': message})
256
257def _clear_test_inboxes():
258 global EMAIL_TEST_INBOX
259 global EMAIL_TEST_MBOX_INBOX
260 EMAIL_TEST_INBOX = []
261 EMAIL_TEST_MBOX_INBOX = []
262
263### ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
264### </Special email test stuff>
265### ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
266
267def send_email(from_addr, to_addrs, subject, message_body):
61ec968b
CAW
268 """
269 Simple email sending wrapper, use this so we can capture messages
270 for unit testing purposes.
271
272 Args:
273 - from_addr: address you're sending the email from
274 - to_addrs: list of recipient email addresses
275 - subject: subject of the email
276 - message_body: email body text
277 """
ae3bc7fa 278 if common.TESTS_ENABLED or mg_globals.app_config['email_debug_mode']:
4d4f6050 279 mhost = FakeMhost()
6ae8b541 280 elif not mg_globals.app_config['email_debug_mode']:
d71170ad
JW
281 mhost = smtplib.SMTP(
282 mg_globals.app_config['email_smtp_host'],
283 mg_globals.app_config['email_smtp_port'])
284
285 # SMTP.__init__ Issues SMTP.connect implicitly if host
286 if not mg_globals.app_config['email_smtp_host']: # e.g. host = ''
287 mhost.connect() # We SMTP.connect explicitly
288
289 if mg_globals.app_config['email_smtp_user'] \
290 or mg_globals.app_config['email_smtp_pass']:
47364ead
JW
291 mhost.login(
292 mg_globals.app_config['email_smtp_user'],
293 mg_globals.app_config['email_smtp_pass'])
4d4f6050
CAW
294
295 message = MIMEText(message_body.encode('utf-8'), 'plain', 'utf-8')
296 message['Subject'] = subject
297 message['From'] = from_addr
298 message['To'] = ', '.join(to_addrs)
299
ae3bc7fa 300 if common.TESTS_ENABLED:
4d4f6050
CAW
301 EMAIL_TEST_INBOX.append(message)
302
6ae8b541 303 if mg_globals.app_config['email_debug_mode']:
29f3fb70
CAW
304 print u"===== Email ====="
305 print u"From address: %s" % message['From']
306 print u"To addresses: %s" % message['To']
307 print u"Subject: %s" % message['Subject']
308 print u"-- Body: --"
309 print message.get_payload(decode=True)
310
21919313 311 return mhost.sendmail(from_addr, to_addrs, message.as_string())
20c834ff 312
8b28bee4 313
ae3bc7fa
AW
314# ###################
315# # Translation tools
316# ###################
8b28bee4
CAW
317
318
ae3bc7fa
AW
319# TRANSLATIONS_PATH = pkg_resources.resource_filename(
320# 'mediagoblin', 'i18n')
b77eec65
CAW
321
322
ae3bc7fa
AW
323# def locale_to_lower_upper(locale):
324# """
325# Take a locale, regardless of style, and format it like "en-us"
326# """
327# if '-' in locale:
328# lang, country = locale.split('-', 1)
329# return '%s_%s' % (lang.lower(), country.upper())
330# elif '_' in locale:
331# lang, country = locale.split('_', 1)
332# return '%s_%s' % (lang.lower(), country.upper())
333# else:
334# return locale.lower()
8b28bee4
CAW
335
336
ae3bc7fa
AW
337# def locale_to_lower_lower(locale):
338# """
339# Take a locale, regardless of style, and format it like "en_US"
340# """
341# if '_' in locale:
342# lang, country = locale.split('_', 1)
343# return '%s-%s' % (lang.lower(), country.lower())
344# else:
345# return locale.lower()
8b28bee4
CAW
346
347
ae3bc7fa
AW
348# def get_locale_from_request(request):
349# """
350# Figure out what target language is most appropriate based on the
351# request
352# """
353# request_form = request.GET or request.POST
8b28bee4 354
ae3bc7fa
AW
355# if request_form.has_key('lang'):
356# return locale_to_lower_upper(request_form['lang'])
8b28bee4 357
ae3bc7fa 358# accept_lang_matches = request.accept_language.best_matches()
8b28bee4 359
ae3bc7fa
AW
360# # Your routing can explicitly specify a target language
361# matchdict = request.matchdict or {}
bae8f3d8 362
ae3bc7fa
AW
363# if matchdict.has_key('locale'):
364# target_lang = matchdict['locale']
365# elif request.session.has_key('target_lang'):
366# target_lang = request.session['target_lang']
367# # Pull the first acceptable language
368# elif accept_lang_matches:
369# target_lang = accept_lang_matches[0]
370# # Fall back to English
371# else:
372# target_lang = 'en'
8b28bee4 373
ae3bc7fa 374# return locale_to_lower_upper(target_lang)
b77eec65
CAW
375
376
a68ee555
CAW
377# A super strict version of the lxml.html cleaner class
378HTML_CLEANER = Cleaner(
379 scripts=True,
380 javascript=True,
381 comments=True,
382 style=True,
383 links=True,
384 page_structure=True,
385 processing_instructions=True,
386 embedded=True,
387 frames=True,
388 forms=True,
389 annoying_tags=True,
390 allow_tags=[
391 'div', 'b', 'i', 'em', 'strong', 'p', 'ul', 'ol', 'li', 'a', 'br'],
392 remove_unknown_tags=False, # can't be used with allow_tags
393 safe_attrs_only=True,
394 add_nofollow=True, # for now
395 host_whitelist=(),
396 whitelist_tags=set([]))
397
398
399def clean_html(html):
4fd18da0
CAW
400 # clean_html barfs on an empty string
401 if not html:
402 return u''
403
a68ee555
CAW
404 return HTML_CLEANER.clean_html(html)
405
406
0712a06d 407def convert_to_tag_list_of_dicts(tag_string):
cdf538bd 408 """
909371cd 409 Filter input from incoming string containing user tags,
4bf8e888 410
cdf538bd 411 Strips trailing, leading, and internal whitespace, and also converts
cc7ff3c5 412 the "tags" text into an array of tags
cdf538bd 413 """
6f2e4585 414 taglist = []
cdf538bd 415 if tag_string:
cc7ff3c5
CFD
416
417 # Strip out internal, trailing, and leading whitespace
93e3468a 418 stripped_tag_string = u' '.join(tag_string.strip().split())
cc7ff3c5
CFD
419
420 # Split the tag string into a list of tags
10d7496d
CFD
421 for tag in stripped_tag_string.split(
422 mg_globals.app_config['tags_delimiter']):
cc7ff3c5 423
f99b5cae
CFD
424 # Ignore empty or duplicate tags
425 if tag.strip() and tag.strip() not in [t['name'] for t in taglist]:
cc7ff3c5 426
1b89b817 427 taglist.append({'name': tag.strip(),
ae3bc7fa 428 'slug': url.slugify(tag.strip())})
6f2e4585 429 return taglist
cdf538bd
CFD
430
431
0712a06d
CFD
432def media_tags_as_string(media_entry_tags):
433 """
434 Generate a string from a media item's tags, stored as a list of dicts
435
436 This is the opposite of convert_to_tag_list_of_dicts
437 """
438 media_tag_string = ''
439 if media_entry_tags:
440 media_tag_string = mg_globals.app_config['tags_delimiter'].join(
441 [tag['name'] for tag in media_entry_tags])
442 return media_tag_string
443
909371cd
CFD
444TOO_LONG_TAG_WARNING = \
445 u'Tags must be shorter than %s characters. Tags that are too long: %s'
446
447def tag_length_validator(form, field):
448 """
449 Make sure tags do not exceed the maximum tag length.
450 """
0712a06d 451 tags = convert_to_tag_list_of_dicts(field.data)
909371cd 452 too_long_tags = [
0712a06d
CFD
453 tag['name'] for tag in tags
454 if len(tag['name']) > mg_globals.app_config['tags_max_length']]
909371cd
CFD
455
456 if too_long_tags:
457 raise wtforms.ValidationError(
10d7496d
CFD
458 TOO_LONG_TAG_WARNING % (mg_globals.app_config['tags_max_length'], \
459 ', '.join(too_long_tags)))
4bf8e888
CAW
460
461
cdf538bd 462MARKDOWN_INSTANCE = markdown.Markdown(safe_mode='escape')
4bf8e888
CAW
463
464def cleaned_markdown_conversion(text):
465 """
466 Take a block of text, run it through MarkDown, and clean its HTML.
467 """
82688846
CAW
468 # Markdown will do nothing with and clean_html can do nothing with
469 # an empty string :)
470 if not text:
471 return u''
472
4bf8e888
CAW
473 return clean_html(MARKDOWN_INSTANCE.convert(text))
474
475
ae3bc7fa 476# SETUP_GETTEXTS = {}
f99f61c6 477
ae3bc7fa
AW
478# def setup_gettext(locale):
479# """
480# Setup the gettext instance based on this locale
481# """
482# # Later on when we have plugins we may want to enable the
483# # multi-translations system they have so we can handle plugin
484# # translations too
b77eec65 485
ae3bc7fa
AW
486# # TODO: fallback nicely on translations from pt_PT to pt if not
487# # available, etc.
488# if SETUP_GETTEXTS.has_key(locale):
489# this_gettext = SETUP_GETTEXTS[locale]
490# else:
491# this_gettext = gettext.translation(
492# 'mediagoblin', TRANSLATIONS_PATH, [locale], fallback=True)
493# if exists(locale):
494# SETUP_GETTEXTS[locale] = this_gettext
b77eec65 495
ae3bc7fa
AW
496# mg_globals.setup_globals(
497# translations=this_gettext)
ae85ed0f
BK
498
499
ae3bc7fa
AW
500# # Force en to be setup before anything else so that
501# # mg_globals.translations is never None
502# setup_gettext('en')
03e5bd6d
CAW
503
504
ae3bc7fa
AW
505# def pass_to_ugettext(*args, **kwargs):
506# """
507# Pass a translation on to the appropriate ugettext method.
03e5bd6d 508
ae3bc7fa
AW
509# The reason we can't have a global ugettext method is because
510# mg_globals gets swapped out by the application per-request.
511# """
512# return mg_globals.translations.ugettext(
513# *args, **kwargs)
03e5bd6d
CAW
514
515
ae3bc7fa
AW
516# def lazy_pass_to_ugettext(*args, **kwargs):
517# """
518# Lazily pass to ugettext.
1c266dc3 519
ae3bc7fa
AW
520# This is useful if you have to define a translation on a module
521# level but you need it to not translate until the time that it's
522# used as a string.
523# """
524# return LazyProxy(pass_to_ugettext, *args, **kwargs)
1c266dc3
CAW
525
526
ae3bc7fa
AW
527# def pass_to_ngettext(*args, **kwargs):
528# """
529# Pass a translation on to the appropriate ngettext method.
1c266dc3 530
ae3bc7fa
AW
531# The reason we can't have a global ngettext method is because
532# mg_globals gets swapped out by the application per-request.
533# """
534# return mg_globals.translations.ngettext(
535# *args, **kwargs)
1c266dc3
CAW
536
537
ae3bc7fa
AW
538# def lazy_pass_to_ngettext(*args, **kwargs):
539# """
540# Lazily pass to ngettext.
1c266dc3 541
ae3bc7fa
AW
542# This is useful if you have to define a translation on a module
543# level but you need it to not translate until the time that it's
544# used as a string.
545# """
546# return LazyProxy(pass_to_ngettext, *args, **kwargs)
1c266dc3
CAW
547
548
ae3bc7fa
AW
549# def fake_ugettext_passthrough(string):
550# """
551# Fake a ugettext call for extraction's sake ;)
1c266dc3 552
ae3bc7fa
AW
553# In wtforms there's a separate way to define a method to translate
554# things... so we just need to mark up the text so that it can be
555# extracted, not so that it's actually run through gettext.
556# """
557# return string
1c266dc3
CAW
558
559
b9e9610b
CAW
560PAGINATION_DEFAULT_PER_PAGE = 30
561
ae85ed0f
BK
562class Pagination(object):
563 """
dffa0b09
CAW
564 Pagination class for mongodb queries.
565
566 Initialization through __init__(self, cursor, page=1, per_page=2),
567 get actual data slice through __call__().
ae85ed0f 568 """
ca3ca51c 569
af2fcba5
JW
570 def __init__(self, page, cursor, per_page=PAGINATION_DEFAULT_PER_PAGE,
571 jump_to_id=False):
44e3e917 572 """
a98d5254
CAW
573 Initializes Pagination
574
575 Args:
576 - page: requested page
577 - per_page: number of objects per page
578 - cursor: db cursor
af2fcba5
JW
579 - jump_to_id: ObjectId, sets the page to the page containing the object
580 with _id == jump_to_id.
44e3e917 581 """
af2fcba5 582 self.page = page
ca3ca51c
BK
583 self.per_page = per_page
584 self.cursor = cursor
ca3ca51c 585 self.total_count = self.cursor.count()
af2fcba5
JW
586 self.active_id = None
587
588 if jump_to_id:
589 cursor = copy.copy(self.cursor)
590
591 for (doc, increment) in izip(cursor, count(0)):
592 if doc['_id'] == jump_to_id:
593 self.page = 1 + int(floor(increment / self.per_page))
594
595 self.active_id = jump_to_id
596 break
597
ca3ca51c
BK
598
599 def __call__(self):
44e3e917 600 """
a98d5254 601 Returns slice of objects for the requested page
44e3e917 602 """
140e2102
CAW
603 return self.cursor.skip(
604 (self.page - 1) * self.per_page).limit(self.per_page)
ae85ed0f
BK
605
606 @property
607 def pages(self):
608 return int(ceil(self.total_count / float(self.per_page)))
609
610 @property
611 def has_prev(self):
612 return self.page > 1
613
614 @property
615 def has_next(self):
616 return self.page < self.pages
617
618 def iter_pages(self, left_edge=2, left_current=2,
619 right_current=5, right_edge=2):
620 last = 0
621 for num in xrange(1, self.pages + 1):
622 if num <= left_edge or \
623 (num > self.page - left_current - 1 and \
624 num < self.page + right_current) or \
625 num > self.pages - right_edge:
626 if last + 1 != num:
627 yield None
628 yield num
629 last = num
44e3e917 630
50c880ac 631 def get_page_url_explicit(self, base_url, get_params, page_no):
44e3e917 632 """
50c880ac 633 Get a page url by adding a page= parameter to the base url
44e3e917
BK
634 """
635 new_get_params = copy.copy(get_params or {})
636 new_get_params['page'] = page_no
637 return "%s?%s" % (
50c880ac
CAW
638 base_url, urllib.urlencode(new_get_params))
639
640 def get_page_url(self, request, page_no):
641 """
642 Get a new page url based of the request, and the new page number.
643
644 This is a nice wrapper around get_page_url_explicit()
645 """
646 return self.get_page_url_explicit(
647 request.path_info, request.GET, page_no)
b5017dba
CAW
648
649
ae3bc7fa
AW
650# def gridify_list(this_list, num_cols=5):
651# """
652# Generates a list of lists where each sub-list's length depends on
653# the number of columns in the list
654# """
655# grid = []
b5017dba 656
ae3bc7fa
AW
657# # Figure out how many rows we should have
658# num_rows = int(ceil(float(len(this_list)) / num_cols))
b5017dba 659
ae3bc7fa
AW
660# for row_num in range(num_rows):
661# slice_min = row_num * num_cols
662# slice_max = (row_num + 1) * num_cols
b5017dba 663
ae3bc7fa 664# row = this_list[slice_min:slice_max]
b5017dba 665
ae3bc7fa 666# grid.append(row)
b5017dba 667
ae3bc7fa 668# return grid
b5017dba
CAW
669
670
ae3bc7fa
AW
671# def gridify_cursor(this_cursor, num_cols=5):
672# """
673# Generates a list of lists where each sub-list's length depends on
674# the number of columns in the list
675# """
676# return gridify_list(list(this_cursor), num_cols)
bae8f3d8
CAW
677
678
679def render_404(request):
680 """
681 Render a 404.
682 """
683 return render_to_response(
684 request, 'mediagoblin/404.html', {}, status=400)
502073f2
JW
685
686def delete_media_files(media):
687 """
688 Delete all files associated with a MediaEntry
689
690 Arguments:
691 - media: A MediaEntry document
692 """
63e7abdf 693 for listpath in media['media_files'].itervalues():
502073f2
JW
694 mg_globals.public_store.delete_file(
695 listpath)
696
697 for attachment in media['attachment_files']:
698 mg_globals.public_store.delete_file(
699 attachment['filepath'])