Implement Django 1.8 compatibility.

* Get rid of addons.query - this will need some benchmarking
   but it was super ugly so I removed it
 * Make sure email fields are using 75 char maxlength (original default)
 * Add replacement for shortcuts.render
 * various stuff along the way...
 * Fix zadmin render helper
 * Less django.shortcuts:render, more our own for compat reasons.
 * Fix dynamic endpoints compat
 * Fix render support for blocklist
 * Save translated fields before we actually save the instance.
   This avoids getting caught by django's sanity check that avoids
   an instance being saved with unsaved related fields set.
 * Fix fix_let_scope_bustage management command
 * Fix another horribly wrong select_related statement
 * use django UUIDField where possible
 * Fix password reset email rendering
 * Fix slugify to work with translation objects
 * Fix stats csv test with updated cache-control header
 * Accept both, verbose and hex version of rss keys
 * Add migration to normalize devhub rss keys
 * Support both formats with and without microseconds for handle_date
 * Copy over render_to_string from jingo and fix it.
 * Unify usage of smart/force text/bytes, also to make sure 'Translation' objects are passed through properly.
 * Make translation model json renderable
 * Unify JSONEncoder usage
This commit is contained in:
Christopher Grebs 2016-07-13 13:56:43 +02:00
Родитель f2012f52ae
Коммит 7c560174c6
83 изменённых файлов: 499 добавлений и 552 удалений

Просмотреть файл

@ -3,7 +3,6 @@
## Forked.
-e git+https://github.com/EnTeQuAk/django-cache-machine@1cac98a3e1dcc3cb654665cc67b6b15ab60d62c4#egg=django-cache-machine
-e git+https://github.com/andymckay/django-uuidfield.git@029dd1263794ec36c327617cd6c2346da81c8c33#egg=django-uuidfield
## Out of date or not sure on pypi.
-e git+https://github.com/jbalogh/django-mobility.git@e2b60a1f96e4c4aed736395c01bf707e969d8e83#egg=django-mobility

Просмотреть файл

@ -501,7 +501,7 @@ class TestRegisterUser(TestCase):
assert not user.has_usable_password()
@override_settings(FXA_CONFIG={'default': FXA_CONFIG})
@override_settings(FXA_CONFIG={'default': FXA_CONFIG, 'internal': FXA_CONFIG})
class BaseAuthenticationView(APITestCase, InitializeSessionMixin):
def setUp(self):

Просмотреть файл

@ -1,5 +1,4 @@
from django.db.transaction import non_atomic_requests
from django.shortcuts import render
from django.utils.translation import (
ugettext as _, ugettext_lazy as _lazy, pgettext_lazy)
@ -9,6 +8,7 @@ import jinja2
from olympia import amo
from olympia.amo.helpers import urlparams
from olympia.amo.urlresolvers import reverse
from olympia.amo.utils import render
@jinja2.contextfunction

Просмотреть файл

@ -5,6 +5,7 @@ from olympia.files.tasks import fix_let_scope_bustage_in_addons
class Command(BaseCommand):
use_argparse = False
args = '<addon_id addon_id ...>'
help = """Fix the "let scope bustage" (bug 1224686) for a list of add-ons.
Only the last version of each add-on will be fixed, and its version bumped."""

Просмотреть файл

