diff --git a/apps/addons/forms.py b/apps/addons/forms.py index c70da334965..69dbb96e777 100644 --- a/apps/addons/forms.py +++ b/apps/addons/forms.py @@ -296,9 +296,6 @@ def __init__(self, *args, **kw): cats = dict(self.addon.app_categories).get(app, []) self.initial.append({'categories': [c.id for c in cats]}) - # Reconstruct the forms according to the initial data. - self._construct_forms() - for app, form in zip(apps, self.forms): key = app.id if app else None form.request = self.request diff --git a/apps/addons/models.py b/apps/addons/models.py index 56157276b60..16489e1dcb7 100644 --- a/apps/addons/models.py +++ b/apps/addons/models.py @@ -1354,7 +1354,7 @@ def tags_partitioned_by_developer(self): """Returns a tuple of developer tags and user tags for this addon.""" tags = self.tags.not_blacklisted() if self.is_persona: - return models.query.EmptyQuerySet(), tags + return [], tags user_tags = tags.exclude(addon_tags__user__in=self.listed_authors) dev_tags = tags.exclude(id__in=[t.id for t in user_tags]) return dev_tags, user_tags @@ -1714,7 +1714,7 @@ def check_ownership(self, request, require_owner, require_author, class AddonDeviceType(amo.models.ModelBase): - addon = models.ForeignKey(Addon) + addon = models.ForeignKey(Addon, db_constraint=False) device_type = models.PositiveIntegerField( default=amo.DEVICE_DESKTOP, choices=do_dictsort(amo.DEVICE_TYPES), db_index=True) diff --git a/apps/addons/query.py b/apps/addons/query.py index 7a2c941886e..d6179687893 100644 --- a/apps/addons/query.py +++ b/apps/addons/query.py @@ -77,11 +77,12 @@ def get_from_clause(self): 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, lhs_col, col, nullable = self.query.alias_map[alias] + 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. @@ -93,13 +94,25 @@ def get_from_clause(self): else: use_index = '' if join_type and not first: - # If you really need a LEFT OUTER JOIN, file a bug. - join_type = 'INNER JOIN' - result.append('%s %s%s %s ON (%s.%s = %s.%s)' - % (join_type, qn(name), alias_str, use_index, qn(lhs), - qn2(lhs_col), qn(alias), qn2(col))) + 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 = not first and ', ' or '' + connector = connector = '' if first else ', ' result.append('%s%s%s %s' % (connector, qn(name), alias_str, use_index)) ### jbalogh out. ### first = False @@ -112,4 +125,4 @@ def get_from_clause(self): connector = not first and ', ' or '' result.append('%s%s' % (connector, qn(alias))) first = False - return result, [] + return result, from_params diff --git a/apps/addons/tests/test_models.py b/apps/addons/tests/test_models.py index e9fea169252..b316b5ddff1 100644 --- a/apps/addons/tests/test_models.py +++ b/apps/addons/tests/test_models.py @@ -2198,29 +2198,32 @@ def setUp(self): def test_extract(self): File.objects.create(platform=self.platform_mob, version=self.version, - filename=self.xpi_path('langpack-localepicker')) - assert self.addon.get_localepicker() + filename=self.xpi_path('langpack-localepicker'), + status=amo.STATUS_PUBLIC) + assert self.addon.reload().get_localepicker() assert 'title=Select a language' in self.addon.get_localepicker() def test_extract_no_file(self): File.objects.create(platform=self.platform_mob, version=self.version, - filename=self.xpi_path('langpack')) - eq_(self.addon.get_localepicker(), '') + filename=self.xpi_path('langpack'), status=amo.STATUS_PUBLIC) + eq_(self.addon.reload().get_localepicker(), '') def test_extract_no_files(self): eq_(self.addon.get_localepicker(), '') def test_extract_not_language_pack(self): File.objects.create(platform=self.platform_mob, version=self.version, - filename=self.xpi_path('langpack-localepicker')) - assert self.addon.get_localepicker() + filename=self.xpi_path('langpack-localepicker'), + status=amo.STATUS_PUBLIC) + assert self.addon.reload().get_localepicker() self.addon.update(type=amo.ADDON_EXTENSION) eq_(self.addon.get_localepicker(), '') def test_extract_not_platform_mobile(self): File.objects.create(platform=self.platform_all, version=self.version, - filename=self.xpi_path('langpack-localepicker')) - eq_(self.addon.get_localepicker(), '') + filename=self.xpi_path('langpack-localepicker'), + status=amo.STATUS_PUBLIC) + eq_(self.addon.reload().get_localepicker(), '') class TestMarketplace(amo.tests.TestCase): diff --git a/apps/amo/forms.py b/apps/amo/forms.py index 3c4772116da..515ade24fc5 100644 --- a/apps/amo/forms.py +++ b/apps/amo/forms.py @@ -37,7 +37,7 @@ def _get_changed_data(self): """ Model = self._meta.model if self._changed_data is None: - changed = copy(super(AMOModelForm, self)._get_changed_data()) + changed = copy(forms.ModelForm.changed_data.__get__(self)) fieldnames = [f.name for f in Model._meta.fields] fields = [(name, Model._meta.get_field(name)) for name in changed if name in fieldnames] diff --git a/apps/amo/models.py b/apps/amo/models.py index 6f4e78c03e7..10ab38a82ae 100644 --- a/apps/amo/models.py +++ b/apps/amo/models.py @@ -163,8 +163,8 @@ class ManagerBase(caching.base.CachingManager, UncachedManagerBase): function. """ - def get_query_set(self): - qs = super(ManagerBase, self).get_query_set() + def get_queryset(self): + qs = super(ManagerBase, self).get_queryset() if getattr(_locals, 'skip_cache', False): qs = qs.no_cache() return self._with_translations(qs) diff --git a/apps/amo/templates/amo/robots.html b/apps/amo/templates/amo/robots.html index b76f35378a6..284417ec973 100644 --- a/apps/amo/templates/amo/robots.html +++ b/apps/amo/templates/amo/robots.html @@ -16,7 +16,7 @@ {% for a in apps -%} Disallow: /{{ l }}/{{ a.short }}{{ url('search.search', add_prefix=False) }} -Disallow: /{{ l }}/{{ a.short }}{{ url('users.pwreset', add_prefix=False) }} +Disallow: /{{ l }}/{{ a.short }}{{ url('password_reset_form', add_prefix=False) }} {% endfor %} {% endfor %} diff --git a/apps/amo/tests/test_helpers.py b/apps/amo/tests/test_helpers.py index 66044c85dc9..7a736bd1e09 100644 --- a/apps/amo/tests/test_helpers.py +++ b/apps/amo/tests/test_helpers.py @@ -436,5 +436,5 @@ def test_absolutify(): def test_timesince(): month_ago = datetime.now() - timedelta(days=30) - eq_(helpers.timesince(month_ago), u'1 month ago') + eq_(helpers.timesince(month_ago), u'1 month ago') eq_(helpers.timesince(None), u'') diff --git a/apps/amo/tests/test_readonly.py b/apps/amo/tests/test_readonly.py index c388a88a561..2fd8303e1f3 100644 --- a/apps/amo/tests/test_readonly.py +++ b/apps/amo/tests/test_readonly.py @@ -3,7 +3,6 @@ from django.utils import importlib import MySQLdb as mysql -from lib.misc import safe_signals from nose.tools import assert_raises, eq_ from pyquery import PyQuery as pq @@ -30,7 +29,6 @@ class ReadOnlyModeTest(amo.tests.TestCase): extra = ('amo.middleware.ReadOnlyMiddleware',) def setUp(self): - safe_signals.Signal.send = safe_signals.unsafe_send models.signals.pre_save.connect(self.db_error) models.signals.pre_delete.connect(self.db_error) self.old_settings = dict((k, quickcopy(getattr(settings, k))) @@ -52,7 +50,6 @@ def tearDown(self): pass models.signals.pre_save.disconnect(self.db_error) models.signals.pre_delete.disconnect(self.db_error) - safe_signals.Signal.send = safe_signals.safe_send def db_error(self, *args, **kwargs): raise mysql.OperationalError("You can't do this in read-only mode.") diff --git a/apps/amo/tests/test_send_mail.py b/apps/amo/tests/test_send_mail.py index bda55001f32..7138f981c23 100644 --- a/apps/amo/tests/test_send_mail.py +++ b/apps/amo/tests/test_send_mail.py @@ -26,6 +26,7 @@ def setUp(self): self._email_blacklist = list(getattr(settings, 'EMAIL_BLACKLIST', [])) def tearDown(self): + translation.activate('en_US') settings.EMAIL_BLACKLIST = self._email_blacklist def test_send_string(self): @@ -202,7 +203,7 @@ def test_send_html_mail_jinja(self): def test_send_attachment(self): path = os.path.join(ATTACHMENTS_DIR, 'bacon.txt') - attachments = [(os.path.basename(path), storage.open(path), + attachments = [(os.path.basename(path), storage.open(path).read(), mimetypes.guess_type(path)[0])] send_mail('test subject', 'test body', from_email='a@example.com', recipient_list=['b@example.com'], attachments=attachments) diff --git a/apps/api/tests/test_views.py b/apps/api/tests/test_views.py index 6f618bb4785..8451cf4eccf 100644 --- a/apps/api/tests/test_views.py +++ b/apps/api/tests/test_views.py @@ -1282,7 +1282,8 @@ def test_search_no_localepicker(self): def setup_localepicker(self, platform): self.addon.update(type=amo.ADDON_LPAPP, status=amo.STATUS_PUBLIC) version = self.addon.versions.all()[0] - File.objects.create(version=version, platform_id=platform) + File.objects.create(version=version, platform_id=platform, + status=amo.STATUS_PUBLIC) def test_search_wrong_platform(self): self.setup_localepicker(amo.PLATFORM_MAC.id) diff --git a/apps/devhub/forms.py b/apps/devhub/forms.py index 150695bf8fa..b4ff51a6621 100644 --- a/apps/devhub/forms.py +++ b/apps/devhub/forms.py @@ -441,7 +441,11 @@ def __init__(self, *args, **kw): self.initial = ([{} for _ in qs] + [{'application': a.id} for a in apps]) self.extra = len(amo.APP_GUIDS) - len(self.forms) - self._construct_forms() + # After these changes, the forms need to be rebuilt. `forms` + # is a cached property, so we delete the existing cache and + # ask for a new one to be built. + del self.forms + self.forms def clean(self): if any(self.errors): @@ -896,7 +900,6 @@ class PackagerCompatBaseFormSet(BaseFormSet): def __init__(self, *args, **kw): super(PackagerCompatBaseFormSet, self).__init__(*args, **kw) self.initial = [{'application': a} for a in amo.APP_USAGE] - self._construct_forms() def clean(self): if any(self.errors): diff --git a/apps/devhub/models.py b/apps/devhub/models.py index b25d18da32a..3e653e415b9 100644 --- a/apps/devhub/models.py +++ b/apps/devhub/models.py @@ -113,7 +113,7 @@ class AppLog(amo.models.ModelBase): """ This table is for indexing the activity log by app. """ - addon = models.ForeignKey(Webapp) + addon = models.ForeignKey(Webapp, db_constraint=False) activity_log = models.ForeignKey('ActivityLog') class Meta: diff --git a/apps/devhub/tests/test_forms.py b/apps/devhub/tests/test_forms.py index cd70fbe92d0..19fb1dcde5a 100644 --- a/apps/devhub/tests/test_forms.py +++ b/apps/devhub/tests/test_forms.py @@ -498,6 +498,7 @@ def test_localize_name_description(self): def test_reupload(self, save_persona_image_mock, create_persona_preview_images_mock, make_checksum_mock): + make_checksum_mock.return_value = 'checksumbeforeyouwrecksome' data = self.get_dict(header_hash='y0l0', footer_hash='abab') self.form = EditThemeForm(data, request=self.request, instance=self.instance) diff --git a/apps/discovery/tests/test_views.py b/apps/discovery/tests/test_views.py index 773080833ca..2d07ba82951 100644 --- a/apps/discovery/tests/test_views.py +++ b/apps/discovery/tests/test_views.py @@ -527,7 +527,8 @@ def test_eula_trickle(self): class TestMonthlyPick(amo.tests.TestCase): - fixtures = ['base/apps', 'base/addon_3615', 'discovery/discoverymodules'] + fixtures = ['base/users', 'base/apps', 'base/addon_3615', + 'discovery/discoverymodules'] def setUp(self): self.url = reverse('discovery.pane.promos', args=['Darwin', '10.0']) diff --git a/apps/editors/helpers.py b/apps/editors/helpers.py index b27d6edd54a..d01de125b1b 100644 --- a/apps/editors/helpers.py +++ b/apps/editors/helpers.py @@ -641,7 +641,7 @@ class ReviewAddon(ReviewBase): def __init__(self, *args, **kwargs): super(ReviewAddon, self).__init__(*args, **kwargs) - self.is_upgrade = (self.addon.status is amo.STATUS_LITE_AND_NOMINATED + self.is_upgrade = (self.addon.status == amo.STATUS_LITE_AND_NOMINATED and self.review_type == 'nominated') def set_data(self, data): diff --git a/apps/editors/tests/test_views_themes.py b/apps/editors/tests/test_views_themes.py index 0738a01b835..c2f541eb924 100644 --- a/apps/editors/tests/test_views_themes.py +++ b/apps/editors/tests/test_views_themes.py @@ -178,10 +178,10 @@ def test_commit(self, copy_mock, create_preview_mock, eq_(themes[4].addon.reload().current_version.version, str(float(old_version) + 1)) else: - eq_(themes[0].addon.status, amo.STATUS_REVIEW_PENDING) - eq_(themes[1].addon.status, amo.STATUS_REVIEW_PENDING) - eq_(themes[2].addon.status, amo.STATUS_REJECTED) - eq_(themes[3].addon.status, amo.STATUS_REJECTED) + eq_(themes[0].addon.reload().status, amo.STATUS_REVIEW_PENDING) + eq_(themes[1].addon.reload().status, amo.STATUS_REVIEW_PENDING) + eq_(themes[2].addon.reload().status, amo.STATUS_REJECTED) + eq_(themes[3].addon.reload().status, amo.STATUS_REJECTED) eq_(themes[4].addon.reload().status, amo.STATUS_PUBLIC) eq_(ActivityLog.objects.count(), 4 if self.rereview else 5) diff --git a/apps/files/tests/test_views.py b/apps/files/tests/test_views.py index da7f8461215..b7c43eaa9a5 100644 --- a/apps/files/tests/test_views.py +++ b/apps/files/tests/test_views.py @@ -381,7 +381,7 @@ def test_directory(self): def test_unicode(self): self.file_viewer.src = unicode_filenames self.file_viewer.extract() - res = self.client.get(self.file_url(iri_to_uri(u'\u1109\u1161\u11a9'))) + res = self.client.get(self.file_url(u'\u1109\u1161\u11a9')) eq_(res.status_code, 200) def test_serve_no_token(self): diff --git a/apps/stats/db.py b/apps/stats/db.py index 82cd9dc98a4..445c16f72ce 100644 --- a/apps/stats/db.py +++ b/apps/stats/db.py @@ -1,10 +1,7 @@ from django.db import models import phpserialize as php -try: - import simplejson as json -except ImportError: - import json +import json class StatsDictField(models.TextField): diff --git a/apps/stats/views.py b/apps/stats/views.py index 780031ff5be..01270585a86 100644 --- a/apps/stats/views.py +++ b/apps/stats/views.py @@ -1,6 +1,7 @@ import cStringIO import csv import itertools +import json import logging import time from datetime import date, timedelta @@ -13,7 +14,6 @@ from django.db import connection from django.db.models import Avg, Count, Q, Sum from django.shortcuts import get_object_or_404 -from django.utils import simplejson from django.utils.cache import add_never_cache_headers, patch_cache_control from django.utils.datastructures import SortedDict @@ -691,5 +691,5 @@ def render_json(request, addon, stats): # Django's encoder supports date and datetime. fudge_headers(response, stats) - simplejson.dump(stats, response, cls=DjangoJSONEncoder) + json.dump(stats, response, cls=DjangoJSONEncoder) return response diff --git a/apps/translations/fields.py b/apps/translations/fields.py index 10ea34506c8..475ec8323d4 100644 --- a/apps/translations/fields.py +++ b/apps/translations/fields.py @@ -242,7 +242,11 @@ def _has_changed(self, initial, data): class LocaleValidationError(forms.ValidationError): def __init__(self, messages, code=None, params=None): - self.messages = messages + self.msgs = messages + + @property + def messages(self): + return self.msgs class TransField(_TransField, forms.CharField): diff --git a/apps/translations/query.py b/apps/translations/query.py index 3d0126f5863..50dd33dba96 100644 --- a/apps/translations/query.py +++ b/apps/translations/query.py @@ -2,7 +2,6 @@ from django.conf import settings from django.db import models -from django.db.models.sql import compiler from django.utils import translation as translation_utils @@ -27,20 +26,23 @@ def order_by_translation(qs, fieldname): model = qs.model field = model._meta.get_field(fieldname) - # (lhs, rhs, lhs_col, rhs_col) => lhs.lhs_col = rhs.rhs_col + # connection is a tuple (lhs, table, join_cols) connection = (model._meta.db_table, field.rel.to._meta.db_table, - field.column, field.rel.field_name) + 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: qs.query.get_initial_alias() - # Force two LEFT JOINs against the translation table. We'll hook up the + # Force two new (reuse is an empty set) LEFT OUTER JOINs against the + # translation table, without reusing any aliases. We'll hook up the # language fallbacks later. qs.query = qs.query.clone(TranslationQuery) - t1 = qs.query.join(connection, always_create=True, promote=True) - t2 = qs.query.join(connection, always_create=True, promote=True) + t1 = qs.query.join(connection, join_field=field, + outer_if_first=True, reuse=set()) + t2 = qs.query.join(connection, join_field=field, + outer_if_first=True, reuse=set()) qs.query.translation_aliases = {field: (t1, t2)} f1, f2 = '%s.`localized_string`' % t1, '%s.`localized_string`' % t2 @@ -108,8 +110,11 @@ def join_with_locale(self, alias, fallback=None): 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 - alias_str = (alias != name and ' %s' % alias or '') + # 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) if isinstance(fallback, models.Field): fallback_str = '%s.%s' % (qn(self.query.model._meta.db_table), @@ -119,5 +124,5 @@ def join_with_locale(self, alias, fallback=None): 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(col), + qn(lhs), qn2(lhs_col), qn(alias), qn2(rhs_col), qn(alias), qn('locale'), fallback_str)) diff --git a/apps/users/models.py b/apps/users/models.py index faa09d103f6..1c5d36faa2e 100644 --- a/apps/users/models.py +++ b/apps/users/models.py @@ -312,7 +312,11 @@ def last_login(self): @amo.cached_property def reviews(self): """All reviews that are not dev replies.""" - return self._reviews_all.filter(reply_to=None) + qs = self._reviews_all.filter(reply_to=None) + # Force the query to occur immediately. Several + # reviews-related tests hang if this isn't done. + list(qs) + return qs def anonymize(self): log.info(u"User (%s: <%s>) is being anonymized." % (self, self.email)) diff --git a/apps/users/templates/users/edit.html b/apps/users/templates/users/edit.html index 4745bafb13f..d63b081d7d7 100644 --- a/apps/users/templates/users/edit.html +++ b/apps/users/templates/users/edit.html @@ -51,7 +51,7 @@
{{ _('Password') }}

