540 строки
16 KiB
Python
540 строки
16 KiB
Python
import collections
|
|
import json as jsonlib
|
|
import random
|
|
import re
|
|
from operator import attrgetter
|
|
|
|
from django.conf import settings
|
|
from django.forms import CheckboxInput
|
|
from django.utils import translation
|
|
from django.utils.encoding import smart_unicode
|
|
from django.template import defaultfilters
|
|
|
|
from babel import Locale
|
|
from babel.support import Format
|
|
import caching.base as caching
|
|
import jinja2
|
|
from jingo import register, env
|
|
from tower import ugettext as _, strip_whitespace
|
|
|
|
import amo
|
|
from amo import utils, urlresolvers
|
|
from translations.query import order_by_translation
|
|
from translations.helpers import truncate
|
|
|
|
# Yanking filters from Django.
|
|
register.filter(defaultfilters.slugify)
|
|
|
|
# Registering some utils as filters:
|
|
urlparams = register.filter(utils.urlparams)
|
|
register.filter(utils.epoch)
|
|
register.filter(utils.isotime)
|
|
register.function(dict)
|
|
register.function(utils.randslice)
|
|
|
|
|
|
@register.filter
|
|
def link(item):
|
|
html = """<a href="%s">%s</a>""" % (item.get_url_path(),
|
|
jinja2.escape(item.name))
|
|
return jinja2.Markup(html)
|
|
|
|
|
|
@register.filter
|
|
def xssafe(value):
|
|
"""
|
|
Like |safe but for strings with interpolation.
|
|
|
|
By using |xssafe you assert that you have written tests proving an
|
|
XSS can't happen here.
|
|
"""
|
|
return jinja2.Markup(value)
|
|
|
|
|
|
@register.filter
|
|
def babel_datetime(t, format='medium'):
|
|
return _get_format().datetime(t, format=format) if t else ''
|
|
|
|
|
|
@register.function
|
|
def locale_url(url):
|
|
"""Take a URL and give it the locale prefix."""
|
|
prefixer = urlresolvers.get_url_prefix()
|
|
script = prefixer.request.META['SCRIPT_NAME']
|
|
parts = [script, prefixer.locale, url.lstrip('/')]
|
|
return '/'.join(parts)
|
|
|
|
|
|
@register.inclusion_tag('includes/refinements.html')
|
|
@jinja2.contextfunction
|
|
def refinements(context, items, title, thing):
|
|
d = dict(context.items())
|
|
d.update(items=items, title=title, thing=thing)
|
|
return d
|
|
|
|
|
|
@register.function
|
|
def url(viewname, *args, **kwargs):
|
|
"""Helper for Django's ``reverse`` in templates."""
|
|
add_prefix = kwargs.pop('add_prefix', True)
|
|
host = kwargs.pop('host', '')
|
|
src = kwargs.pop('src', '')
|
|
url = '%s%s' % (host, urlresolvers.reverse(viewname,
|
|
args=args,
|
|
kwargs=kwargs,
|
|
add_prefix=add_prefix))
|
|
if src:
|
|
url = urlparams(url, src=src)
|
|
return url
|
|
|
|
|
|
@register.function
|
|
def shared_url(viewname, addon, *args, **kwargs):
|
|
"""
|
|
Helper specifically for addons or apps to get urls. Requires
|
|
the viewname, addon (or app). It's assumed that we'll pass the
|
|
slug into the args and we'll look up the right slug (addon or app)
|
|
for you.
|
|
|
|
Viewname should be a normal view eg: `addons.details` or `apps.details`.
|
|
`addons.details` becomes `apps.details`, if we've passed an app, etc.
|
|
|
|
A viewname such as `details` becomes `addons.details` or `apps.details`,
|
|
depending on the add-on type.
|
|
"""
|
|
slug = addon.app_slug if addon.is_webapp() else addon.slug
|
|
prefix = 'apps' if addon.is_webapp() else 'addons'
|
|
|
|
namespace, dot, latter = viewname.partition('.')
|
|
|
|
# If `viewname` is prefixed with `addons.` but we're linking to a
|
|
# webapp, the `viewname` magically gets prefixed with `apps.`.
|
|
if namespace in ('addons', 'apps'):
|
|
viewname = latter
|
|
|
|
# Otherwise, we just slap the appropriate prefix in front of `viewname`.
|
|
viewname = '.'.join([prefix, viewname])
|
|
return url(viewname, *([slug] + list(args)), **kwargs)
|
|
|
|
|
|
@register.function
|
|
def services_url(viewname, *args, **kwargs):
|
|
"""Helper for ``url`` with host=SERVICES_URL."""
|
|
kwargs.update({'host': settings.SERVICES_URL})
|
|
return url(viewname, *args, **kwargs)
|
|
|
|
|
|
@register.filter
|
|
def paginator(pager):
|
|
return Paginator(pager).render()
|
|
|
|
|
|
@register.filter
|
|
def impala_paginator(pager):
|
|
t = env.get_template('amo/impala/paginator.html')
|
|
return jinja2.Markup(t.render(pager=pager))
|
|
|
|
|
|
@register.filter
|
|
def mobile_paginator(pager):
|
|
t = env.get_template('amo/mobile/paginator.html')
|
|
return jinja2.Markup(t.render(pager=pager))
|
|
|
|
|
|
@register.function
|
|
def is_mobile(app):
|
|
return app == amo.MOBILE
|
|
|
|
|
|
@register.function
|
|
def sidebar(app):
|
|
"""Populates the sidebar with (categories, types)."""
|
|
from addons.models import Category
|
|
if app is None:
|
|
return [], []
|
|
|
|
# We muck with query to make order_by and extra_order_by play nice.
|
|
q = Category.objects.filter(application=app.id, weight__gte=0,
|
|
type=amo.ADDON_EXTENSION)
|
|
categories = order_by_translation(q, 'name')
|
|
categories.query.extra_order_by.insert(0, 'weight')
|
|
|
|
Type = collections.namedtuple('Type', 'id name url')
|
|
base = urlresolvers.reverse('home')
|
|
types = [Type(99, _('Collections'), base + 'collections/')]
|
|
|
|
shown_types = {
|
|
amo.ADDON_PERSONA: urlresolvers.reverse('browse.personas'),
|
|
amo.ADDON_DICT: urlresolvers.reverse('browse.language-tools'),
|
|
amo.ADDON_SEARCH: urlresolvers.reverse('browse.search-tools'),
|
|
amo.ADDON_THEME: urlresolvers.reverse('browse.themes'),
|
|
}
|
|
titles = dict(amo.ADDON_TYPES,
|
|
**{amo.ADDON_DICT: _('Dictionaries & Language Packs')})
|
|
for type_, url in shown_types.items():
|
|
if type_ in app.types:
|
|
types.append(Type(type_, titles[type_], url))
|
|
|
|
return categories, sorted(types, key=lambda x: x.name)
|
|
|
|
|
|
class Paginator(object):
|
|
|
|
def __init__(self, pager):
|
|
self.pager = pager
|
|
|
|
self.max = 10
|
|
self.span = (self.max - 1) / 2
|
|
|
|
self.page = pager.number
|
|
self.num_pages = pager.paginator.num_pages
|
|
self.count = pager.paginator.count
|
|
|
|
pager.page_range = self.range()
|
|
pager.dotted_upper = self.num_pages not in pager.page_range
|
|
pager.dotted_lower = 1 not in pager.page_range
|
|
|
|
def range(self):
|
|
"""Return a list of page numbers to show in the paginator."""
|
|
page, total, span = self.page, self.num_pages, self.span
|
|
if total < self.max:
|
|
lower, upper = 0, total
|
|
elif page < span + 1:
|
|
lower, upper = 0, span * 2
|
|
elif page > total - span:
|
|
lower, upper = total - span * 2, total
|
|
else:
|
|
lower, upper = page - span, page + span - 1
|
|
return range(max(lower + 1, 1), min(total, upper) + 1)
|
|
|
|
def render(self):
|
|
c = {'pager': self.pager, 'num_pages': self.num_pages,
|
|
'count': self.count}
|
|
t = env.get_template('amo/paginator.html').render(**c)
|
|
return jinja2.Markup(t)
|
|
|
|
|
|
def _get_format():
|
|
lang = translation.get_language()
|
|
locale = Locale(translation.to_locale(lang))
|
|
return Format(locale)
|
|
|
|
|
|
@register.filter
|
|
def numberfmt(num, format=None):
|
|
return _get_format().decimal(num, format)
|
|
|
|
|
|
@register.filter
|
|
def currencyfmt(num, currency):
|
|
if num is None:
|
|
return ''
|
|
return _get_format().currency(num, currency)
|
|
|
|
|
|
def page_name(app=None):
|
|
"""Determine the correct page name for the given app (or no app)."""
|
|
if app:
|
|
return _(u'Add-ons for {0}').format(app.pretty)
|
|
else:
|
|
return _('Add-ons')
|
|
|
|
|
|
@register.function
|
|
@jinja2.contextfunction
|
|
def login_link(context):
|
|
next = context['request'].path
|
|
|
|
qs = context['request'].GET.urlencode()
|
|
|
|
if qs:
|
|
next += '?' + qs
|
|
|
|
l = urlparams(urlresolvers.reverse('users.login'), to=next)
|
|
return l
|
|
|
|
|
|
@register.function
|
|
@jinja2.contextfunction
|
|
def page_title(context, title, force_webapps=False):
|
|
title = smart_unicode(title)
|
|
if settings.APP_PREVIEW:
|
|
base_title = loc('Apps Developer Preview')
|
|
elif context.get('WEBAPPS') or force_webapps:
|
|
base_title = loc('Apps Marketplace')
|
|
else:
|
|
base_title = page_name(context['request'].APP)
|
|
return u'%s :: %s' % (title, base_title)
|
|
|
|
|
|
@register.function
|
|
@jinja2.contextfunction
|
|
def breadcrumbs(context, items=list(), add_default=True, crumb_size=40):
|
|
"""
|
|
show a list of breadcrumbs. If url is None, it won't be a link.
|
|
Accepts: [(url, label)]
|
|
"""
|
|
if add_default:
|
|
app = context['request'].APP
|
|
crumbs = [(urlresolvers.reverse('home'), page_name(app))]
|
|
else:
|
|
crumbs = []
|
|
|
|
# add user-defined breadcrumbs
|
|
if items:
|
|
try:
|
|
crumbs += items
|
|
except TypeError:
|
|
crumbs.append(items)
|
|
|
|
crumbs = [(url, truncate(label, crumb_size)) for (url, label) in crumbs]
|
|
c = {'breadcrumbs': crumbs}
|
|
t = env.get_template('amo/breadcrumbs.html').render(**c)
|
|
return jinja2.Markup(t)
|
|
|
|
|
|
@register.function
|
|
@jinja2.contextfunction
|
|
def impala_breadcrumbs(context, items=list(), add_default=True, crumb_size=40):
|
|
"""
|
|
show a list of breadcrumbs. If url is None, it won't be a link.
|
|
Accepts: [(url, label)]
|
|
"""
|
|
home = 'apps.home' if context.get('WEBAPPS') else 'home'
|
|
if add_default:
|
|
if context.get('WEBAPPS'):
|
|
base_title = _('Apps Marketplace')
|
|
else:
|
|
base_title = page_name(context['request'].APP)
|
|
crumbs = [(urlresolvers.reverse(home), base_title)]
|
|
else:
|
|
crumbs = []
|
|
|
|
# add user-defined breadcrumbs
|
|
if items:
|
|
try:
|
|
crumbs += items
|
|
except TypeError:
|
|
crumbs.append(items)
|
|
|
|
crumbs = [(url, truncate(label, crumb_size)) for (url, label) in crumbs]
|
|
c = {'breadcrumbs': crumbs, 'has_home': add_default}
|
|
t = env.get_template('amo/impala/breadcrumbs.html').render(**c)
|
|
return jinja2.Markup(t)
|
|
|
|
|
|
@register.filter
|
|
def json(s):
|
|
return jsonlib.dumps(s)
|
|
|
|
|
|
@register.filter
|
|
def absolutify(url):
|
|
"""Takes a URL and prepends the SITE_URL"""
|
|
if url.startswith('http'):
|
|
return url
|
|
else:
|
|
return settings.SITE_URL + url
|
|
|
|
|
|
@register.filter
|
|
def strip_controls(s):
|
|
"""
|
|
Strips control characters from a string.
|
|
"""
|
|
# Translation table of control characters.
|
|
control_trans = dict((n, None) for n in xrange(32) if n not in [10, 13])
|
|
rv = unicode(s).translate(control_trans)
|
|
return jinja2.Markup(rv) if isinstance(s, jinja2.Markup) else rv
|
|
|
|
|
|
@register.filter
|
|
def strip_html(s, just_kidding=False):
|
|
"""Strips HTML. Confirm lets us opt out easily."""
|
|
if just_kidding:
|
|
return s
|
|
|
|
if not s:
|
|
return ''
|
|
else:
|
|
s = re.sub(r'<.*?>', '', smart_unicode(s, errors='ignore'))
|
|
return re.sub(r'<.*?>', '', s)
|
|
|
|
|
|
@register.filter
|
|
def external_url(url):
|
|
"""Bounce a URL off outgoing.mozilla.org."""
|
|
return urlresolvers.get_outgoing_url(unicode(url))
|
|
|
|
|
|
@register.filter
|
|
def shuffle(sequence):
|
|
"""Shuffle a sequence."""
|
|
random.shuffle(sequence)
|
|
return sequence
|
|
|
|
|
|
@register.function
|
|
def license_link(license):
|
|
"""Link to a code license, incl. icon where applicable."""
|
|
if not license:
|
|
return ''
|
|
if not license.builtin:
|
|
return _('Custom License')
|
|
|
|
t = env.get_template('amo/license_link.html').render({'license': license})
|
|
return jinja2.Markup(t)
|
|
|
|
|
|
@register.function
|
|
def field(field, label=None, **attrs):
|
|
if label is not None:
|
|
field.label = label
|
|
# HTML from Django is already escaped.
|
|
return jinja2.Markup(u'%s<p>%s%s</p>' %
|
|
(field.errors, field.label_tag(),
|
|
field.as_widget(attrs=attrs)))
|
|
|
|
|
|
@register.inclusion_tag('amo/category-arrow.html')
|
|
@jinja2.contextfunction
|
|
def category_arrow(context, key, prefix):
|
|
d = dict(context.items())
|
|
d.update(key=key, prefix=prefix)
|
|
return d
|
|
|
|
|
|
@register.filter
|
|
def timesince(time):
|
|
ago = defaultfilters.timesince(time)
|
|
# L10n: relative time in the past, like '4 days ago'
|
|
return _(u'{0} ago').format(ago)
|
|
|
|
|
|
@register.inclusion_tag('amo/recaptcha.html')
|
|
@jinja2.contextfunction
|
|
def recaptcha(context, form):
|
|
d = dict(context.items())
|
|
d.update(form=form)
|
|
return d
|
|
|
|
|
|
@register.filter
|
|
def is_choice_field(value):
|
|
try:
|
|
return isinstance(value.field.widget, CheckboxInput)
|
|
except AttributeError:
|
|
pass
|
|
|
|
|
|
@register.inclusion_tag('amo/mobile/sort_by.html')
|
|
def mobile_sort_by(base_url, options=None, selected=None, extra_sort_opts=None,
|
|
search_filter=None):
|
|
if search_filter:
|
|
selected = search_filter.field
|
|
options = search_filter.opts
|
|
if hasattr(search_filter, 'extras'):
|
|
options += search_filter.extras
|
|
if extra_sort_opts:
|
|
options_dict = dict(options + extra_sort_opts)
|
|
else:
|
|
options_dict = dict(options)
|
|
if selected in options_dict:
|
|
current = options_dict[selected]
|
|
else:
|
|
selected, current = options[0] # Default to the first option.
|
|
return locals()
|
|
|
|
|
|
@register.function
|
|
@jinja2.contextfunction
|
|
def media(context, url, key='MEDIA_URL'):
|
|
"""Get a MEDIA_URL link with a cache buster querystring."""
|
|
if url.endswith('.js'):
|
|
build = context['BUILD_ID_JS']
|
|
elif url.endswith('.css'):
|
|
build = context['BUILD_ID_CSS']
|
|
else:
|
|
build = context['BUILD_ID_IMG']
|
|
return context[key] + utils.urlparams(url, b=build)
|
|
|
|
|
|
@register.function
|
|
@jinja2.contextfunction
|
|
def static(context, url):
|
|
"""Get a STATIC_URL link with a cache buster querystring."""
|
|
return media(context, url, 'STATIC_URL')
|
|
|
|
|
|
@register.function
|
|
@jinja2.evalcontextfunction
|
|
def attrs(ctx, *args, **kw):
|
|
return jinja2.filters.do_xmlattr(ctx, dict(*args, **kw))
|
|
|
|
|
|
@register.function
|
|
@jinja2.contextfunction
|
|
def side_nav(context, addon_type, category=None):
|
|
app = context['request'].APP.id
|
|
cat = str(category.id) if category else 'all'
|
|
return caching.cached(lambda: _side_nav(context, addon_type, category),
|
|
'side-nav-%s-%s-%s' % (app, addon_type, cat))
|
|
|
|
|
|
def _side_nav(context, addon_type, cat):
|
|
# Prevent helpers generating circular imports.
|
|
from addons.models import Category, AddonType
|
|
request = context['request']
|
|
qs = Category.objects.filter(weight__gte=0)
|
|
if addon_type != amo.ADDON_WEBAPP:
|
|
qs = qs.filter(application=request.APP.id)
|
|
sort_key = attrgetter('weight', 'name')
|
|
categories = sorted(qs.filter(type=addon_type), key=sort_key)
|
|
if cat:
|
|
base_url = cat.get_url_path()
|
|
else:
|
|
base_url = AddonType(addon_type).get_url_path()
|
|
ctx = dict(request=request, base_url=base_url, categories=categories,
|
|
addon_type=addon_type, amo=amo)
|
|
return jinja2.Markup(env.get_template('amo/side_nav.html').render(ctx))
|
|
|
|
|
|
@register.function
|
|
@jinja2.contextfunction
|
|
def site_nav(context):
|
|
app = context['request'].APP.id
|
|
return caching.cached(lambda: _site_nav(context), 'site-nav-%s' % app)
|
|
|
|
|
|
def _site_nav(context):
|
|
# Prevent helpers from generating circular imports.
|
|
from addons.models import Category
|
|
request = context['request']
|
|
types = amo.ADDON_EXTENSION, amo.ADDON_PERSONA, amo.ADDON_THEME
|
|
qs = Category.objects.filter(application=request.APP.id, weight__gte=0,
|
|
type__in=types)
|
|
groups = utils.sorted_groupby(qs, key=attrgetter('type'))
|
|
cats = dict((key, sorted(cs, key=attrgetter('weight', 'name')))
|
|
for key, cs in groups)
|
|
ctx = dict(request=request, amo=amo,
|
|
extensions=cats.get(amo.ADDON_EXTENSION, []),
|
|
personas=cats.get(amo.ADDON_PERSONA, []),
|
|
themes=cats.get(amo.ADDON_THEME, []))
|
|
return jinja2.Markup(env.get_template('amo/site_nav.html').render(ctx))
|
|
|
|
|
|
@register.filter
|
|
def premium_text(type):
|
|
return amo.ADDON_PREMIUM_TYPES[type]
|
|
|
|
|
|
@register.function
|
|
def loc(s):
|
|
"""A noop function for strings that are not ready to be localized."""
|
|
return strip_whitespace(s)
|
|
|
|
|
|
@register.function
|
|
def site_event_type(type):
|
|
return amo.SITE_EVENT_CHOICES[type]
|