@ -32,9 +32,9 @@ from olympia.addons.utils import (
from olympia.amo import helpers
from olympia.amo.decorators import use_master, write
from olympia.amo.utils import (
attach_trans_dict, cache_ns_key, chunked, JSONEncoder,
attach_trans_dict, cache_ns_key, chunked,
no_translation, send_mail, slugify, sorted_groupby, timer, to_language,
urlparams, find_language)
urlparams, find_language, AMOJSONEncoder)
from olympia.amo.urlresolvers import get_outgoing_url, reverse
from olympia.constants.categories import CATEGORIES, CATEGORIES_BY_ID
from olympia.files.models import File
@ -48,7 +48,7 @@ from olympia.users.models import UserForeignKey, UserProfile
from olympia.versions.compare import version_int
from olympia.versions.models import inherit_nomination, Version
from . import query, signals
from . import signals
log = commonware.log.getLogger('z.addons')
@ -141,7 +141,6 @@ class AddonManager(ManagerBase):
def get_queryset(self):
qs = super(AddonManager, self).get_queryset()
qs = qs._clone(klass=query.IndexQuerySet)
if not self.include_deleted:
qs = qs.exclude(status=amo.STATUS_DELETED)
if not self.include_unlisted:
@ -1747,7 +1746,7 @@ class Persona(caching.CachingMixin, models.Model):
def json_data(self):
"""Persona JSON Data for Browser/extension preview."""
return json.dumps(self.theme_data,
separators=(',', ':'), cls=JSONEncoder)
separators=(',', ':'), cls=AMOJSONEncoder)
def authors_other_addons(self, app=None):
"""

Просмотреть файл

@ -1,132 +0,0 @@
from django.db import models
from django.db.models.sql import compiler
import caching.base as caching
class IndexQuerySet(caching.CachingQuerySet):
def with_index(self, **kw):
"""
Suggest indexes that should be used with this query as key-value pairs.
qs.with_index(t1='xxx') => INNER JOIN t1 USE INDEX (`xxx`)
"""
q = self._clone()
if not isinstance(q.query, IndexQuery):
q.query = self.query.clone(IndexQuery)
q.query.index_map.update(kw)
return q
def fetch_missed(self, pks):
# Remove the indexes before doing the id query.
if hasattr(self.query, 'index_map'):
index_map = self.query.index_map
self.query.index_map = {}
rv = super(IndexQuerySet, self).fetch_missed(pks)
self.query.index_map = index_map
return rv
else:
return super(IndexQuerySet, self).fetch_missed(pks)
class IndexQuery(models.query.sql.Query):
"""
Extends sql.Query to make it possible to specify indexes to use.
"""
def clone(self, klass=None, **kwargs):
# Maintain index_map across clones.
c = super(IndexQuery, self).clone(klass, **kwargs)
c.index_map = dict(self.index_map)
return c
def get_compiler(self, using=None, connection=None):
# Call super to figure out using and connection.
c = super(IndexQuery, self).get_compiler(using, connection)
return IndexCompiler(self, c.connection, c.using)
def _setup_query(self):
if not hasattr(self, 'index_map'):
self.index_map = {}
def get_count(self, using):
# Don't use the index for counts, it's slower.
index_map = self.index_map
self.index_map = {}
count = super(IndexQuery, self).get_count(using)
self.index_map = index_map
return count
class IndexCompiler(compiler.SQLCompiler):
def get_from_clause(self):
"""
Returns a list of strings that are joined together to go after the
"FROM" part of the query, as well as a list any extra parameters that
need to be included. Sub-classes, can override this to create a
from-clause via a "select".
This should only be called after any SQL construction methods that
might change the tables we need. This means the select columns and
ordering must be done first.
"""
result = []
qn = self.quote_name_unless_alias
qn2 = self.connection.ops.quote_name
index_map = self.query.index_map
first = True
from_params = []
for alias in self.query.tables:
if not self.query.alias_refcount[alias]:
continue
try:
name, alias, join_type, lhs, join_cols, _, join_field = (
self.query.alias_map[alias])
except KeyError:
# Extra tables can end up in self.tables, but not in the
# alias_map if they aren't in a join. That's OK. We skip them.
continue
alias_str = (alias != name and ' %s' % alias or '')
# jbalogh wuz here. #
if name in index_map:
use_index = 'USE INDEX (%s)' % qn(index_map[name])
else:
use_index = ''
if join_type and not first:
extra_cond = join_field.get_extra_restriction(
self.query.where_class, alias, lhs)
if extra_cond:
extra_sql, extra_params = extra_cond.as_sql(
qn, self.connection)
extra_sql = 'AND (%s)' % extra_sql
from_params.extend(extra_params)
else:
extra_sql = ""
result.append('%s %s%s %s ON (' % (join_type, qn(name),
alias_str, use_index))
for index, (lhs_col, rhs_col) in enumerate(join_cols):
if index != 0:
result.append(' AND ')
result.append(
'%s.%s = %s.%s' %
(qn(lhs), qn2(lhs_col), qn(alias), qn2(rhs_col)))
result.append('%s)' % extra_sql)
else:
connector = connector = '' if first else ', '
result.append('%s%s%s %s' % (connector, qn(name), alias_str,
use_index))
# jbalogh out. #
first = False
for t in self.query.extra_tables:
alias, unused = self.query.table_alias(t)
# Only add the alias if it's not already present (the table_alias()
# calls increments the refcount, so an alias refcount of one means
# this is the only reference.
if (alias not in self.query.alias_map or
self.query.alias_refcount[alias] == 1):
connector = not first and ', ' or ''
result.append('%s%s' % (connector, qn(alias)))
first = False
return result, from_params

Просмотреть файл

@ -9,8 +9,7 @@ from operator import attrgetter
from django import http
from django.conf import settings
from django.db.transaction import non_atomic_requests
from django.shortcuts import (
get_list_or_404, get_object_or_404, redirect, render)
from django.shortcuts import get_list_or_404, get_object_or_404, redirect
from django.utils.translation import ugettext as _
from django.views.decorators.cache import cache_control
from django.views.decorators.csrf import csrf_exempt
@ -36,7 +35,7 @@ from olympia import amo
from olympia.amo import messages
from olympia.amo.decorators import post_required
from olympia.amo.forms import AbuseForm
from olympia.amo.utils import randslice
from olympia.amo.utils import randslice, render
from olympia.amo.models import manual_order
from olympia.amo import urlresolvers
from olympia.amo.urlresolvers import reverse
@ -291,27 +290,22 @@ class BaseFilter(object):
return self.base_queryset.top_paid(listed=False)
def filter_popular(self):
return (self.base_queryset.order_by('-weekly_downloads')
.with_index(addons='downloads_type_idx'))
return self.base_queryset.order_by('-weekly_downloads')
def filter_downloads(self):
return self.filter_popular()
def filter_users(self):
return (self.base_queryset.order_by('-average_daily_users')
.with_index(addons='adus_type_idx'))
return self.base_queryset.order_by('-average_daily_users')
def filter_created(self):
return (self.base_queryset.order_by('-created')
.with_index(addons='created_type_idx'))
return self.base_queryset.order_by('-created')
def filter_updated(self):
return (self.base_queryset.order_by('-last_updated')
.with_index(addons='last_updated_type_idx'))
return self.base_queryset.order_by('-last_updated')
def filter_rating(self):
return (self.base_queryset.order_by('-bayesian_rating')
.with_index(addons='rating_type_idx'))
return self.base_queryset.order_by('-bayesian_rating')
def filter_hotness(self):
return self.base_queryset.order_by('-hotness')

Просмотреть файл

@ -10,11 +10,11 @@ from django.db import connection, transaction
import commonware.log
from olympia.amo import get_user, set_user
from olympia.accounts.utils import redirect_for_login
from olympia.users.utils import get_task_user
from . import models as context
from .utils import JSONEncoder
from olympia.accounts.utils import redirect_for_login
from .utils import AMOJSONEncoder
task_log = commonware.log.getLogger('z.task')
@ -107,7 +107,7 @@ def json_response(response, has_trans=False, status_code=200):
then use the json_view decorator.
"""
if has_trans:
response = json.dumps(response, cls=JSONEncoder)
response = json.dumps(response, cls=AMOJSONEncoder)
else:
response = json.dumps(response)
return http.HttpResponse(response,

Просмотреть файл

@ -12,7 +12,7 @@ from django.core.exceptions import ObjectDoesNotExist
from django.forms import CheckboxInput
from django.utils.translation import (
ugettext as _, trim_whitespace, to_locale, get_language)
from django.utils.encoding import smart_unicode
from django.utils.encoding import force_text
from django.utils.html import format_html
from django.template import defaultfilters
@ -232,7 +232,7 @@ def page_name(app=None):
@register.function
@jinja2.contextfunction
def page_title(context, title):
title = smart_unicode(title)
title = force_text(title)
base_title = page_name(context['request'].APP)
# The following line doesn't use string formatting because we want to
# preserve the type of `title` in case it's a jinja2 `Markup` (safe,
@ -326,7 +326,7 @@ def strip_html(s, just_kidding=False):
if not s:
return ''
else:
s = re.sub(r'&lt;.*?&gt;', '', smart_unicode(s, errors='ignore'))
s = re.sub(r'&lt;.*?&gt;', '', force_text(s, errors='ignore'))
return re.sub(r'<.*?>', '', s)
@ -587,7 +587,7 @@ def inline_css(bundle, media=False, debug=None):
Added: turns relative links to absolute ones using STATIC_URL.
"""
if debug is None:
debug = getattr(settings, 'TEMPLATE_DEBUG', False)
debug = getattr(settings, 'DEBUG', False)
if debug:
items = [_get_compiled_css_url(i)

Просмотреть файл

@ -14,14 +14,14 @@ from django.contrib.sessions.middleware import SessionMiddleware
from django.core.urlresolvers import is_valid_path
from django.http import HttpResponseRedirect, HttpResponsePermanentRedirect
from django.middleware import common
from django.shortcuts import render
from django.utils.cache import patch_vary_headers, patch_cache_control
from django.utils.encoding import iri_to_uri, smart_str
from django.utils.encoding import iri_to_uri, force_bytes
from django.utils.translation import activate
import MySQLdb as mysql
from olympia import amo
from olympia.amo.utils import render
from . import urlresolvers
from .helpers import urlparams
@ -58,7 +58,7 @@ class LocaleAndAppURLMiddleware(object):
# from query params so we don't have an infinite loop.
prefixer.locale = ''
new_path = prefixer.fix(prefixer.shortened_path)
query = dict((smart_str(k), request.GET[k]) for k in request.GET)
query = dict((force_bytes(k), request.GET[k]) for k in request.GET)
query.pop('lang')
return redirect_type(urlparams(new_path, **query))

Просмотреть файл

@ -3,7 +3,8 @@ import threading
from django.conf import settings
from django.db import models, transaction
from django.utils import encoding, translation
from django.utils import translation
from django.utils.encoding import force_text
import caching.base
import elasticsearch
@ -11,6 +12,7 @@ import multidb.pinning
from django_statsd.clients import statsd
import olympia.lib.queryset_transform as queryset_transform
from olympia.translations.hold import save_translations
from . import search
@ -41,31 +43,6 @@ def skip_cache():
_locals.skip_cache = old
# This is sadly a copy and paste of annotate to get around this
# ticket http://code.djangoproject.com/ticket/14707
def annotate(self, *args, **kwargs):
for arg in args:
if arg.default_alias in kwargs:
raise ValueError("The %s named annotation conflicts with the "
"default name for another annotation."
% arg.default_alias)
kwargs[arg.default_alias] = arg
obj = self._clone()
obj._setup_aggregate_query(kwargs.keys())
# Add the aggregates to the query
for (alias, aggregate_expr) in kwargs.items():
obj.query.add_aggregate(
aggregate_expr, self.model, alias, is_summary=False)
return obj
models.query.QuerySet.annotate = annotate
class TransformQuerySet(queryset_transform.TransformQuerySet):
def pop_transforms(self):
@ -367,7 +344,7 @@ class ModelBase(SearchMixin, caching.base.CachingMixin, models.Model):
need it and it avoids invalidation bugs with FETCH_BY_ID.
"""
key_parts = ('o', cls._meta, pk, 'default')
return ':'.join(map(encoding.smart_unicode, key_parts))
return ':'.join(map(force_text, key_parts))
def reload(self):
"""Reloads the instance from the database."""
@ -414,6 +391,15 @@ class ModelBase(SearchMixin, caching.base.CachingMixin, models.Model):
models.signals.post_save.send(sender=cls, instance=self,
created=False)
def save(self, **kwargs):
# Unfortunately we have to save our translations before we call `save`
# since Django verifies m2n relations with unsaved parent relations
# and throws an error.
# https://docs.djangoproject.com/en/1.9/topics/db/examples/one_to_one/
if hasattr(self._meta, 'translated_fields'):
save_translations(id(self))
return super(ModelBase, self).save(**kwargs)
def manual_order(qs, pks, pk_name='id'):
"""

Просмотреть файл

@ -7,7 +7,7 @@ require leading directories to exist. The default Django file system storage
"""
from django.core.files.storage import default_storage
from django.utils.encoding import smart_str
from django.utils.encoding import force_bytes
DEFAULT_CHUNK_SIZE = 64 * 2 ** 10 # 64kB
@ -34,8 +34,8 @@ def walk_storage(path, topdown=True, onerror=None, followlinks=False,
new_roots = []
for root in roots:
dirs, files = storage.listdir(root)
files = [smart_str(f) for f in files]
dirs = [smart_str(d) for d in dirs]
files = [force_bytes(f) for f in files]
dirs = [force_bytes(d) for d in dirs]
yield root, dirs, files
for dn in dirs:
new_roots.append('%s/%s' % (root, dn))

Просмотреть файл

@ -55,7 +55,7 @@ from olympia.users.models import UserProfile
from . import dynamic_urls
# We might now have gettext available in jinja2.env.globals when running tests.
# We might not have gettext available in jinja2.env.globals when running tests.
# It's only added to the globals when activating a language (which
# is usually done in the middlewares). During tests, however, we might not be
# running middlewares, and thus not activating a language, and thus not
@ -573,7 +573,10 @@ def _get_created(created):
random.randint(0, 59)) # Seconds
def addon_factory(status=amo.STATUS_PUBLIC, version_kw={}, file_kw={}, **kw):
def addon_factory(
status=amo.STATUS_PUBLIC, version_kw=None, file_kw=None, **kw):
version_kw = version_kw or {}
# Disconnect signals until the last save.
post_save.disconnect(addon_update_search_index, sender=Addon,
dispatch_uid='addons.search.index')
@ -606,32 +609,38 @@ def addon_factory(status=amo.STATUS_PUBLIC, version_kw={}, file_kw={}, **kw):
# Save 1.
if type_ == amo.ADDON_PERSONA:
# Personas need to start life as an extension for versioning.
a = Addon.objects.create(type=amo.ADDON_EXTENSION, **kwargs)
addon = Addon.objects.create(type=amo.ADDON_EXTENSION, **kwargs)
else:
a = Addon.objects.create(type=type_, **kwargs)
version = version_factory(file_kw, addon=a, **version_kw) # Save 2.
a.update_version()
a.status = status
addon = Addon.objects.create(type=type_, **kwargs)
# Save 2.
version = version_factory(file_kw, addon=addon, **version_kw)
addon.update_version()
addon.status = status
if type_ == amo.ADDON_PERSONA:
a.type = type_
persona_id = persona_id if persona_id is not None else a.id
Persona.objects.create(addon=a, popularity=a.weekly_downloads,
persona_id=persona_id) # Save 3.
addon.type = type_
persona_id = persona_id if persona_id is not None else addon.id
# Save 3.
Persona.objects.create(
addon=addon, popularity=addon.weekly_downloads,
persona_id=persona_id)
for tag in tags:
Tag(tag_text=tag).save_tag(a)
Tag(tag_text=tag).save_tag(addon)
# Put signals back.
post_save.connect(addon_update_search_index, sender=Addon,
dispatch_uid='addons.search.index')
a.save() # Save 4.
# Save 4.
addon.save()
if 'nomination' in version_kw:
# If a nomination date was set on the version, then it might have been
# erased at post_save by addons.models.watch_status()
version.save()
return a
return addon
def collection_factory(**kw):
@ -700,7 +709,7 @@ def user_factory(**kw):
return user
def version_factory(file_kw={}, **kw):
def version_factory(file_kw=None, **kw):
# We can't create duplicates of AppVersions, so make sure the versions are
# not already created in fixtures (use fake versions).
min_app_version = kw.pop('min_app_version', '4.0.99')
@ -718,6 +727,7 @@ def version_factory(file_kw={}, **kw):
ApplicationsVersions.objects.get_or_create(application=application,
version=v, min=av_min,
max=av_max)
file_kw = file_kw or {}
file_factory(version=v, **file_kw)
return v
@ -740,8 +750,15 @@ class ESTestCase(TestCase):
@classmethod
def setUpClass(cls):
cls.es = amo_search.get_es(timeout=settings.ES_TIMEOUT)
cls._SEARCH_ANALYZER_MAP = amo.SEARCH_ANALYZER_MAP
amo.SEARCH_ANALYZER_MAP = {
'english': ['en-us'],
'spanish': ['es'],
}
super(ESTestCase, cls).setUpClass()
@classmethod
def setUpTestData(cls):
try:
cls.es.cluster.health()
except Exception, e:
@ -751,11 +768,6 @@ class ESTestCase(TestCase):
list(e.args[1:]))
raise
cls._SEARCH_ANALYZER_MAP = amo.SEARCH_ANALYZER_MAP
amo.SEARCH_ANALYZER_MAP = {
'english': ['en-us'],
'spanish': ['es'],
}
aliases_and_indexes = set(settings.ES_INDEXES.values() +
cls.es.indices.get_aliases().keys())
for key in aliases_and_indexes:
@ -778,6 +790,7 @@ class ESTestCase(TestCase):
'alias': settings.ES_INDEXES['stats']}}
]
cls.es.indices.update_aliases({'actions': actions})
super(ESTestCase, cls).setUpTestData()
@classmethod
def tearDownClass(cls):
@ -813,8 +826,8 @@ class ESTestCase(TestCase):
class ESTestCaseWithAddons(ESTestCase):
@classmethod
def setUp(cls):
super(ESTestCaseWithAddons, cls).setUpClass()
def setUpTestData(cls):
super(ESTestCaseWithAddons, cls).setUpTestData()
# Load the fixture here, to not be overloaded by a child class'
# fixture attribute.
call_command('loaddata', 'addons/base_es')
@ -827,7 +840,7 @@ class ESTestCaseWithAddons(ESTestCase):
cls.refresh()
@classmethod
def tearDown(cls):
def tearDownClass(cls):
try:
unindex_addons([a.id for a in cls._addons])
cls._addons = []
@ -908,14 +921,13 @@ class WithDynamicEndpoints(TestCase):
is_class = False
if is_class:
view = view.as_view()
dynamic_urls.urlpatterns = django_urls.patterns(
'',
django_urls.url(url_regex, view),
)
dynamic_urls.urlpatterns = [django_urls.url(url_regex, view)]
self.addCleanup(self._clean_up_dynamic_urls)
def _clean_up_dynamic_urls(self):
dynamic_urls.urlpatterns = None
dynamic_urls.urlpatterns = []
def get_temp_filename():

Просмотреть файл

@ -1,3 +1,3 @@
# In an effort to make the tests more readable, this is populated at the
# start of each test. See amo/tests/__init__.py:WithDynamicEndpoints
urlpatterns = None
urlpatterns = []

Просмотреть файл

@ -1,10 +1,10 @@
import jingo
import mock
from django.shortcuts import render
import pytest
from olympia.amo.utils import render
pytestmark = pytest.mark.django_db

Просмотреть файл

@ -23,7 +23,7 @@ class HttpHttpsOnlyURLFieldTestCase(TestCase):
self.field.clean(u'ftps://foo.com/')
def test_no_scheme_assumes_http(self):
assert self.field.clean(u'foo.com') == 'http://foo.com/'
assert self.field.clean(u'foo.com') == 'http://foo.com'
def test_http_scheme(self):
assert self.field.clean(u'http://foo.com/') == u'http://foo.com/'

Просмотреть файл

@ -8,7 +8,7 @@ from django.conf import settings
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test.client import RequestFactory
from django.test.utils import override_settings
from django.utils import encoding
from django.utils.encoding import force_bytes
import jingo
import pytest
@ -78,7 +78,7 @@ def test_page_title():
request.APP = amo.FIREFOX
s = render('{{ page_title(x) }}',
{'request': request,
'x': encoding.smart_str(u'\u05d0\u05d5\u05e1\u05e3')})
'x': force_bytes(u'\u05d0\u05d5\u05e1\u05e3')})
def test_page_title_markup():

Просмотреть файл

@ -11,7 +11,7 @@ import jinja2
from django.conf import settings
from django.core import urlresolvers
from django.utils import encoding
from django.utils.encoding import force_bytes
from django.utils.translation.trans_real import parse_accept_lang_header
from olympia import amo
@ -185,7 +185,7 @@ def get_outgoing_url(url):
url_netloc in settings.REDIRECT_URL_WHITELIST):
return url
url = encoding.smart_str(jinja2.utils.Markup(url).unescape())
url = force_bytes(jinja2.utils.Markup(url).unescape())
sig = hmac.new(settings.REDIRECT_SECRET_KEY,
msg=url, digestmod=hashlib.sha256).hexdigest()
# Let '&=' through so query params aren't escaped. We probably shouldn't

Просмотреть файл