- {% trans reset_url=url('users.pwreset') -%} + {% trans reset_url=url('password_reset_form') -%} Change your password. If you forgot your password, you can use the reset form. {%- endtrans %}

diff --git a/apps/users/templates/users/login_help.html b/apps/users/templates/users/login_help.html index d5b5c2ffe36..1ed303d5618 100644 --- a/apps/users/templates/users/login_help.html +++ b/apps/users/templates/users/login_help.html @@ -3,7 +3,7 @@

{{ _('Login Problems?') }}

diff --git a/apps/users/templates/users/mobile/login.html b/apps/users/templates/users/mobile/login.html index c8ef394ddca..bd684e60ff4 100644 --- a/apps/users/templates/users/mobile/login.html +++ b/apps/users/templates/users/mobile/login.html @@ -79,7 +79,7 @@

{{ _('Log In') }}

{{ _('Login Problems?') }}

{% endif %} diff --git a/apps/users/tests/test_forms.py b/apps/users/tests/test_forms.py index 139f30f0ac5..3b09217c664 100644 --- a/apps/users/tests/test_forms.py +++ b/apps/users/tests/test_forms.py @@ -3,7 +3,7 @@ from django.contrib.auth.models import User from django.contrib.auth.tokens import default_token_generator from django.core import mail -from django.utils.http import int_to_base36 +from django.utils.http import urlsafe_base64_encode from django.conf import settings from mock import Mock, patch @@ -26,21 +26,22 @@ class UserFormBase(amo.tests.TestCase): def setUp(self): self.user = User.objects.get(id='4043307') self.user_profile = self.user.get_profile() - self.uidb36 = int_to_base36(self.user.id) + self.uidb64 = urlsafe_base64_encode(str(self.user.id)) self.token = default_token_generator.make_token(self.user) class TestSetPasswordForm(UserFormBase): def _get_reset_url(self): - return "/en-US/firefox/users/pwreset/%s/%s" % (self.uidb36, self.token) + return "/en-US/firefox/users/pwreset/%s/%s" % (self.uidb64, self.token) def test_url_fail(self): r = self.client.get('/users/pwreset/junk/', follow=True) eq_(r.status_code, 404) r = self.client.get('/en-US/firefox/users/pwreset/%s/12-345' % - self.uidb36) + self.uidb64 +) self.assertContains(r, "Password reset unsuccessful") def test_set_fail(self): @@ -106,15 +107,7 @@ def test_request_success(self): eq_(len(mail.outbox), 1) assert mail.outbox[0].subject.find('Password reset') == 0 - assert mail.outbox[0].body.find('pwreset/%s' % self.uidb36) > 0 - - def test_amo_user_but_no_django_user(self): - # Password reset should work without a Django user. - self.user_profile.update(user=None, _signal=True) - self.user.delete() - self.client.post('/en-US/firefox/users/pwreset', - {'email': self.user.email}) - eq_(len(mail.outbox), 1) + assert mail.outbox[0].body.find('pwreset/%s' % self.uidb64) > 0 class TestUserDeleteForm(UserFormBase): @@ -280,7 +273,7 @@ def test_credential_fail(self): 'password': 'wrongpassword'}) self.assertFormError(r, 'form', '', ("Please enter a correct username " "and password. Note that both " - "fields are case-sensitive.")) + "fields may be case-sensitive.")) def test_credential_success(self): user = UserProfile.objects.get(email='jbalogh@mozilla.com') @@ -358,7 +351,7 @@ def test_disabled_account(self): 'password': 'foo'}, follow=True) self.assertNotContains(r, "Welcome, Jeff") self.assertContains(r, 'Please enter a correct username and password. ' - 'Note that both fields are case-sensitive.') + 'Note that both fields may be case-sensitive.') def test_successful_login_logging(self): t = datetime.now() diff --git a/apps/users/tests/test_models.py b/apps/users/tests/test_models.py index 4239d71dc3e..67886d179e9 100644 --- a/apps/users/tests/test_models.py +++ b/apps/users/tests/test_models.py @@ -7,7 +7,7 @@ from django import forms from django.conf import settings from django.contrib.auth.hashers import (is_password_usable, - check_password, make_password) + check_password, make_password, load_hashers, identify_hasher) from django.contrib.auth.models import User from django.core import mail from django.utils import encoding, translation @@ -338,6 +338,13 @@ def test_sha512(self): self.assertTrue(is_password_usable(encoded)) self.assertTrue(check_password('lètmein', encoded)) self.assertFalse(check_password('lètmeinz', encoded)) + self.assertEqual(identify_hasher(encoded).algorithm, "sha512") + # Blank passwords + blank_encoded = make_password('', 'seasalt', 'sha512') + self.assertTrue(blank_encoded.startswith('sha512$')) + self.assertTrue(is_password_usable(blank_encoded)) + self.assertTrue(check_password('', blank_encoded)) + self.assertFalse(check_password(' ', blank_encoded)) class TestBlacklistedUsername(amo.tests.TestCase): diff --git a/apps/users/tests/test_views.py b/apps/users/tests/test_views.py index 7c658028c26..97d22d1f5ff 100644 --- a/apps/users/tests/test_views.py +++ b/apps/users/tests/test_views.py @@ -9,7 +9,7 @@ from django.contrib.auth.models import User from django.contrib.auth.tokens import default_token_generator from django.forms.models import model_to_dict -from django.utils.http import int_to_base36 +from django.utils.http import urlsafe_base64_encode from mock import ANY, Mock, patch from nose.tools import eq_ @@ -1050,7 +1050,7 @@ class TestReset(UserViewBase): def setUp(self): user = User.objects.get(email='editor@mozilla.com').get_profile() - self.token = [int_to_base36(user.id), + self.token = [urlsafe_base64_encode(str(user.id)), default_token_generator.make_token(user)] def test_reset_msg(self): diff --git a/apps/users/urls.py b/apps/users/urls.py index e58423118c4..7f5aed53a15 100644 --- a/apps/users/urls.py +++ b/apps/users/urls.py @@ -43,11 +43,11 @@ {'template_name': 'users/pwreset_request.html', 'email_template_name': 'users/email/pwreset.ltxt', 'password_reset_form': forms.PasswordResetForm, - }, name="users.pwreset"), + }, name='password_reset_form'), url(r'^pwresetsent$', auth_views.password_reset_done, {'template_name': 'users/pwreset_sent.html'}, - name="users.pwreset_sent"), - url(r'^pwreset/(?P\w{1,13})/(?P\w{1,13}-\w{1,20})$', + name="password_reset_done"), + url(r'^pwreset/(?P[0-9A-Za-z_\-]+)/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})', views.password_reset_confirm, name="users.pwreset_confirm"), url(r'^pwresetcomplete$', auth_views.password_reset_complete, diff --git a/apps/users/views.py b/apps/users/views.py index c901915397b..35574ab582b 100644 --- a/apps/users/views.py +++ b/apps/users/views.py @@ -12,7 +12,7 @@ from django.template import Context, loader from django.views.decorators.cache import never_cache from django.views.decorators.csrf import csrf_exempt -from django.utils.http import base36_to_int, is_safe_url +from django.utils.http import urlsafe_base64_decode, is_safe_url import commonware.log import jingo @@ -740,15 +740,15 @@ def report_abuse(request, user): @never_cache -def password_reset_confirm(request, uidb36=None, token=None): +def password_reset_confirm(request, uidb64=None, token=None): """ Pulled from django contrib so that we can add user into the form so then we can show relevant messages about the user. """ - assert uidb36 is not None and token is not None + assert uidb64 is not None and token is not None user = None try: - uid_int = base36_to_int(uidb36) + uid_int = urlsafe_base64_decode(uidb64) user = UserProfile.objects.get(id=uid_int) except (ValueError, UserProfile.DoesNotExist): pass diff --git a/apps/zadmin/tests/test_views.py b/apps/zadmin/tests/test_views.py index f348e0324c0..898d266890b 100644 --- a/apps/zadmin/tests/test_views.py +++ b/apps/zadmin/tests/test_views.py @@ -1478,7 +1478,7 @@ def test_no_webapps(self): rows = doc('#result_list tbody tr') eq_(rows.length, 1) eq_(rows.find('a').attr('href'), - '3615/') + '/en-US/admin/models/addons/addon/3615/') class TestAddonManagement(amo.tests.TestCase): diff --git a/lib/misc/safe_signals.py b/lib/misc/safe_signals.py deleted file mode 100644 index 333886eb0a4..00000000000 --- a/lib/misc/safe_signals.py +++ /dev/null @@ -1,51 +0,0 @@ -""" -A monkeypatch for ``django.dispatch`` to send signals safely. - -Usage:: - - >>> import safe_signals - >>> safe_signals.start_the_machine() - -``django.dispatch.Signal.send`` is replaced with a safer function that catches -and logs errors. It's like ``Signal.send_robust`` but with logging. - -""" -import logging - -from django.dispatch.dispatcher import Signal, _make_id -from django.conf import settings - - -log = logging.getLogger('signals') - - -def safe_send(self, sender, **named): - responses = [] - if not self.receivers: - return responses - - do_raise = getattr(settings, 'RAISE_ON_SIGNAL_ERROR', False) - - # Call each receiver with whatever arguments it can accept. - # Return a list of tuple pairs [(receiver, response), ... ]. - for receiver in self._live_receivers(_make_id(sender)): - try: - response = receiver(signal=self, sender=sender, **named) - except Exception, err: - if do_raise: - raise - log.error('Error calling signal', exc_info=True) - responses.append((receiver, err)) - else: - responses.append((receiver, response)) - return responses - - -safe_send.__doc__ = Signal.send_robust.__doc__ -unsafe_send = Signal.send - - -def start_the_machine(): - # Monkeypatch! - Signal.send = safe_send - Signal.send_robust = safe_send diff --git a/manage.py b/manage.py index ffe19056015..14f2dc94951 100755 --- a/manage.py +++ b/manage.py @@ -31,8 +31,7 @@ # No third-party imports until we've added all our sitedirs! -from django.core.management import (call_command, execute_manager, - setup_environ) +from django.core.management import call_command, execute_from_command_line # Figuring out what settings file to use. # 1. Look first for the command line setting. @@ -70,14 +69,6 @@ if not settings.DEBUG: warnings.simplefilter('ignore') -# The first thing execute_manager does is call `setup_environ`. Logging config -# needs to access settings, so we'll setup the environ early. -setup_environ(settings) - -# Hardcore monkeypatching action. -import safe_django_forms -safe_django_forms.monkeypatch() - import session_csrf session_csrf.monkeypatch() @@ -93,6 +84,9 @@ #imported before anything else imports waffle. import amo +# Hardcore monkeypatching action. +import safe_django_forms +safe_django_forms.monkeypatch() def new(self, arg): try: @@ -137,4 +131,4 @@ def new(self, arg): call_command('update_product_details') product_details.__init__() # reload the product details - execute_manager(settings) + execute_from_command_line() diff --git a/mkt/api/tests/test_oauth.py b/mkt/api/tests/test_oauth.py index 1aba1729ea1..87b29060795 100644 --- a/mkt/api/tests/test_oauth.py +++ b/mkt/api/tests/test_oauth.py @@ -7,7 +7,7 @@ from django.conf import settings from django.contrib.auth.models import User from django.test.client import Client, FakePayload -from django.utils.encoding import smart_str +from django.utils.encoding import iri_to_uri, smart_str from nose.tools import eq_ from oauthlib import oauth1 @@ -153,7 +153,10 @@ class RestOAuthClient(OAuthClient): def __init__(self, access): super(OAuthClient, self).__init__(self) self.access = access - self.get_absolute_url = absolutify + + def get_absolute_url(self, url): + unquoted_url = urlparse.unquote(url) + return absolutify(iri_to_uri(unquoted_url)) class RestOAuth(BaseOAuth): diff --git a/mkt/developers/forms.py b/mkt/developers/forms.py index 3942d192ab5..88302101cab 100644 --- a/mkt/developers/forms.py +++ b/mkt/developers/forms.py @@ -927,7 +927,7 @@ class AppFormTechnical(addons.forms.AddonFormBase): class Meta: model = Addon - fields = 'public_stats', + fields = ('public_stats',) def __init__(self, *args, **kw): super(AppFormTechnical, self).__init__(*args, **kw) @@ -935,11 +935,11 @@ def __init__(self, *args, **kw): def save(self, addon, commit=False): uses_flash = self.cleaned_data.get('flash') + self.instance = super(AppFormTechnical, self).save(commit=True) af = self.instance.get_latest_file() if af is not None: af.update(uses_flash=bool(uses_flash)) - - return super(AppFormTechnical, self).save(commit=True) + return self.instance class TransactionFilterForm(happyforms.Form): diff --git a/mkt/developers/tests/test_forms.py b/mkt/developers/tests/test_forms.py index 1a7e835c273..15feb65f1b4 100644 --- a/mkt/developers/tests/test_forms.py +++ b/mkt/developers/tests/test_forms.py @@ -1,4 +1,4 @@ -# -*- coding: utf8 -*- +# -*- coding: utf-8 -*- import json import os import shutil @@ -657,7 +657,7 @@ def test_changing_cert(self, storefront_mock): ok_(form.is_valid(), form.errors) form.save(self.app) - iarc_info = self.app.iarc_info + iarc_info = self.app.iarc_info.reload() eq_(iarc_info.submission_id, 2) eq_(iarc_info.security_code, 'b') assert storefront_mock.called diff --git a/mkt/developers/tests/test_views_api.py b/mkt/developers/tests/test_views_api.py index 07eaa37f3e3..34103b7631a 100644 --- a/mkt/developers/tests/test_views_api.py +++ b/mkt/developers/tests/test_views_api.py @@ -25,7 +25,7 @@ def test_non_url(self): res = self.client.post( self.url, {'app_name': 'test', 'redirect_uri': 'mailto:cvan@example.com'}) - self.assertFormError(res, 'form', 'redirect_uri', ['Enter a valid value.']) + self.assertFormError(res, 'form', 'redirect_uri', ['Enter a valid URL.']) def test_create(self): Access.objects.create(user=self.user, key='foo', secret='bar') diff --git a/mkt/developers/tests/test_views_edit.py b/mkt/developers/tests/test_views_edit.py index 79cc059da05..60481879461 100644 --- a/mkt/developers/tests/test_views_edit.py +++ b/mkt/developers/tests/test_views_edit.py @@ -1331,7 +1331,7 @@ def test_mozilla_contact_invalid(self): mozilla_contact='@mozilla.com') webapp = self.get_webapp() self.assertFormError(r, 'form', 'mozilla_contact', - 'Enter a valid e-mail address.') + 'Enter a valid email address.') eq_(webapp.mozilla_contact, '') def test_staff(self): diff --git a/mkt/developers/views.py b/mkt/developers/views.py index 23dce5b4bda..4590d75f6c7 100644 --- a/mkt/developers/views.py +++ b/mkt/developers/views.py @@ -553,7 +553,6 @@ def ownership(request, addon_id, addon, webapp=False): author.get_role_display(), addon) messages.success(request, _('Changes successfully saved.')) - return redirect(redirect_url) ctx = dict(addon=addon, webapp=webapp, user_form=user_form) diff --git a/mkt/search/tests/test_api.py b/mkt/search/tests/test_api.py index 2fd6e28aaf2..82eeca72191 100644 --- a/mkt/search/tests/test_api.py +++ b/mkt/search/tests/test_api.py @@ -132,7 +132,8 @@ def setUp(self): def tearDown(self): unindex_webapps(list(Webapp.with_deleted.values_list('id', flat=True))) - Webapp.objects.all().delete() + for w in Webapp.objects.all(): + w.delete() super(TestApi, self).tearDown() def test_verbs(self): diff --git a/mkt/submit/forms.py b/mkt/submit/forms.py index 9446360bde4..023789999ef 100644 --- a/mkt/submit/forms.py +++ b/mkt/submit/forms.py @@ -410,15 +410,13 @@ def clean_app_slug(self): return slug.lower() def save(self, *args, **kw): + self.instance = super(AppDetailsBasicForm, self).save(commit=True) uses_flash = self.cleaned_data.get('flash') af = self.instance.get_latest_file() if af is not None: af.update(uses_flash=bool(uses_flash)) - form = super(AppDetailsBasicForm, self).save(commit=False) - form.save() - - return form + return self.instance class AppFeaturesForm(happyforms.ModelForm): diff --git a/mkt/submit/tests/test_forms.py b/mkt/submit/tests/test_forms.py index 3928bfb66be..b865e021a53 100644 --- a/mkt/submit/tests/test_forms.py +++ b/mkt/submit/tests/test_forms.py @@ -187,7 +187,6 @@ def test_slug(self): instance=app) assert form.is_valid() form.save() - app.reload() eq_(app.app_slug, 'thisisaslug') diff --git a/mkt/submit/tests/test_views.py b/mkt/submit/tests/test_views.py index 373a1333afd..b8405f6944d 100644 --- a/mkt/submit/tests/test_views.py +++ b/mkt/submit/tests/test_views.py @@ -720,7 +720,6 @@ def test_success_paid(self): self.webapp = self.get_webapp() self.make_premium(self.webapp) - data = self.get_dict() r = self.client.post(self.url, data) self.assertNoFormErrors(r) @@ -919,7 +918,7 @@ def test_support_email_invalid(self): self._step() r = self.client.post(self.url, self.get_dict(support_email='xxx')) self.assertFormError(r, 'form_basic', 'support_email', - 'Enter a valid e-mail address.') + 'Enter a valid email address.') def test_categories_required(self): self._step() diff --git a/mkt/submit/views.py b/mkt/submit/views.py index eb5116fab1b..acd18e0eb34 100644 --- a/mkt/submit/views.py +++ b/mkt/submit/views.py @@ -168,14 +168,12 @@ def details(request, addon_id, addon): 'form_icon': form_icon, 'form_previews': form_previews, } - if request.POST and all(f.is_valid() for f in forms.itervalues()): addon = form_basic.save(addon) form_cats.save() form_icon.save(addon) for preview in form_previews.forms: preview.save(addon) - # If this is an incomplete app from the legacy submission flow, it may # not have device types set yet - so assume it works everywhere. if not addon.device_types: diff --git a/mkt/webapps/models.py b/mkt/webapps/models.py index 09d12a40305..5f256d85c43 100644 --- a/mkt/webapps/models.py +++ b/mkt/webapps/models.py @@ -67,7 +67,7 @@ def reverse_version(version): The try/except AttributeError allows this to be used where the input is ambiguous, and could be either an already-reversed URL or a Version object. """ - if version: + if version and settings.MARKETPLACE: try: return reverse('version-detail', kwargs={'pk': version.pk}) except AttributeError: diff --git a/mkt/zadmin/tests/test_views.py b/mkt/zadmin/tests/test_views.py index d3a2bddf6e2..04a55624e28 100644 --- a/mkt/zadmin/tests/test_views.py +++ b/mkt/zadmin/tests/test_views.py @@ -103,7 +103,7 @@ def test_no_webapps(self): doc = pq(res.content) rows = doc('#result_list tbody tr') eq_(rows.length, 1) - eq_(rows.find('a').attr('href'), '337141/') + eq_(rows.find('a').attr('href'), '/admin/models/addons/addon/337141/') class TestManifestRevalidation(amo.tests.TestCase): diff --git a/requirements/dev.txt b/requirements/dev.txt index 811838ff6b9..bca33439758 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,5 +1,5 @@ -r prod.txt -r compiled.txt -django-debug-toolbar==0.9.4 +django-debug-toolbar==0.11.0 django-fixture-magic==0.0.4 diff --git a/requirements/prod.txt b/requirements/prod.txt index 654e5e369a3..a8809f8ef90 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -16,11 +16,10 @@ cssutils==0.9.7b3 curling==0.2.6 defusedxml==0.4.1 dennis==0.3.10 -Django==1.4.8 +Django==1.6.2 dj-database-url==0.2.2 django-aesfield==0.1.2 django-browserid==0.8 -django-cache-machine==0.8 django-cache-nuggets==0.1.1 django-celery==3.0.23 django-cronjobs==0.2.3 @@ -32,7 +31,7 @@ django-multidb-router==0.5.1 django-mysql-pool==0.2 django-mysql-pymysql==0.1 django-nose==1.2 -django-quieter-formset==0.3 +django-quieter-formset==0.4 django-raven-heka==0.3 django-recaptcha-mozilla==0.0.1 djangorestframework==2.3.12 @@ -104,14 +103,15 @@ suds==0.4 # Temporary fork. -e git+https://github.com/jsocol/jingo-minify.git@b7405d3f93628190bf83cc43b0ff44e2fbb8b3c0#egg=jingo_minify --e git+https://github.com/mozilla/nuggets.git@96e80a64aa4bfcfef4f43fc3ab6966450ccd7325#egg=nuggets --e git+https://github.com/jbalogh/test-utils.git@ce5136a257cd44a1c663319124a255c1d10a9834#egg=test-utils --e git+https://github.com/fwenzel/django-mozilla-product-details.git@36ef06539d6b34c4f345fd0d3e16937d0db9a752#egg=django-mozilla-product-details +-e git+https://github.com/washort/nuggets.git@02798dfce84030fca64775eaf56e92400f394e4f#egg=nuggets +-e git+https://github.com/washort/test-utils.git@4917197a64b6d0f2068c9ad1bfea0427c3e412e1#egg=test-utils +-e git+https://github.com/fwenzel/django-mozilla-product-details@f5a3c3c846fb75e12ad890b22ed5375d5b85ccb4#egg=django-mozilla-product-details -e git+https://github.com/mozilla/signing-clients@a8bd730b202391c080113d224d223463e03088e9#egg=signing-clients -e git+https://github.com/mozilla/django-session-csrf@f00ad913c62e139d36078e8a7e07dab65a021386#egg=django-session-csrf +-e git+https://github.com/jbalogh/django-cache-machine@449861a61bcf096b792039128f044659e3e96eb6#egg=django-cache-machine ## Forked. --e git+https://github.com/andymckay/django-piston-oauth2.git@fa28ee#egg=django-piston-oauth2 +-e git+https://github.com/andymckay/django-piston-oauth2.git@177aaf937860318af8d9c2bb74adc27860803eb9#egg=django-piston-oauth2 -e git+https://github.com/kumar303/django-qunit.git@b0f468dcf33439488158c845df37ef3261852b55#egg=django-qunit -e git+https://github.com/andymckay/django-uuidfield.git@029dd1263794ec36c327617cd6c2346da81c8c33#egg=django-uuidfield diff --git a/services/pfs.py b/services/pfs.py index 259fa27bcdf..ba594decd61 100644 --- a/services/pfs.py +++ b/services/pfs.py @@ -6,7 +6,6 @@ from time import time from urlparse import parse_qsl -from django.core.management import setup_environ import commonware.log import jinja2 @@ -14,7 +13,6 @@ from utils import log_configure import settings_local as settings -setup_environ(settings) # This has to be imported after the settings so statsd knows where to log to. from django_statsd.clients import statsd diff --git a/services/theme_update.py b/services/theme_update.py index cf66612f061..fb347803c57 100644 --- a/services/theme_update.py +++ b/services/theme_update.py @@ -6,13 +6,10 @@ from time import time from wsgiref.handlers import format_date_time -from django.core.management import setup_environ - from constants import base from utils import log_configure, log_exception, mypool from services.utils import settings -setup_environ(settings) # Configure the log. log_configure() diff --git a/services/update.py b/services/update.py index 8fe51688626..ecff2dbea52 100644 --- a/services/update.py +++ b/services/update.py @@ -7,11 +7,10 @@ from time import time from urlparse import parse_qsl -from django.core.management import setup_environ from django.utils.http import urlencode import settings_local as settings -setup_environ(settings) + # This has to be imported after the settings so statsd knows where to log to. from django_statsd.clients import statsd diff --git a/services/utils.py b/services/utils.py index 7df2c35ba53..aa17e83f7f7 100644 --- a/services/utils.py +++ b/services/utils.py @@ -20,14 +20,11 @@ import MySQLdb as mysql import sqlalchemy.pool as pool -from django.core.management import setup_environ import commonware.log from django.utils import importlib settings = importlib.import_module(settingmodule) -# Pyflakes will complain about these, but they are required for setup. -setup_environ(settings) from lib.log_settings_base import formatters, handlers, loggers # Ugh. But this avoids any zamboni or django imports at all. diff --git a/services/verify.py b/services/verify.py index e86c66e11d2..6aa61a046e8 100644 --- a/services/verify.py +++ b/services/verify.py @@ -6,14 +6,12 @@ from urlparse import parse_qsl, urlparse from wsgiref.handlers import format_date_time -from django.core.management import setup_environ from utils import (log_configure, log_exception, log_info, mypool, ADDON_PREMIUM, CONTRIB_CHARGEBACK, CONTRIB_NO_CHARGE, CONTRIB_PURCHASE, CONTRIB_REFUND) from services.utils import settings -setup_environ(settings) # Go configure the log. log_configure() diff --git a/settings_test.py b/settings_test.py index 522fb7782da..a45e03cfd2b 100644 --- a/settings_test.py +++ b/settings_test.py @@ -79,7 +79,7 @@ def _polite_tmpdir(): # COUNT() caching can't be invalidated, it just expires after x seconds. This # is just too annoying for tests, so disable it. -CACHE_COUNT_TIMEOUT = None +CACHE_COUNT_TIMEOUT = -1 # No more failures! APP_PREVIEW = False @@ -127,8 +127,8 @@ def lazy_langs(languages): TASK_USER_ID = '4043307' PASSWORD_HASHERS = ( - 'users.models.SHA512PasswordHasher', 'django.contrib.auth.hashers.MD5PasswordHasher', + 'users.models.SHA512PasswordHasher', ) SQL_RESET_SEQUENCES = False