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 = """%s""" % (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) @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
%s%s
' % (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]