@ -2,7 +2,6 @@ import calendar
import collections
import contextlib
import datetime
import decimal
import errno
import functools
import itertools
@ -18,19 +17,17 @@ import urllib
import urlparse
import django.core.mail
from django import http
from django.http import HttpResponse
from django.conf import settings
from django.core import paginator
from django.core.cache import cache
from django.core.files.storage import (FileSystemStorage,
default_storage as storage)
from django.core.serializers import json as json_serializer
from django.core.validators import validate_slug, ValidationError
from django.forms.fields import Field
from django.template import Context, loader
from django.utils import translation
from django.utils.encoding import smart_str, smart_unicode
from django.utils.functional import Promise
from django.utils.encoding import force_bytes, force_text
from django.utils.http import urlunquote
import bleach
@ -41,11 +38,11 @@ from babel import Locale
from django_statsd.clients import statsd
from easy_thumbnails import processors
from html5lib.serializer.htmlserializer import HTMLSerializer
from jingo import get_env
from jingo import get_env, get_standard_processors
from PIL import Image
from validator import unicodehelper
from rest_framework.utils.encoders import JSONEncoder
from olympia import amo
from olympia.amo import search
from olympia.amo import ADDON_ICON_SIZES
from olympia.amo.urlresolvers import linkify_with_outgoing, reverse
@ -56,6 +53,32 @@ from olympia.users.utils import UnsubscribeCode
from . import logger_log as log
def render_to_string(request, template, context=None):
"""Render a template into a string.
This is copied and fixed from jingo.
"""
def get_context():
c = {}
for processor in get_standard_processors():
c.update(processor(request))
if context is not None:
c.update(context.copy())
return c
# If it's not a Template, it must be a path to be loaded.
if not isinstance(template, jinja2.environment.Template):
template = get_env().get_template(template)
return template.render(get_context())
def render(request, template, ctx=None, status=None, content_type=None):
rendered = render_to_string(request, template, ctx)
return HttpResponse(rendered, status=status, content_type=content_type)
def days_ago(n):
return datetime.datetime.now() - datetime.timedelta(days=n)
@ -72,7 +95,7 @@ def urlparams(url_, hash=None, **query):
# Use dict(parse_qsl) so we don't get lists of values.
q = url.query
query_dict = dict(urlparse.parse_qsl(smart_str(q))) if q else {}
query_dict = dict(urlparse.parse_qsl(force_bytes(q))) if q else {}
query_dict.update((k, v) for k, v in query.items())
query_string = urlencode([(k, urlunquote(v)) for k, v in query_dict.items()
@ -317,31 +340,6 @@ def send_html_mail_jinja(subject, html_template, text_template, context,
return msg
class JSONEncoder(json_serializer.DjangoJSONEncoder):
def default(self, obj):
from olympia.versions.models import ApplicationsVersions
unicodable = (Translation, Promise)
if isinstance(obj, unicodable):
return unicode(obj)
if isinstance(obj, ApplicationsVersions):
return {unicode(amo.APP_IDS[obj.application].pretty): {
'min': unicode(obj.min), 'max': unicode(obj.max)}}
return super(JSONEncoder, self).default(obj)
class DecimalJSONEncoder(json_serializer.DjangoJSONEncoder):
def default(self, obj):
if isinstance(obj, decimal.Decimal):
return float(obj)
return super(DecimalJSONEncoder, self).default(obj)
def chunked(seq, n):
"""
Yield successive n-sized chunks from seq.
@ -365,7 +363,7 @@ def urlencode(items):
try:
return urllib.urlencode(items)
except UnicodeEncodeError:
return urllib.urlencode([(k, smart_str(v)) for k, v in items])
return urllib.urlencode([(k, force_bytes(v)) for k, v in items])
def randslice(qs, limit, exclude=None):
@ -397,7 +395,8 @@ def slugify(s, ok=SLUG_OK, lower=True, spaces=False, delimiter='-'):
# L and N signify letter/number.
# http://www.unicode.org/reports/tr44/tr44-4.html#GC_Values_Table
rv = []
for c in smart_unicode(s):
for c in force_text(s):
cat = unicodedata.category(c)[0]
if cat in 'LN' or c in ok:
rv.append(c)
@ -591,7 +590,7 @@ def get_locale_from_lang(lang):
return Locale.parse(translation.to_locale(lang))
class HttpResponseSendFile(http.HttpResponse):
class HttpResponseSendFile(HttpResponse):
def __init__(self, request, path, content=None, status=None,
content_type='application/octet-stream', etag=None):
@ -689,8 +688,8 @@ class ESPaginator(paginator.Paginator):
def smart_path(string):
"""Returns a string you can pass to path.path safely."""
if os.path.supports_unicode_filenames:
return smart_unicode(string)
return smart_str(string)
return force_text(string)
return force_bytes(string)
@contextlib.contextmanager
@ -714,7 +713,7 @@ def escape_all(v, linkify_only_full=False):
"""
if isinstance(v, basestring):
v = jinja2.escape(smart_unicode(v))
v = jinja2.escape(force_text(v))
v = linkify_with_outgoing(v, only_full=linkify_only_full)
return v
elif isinstance(v, list):
@ -724,7 +723,7 @@ def escape_all(v, linkify_only_full=False):
for k, lv in v.iteritems():
v[k] = escape_all(lv, linkify_only_full=linkify_only_full)
elif isinstance(v, Translation):
v = jinja2.escape(smart_unicode(v.localized_string))
v = jinja2.escape(force_text(v))
return v
@ -775,8 +774,8 @@ class LocalFileStorage(FileSystemStorage):
def _smart_path(self, string):
if os.path.supports_unicode_filenames:
return smart_unicode(string)
return smart_str(string)
return force_text(string)
return force_bytes(string)
def translations_for_field(field):
@ -928,3 +927,10 @@ def utc_millesecs_from_epoch(for_datetime=None):
if not for_datetime:
for_datetime = datetime.datetime.now()
return calendar.timegm(for_datetime.utctimetuple()) * 1000
class AMOJSONEncoder(JSONEncoder):
def default(self, obj):
if isinstance(obj, Translation):
return force_text(obj)
return super(AMOJSONEncoder, self).default(obj)

Просмотреть файл

@ -5,12 +5,12 @@ from django import http
from django.conf import settings
from django.db.transaction import non_atomic_requests
from django.http import HttpResponse
from django.shortcuts import render
from django.views.decorators.cache import never_cache
from django_statsd.clients import statsd
from olympia import amo, legacy_api
from olympia.amo.utils import render
from . import monitors

Просмотреть файл

@ -60,6 +60,11 @@ class BaseESSerializer(ModelSerializer):
def handle_date(self, value):
if not value:
return None
# Don't be picky about microseconds here. We get them some time
# so we have to support them. So let's strip microseconds and handle
# the datetime in a unified way.
value = value.partition('.')[0]
return datetime.strptime(value, u'%Y-%m-%dT%H:%M:%S')
def _attach_fields(self, obj, data, field_names):

Просмотреть файл

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
from datetime import datetime
from olympia.addons.models import Addon
from olympia.api.serializers import BaseESSerializer
class BasicSerializer(BaseESSerializer):
class Meta:
model = Addon
fields = ()
def test_handle_date_strips_microseconds():
serializer = BasicSerializer()
date = datetime.utcnow()
assert date.microsecond
assert (
serializer.handle_date(date.isoformat()) ==
date.replace(microsecond=0))

Просмотреть файл

@ -8,6 +8,7 @@ from olympia.applications.models import AppVersion
class Command(BaseCommand):
use_argparse = False
help = ('Add a new version of an application. Syntax: \n'
' ./manage.py addnewversion <application_name> <version>')
log = commonware.log.getLogger('z.appversions')

Просмотреть файл

@ -1,5 +1,4 @@
from django.db.transaction import non_atomic_requests
from django.shortcuts import render
from django.utils.translation import ugettext as _
import caching.base as caching
@ -7,6 +6,7 @@ import caching.base as caching
from olympia import amo
from olympia.amo.helpers import url, absolutify
from olympia.amo.feeds import NonAtomicFeed
from olympia.amo.utils import render
from .models import AppVersion

Просмотреть файл

@ -90,6 +90,8 @@ class CollectionManager(ManagerBase):
class Collection(ModelBase):
TYPE_CHOICES = amo.COLLECTION_CHOICES.items()
# TODO: Use models.UUIDField but it uses max_length=32 hex (no hyphen)
# uuids so needs some migration.
uuid = models.CharField(max_length=36, blank=True, unique=True)
name = TranslatedField(require_locale=False)
# nickname is deprecated. Use slug.

Просмотреть файл

@ -5,7 +5,7 @@ from django.core.cache import cache
from django.forms import ValidationError
import django.test
from django.utils.datastructures import MultiValueDict
from django.utils import encoding
from django.utils.encoding import force_bytes
import pytest
from mock import patch, Mock
@ -79,11 +79,11 @@ class HappyUnicodeClient(django.test.Client):
"""
def get(self, path_, *args, **kw):
path_ = encoding.smart_str(path_)
path_ = force_bytes(path_)
return super(HappyUnicodeClient, self).get(path_, *args, **kw)
def post(self, path_, *args, **kw):
path_ = encoding.smart_str(path_)
path_ = force_bytes(path_)
return super(HappyUnicodeClient, self).post(path_, *args, **kw)
# Add head, put, options, delete if you need them.

Просмотреть файл

@ -7,7 +7,7 @@ from django.core.cache import cache
from django.core.exceptions import PermissionDenied
from django.db.models import Q
from django.db.transaction import non_atomic_requests
from django.shortcuts import get_object_or_404, redirect, render
from django.shortcuts import get_object_or_404, redirect
from django.views.decorators.http import require_POST
from django.views.decorators.csrf import csrf_protect
from django.utils.translation import ugettext_lazy as _lazy, ugettext as _
@ -22,7 +22,7 @@ from olympia.amo.decorators import (
allow_mine, json_view, login_required, post_required, restricted_content,
write)
from olympia.amo.urlresolvers import reverse
from olympia.amo.utils import paginate, urlparams
from olympia.amo.utils import paginate, urlparams, render
from olympia.access import acl
from olympia.accounts.utils import redirect_for_login
from olympia.addons.models import Addon
@ -197,8 +197,7 @@ class CollectionAddonFilter(BaseFilter):
return order_by_translation(self.base_queryset, 'name')
def filter_popular(self):
return (self.base_queryset.order_by('-weekly_downloads')
.with_index(addons='downloads_type_idx'))
return self.base_queryset.order_by('-weekly_downloads')
@allow_mine

Просмотреть файл

@ -6,6 +6,7 @@ from .models import BlocklistApp, BlocklistPlugin
class BlocklistPluginForm(forms.ModelForm):
class Meta:
model = BlocklistPlugin
fields = '__all__'
def clean(self):
severity = self.cleaned_data.get('severity')
@ -21,6 +22,7 @@ class BlocklistPluginForm(forms.ModelForm):
class BlocklistAppForm(forms.ModelForm):
class Meta:
model = BlocklistApp
fields = '__all__'
def clean(self):
blthings = [self.cleaned_data.get('blitem'),

Просмотреть файл

@ -9,11 +9,11 @@ from django.core.cache import cache
from django.db.models import Q, signals as db_signals
from django.db.transaction import non_atomic_requests
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, render
from django.shortcuts import get_object_or_404
from django.utils.cache import patch_cache_control
from django.utils.encoding import smart_str
from django.utils.encoding import force_bytes
from olympia.amo.utils import sorted_groupby
from olympia.amo.utils import sorted_groupby, render
from olympia.versions.compare import version_int
from .models import (
@ -32,7 +32,7 @@ BlItem = collections.namedtuple('BlItem', 'rows os modified block_id prefs')
def blocklist(request, apiver, app, appver):
key = 'blocklist:%s:%s:%s' % (apiver, app, appver)
# Use md5 to make sure the memcached key is clean.
key = hashlib.md5(smart_str(key)).hexdigest()
key = hashlib.md5(force_bytes(key)).hexdigest()
cache.add('blocklist:keyversion', 1)
version = cache.get('blocklist:keyversion')
response = cache.get(key, version=version)

Просмотреть файл

@ -5,7 +5,7 @@ from django.conf import settings
from django.db.transaction import non_atomic_requests
from django.http import (Http404, HttpResponsePermanentRedirect,
HttpResponseRedirect)
from django.shortcuts import get_object_or_404, redirect, render
from django.shortcuts import get_object_or_404, redirect
from django.views.decorators.cache import cache_page
from django.utils.translation import ugettext_lazy as _lazy
@ -15,6 +15,7 @@ from mobility.decorators import mobile_template
from olympia import amo
from olympia.amo.models import manual_order
from olympia.amo.urlresolvers import reverse
from olympia.amo.utils import render
from olympia.addons.models import Addon, AddonCategory, Category, FrozenAddon
from olympia.addons.utils import get_featured_ids, get_creatured_ids
from olympia.addons.views import BaseFilter, ESBaseFilter

Просмотреть файл

@ -4,13 +4,13 @@ import re
from django import http
from django.db.models import Count
from django.db.transaction import non_atomic_requests
from django.shortcuts import redirect, render
from django.shortcuts import redirect
from django.views.decorators.csrf import csrf_exempt
from olympia import amo
from olympia.amo import utils as amo_utils
from olympia.addons.decorators import owner_or_unlisted_reviewer
from olympia.amo.utils import render, paginate
from olympia.amo.decorators import post_required
from olympia.addons.decorators import owner_or_unlisted_reviewer
from olympia.addons.models import Addon
from olympia.search.utils import floor_version
from olympia.versions.compare import version_dict as vdict
@ -102,7 +102,7 @@ def reporter_detail(request, guid):
works_properly = request.GET.get('works_properly')
if works_properly:
qs = qs.filter(works_properly=works_properly)
reports = amo_utils.paginate(request, qs.order_by('-created'), 100)
reports = paginate(request, qs.order_by('-created'), 100)
return render(request, 'compat/reporter_detail.html',
dict(reports=reports, works=works,

Просмотреть файл

@ -32,7 +32,6 @@ EMAIL_QA_WHITELIST = env.list('EMAIL_QA_WHITELIST')
ENV = env('ENV')
DEBUG = False
TEMPLATE_DEBUG = DEBUG
DEBUG_PROPAGATE_EXCEPTIONS = False
SESSION_COOKIE_SECURE = True
CRONJOB_LOCK_PREFIX = DOMAIN

Просмотреть файл

@ -22,7 +22,6 @@ SEND_REAL_EMAIL = True
ENV = env('ENV')
DEBUG = False
TEMPLATE_DEBUG = DEBUG
DEBUG_PROPAGATE_EXCEPTIONS = False
SESSION_COOKIE_SECURE = True

Просмотреть файл

@ -31,7 +31,6 @@ EMAIL_QA_WHITELIST = env.list('EMAIL_QA_WHITELIST')
ENV = env('ENV')
DEBUG = False
TEMPLATE_DEBUG = DEBUG
DEBUG_PROPAGATE_EXCEPTIONS = False
SESSION_COOKIE_SECURE = True
CRONJOB_LOCK_PREFIX = DOMAIN

Просмотреть файл

@ -1,3 +1,6 @@
import uuid
from django import http
from django.contrib.syndication.views import Feed
from django.shortcuts import get_object_or_404
from django.utils.feedgenerator import Rss201rev2Feed as RSS
@ -13,8 +16,13 @@ class ActivityFeedRSS(Feed):
feed_type = RSS
def get_object(self, request):
rsskey = request.GET.get('privaterss')
key = get_object_or_404(RssKey, key=rsskey)
try:
rsskey = request.GET.get('privaterss')
rsskey = uuid.UUID(rsskey)
except ValueError:
raise http.Http404
key = get_object_or_404(RssKey, key=rsskey.hex)
return key
def items(self, key):

Просмотреть файл

@ -1,7 +1,8 @@
from copy import copy
from datetime import datetime
import json
import string
import uuid
from copy import copy
from datetime import datetime
from django.apps import apps
from django.conf import settings
@ -11,7 +12,6 @@ from django.utils.translation import ugettext as _
import commonware.log
import jinja2
from uuidfield.fields import UUIDField
from olympia import amo
from olympia.amo.models import ModelBase, ManagerBase
@ -25,11 +25,17 @@ from olympia.users.helpers import user_link
from olympia.users.models import UserProfile
from olympia.versions.models import Version
log = commonware.log.getLogger('devhub')
class RssKey(models.Model):
key = UUIDField(db_column='rsskey', auto=True, unique=True)
# TODO: Convert to `models.UUIDField` but apparently we have a max_length
# of 36 defined in the database and maybe store things with a hyphen
# or maybe not...
key = models.CharField(
db_column='rsskey', max_length=36,
default=lambda: uuid.uuid4().hex, unique=True)
addon = models.ForeignKey(Addon, null=True, unique=True)
user = models.ForeignKey(UserProfile, null=True, unique=True)
created = models.DateField(default=datetime.now)

Просмотреть файл

@ -1,3 +1,4 @@
import uuid
from urllib import urlencode
from pyquery import PyQuery as pq
@ -174,10 +175,22 @@ class TestActivity(HubTest):
# This will give us a new RssKey
r = self.get_response()
key = RssKey.objects.get()
# Make sure we generate none-verbose uuid key by default.
assert '-' not in key.key
r = self.get_response(privaterss=key.key)
assert r['content-type'] == 'application/rss+xml; charset=utf-8'
assert '<title>Recent Changes for My Add-ons</title>' in r.content
def test_rss_accepts_verbose(self):
self.log_creates(5)
r = self.get_response()
key = RssKey.objects.get()
r = self.get_response(privaterss=str(uuid.UUID(key.key)))
assert r['content-type'] == 'application/rss+xml; charset=utf-8'
assert '<title>Recent Changes for My Add-ons</title>' in r.content
def test_rss_single(self):
self.log_creates(5)
self.log_creates(13, self.addon2)

Просмотреть файл

@ -2257,7 +2257,9 @@ class TestUpload(BaseUploadTest):
def test_redirect(self):
r = self.post()
upload = FileUpload.objects.get()
url = reverse('devhub.upload_detail', args=[upload.uuid, 'json'])
url = reverse(
'devhub.upload_detail',
args=[upload.uuid.hex, 'json'])
self.assert3xx(r, url)
@mock.patch('validator.validate.validate')
@ -2314,14 +2316,14 @@ class TestUploadDetail(BaseUploadTest):
upload = FileUpload.objects.get()
r = self.client.get(reverse('devhub.upload_detail',
args=[upload.uuid, 'json']))
args=[upload.uuid.hex, 'json']))
assert r.status_code == 200
data = json.loads(r.content)
assert data['validation']['errors'] == 2
assert data['url'] == (
reverse('devhub.upload_detail', args=[upload.uuid, 'json']))
reverse('devhub.upload_detail', args=[upload.uuid.hex, 'json']))
assert data['full_report_url'] == (
reverse('devhub.upload_detail', args=[upload.uuid]))
reverse('devhub.upload_detail', args=[upload.uuid.hex]))
assert data['processed_by_addons_linter'] is False
# We must have tiers
assert len(data['validation']['messages'])
@ -2333,7 +2335,7 @@ class TestUploadDetail(BaseUploadTest):
upload = FileUpload.objects.get()
r = self.client.get(reverse('devhub.upload_detail',
args=[upload.uuid, 'json']))
args=[upload.uuid.hex, 'json']))
assert r.status_code == 200
data = json.loads(r.content)
assert data['processed_by_addons_linter'] is True
@ -2342,14 +2344,18 @@ class TestUploadDetail(BaseUploadTest):
self.post()
upload = FileUpload.objects.filter().order_by('-created').first()
r = self.client.get(reverse('devhub.upload_detail',
args=[upload.uuid]))
args=[upload.uuid.hex]))
assert r.status_code == 200
doc = pq(r.content)
assert (doc('header h2').text() ==
'Validation Results for {0}_animated.png'.format(upload.uuid))
expected = 'Validation Results for {0}_animated.png'.format(
upload.uuid.hex)
assert doc('header h2').text() == expected
suite = doc('#addon-validator-suite')
assert suite.attr('data-validateurl') == (
reverse('devhub.standalone_upload_detail', args=[upload.uuid]))
expected = reverse(
'devhub.standalone_upload_detail',
args=[upload.uuid.hex])
assert suite.attr('data-validateurl') == expected
@mock.patch('olympia.devhub.tasks.run_validator')
def check_excluded_platforms(self, xpi, platforms, v):
@ -2357,7 +2363,7 @@ class TestUploadDetail(BaseUploadTest):
self.upload_file(xpi)
upload = FileUpload.objects.get()
r = self.client.get(reverse('devhub.upload_detail',
args=[upload.uuid, 'json']))
args=[upload.uuid.hex, 'json']))
assert r.status_code == 200
data = json.loads(r.content)
assert sorted(data['platforms_to_exclude']) == sorted(platforms)
@ -2414,7 +2420,7 @@ class TestUploadDetail(BaseUploadTest):
self.upload_file('unopenable.xpi')
upload = FileUpload.objects.get()
r = self.client.get(reverse('devhub.upload_detail',
args=[upload.uuid, 'json']))
args=[upload.uuid.hex, 'json']))
data = json.loads(r.content)
message = [(m['message'], m.get('fatal', False))
for m in data['validation']['messages']]
@ -2428,7 +2434,7 @@ class TestUploadDetail(BaseUploadTest):
self.upload_file('../../../files/fixtures/files/experiment.xpi')
upload = FileUpload.objects.get()
response = self.client.get(reverse('devhub.upload_detail',
args=[upload.uuid, 'json']))
args=[upload.uuid.hex, 'json']))
data = json.loads(response.content)
assert data['validation']['messages'] == []
@ -2438,7 +2444,7 @@ class TestUploadDetail(BaseUploadTest):
self.upload_file('../../../files/fixtures/files/experiment.xpi')
upload = FileUpload.objects.get()
response = self.client.get(reverse('devhub.upload_detail',
args=[upload.uuid, 'json']))
args=[upload.uuid.hex, 'json']))
data = json.loads(response.content)
assert data['validation']['messages'] == [
{u'tier': 1, u'message': u'You cannot submit this type of add-on',
@ -2542,7 +2548,7 @@ class TestVersionAddFile(UploadTest):
a.save()
def post(self, platform=amo.PLATFORM_MAC, source=None, beta=False):
return self.client.post(self.url, dict(upload=self.upload.uuid,
return self.client.post(self.url, dict(upload=self.upload.uuid.hex,
platform=platform.id,
source=source, beta=beta))
@ -2608,7 +2614,7 @@ class TestVersionAddFile(UploadTest):
platform = amo.PLATFORM_MAC.id
form = {'DELETE': 'checked', 'id': file_id, 'platform': platform}
data = formset(form, platform=platform, upload=self.upload.uuid,
data = formset(form, platform=platform, upload=self.upload.uuid.hex,
initial_count=1, prefix='files')
r = self.client.post(self.edit_url, data)
@ -2621,7 +2627,7 @@ class TestVersionAddFile(UploadTest):
platform = amo.PLATFORM_MAC.id
form = {'DELETE': 'checked', 'id': file_id, 'platform': platform}
data = formset(form, platform=platform, upload=self.upload.uuid,
data = formset(form, platform=platform, upload=self.upload.uuid.hex,
initial_count=1, prefix='files')
data.update(formset(total_count=1, initial_count=1))
@ -2999,7 +3005,7 @@ class AddVersionTest(UploadTest):
def post(self, supported_platforms=[amo.PLATFORM_MAC],
override_validation=False, expected_status=200, source=None,
beta=False, nomination_type=None):
d = dict(upload=self.upload.uuid, source=source,
d = dict(upload=self.upload.uuid.hex, source=source,
supported_platforms=[p.id for p in supported_platforms],
admin_override_validation=override_validation, beta=beta)
if nomination_type:
@ -3250,7 +3256,7 @@ class TestAddBetaVersion(AddVersionTest):
def post_additional(self, version, platform=amo.PLATFORM_MAC):
url = reverse('devhub.versions.add_file',
args=[self.addon.slug, version.id])
return self.client.post(url, dict(upload=self.upload.uuid,
return self.client.post(url, dict(upload=self.upload.uuid.hex,
platform=platform.id, beta=True))
def test_add_multi_file_beta(self):
@ -3393,7 +3399,7 @@ class UploadAddon(object):
def post(self, supported_platforms=[amo.PLATFORM_ALL], expect_errors=False,
source=None, is_listed=True, is_sideload=False, status_code=200):
d = dict(upload=self.upload.uuid, source=source,
d = dict(upload=self.upload.uuid.hex, source=source,
supported_platforms=[p.id for p in supported_platforms],
is_unlisted=not is_listed, is_sideload=is_sideload)
r = self.client.post(self.url, d, follow=True)
@ -3550,7 +3556,7 @@ class TestCreateAddon(BaseUploadTest, UploadAddon, TestCase):
assert mock_sign_file.called
def test_missing_platforms(self):
r = self.client.post(self.url, dict(upload=self.upload.uuid))
r = self.client.post(self.url, dict(upload=self.upload.uuid.hex))
assert r.status_code == 200
assert r.context['new_addon_form'].errors.as_text() == (
'* supported_platforms\n * Need at least one platform.')

Просмотреть файл

@ -36,7 +36,7 @@ class TestUploadValidation(BaseUploadTest):
def test_no_html_in_messages(self):
upload = FileUpload.objects.get(name='invalid-id-20101206.xpi')
resp = self.client.get(reverse('devhub.upload_detail',
args=[upload.uuid, 'json']))
args=[upload.uuid.hex, 'json']))
assert resp.status_code == 200
data = json.loads(resp.content)
msg = data['validation']['messages'][1]
@ -49,7 +49,7 @@ class TestUploadValidation(BaseUploadTest):
def test_date_on_upload(self):
upload = FileUpload.objects.get(name='invalid-id-20101206.xpi')
resp = self.client.get(reverse('devhub.upload_detail',
args=[upload.uuid]))
args=[upload.uuid.hex]))
assert resp.status_code == 200
doc = pq(resp.content)
assert doc('td').text() == 'December 6, 2010'

Просмотреть файл

@ -13,7 +13,7 @@ from django.core.exceptions import PermissionDenied
from django.core.files.storage import default_storage as storage
from django.db import transaction
from django.db.models import Count
from django.shortcuts import get_object_or_404, redirect, render
from django.shortcuts import get_object_or_404, redirect
from django.template import Context, loader
from django.utils.http import urlquote
from django.utils.translation import ugettext as _, ugettext_lazy as _lazy
@ -36,7 +36,7 @@ from olympia.amo import messages
from olympia.amo.decorators import json_view, login_required, post_required
from olympia.amo.helpers import absolutify, urlparams
from olympia.amo.urlresolvers import reverse
from olympia.amo.utils import escape_all, MenuItem, send_mail
from olympia.amo.utils import escape_all, MenuItem, send_mail, render
from olympia.api.models import APIKey
from olympia.applications.models import AppVersion
from olympia.devhub.decorators import dev_required
@ -613,7 +613,7 @@ def handle_upload(filedata, user, app_id=None, version_id=None, addon=None,
upload = FileUpload.from_post(filedata, filedata.name, filedata.size,
automated_signing=automated, addon=addon)
log.info('FileUpload created: %s' % upload.uuid)
log.info('FileUpload created: %s' % upload.uuid.hex)
if user.is_authenticated():
upload.user = user
upload.save()
@ -648,11 +648,11 @@ def upload(request, addon=None, is_standalone=False, is_listed=True,
disallow_preliminary_review=no_prelim)
if addon:
return redirect('devhub.upload_detail_for_addon',
addon.slug, upload.uuid)
addon.slug, upload.uuid.hex)
elif is_standalone:
return redirect('devhub.standalone_upload_detail', upload.uuid)
return redirect('devhub.standalone_upload_detail', upload.uuid.hex)
else:
return redirect('devhub.upload_detail', upload.uuid, 'json')
return redirect('devhub.upload_detail', upload.uuid.hex, 'json')
@post_required
@ -862,10 +862,12 @@ def upload_validation_context(request, upload, addon_slug=None, addon=None,
if not url:
if addon:
url = reverse('devhub.upload_detail_for_addon',
args=[addon.slug, upload.uuid])
args=[addon.slug, upload.uuid.hex])
else:
url = reverse('devhub.upload_detail', args=[upload.uuid, 'json'])
full_report_url = reverse('devhub.upload_detail', args=[upload.uuid])
url = reverse(
'devhub.upload_detail',
args=[upload.uuid.hex, 'json'])
full_report_url = reverse('devhub.upload_detail', args=[upload.uuid.hex])
validation = upload.processed_validation or ''
@ -874,7 +876,7 @@ def upload_validation_context(request, upload, addon_slug=None, addon=None,
validation.get('metadata', {}).get(
'processed_by_addons_linter', False))
return {'upload': upload.uuid,
return {'upload': upload.uuid.hex,
'validation': validation,
'error': None,
'url': url,
@ -901,7 +903,7 @@ def upload_detail(request, uuid, format='html'):
upload = get_object_or_404(FileUpload, uuid=uuid)
validate_url = reverse('devhub.standalone_upload_detail',
args=[upload.uuid])
args=[upload.uuid.hex])
if upload.compat_with_app:
return _compat_result(request, validate_url,
@ -1382,12 +1384,17 @@ def version_bounce(request, addon_id, addon, version):
@dev_required
def version_stats(request, addon_id, addon):
qs = Version.objects.filter(addon=addon)
reviews = (qs.annotate(reviews=Count('reviews'))
.values('id', 'version', 'reviews'))
reviews = (qs.annotate(review_count=Count('reviews'))
.values('id', 'version', 'review_count'))
d = dict((v['id'], v) for v in reviews)
files = qs.annotate(files=Count('files')).values_list('id', 'files')
for id, files in files:
d[id]['files'] = files
files = (
qs
.annotate(file_count=Count('files'))
.values_list('id', 'file_count'))
for id_, file_count in files:
# For backwards compatibility
d[id_]['files'] = file_count
d[id_]['reviews'] = d[id_].pop('review_count')
return d

Просмотреть файл

@ -3,7 +3,7 @@ import datetime
import json
from django.conf import settings
from django.utils.encoding import smart_unicode
from django.utils.encoding import smart_text
import mock
from pyquery import PyQuery as pq
@ -681,7 +681,7 @@ class TestDeletedThemeLookup(TestCase):
response = self.client.get(reverse('editors.themes.deleted'))
assert response.status_code == 200
assert (self.deleted.name.localized_string in
smart_unicode(response.content))
smart_text(response.content))
def test_perm(self):
self.login('persona_reviewer@mozilla.com')

Просмотреть файл

@ -8,7 +8,7 @@ from django.conf import settings
from django.core.cache import cache
from django.core.exceptions import PermissionDenied
from django.db.models import Q
from django.shortcuts import get_object_or_404, redirect, render
from django.shortcuts import get_object_or_404, redirect
from django.utils.datastructures import SortedDict
from django.views.decorators.cache import never_cache
from django.utils.translation import ugettext as _, pgettext
@ -20,7 +20,7 @@ from olympia.access import acl
from olympia.addons.decorators import addon_view, addon_view_factory
from olympia.addons.models import Addon, Version
from olympia.amo.decorators import json_view, post_required
from olympia.amo.utils import paginate
from olympia.amo.utils import paginate, render
from olympia.amo.urlresolvers import reverse
from olympia.constants.base import REVIEW_LIMITED_DELAY_HOURS
from olympia.devhub.models import ActivityLog, AddonLog, CommentLog

Просмотреть файл

@ -5,7 +5,7 @@ from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Q
from django.forms.formsets import formset_factory
from django.shortcuts import get_object_or_404, redirect, render
from django.shortcuts import get_object_or_404, redirect
from django.utils.datastructures import MultiValueDictKeyError
from django.utils.translation import ugettext as _, ungettext as ngettext
@ -15,7 +15,7 @@ from olympia.access import acl
from olympia.addons.models import Addon, Persona
from olympia.amo.decorators import json_view, post_required
from olympia.amo.urlresolvers import reverse
from olympia.amo.utils import paginate
from olympia.amo.utils import paginate, render
from olympia.devhub.models import ActivityLog
from olympia.editors import forms
from olympia.editors.models import RereviewQueueTheme, ReviewerScore, ThemeLock

Просмотреть файл

@ -6,7 +6,7 @@ import stat
from django.conf import settings
from django.core.files.storage import default_storage as storage
from django.utils.datastructures import SortedDict
from django.utils.encoding import smart_unicode
from django.utils.encoding import force_text
from django.utils.translation import get_language, ugettext as _
from django.template.defaultfilters import filesizeformat
from validator.testcases.packagelayout import (
@ -289,8 +289,8 @@ class FileViewer(object):
iterate(self.dest)
for path in all_files:
filename = smart_unicode(os.path.basename(path), errors='replace')
short = smart_unicode(path[len(self.dest) + 1:], errors='replace')
filename = force_text(os.path.basename(path), errors='replace')
short = force_text(path[len(self.dest) + 1:], errors='replace')
mime, encoding = mimetypes.guess_type(filename)
directory = os.path.isdir(path)

Просмотреть файл

@ -13,12 +13,11 @@ from django.core.files.storage import default_storage as storage
from django.db import models
from django.dispatch import receiver
from django.template.defaultfilters import slugify
from django.utils.encoding import smart_str, force_text
from django.utils.encoding import force_bytes, force_text
import commonware
from cache_nuggets.lib import memoize
from django_statsd.clients import statsd
from uuidfield.fields import UUIDField
from olympia import amo
from olympia.amo.models import OnChangeMixin, ModelBase, UncachedManagerBase
@ -134,7 +133,8 @@ class File(OnChangeMixin, ModelBase):
else:
host = user_media_url('addons')
return posixpath.join(*map(smart_str, [host, addon.id, self.filename]))
return posixpath.join(
*map(force_bytes, [host, addon.id, self.filename]))
def get_url_path(self, src):
return self._make_download_url('downloads.file', src)
@ -327,7 +327,8 @@ class File(OnChangeMixin, ModelBase):
log.info(msg % (src, dst))
move_stored_file(src, dst)
except UnicodeEncodeError:
log.error('Move Failure: %s %s' % (smart_str(src), smart_str(dst)))
msg = 'Move Failure: %s %s' % (force_bytes(src), force_bytes(dst))
log.error(msg)
def hide_disabled_file(self):
"""Move a disabled file to the guarded file path."""
@ -337,10 +338,10 @@ class File(OnChangeMixin, ModelBase):
self.mv(src, dst, 'Moving disabled file: %s => %s')
# Remove the file from the mirrors if necessary.
if (self.mirror_file_path and
storage.exists(smart_str(self.mirror_file_path))):
storage.exists(force_bytes(self.mirror_file_path))):
log.info('Unmirroring disabled file: %s'
% self.mirror_file_path)
storage.delete(smart_str(self.mirror_file_path))
storage.delete(force_bytes(self.mirror_file_path))
def unhide_disabled_file(self):
if not self.filename:
@ -371,8 +372,8 @@ class File(OnChangeMixin, ModelBase):
copy_stored_file(self.file_path, dst)
except UnicodeEncodeError:
log.info('Copy Failure: %s %s %s' %
(self.id, smart_str(self.filename),
smart_str(self.file_path)))
(self.id, force_bytes(self.filename),
force_bytes(self.file_path)))
_get_localepicker = re.compile('^locale browser ([\w\-_]+) (.*)$', re.M)
@ -554,7 +555,7 @@ def track_file_status_change(file_):
class FileUpload(ModelBase):
"""Created when a file is uploaded for validation/submission."""
uuid = UUIDField(auto=True)
uuid = models.UUIDField(default=uuid.uuid4, editable=False)
path = models.CharField(max_length=255, default='')
name = models.CharField(max_length=255, default='',
help_text="The user's original filename")
@ -578,7 +579,7 @@ class FileUpload(ModelBase):
db_table = 'file_uploads'
def __unicode__(self):
return self.uuid
return unicode(self.uuid.hex)
def save(self, *args, **kw):
if self.validation:
@ -588,8 +589,8 @@ class FileUpload(ModelBase):
def add_file(self, chunks, filename, size):
if not self.uuid:
self.uuid = self._meta.get_field('uuid')._create_uuid().hex
filename = smart_str(u'{0}_{1}'.format(self.uuid, filename))
self.uuid = self._meta.get_field('uuid')._create_uuid()
filename = force_bytes(u'{0}_{1}'.format(self.uuid.hex, filename))
loc = os.path.join(user_media_path('addons'), 'temp', uuid.uuid4().hex)
base, ext = os.path.splitext(smart_path(filename))
is_crx = False

Просмотреть файл

@ -609,7 +609,7 @@ class TestFileUpload(UploadTest):
def test_from_post_filename(self):
upload = self.upload()
assert upload.uuid
assert upload.name == '{0}_filename.xpi'.format(upload.uuid)
assert upload.name == '{0}_filename.xpi'.format(upload.uuid.hex)
def test_from_post_hash(self):
hash = hashlib.sha256(self.data).hexdigest()

Просмотреть файл

@ -1,6 +1,5 @@
from django import http, shortcuts
from django.db.transaction import non_atomic_requests
from django.shortcuts import render
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import condition
@ -13,7 +12,7 @@ from cache_nuggets.lib import Message, Token
from olympia.access import acl
from olympia.amo.decorators import json_view
from olympia.amo.urlresolvers import reverse
from olympia.amo.utils import HttpResponseSendFile, urlparams
from olympia.amo.utils import HttpResponseSendFile, urlparams, render
from olympia.files.decorators import (
etag, file_view, compare_file_view, file_view_token, last_modified)
from olympia.files.tasks import extract_file

Просмотреть файл

@ -2,6 +2,7 @@ from optparse import make_option
from django.conf import settings
from django.core.management.base import BaseCommand, CommandError
from django.utils import translation
from olympia.landfill.generators import generate_addons
@ -40,7 +41,10 @@ class Command(BaseCommand):
if not settings.DEBUG:
raise CommandError('You can only run this command with your '
'DEBUG setting set to True.')
num = int(args[0])
email = kwargs.get('email')
app_name = kwargs.get('app_name')
generate_addons(num, email, app_name)
with translation.override(settings.LANGUAGE_CODE):
generate_addons(num, email, app_name)

Просмотреть файл

@ -2,6 +2,7 @@ from optparse import make_option
from django.conf import settings
from django.core.management.base import BaseCommand, CommandError
from django.utils import translation
from olympia.landfill.generators import generate_themes
@ -37,4 +38,6 @@ class Command(BaseCommand):
'DEBUG setting set to True.')
num = int(args[0])
email = kwargs.get('email')
generate_themes(num, email)
with translation.override(settings.LANGUAGE_CODE):
generate_themes(num, email)

Просмотреть файл

@ -60,8 +60,10 @@ def addon_to_dict(addon, disco=False, src='api'):
if v:
d['version'] = v.version
d['platforms'] = [unicode(a.name) for a in v.supported_platforms]
d['compatible_apps'] = v.compatible_apps.values()
d['compatible_apps'] = [
{unicode(amo.APP_IDS[obj.application].pretty): {
'min': unicode(obj.min), 'max': unicode(obj.max)}}
for obj in v.compatible_apps.values()]
if addon.eula:
d['eula'] = unicode(addon.eula)

Просмотреть файл

@ -11,15 +11,15 @@ from datetime import date, timedelta
from django.core.cache import cache
from django.db.transaction import non_atomic_requests
from django.http import HttpResponse, HttpResponsePermanentRedirect
from django.template.context import get_standard_processors
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext as _, ugettext_lazy, get_language
from django.utils.encoding import smart_str
from django.utils.encoding import force_bytes
import commonware.log
import jingo
import waffle
from caching.base import cached_with
from jingo import get_standard_processors
from olympia import amo, legacy_api
from olympia.addons.models import Addon, CompatOverride
@ -27,7 +27,7 @@ from olympia.amo.decorators import (
allow_cross_site_request, json_view)
from olympia.amo.models import manual_order
from olympia.amo.urlresolvers import get_url_prefix
from olympia.amo.utils import JSONEncoder
from olympia.amo.utils import AMOJSONEncoder
from olympia.legacy_api.utils import addon_to_dict, extract_filters
from olympia.search.views import (
AddonSuggestionsAjax, PersonaSuggestionsAjax, name_query)
@ -276,7 +276,7 @@ class AddonDetailView(APIView):
return self.render('legacy_api/addon_detail.xml', {'addon': addon})
def render_json(self, context):
return json.dumps(addon_to_dict(context['addon']), cls=JSONEncoder)
return json.dumps(addon_to_dict(context['addon']), cls=AMOJSONEncoder)
@non_atomic_requests
@ -285,7 +285,7 @@ def guid_search(request, api_version, guids):
def guid_search_cache_key(guid):
key = 'guid_search:%s:%s:%s' % (api_version, lang, guid)
return hashlib.md5(smart_str(key)).hexdigest()
return hashlib.md5(force_bytes(key)).hexdigest()
guids = [g.strip() for g in guids.split(',')] if guids else []
@ -465,7 +465,7 @@ class ListView(APIView):
def f():
return self._process(addons, *args)
return cached_with(addons, f, map(smart_str, args))
return cached_with(addons, f, map(force_bytes, args))
def _process(self, addons, *args):
return self.render('legacy_api/list.xml',
@ -473,7 +473,7 @@ class ListView(APIView):
def render_json(self, context):
return json.dumps([addon_to_dict(a) for a in context['addons']],
cls=JSONEncoder)
cls=AMOJSONEncoder)
class LanguageView(APIView):

Просмотреть файл

@ -1,6 +1,6 @@
from django.core.cache import cache
from django.test.utils import override_settings
from django.utils.encoding import smart_unicode
from django.utils.encoding import smart_text
from django.utils.translation import trim_whitespace
import mock
@ -127,7 +127,7 @@ class TestPromos(TestCase):
def _test_response_contains_addons(self, response):
assert response.status_code == 200
assert response.content
content = smart_unicode(response.content)
content = smart_text(response.content)
assert unicode(self.addon1.name) in content
assert unicode(self.addon2.name) in content
assert unicode(self.addon3.name) in content
@ -179,7 +179,7 @@ class TestPromos(TestCase):
response = self.client.get(self.get_disco_url('10.0', 'Darwin'))
assert response.status_code == 200
assert response.content
content = smart_unicode(response.content)
content = smart_text(response.content)
assert unicode(self.addon1.name) not in content
assert unicode(self.addon2.name) in content
assert unicode(self.addon3.name) in content

Просмотреть файл

@ -3,13 +3,14 @@ import json
from django import http
from django.db.transaction import non_atomic_requests
from django.forms.models import modelformset_factory
from django.shortcuts import get_object_or_404, redirect, render
from django.shortcuts import get_object_or_404, redirect
import commonware.log
from olympia import amo
from olympia.amo.models import manual_order
from olympia.amo.urlresolvers import reverse
from olympia.amo.utils import render
from olympia.addons.decorators import addon_view_factory
from olympia.addons.models import Addon
from olympia.addons.utils import get_featured_ids

Просмотреть файл

@ -1,5 +1,5 @@
import datetime
from django.db import models
from django.utils import timezone
class ReindexingManager(models.Manager):
@ -54,7 +54,7 @@ class Reindexing(models.Model):
SITE_CHOICES = (
('amo', 'AMO'),
)
start_date = models.DateTimeField(default=datetime.datetime.now())
start_date = models.DateTimeField(default=timezone.now)
old_index = models.CharField(max_length=255, null=True)
new_index = models.CharField(max_length=255)
alias = models.CharField(max_length=255)

Просмотреть файл

@ -48,8 +48,16 @@ def path(*folders):
ROOT_PACKAGE = os.path.basename(ROOT)
DEBUG = True
TEMPLATE_DEBUG = DEBUG
DEBUG_PROPAGATE_EXCEPTIONS = True
SILENCED_SYSTEM_CHECKS = (
# Recommendation to use OneToOneField instead of ForeignKey(unique=True)
# but our translations are the way they are...
'fields.W342',
# TEMPLATE_DIRS is required by jingo, remove this line here once we
# get rid of jingo
'1_8.W001',
)
# LESS CSS OPTIONS (Debug only).
LESS_PREPROCESS = True # Compile LESS with Node, rather than client-side JS?
@ -250,21 +258,12 @@ SECRET_KEY = 'this-is-a-dummy-key-and-its-overridden-for-prod-servers'
# Templates
# List of callables that know how to import templates from various sources.
TEMPLATE_LOADERS = (
'olympia.lib.template_loader.Loader',
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
)
# We don't want jingo's template loaded to pick up templates for third party
# apps that don't use Jinja2. The Following is a list of prefixes for jingo to
# ignore.
JINGO_EXCLUDE_APPS = (
'django_extensions',
'admin',
'toolbar_statsd',
'registration',
'rest_framework',
'waffle',
)
@ -275,29 +274,46 @@ JINGO_EXCLUDE_PATHS = (
'editors/emails',
'amo/emails',
'devhub/email/revoked-key-email.ltxt',
'devhub/email/new-key-email.ltxt'
'devhub/email/new-key-email.ltxt',
# Django specific templates
'registration/password_reset_subject.txt'
)
TEMPLATE_CONTEXT_PROCESSORS = (
'django.contrib.auth.context_processors.auth',
'django.core.context_processors.debug',
'django.core.context_processors.media',
'django.core.context_processors.request',
'session_csrf.context_processor',
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': (
path('media', 'docs'),
path('src/olympia/templates'),
),
'OPTIONS': {
'context_processors': (
'django.contrib.auth.context_processors.auth',
'django.core.context_processors.debug',
'django.core.context_processors.media',
'django.core.context_processors.request',
'session_csrf.context_processor',
'django.contrib.messages.context_processors.messages',
'django.contrib.messages.context_processors.messages',
'olympia.amo.context_processors.app',
'olympia.amo.context_processors.i18n',
'olympia.amo.context_processors.global_settings',
'olympia.amo.context_processors.static_url',
'jingo_minify.helpers.build_ids',
)
'olympia.amo.context_processors.app',
'olympia.amo.context_processors.i18n',
'olympia.amo.context_processors.global_settings',
'olympia.amo.context_processors.static_url',
'jingo_minify.helpers.build_ids',
),
'loaders': (
'olympia.lib.template_loader.Loader',
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
)
}
}
]
TEMPLATE_DIRS = (
path('media', 'docs'),
path('src/olympia/templates'),
)
# jingo still looks at TEMPLATE_DIRS
TEMPLATE_DIRS = TEMPLATES[0]['DIRS']
def JINJA_CONFIG():
@ -328,6 +344,10 @@ def JINJA_CONFIG():
config['bytecode_cache'] = bc
return config
X_FRAME_OPTIONS = 'DENY'
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_HSTS_SECONDS = 31536000
MIDDLEWARE_CLASSES = (
# AMO URL middleware comes first so everyone else sees nice URLs.
@ -342,10 +362,9 @@ MIDDLEWARE_CLASSES = (
# Munging REMOTE_ADDR must come before ThreadRequest.
'commonware.middleware.SetRemoteAddrFromForwardedFor',
'commonware.middleware.FrameOptionsHeader',
'commonware.middleware.XSSProtectionHeader',
'commonware.middleware.ContentTypeOptionsHeader',
'commonware.middleware.StrictTransportMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'multidb.middleware.PinningRouterMiddleware',
'waffle.middleware.WaffleMiddleware',
@ -1480,19 +1499,6 @@ MIN_NOT_D2C_VERSION = '37'
# True when the Django app is running from the test suite.
IN_TEST_SUITE = False
# The configuration for the client that speaks to solitude.
# A tuple of the solitude hosts.
SOLITUDE_HOSTS = ('',)
# The oAuth key and secret that solitude needs.
SOLITUDE_KEY = ''
SOLITUDE_SECRET = ''
# The timeout we'll give solitude.
SOLITUDE_TIMEOUT = 10
# The OAuth keys to connect to the solitude host specified above.
SOLITUDE_OAUTH = {'key': '', 'secret': ''}
# Temporary flag to work with navigator.mozPay() on devices that don't
# support it natively.
SIMULATE_NAV_PAY = False

Просмотреть файл

@ -0,0 +1 @@
UPDATE hubrsskeys SET rsskey=REPLACE(rsskey,'-','');

Просмотреть файл

@ -2,10 +2,10 @@ from collections import defaultdict
from django.conf import settings
from django.db.transaction import non_atomic_requests
from django.shortcuts import render
from olympia.devhub.models import ActivityLog
from olympia.users.models import UserProfile
from olympia.amo.utils import render
@non_atomic_requests

Просмотреть файл

@ -6,7 +6,7 @@ from django import http
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.db.transaction import non_atomic_requests
from django.shortcuts import get_object_or_404, redirect, render
from django.shortcuts import get_object_or_404, redirect
from django.template import Context, loader
from django.utils.http import urlquote
from django.utils.translation import ugettext as _
@ -23,7 +23,8 @@ from olympia import amo
from olympia.amo import messages
from olympia.amo.decorators import (
json_view, login_required, post_required, restricted_content)
from olympia.amo import helpers, utils as amo_utils
from olympia.amo import helpers
from olympia.amo.utils import render, paginate, send_mail as amo_send_mail
from olympia.access import acl
from olympia.addons.decorators import addon_view_factory
from olympia.addons.models import Addon
@ -42,9 +43,9 @@ addon_view = addon_view_factory(qs=Addon.objects.valid)
def send_mail(template, subject, emails, context, perm_setting):
template = loader.get_template(template)
amo_utils.send_mail(subject, template.render(Context(context,
autoescape=False)),
recipient_list=emails, perm_setting=perm_setting)
amo_send_mail(
subject, template.render(Context(context, autoescape=False)),
recipient_list=emails, perm_setting=perm_setting)
@addon_view
@ -76,7 +77,7 @@ def review_list(request, addon, review_id=None, user_id=None, template=None):
ctx['page'] = 'list'
qs = qs.filter(is_latest=True)
ctx['reviews'] = reviews = amo_utils.paginate(request, qs)
ctx['reviews'] = reviews = paginate(request, qs)
ctx['replies'] = Review.get_replies(reviews.object_list)
if request.user.is_authenticated():
ctx['review_perms'] = {

Просмотреть файл

@ -1,9 +1,9 @@
import logging
from django.shortcuts import render
from elasticsearch import TransportError
from olympia.amo.utils import render
log = logging.getLogger('z.es')

Просмотреть файл

@ -1,9 +1,8 @@
from django import http
from django.db.models import Q
from django.db.transaction import non_atomic_requests
from django.shortcuts import render
from django.utils import translation
from django.utils.encoding import smart_str
from django.utils.encoding import force_bytes
from django.utils.translation import ugettext as _
from django.views.decorators.vary import vary_on_headers
@ -16,7 +15,7 @@ from olympia.browse.views import personas_listing as personas_listing_view
from olympia.addons.models import Addon, Category
from olympia.amo.decorators import json_view
from olympia.amo.helpers import locale_url, urlparams
from olympia.amo.utils import sorted_groupby
from olympia.amo.utils import sorted_groupby, render
from olympia.bandwagon.models import Collection
from olympia.versions.compare import dict_from_int, version_dict, version_int
@ -601,7 +600,7 @@ def tag_sidebar(request, form_data, aggregations):
def fix_search_query(query, extra_params=None):
rv = dict((smart_str(k), v) for k, v in query.items())
rv = dict((force_bytes(k), v) for k, v in query.items())
changed = False
# Change old keys to new names.
keys = {

Просмотреть файл

@ -17,7 +17,7 @@ class FileUploadSerializer(serializers.ModelSerializer):
passed_review = serializers.SerializerMethodField()
# For backwards-compatibility reasons, we return the uuid as "pk".
pk = serializers.CharField(source='uuid')
pk = serializers.UUIDField(source='uuid', format='hex')
processed = serializers.BooleanField()
reviewed = serializers.SerializerMethodField()
valid = serializers.BooleanField(source='passed_all_validations')
@ -49,11 +49,11 @@ class FileUploadSerializer(serializers.ModelSerializer):
def get_url(self, instance):
return absolutify(reverse('signing.version', args=[instance.addon.guid,
instance.version,
instance.uuid]))
instance.uuid.hex]))
def get_validation_url(self, instance):
return absolutify(reverse('devhub.upload_detail',
args=[instance.uuid]))
args=[instance.uuid.hex]))
def get_files(self, instance):
if self.version is not None:

Просмотреть файл

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*
import os
import json
@ -487,10 +488,10 @@ class TestCheckVersion(BaseUploadVersionCase):
newer_upload = FileUpload.objects.latest()
assert newer_upload != upload
response = self.get(self.url(self.guid, '3.0', upload.uuid))
response = self.get(self.url(self.guid, '3.0', upload.uuid.hex))
assert response.status_code == 200
# For backwards-compatibility reasons, we return the uuid as "pk".
assert response.data['pk'] == upload.uuid
assert response.data['pk'] == upload.uuid.hex
assert 'processed' in response.data
def test_version_exists_with_pk_not_owner(self):
@ -506,7 +507,8 @@ class TestCheckVersion(BaseUploadVersionCase):
upload = FileUpload.objects.latest()
# Check that the user that created the upload can access it properly.
response = self.get(self.url('@create-version', '1.0', upload.uuid))
response = self.get(
self.url('@create-version', '1.0', upload.uuid.hex))
assert response.status_code == 200
assert 'processed' in response.data
@ -517,7 +519,7 @@ class TestCheckVersion(BaseUploadVersionCase):
# Check that we can't access the FileUpload by uuid even if we pass in
# an add-on and version that we own if we don't own the FileUpload.
response = self.get(self.url(self.guid, '3.0', upload.uuid))
response = self.get(self.url(self.guid, '3.0', upload.uuid.hex))
assert response.status_code == 404
assert 'error' in response.data

Просмотреть файл

@ -314,12 +314,19 @@ class TestCSVs(ESStatsTest):
self.url_args = {'start': '20200101', 'end': '20200130', 'addon_id': 4}
response = self.get_view_response('stats.versions_series', head=True,
group='day', format='csv')
assert response["cache-control"] == 'max-age=0'
assert (
set(response['cache-control'].split(', ')),
{'max-age=0', 'no-cache', 'no-store', 'must-revalidate'},
)
self.url_args = {'start': '20200101', 'end': '20200130', 'addon_id': 4}
response = self.get_view_response('stats.versions_series', head=True,
group='day', format='json')
assert response["cache-control"] == 'max-age=0'
assert (
set(response['cache-control'].split(', ')),
{'max-age=0', 'no-cache', 'no-store', 'must-revalidate'},
)
def test_usage_series_no_data(self):
url_args = [

Просмотреть файл

@ -6,7 +6,6 @@ import logging
import os
import time
from datetime import date, timedelta, datetime
from types import GeneratorType
from django import http
from django.conf import settings
@ -15,7 +14,7 @@ from django.core.files.storage import get_storage_class
from django.db import connection
from django.db.models import Avg, Count, Q, Sum
from django.db.transaction import non_atomic_requests
from django.shortcuts import get_object_or_404, render
from django.shortcuts import get_object_or_404
from django.utils.cache import add_never_cache_headers, patch_cache_control
from django.utils.datastructures import SortedDict
@ -28,7 +27,7 @@ from rest_framework.response import Response
from rest_framework.views import APIView
from olympia import amo
from olympia.amo.utils import DecimalJSONEncoder
from olympia.amo.utils import render, AMOJSONEncoder
from olympia.access import acl
from olympia.addons.decorators import addon_view_factory
from olympia.addons.models import Addon
@ -700,13 +699,9 @@ def render_json(request, addon, stats):
"""Render a stats series in JSON."""
response = http.HttpResponse(content_type='text/json')
# XXX: Subclass DjangoJSONEncoder to handle generators.
if isinstance(stats, GeneratorType):
stats = list(stats)
# Django's encoder supports date and datetime.
fudge_headers(response, stats)
json.dump(stats, response, cls=DecimalJSONEncoder)
json.dump(stats, response, cls=AMOJSONEncoder)
return response

Просмотреть файл

@ -1,7 +1,7 @@
from django.conf import settings
from django.utils import translation
from django.utils.translation.trans_real import to_language
from django.utils.encoding import smart_unicode
from django.utils.encoding import force_text
import bleach
import jinja2
@ -43,7 +43,7 @@ def truncate(s, length=255, killwords=True, end='...'):
return ''
if hasattr(s, '__truncate__'):
return s.__truncate__(length, killwords, end)
return jinja2.filters.do_truncate(smart_unicode(s), length, killwords, end)
return jinja2.filters.do_truncate(force_text(s), length, killwords, end)
@jingo.register.inclusion_tag('translations/trans-menu.html')

Просмотреть файл

@ -1,6 +1,6 @@
from django.db import connections, models, router
from django.db.models.deletion import Collector
from django.utils import encoding
from django.utils.encoding import force_text
import bleach
import commonware.log
@ -112,7 +112,7 @@ class Translation(ModelBase):
# Like in ModelBase, we avoid putting the real db in the key because it
# does us more harm than good.
key_parts = ('o', 'translations.translation', pk, 'default')
return ':'.join(map(encoding.smart_unicode, key_parts))
return ':'.join(map(force_text, key_parts))
@classmethod
def new(cls, string, locale, id=None):
@ -129,18 +129,22 @@ class Translation(ModelBase):
if id is None:
# Get a sequence key for the new translation.
cursor = connections['default'].cursor()
cursor.execute("""UPDATE translations_seq
SET id=LAST_INSERT_ID(
id + @@global.auto_increment_increment)""")
cursor.execute("""
UPDATE translations_seq
SET id=LAST_INSERT_ID(
id + @@global.auto_increment_increment
)
""")
# The sequence table should never be empty. But alas, if it is,
# let's fix it.
if not cursor.rowcount > 0:
cursor.execute("""INSERT INTO translations_seq (id)
VALUES(LAST_INSERT_ID(
id + @@global.auto_increment_increment)
)""")
cursor.execute("""
INSERT INTO translations_seq (id)
VALUES(LAST_INSERT_ID(
id + @@global.auto_increment_increment
))
""")
cursor.execute('SELECT LAST_INSERT_ID()')
id = cursor.fetchone()[0]

Просмотреть файл

@ -2,10 +2,10 @@ import itertools
from django.conf import settings
from django.db import models
from django.utils import translation as translation_utils
from olympia.addons.query import IndexCompiler, IndexQuery
from django.db.models.sql import compiler
from django.db.models.sql.constants import LOUTER
from django.db.models.sql.datastructures import Join
def order_by_translation(qs, fieldname):
@ -26,10 +26,6 @@ def order_by_translation(qs, fieldname):
model = qs.model
field = model._meta.get_field(fieldname)
# connection is a tuple (lhs, table, join_cols)
connection = (model._meta.db_table, field.rel.to._meta.db_table,
field.rel.field_name)
# Doing the manual joins is flying under Django's radar, so we need to make
# sure the initial alias (the main table) is set up.
if not qs.query.tables:
@ -43,10 +39,15 @@ def order_by_translation(qs, fieldname):
# remove results and happily simplifies the LEFT OUTER JOINs to
# INNER JOINs)
qs.query = qs.query.clone(TranslationQuery)
t1 = qs.query.join(connection, join_field=field, reuse=set(),
nullable=True)
t2 = qs.query.join(connection, join_field=field, reuse=set(),
nullable=True)
t1 = qs.query.join(
Join(field.rel.to._meta.db_table, model._meta.db_table,
None, LOUTER, field, True),
reuse=set())
t2 = qs.query.join(
Join(field.rel.to._meta.db_table, model._meta.db_table,
None, LOUTER, field, True),
reuse=set())
qs.query.translation_aliases = {field: (t1, t2)}
f1, f2 = '%s.`localized_string`' % t1, '%s.`localized_string`' % t2
@ -58,7 +59,7 @@ def order_by_translation(qs, fieldname):
order_by=[prefix + name])
class TranslationQuery(IndexQuery):
class TranslationQuery(models.sql.query.Query):
"""
Overrides sql.Query to hit our special compiler that knows how to JOIN
translations.
@ -76,7 +77,7 @@ class TranslationQuery(IndexQuery):
return SQLCompiler(self, c.connection, c.using)
class SQLCompiler(IndexCompiler):
class SQLCompiler(compiler.SQLCompiler):
"""Overrides get_from_clause to LEFT JOIN translations with a locale."""
def get_from_clause(self):
@ -84,7 +85,8 @@ class SQLCompiler(IndexCompiler):
# doesn't create joins against them.
old_tables = list(self.query.tables)
for table in itertools.chain(*self.query.translation_aliases.values()):
self.query.tables.remove(table)
if table in self.query.tables:
self.query.tables.remove(table)
joins, params = super(SQLCompiler, self).get_from_clause()
@ -113,12 +115,12 @@ class SQLCompiler(IndexCompiler):
# objects here instead of a bunch of strings.
qn = self.quote_name_unless_alias
qn2 = self.connection.ops.quote_name
mapping = self.query.alias_map[alias]
# name, alias, join_type, lhs, lhs_col, col, nullable = mapping
name, alias, join_type, lhs, join_cols, _, join_field = mapping
lhs_col = join_field.column
rhs_col = join_cols
alias_str = '' if alias == name else (' %s' % alias)
join = self.query.alias_map[alias]
lhs_col, rhs_col = join.join_cols[0]
alias_str = (
'' if join.table_alias == join.table_name
else ' %s' % join.table_alias)
if isinstance(fallback, models.Field):
fallback_str = '%s.%s' % (qn(self.query.model._meta.db_table),
@ -127,6 +129,7 @@ class SQLCompiler(IndexCompiler):
fallback_str = '%s'
return ('%s %s%s ON (%s.%s = %s.%s AND %s.%s = %s)' %
(join_type, qn(name), alias_str,
qn(lhs), qn2(lhs_col), qn(alias), qn2(rhs_col),
qn(alias), qn('locale'), fallback_str))
(join.join_type, qn(join.table_name), alias_str,
qn(join.parent_alias), qn2(lhs_col), qn(join.table_alias),
qn2(rhs_col), qn(join.table_alias), qn('locale'),
fallback_str))

Просмотреть файл

@ -12,6 +12,7 @@ class TestForm(forms.TranslationFormMixin, ModelForm):
class Meta:
model = TranslatedModel
fields = '__all__'
class TestTranslationFormMixin(TestCase):

Просмотреть файл

@ -165,7 +165,7 @@ urlpatterns = patterns(
)
if settings.TEMPLATE_DEBUG:
if settings.DEBUG:
# Remove leading and trailing slashes so the regex matches.
media_url = settings.MEDIA_URL.lstrip('/').rstrip('/')

Просмотреть файл

@ -1,8 +1,8 @@
from django.contrib import admin, messages
from django.db.utils import IntegrityError
from django.shortcuts import render
from olympia.access.admin import GroupUserInline
from olympia.amo.utils import render
from .models import UserProfile, BlacklistedName, BlacklistedEmailDomain
from . import forms

Просмотреть файл

@ -1,5 +1,5 @@
from django.conf import settings
from django.utils.encoding import smart_unicode
from django.utils.encoding import force_text
from django.utils.translation import pgettext
import jinja2
@ -77,7 +77,7 @@ def _user_link(user, max_text_length=None):
return u'<a href="%s" title="%s">%s</a>' % (
user.get_url_path(), jinja2.escape(user.name),
jinja2.escape(smart_unicode(username)))
jinja2.escape(force_text(username)))
@register.filter

Просмотреть файл

@ -15,10 +15,11 @@ from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
from django.core import validators
from django.db import models, transaction
from django.template import Context, loader
from django.utils import timezone
from django.utils.translation import ugettext as _, get_language, activate
from django.utils.crypto import constant_time_compare
from django.utils.datastructures import SortedDict
from django.utils.encoding import smart_str, smart_unicode
from django.utils.encoding import force_bytes, force_text
from django.utils.functional import lazy
import caching.base as caching
@ -45,7 +46,7 @@ class SHA512PasswordHasher(BasePasswordHasher):
assert password is not None
assert salt and '$' not in salt
hash = hashlib.new(self.algorithm,
smart_str(salt + password)).hexdigest()
force_bytes(salt + password)).hexdigest()
return "%s$%s$%s" % (self.algorithm, salt, hash)
def verify(self, password, encoded):
@ -75,9 +76,9 @@ def get_hexdigest(algorithm, salt, raw_password):
# users from getpersonas.com. The password is md5 hashed
# and then sha512'd.
md5 = hashlib.new('md5', raw_password).hexdigest()
return hashlib.new('sha512', smart_str(salt + md5)).hexdigest()
return hashlib.new('sha512', force_bytes(salt + md5)).hexdigest()
return hashlib.new(algorithm, smart_str(salt + raw_password)).hexdigest()
return hashlib.new(algorithm, force_bytes(salt + raw_password)).hexdigest()
def rand_string(length):
@ -99,7 +100,7 @@ class UserForeignKey(models.ForeignKey):
"""
def __init__(self, *args, **kw):
super(UserForeignKey, self).__init__(UserProfile, *args, **kw)
super(UserForeignKey, self).__init__('users.UserProfile', *args, **kw)
def value_from_object(self, obj):
return getattr(obj, self.name).email
@ -131,7 +132,10 @@ class UserManager(BaseUserManager, ManagerBase):
def create_user(self, username, email, password=None, fxa_id=None):
# We'll send username=None when registering through FxA to try and
# generate a username from the email.
user = self.model(username=username, email=email, fxa_id=fxa_id)
now = timezone.now()
user = self.model(
username=username, email=email, fxa_id=fxa_id,
last_login=now)
if username is None:
user.anonymize_username()
# FxA won't set a password so don't let a user log in with one.
@ -157,8 +161,7 @@ class UserManager(BaseUserManager, ManagerBase):
AbstractBaseUser._meta.get_field('password').max_length = 255
class UserProfile(OnChangeMixin, ModelBase,
AbstractBaseUser):
class UserProfile(OnChangeMixin, ModelBase, AbstractBaseUser):
objects = UserManager()
USERNAME_FIELD = 'username'
REQUIRED_FIELDS = ['email']
@ -166,7 +169,7 @@ class UserProfile(OnChangeMixin, ModelBase,
display_name = models.CharField(max_length=255, default='', null=True,
blank=True)
email = models.EmailField(unique=True, null=True)
email = models.EmailField(unique=True, null=True, max_length=75)
averagerating = models.CharField(max_length=255, blank=True, null=True)
bio = NoLinksField(short=False)
@ -206,7 +209,7 @@ class UserProfile(OnChangeMixin, ModelBase,
def __init__(self, *args, **kw):
super(UserProfile, self).__init__(*args, **kw)
if self.username:
self.username = smart_unicode(self.username)
self.username = force_text(self.username)
def __unicode__(self):
return u'%s: %s' % (self.id, self.display_name or self.username)
@ -346,14 +349,14 @@ class UserProfile(OnChangeMixin, ModelBase,
@property
def name(self):
if self.display_name:
return smart_unicode(self.display_name)
return force_text(self.display_name)
elif self.has_anonymous_username():
# L10n: {id} will be something like "13ad6a", just a random number
# to differentiate this user from other anonymous users.
return _('Anonymous user {id}').format(
id=self._anonymous_username_id())
else:
return smart_unicode(self.username)
return force_text(self.username)
welcome_name = name
@ -664,7 +667,7 @@ class BlacklistedPassword(ModelBase):
class UserHistory(ModelBase):
email = models.EmailField()
email = models.EmailField(max_length=75)
user = models.ForeignKey(UserProfile, related_name='history')
class Meta:

Просмотреть файл

@ -8,7 +8,8 @@ from django.conf import settings
from django.contrib.auth.hashers import (is_password_usable, check_password,
make_password, identify_hasher)
from django.core import mail
from django.utils import encoding, translation
from django.utils import translation
from django.utils.encoding import force_bytes
import pytest
from mock import patch
@ -343,7 +344,7 @@ class TestPasswords(TestCase):
assert u.has_usable_password() is True
def test_valid_old_password(self):
hsh = hashlib.md5(encoding.smart_str(self.utf)).hexdigest()
hsh = hashlib.md5(force_bytes(self.utf)).hexdigest()
u = UserProfile(password=hsh)
assert u.check_password(self.utf) is True
# Make sure we updated the old password.

Просмотреть файл

@ -9,7 +9,7 @@ from django.core import mail
from django.core.cache import cache
from django.contrib.auth.tokens import default_token_generator
from django.forms.models import model_to_dict
from django.utils.encoding import smart_unicode
from django.utils.encoding import force_text
from django.utils.http import urlsafe_base64_encode
from lxml.html import fromstring, HTMLParser
@ -921,7 +921,7 @@ class TestRegistration(UserViewBase):
def test_redirects_to_login(self):
"""Register should redirect to login."""
response = self.client.get(reverse('users.register'))
response = self.client.get(reverse('users.register'), follow=True)
self.assert3xx(response, reverse('users.login'), status_code=301)
@ -1271,7 +1271,7 @@ class TestThemesProfile(TestCase):
results = doc('.personas-grid .persona.hovercard')
assert results.length == 1
assert smart_unicode(
assert force_text(
results.find('h3').html()) == unicode(self.theme.name)
def test_bad_user(self):

Просмотреть файл

@ -7,8 +7,7 @@ from django.conf import settings
from django.contrib import auth
from django.contrib.auth.tokens import default_token_generator
from django.db.transaction import non_atomic_requests
from django.shortcuts import (get_list_or_404, get_object_or_404, redirect,
render)
from django.shortcuts import get_list_or_404, get_object_or_404, redirect
from django.template import Context, loader
from django.utils.http import is_safe_url, urlsafe_base64_decode
from django.views.decorators.cache import never_cache
@ -33,7 +32,7 @@ from olympia.amo.decorators import (
post_required, write)
from olympia.amo.forms import AbuseForm
from olympia.amo.urlresolvers import get_url_prefix, reverse
from olympia.amo.utils import escape_all, send_mail, urlparams
from olympia.amo.utils import escape_all, send_mail, urlparams, render
from olympia.bandwagon.models import Collection
from olympia.browse.views import PersonasFilter
from olympia.users.models import UserNotification

Просмотреть файл

@ -1,6 +1,6 @@
import re
from django.utils.encoding import smart_str
from django.utils.encoding import force_bytes
MAXVERSION = 2 ** 63 - 1
@ -60,7 +60,7 @@ def version_dict(version):
def version_int(version):
d = version_dict(smart_str(version))
d = version_dict(force_bytes(version))
for key in ['alpha_ver', 'major', 'minor1', 'minor2', 'minor3',
'pre_ver']:
if not d[key]:

Просмотреть файл

@ -17,7 +17,6 @@ from django_statsd.clients import statsd
from olympia import amo
from olympia.amo.models import ManagerBase, ModelBase, OnChangeMixin
from olympia.amo.utils import sorted_groupby, utc_millesecs_from_epoch
from olympia.addons.query import IndexQuerySet
from olympia.amo.decorators import use_master
from olympia.amo.urlresolvers import reverse
from olympia.amo.helpers import user_media_path, id_to_path
@ -46,7 +45,6 @@ class VersionManager(ManagerBase):
def get_queryset(self):
qs = super(VersionManager, self).get_queryset()
qs = qs._clone(klass=IndexQuerySet)
if not self.include_deleted:
qs = qs.exclude(deleted=True)
return qs.transform(Version.transformer)
@ -306,7 +304,7 @@ class Version(OnChangeMixin, ModelBase):
@amo.cached_property(writable=True)
def compatible_apps(self):
"""Get a mapping of {APP: ApplicationVersion}."""
avs = self.apps.select_related('versions', 'license')
avs = self.apps.select_related('version')
return self._compat_map(avs)
@amo.cached_property
@ -477,7 +475,7 @@ class Version(OnChangeMixin, ModelBase):
# FIXME: find out why we have no_cache() here and try to remove it.
avs = (ApplicationsVersions.objects.filter(version__in=ids)
.select_related('application', 'apps', 'min_set', 'max_set')
.select_related('min', 'max')
.no_cache())
files = File.objects.filter(version__in=ids).no_cache()

Просмотреть файл

@ -2,7 +2,7 @@ import os
from django import http
from django.db.transaction import non_atomic_requests
from django.shortcuts import get_object_or_404, redirect, render
from django.shortcuts import get_object_or_404, redirect
import caching.base as caching
import commonware.log
@ -10,7 +10,7 @@ from mobility.decorators import mobile_template
from olympia import amo
from olympia.amo.urlresolvers import reverse
from olympia.amo.utils import HttpResponseSendFile, urlparams
from olympia.amo.utils import HttpResponseSendFile, urlparams, render
from olympia.access import acl
from olympia.addons.decorators import (
addon_view_factory, owner_or_unlisted_reviewer)

Просмотреть файл

@ -1,25 +1,7 @@
from django.shortcuts import render
import jingo
from django.template import loader
from django.template.response import SimpleTemplateResponse
import jingo
def jinja_for_django(template_name, context=None, **kw):
"""
If you want to use some built in logic (or a contrib app) but need to
override the templates to work with Jinja, replace the object's
render_to_response function with this one. That will render a Jinja
template through Django's functions. An example can be found in the users
app.
"""
if context is None:
context = {}
context_instance = kw.pop('context_instance')
request = context_instance['request']
for d in context_instance.dicts:
context.update(d)
return render(request, template_name, context, **kw)
from django.template.backends.django import Template
# We monkeypatch SimpleTemplateResponse.rendered_content to use our jinja
@ -29,10 +11,9 @@ def jinja_for_django(template_name, context=None, **kw):
def rendered_content(self):
template = self.template_name
context_instance = self.resolve_context(self.context_data)
request = context_instance['request']
# Gross, let's figure out if we're in the admin.
if self._current_app == 'admin':
if getattr(self._request, 'current_app', None) == 'admin':
source = loader.render_to_string(template, context_instance)
template = jingo.get_env().from_string(source)
# This interferes with our media() helper.
@ -43,6 +24,10 @@ def rendered_content(self):
# not a list.
if isinstance(template, (list, tuple)):
template = loader.select_template(template)
return jingo.render_to_string(request, template, self.context_data)
if isinstance(template, Template):
template = template.template
return jingo.render_to_string(
self._request, template, self.context_data)
SimpleTemplateResponse.rendered_content = property(rendered_content)

Просмотреть файл

@ -51,7 +51,7 @@ class ValidationJob(ModelBase):
related_name='validation_current_set')
target_version = models.ForeignKey(AppVersion,
related_name='validation_target_set')
finish_email = models.EmailField(null=True)
finish_email = models.EmailField(null=True, max_length=75)
completed = models.DateTimeField(null=True, db_index=True)
creator = models.ForeignKey('users.UserProfile', null=True)
@ -242,7 +242,7 @@ class EmailPreview(ModelBase):
"""
topic = models.CharField(max_length=255, db_index=True)
recipient_list = models.TextField() # comma separated list of emails
from_email = models.EmailField()
from_email = models.EmailField(max_length=75)
subject = models.CharField(max_length=255)
body = models.TextField()

Просмотреть файл

@ -1780,11 +1780,10 @@ class TestFileDownload(TestCase):
assert resp.status_code == 302
self.upload = FileUpload.objects.get()
self.url = reverse('zadmin.download_file', args=[self.upload.uuid])
self.url = reverse('zadmin.download_file', args=[self.upload.uuid.hex])
def test_download(self):
"""Test that downloading file_upload objects works."""
resp = self.client.get(self.url)
assert resp.status_code == 200
assert resp.content == self.file.read()

Просмотреть файл

@ -12,7 +12,7 @@ from django.core.cache import cache
from django.core.exceptions import PermissionDenied
from django.core.files.storage import default_storage as storage
from django.db.models import Q
from django.shortcuts import get_object_or_404, redirect, render
from django.shortcuts import get_object_or_404, redirect
from django.utils.translation import ugettext_lazy as _lazy
from django.utils.html import format_html
from django.views import debug
@ -31,7 +31,7 @@ from olympia.amo.decorators import (
any_permission_required, json_view, login_required, post_required)
from olympia.amo.mail import DevEmailBackend
from olympia.amo.urlresolvers import reverse
from olympia.amo.utils import HttpResponseSendFile, chunked
from olympia.amo.utils import HttpResponseSendFile, chunked, render
from olympia.bandwagon.models import Collection
from olympia.compat import FIREFOX_COMPAT
from olympia.compat.models import AppCompat, CompatTotals