Browse Source

Merge pull request #1077 from rafalp/0.19.x

0.19.1
Rafał Pitoń 6 years ago
parent
commit
cf03f6d735

+ 2 - 1
.gitignore

@@ -71,8 +71,9 @@ db.sqlite3
 # Local development files
 # Local development files
 /avatargallery/
 /avatargallery/
 /avatar_store/
 /avatar_store/
-/manage.py
 /devproject/
 /devproject/
 /testproject/
 /testproject/
 /media/
 /media/
 /static/
 /static/
+/userdata/
+/manage.py

+ 6 - 5
initdev

@@ -6,8 +6,8 @@ python setup.py develop
 rm -f /srv/misago/.DS_Store
 rm -f /srv/misago/.DS_Store
 rm -f /srv/misago/Thumbs.db
 rm -f /srv/misago/Thumbs.db
 
 
-# If user specified "--f", clear after previous devinit
-if [ "$1" = "--f" ]
+# If user specified "-f", clear after previous devinit
+if [ "$1" = "-f" ]
 then
 then
     echo "Cleaned files created by previous initdev"
     echo "Cleaned files created by previous initdev"
     rm -f /srv/misago/cron.txt
     rm -f /srv/misago/cron.txt
@@ -17,6 +17,7 @@ then
     rm -rf /srv/misago/media
     rm -rf /srv/misago/media
     rm -rf /srv/misago/static
     rm -rf /srv/misago/static
     rm -rf /srv/misago/theme
     rm -rf /srv/misago/theme
+    rm -rf /srv/misago/userdata
 fi
 fi
 
 
 # Create new project
 # Create new project
@@ -48,10 +49,10 @@ echo "  - python: can't open file 'manage.py': [Errno 2] No such file or directo
 echo ""
 echo ""
 echo "If you are experiencing either of those errors, this means that files are"
 echo "If you are experiencing either of those errors, this means that files are"
 echo "present in the repository's main directory preventing 'initdev' from succedding."
 echo "present in the repository's main directory preventing 'initdev' from succedding."
-echo "Please try running the 'initdev' with \"--f\" option to force old files deletion:"
+echo "Please try running the 'initdev' with \"-f\" option to force old files deletion:"
 echo ""
 echo ""
-echo "  docker-compose run --rm misago initdev --f"
+echo "  docker-compose run --rm misago initdev -f"
 echo ""
 echo ""
 echo -e "${RED}Warning:${DEFAULT} if you have uncommited changes to Misago's setup that should be included"
 echo -e "${RED}Warning:${DEFAULT} if you have uncommited changes to Misago's setup that should be included"
 echo "in next release, make sure that they are commited to 'misago/project_template'"
 echo "in next release, make sure that they are commited to 'misago/project_template'"
-echo "or 'initdev --f' will overwrite the files causing them to be lost."
+echo "or 'initdev -f' will overwrite the files causing them to be lost."

+ 1 - 1
misago/__init__.py

@@ -1 +1 @@
-__version__ = '0.19.0'
+__version__ = '0.19.1'

+ 6 - 2
misago/core/templatetags/misago_absoluteurl.py

@@ -1,5 +1,5 @@
 from django import template
 from django import template
-from django.urls import reverse
+from django.urls import NoReverseMatch, reverse
 
 
 from misago.conf import settings
 from misago.conf import settings
 
 
@@ -13,7 +13,11 @@ def absoluteurl(url_or_name, *args, **kwargs):
 
 
     absolute_url_prefix = settings.MISAGO_ADDRESS.rstrip('/')
     absolute_url_prefix = settings.MISAGO_ADDRESS.rstrip('/')
 
 
-    if '/' not in url_or_name:
+    try:
         url_or_name = reverse(url_or_name, args=args, kwargs=kwargs)
         url_or_name = reverse(url_or_name, args=args, kwargs=kwargs)
+    except NoReverseMatch:
+        # don't use URLValidator because its too explicit
+        if not url_or_name.startswith('/'):
+            return url_or_name
     
     
     return u'{}{}'.format(absolute_url_prefix, url_or_name)
     return u'{}{}'.format(absolute_url_prefix, url_or_name)

+ 8 - 1
misago/core/tests/test_templatetags.py

@@ -25,11 +25,18 @@ class AbsoluteUrlTests(TestCase):
 
 
     
     
     @override_settings(MISAGO_ADDRESS=TEST_ADDRESS)
     @override_settings(MISAGO_ADDRESS=TEST_ADDRESS)
-    def test_rprefix_url_name(self):
+    def test_prefix_url_name(self):
         """template tag reverses url name and prefixes it"""
         """template tag reverses url name and prefixes it"""
         result = absoluteurl('misago:index')
         result = absoluteurl('misago:index')
         self.assertEqual(result, TEST_ADDRESS)
         self.assertEqual(result, TEST_ADDRESS)
 
 
+    @override_settings(MISAGO_ADDRESS=TEST_ADDRESS)
+    def test_dont_change_absolute_url(self):
+        """template tag doesn't change already absolute urls"""
+        absolute_url = "https://github.com/rafalp/Misago/issues/1067"
+        result = absoluteurl(absolute_url)
+        self.assertEqual(result, absolute_url)
+
 
 
 class CaptureTests(TestCase):
 class CaptureTests(TestCase):
     def setUp(self):
     def setUp(self):

+ 19 - 6
misago/legal/tests/test_utils.py

@@ -52,8 +52,6 @@ class GetParsedAgreementTextTests(TestCase):
 
 
 class GetRequiredUserAgreementTests(UserTestCase):
 class GetRequiredUserAgreementTests(UserTestCase):
     def setUp(self):
     def setUp(self):
-        Agreement.objects.invalidate_cache()
-
         self.agreement = Agreement.objects.create(
         self.agreement = Agreement.objects.create(
             type=Agreement.TYPE_PRIVACY,
             type=Agreement.TYPE_PRIVACY,
             link='https://somewhre.com',
             link='https://somewhre.com',
@@ -61,10 +59,7 @@ class GetRequiredUserAgreementTests(UserTestCase):
             is_active=True,
             is_active=True,
         )
         )
 
 
-        self.agreements = Agreement.objects.get_agreements()
-
-    def tearDown(self):
-        Agreement.objects.invalidate_cache()
+        self.agreements = Agreement.objects.get_agreements_from_db()
 
 
     def test_anonymous_user(self):
     def test_anonymous_user(self):
         anonymous_user = self.get_anonymous_user()
         anonymous_user = self.get_anonymous_user()
@@ -88,6 +83,24 @@ class GetRequiredUserAgreementTests(UserTestCase):
         result = get_required_user_agreement(authenticated_user, self.agreements)
         result = get_required_user_agreement(authenticated_user, self.agreements)
         self.assertIsNone(result)
         self.assertIsNone(result)
 
 
+    def test_prioritize_terms_of_service(self):
+        terms_of_service = Agreement.objects.create(
+            type=Agreement.TYPE_TOS,
+            link='https://somewhre.com',
+            text='Lorem ipsum',
+            is_active=True,
+        )
+
+        agreements = Agreement.objects.get_agreements_from_db()
+        agreements_in_wrong_order = {
+            Agreement.TYPE_PRIVACY: agreements[Agreement.TYPE_PRIVACY],
+            Agreement.TYPE_TOS: agreements[Agreement.TYPE_TOS],
+        }
+
+        authenticated_user = self.get_authenticated_user()
+        result = get_required_user_agreement(authenticated_user, agreements_in_wrong_order)
+        self.assertEqual(result, terms_of_service)
+
 
 
 class SaveUserAgreementAcceptance(UserTestCase):
 class SaveUserAgreementAcceptance(UserTestCase):
     def test_no_commit(self):
     def test_no_commit(self):

+ 3 - 2
misago/legal/utils.py

@@ -23,8 +23,9 @@ def get_required_user_agreement(user, agreements):
     if user.is_anonymous:
     if user.is_anonymous:
         return None
         return None
 
 
-    for agreement in agreements.values():
-        if agreement['id'] not in user.agreements:
+    for agreement_type, _ in Agreement.TYPE_CHOICES:
+        agreement = agreements.get(agreement_type)
+        if agreement and agreement['id'] not in user.agreements:
             try:
             try:
                 return Agreement.objects.get(id=agreement['id'])
                 return Agreement.objects.get(id=agreement['id'])
             except Agreement.DoesNotExist:
             except Agreement.DoesNotExist:

+ 10 - 10
misago/users/avatars/store.py

@@ -16,15 +16,15 @@ def normalize_image(image):
     return image.copy().convert('RGBA')
     return image.copy().convert('RGBA')
 
 
 
 
-def delete_avatar(user):
-    if user.avatar_tmp:
-        user.avatar_tmp.delete(False)
+def delete_avatar(user, delete_tmp=True, delete_src=True):
+    if delete_tmp and user.avatar_tmp:
+        user.avatar_tmp.delete(save=False)
 
 
-    if user.avatar_src:
-        user.avatar_src.delete(False)
+    if delete_src and user.avatar_src:
+        user.avatar_src.delete(save=False)
 
 
     for avatar in user.avatar_set.all():
     for avatar in user.avatar_set.all():
-        avatar.image.delete(False)
+        avatar.image.delete(save=False)
     user.avatar_set.all().delete()
     user.avatar_set.all().delete()
 
 
 
 
@@ -51,8 +51,8 @@ def store_avatar(user, image):
     user.save(update_fields=['avatars'])
     user.save(update_fields=['avatars'])
 
 
 
 
-def store_new_avatar(user, image):
-    delete_avatar(user)
+def store_new_avatar(user, image, delete_tmp=True, delete_src=True):
+    delete_avatar(user, delete_tmp=delete_tmp, delete_src=delete_src)
     store_avatar(user, image)
     store_avatar(user, image)
 
 
 
 
@@ -63,7 +63,7 @@ def store_temporary_avatar(user, image):
     image.save(image_stream, "PNG")
     image.save(image_stream, "PNG")
 
 
     if user.avatar_tmp:
     if user.avatar_tmp:
-        user.avatar_tmp.delete(False)
+        user.avatar_tmp.delete(save=False)
 
 
     user.avatar_tmp = ContentFile(image_stream.getvalue(), 'avatar')
     user.avatar_tmp = ContentFile(image_stream.getvalue(), 'avatar')
     user.save(update_fields=['avatar_tmp'])
     user.save(update_fields=['avatar_tmp'])
@@ -71,7 +71,7 @@ def store_temporary_avatar(user, image):
 
 
 def store_original_avatar(user):
 def store_original_avatar(user):
     if user.avatar_src:
     if user.avatar_src:
-        user.avatar_src.delete(False)
+        user.avatar_src.delete(save=False)
     user.avatar_src = user.avatar_tmp
     user.avatar_src = user.avatar_tmp
     user.avatar_tmp = None
     user.avatar_tmp = None
     user.save(update_fields=['avatar_tmp', 'avatar_src'])
     user.save(update_fields=['avatar_tmp', 'avatar_src'])

+ 3 - 1
misago/users/avatars/uploaded.py

@@ -134,9 +134,11 @@ def crop_source_image(user, source, crop):
             int(round((crop['y'] - min_size) * upscale * -1, 0)),
             int(round((crop['y'] - min_size) * upscale * -1, 0)),
         ))
         ))
 
 
-    store.store_avatar(user, cropped_image)
     if source == 'tmp':
     if source == 'tmp':
+        store.store_new_avatar(user, cropped_image, delete_tmp=False)
         store.store_original_avatar(user)
         store.store_original_avatar(user)
+    else:
+        store.store_new_avatar(user, cropped_image, delete_src=False)
 
 
     return crop
     return crop
 
 

+ 3 - 12
misago/users/models/user.py

@@ -7,7 +7,6 @@ from django.contrib.auth.password_validation import validate_password
 from django.contrib.postgres.fields import ArrayField, HStoreField, JSONField
 from django.contrib.postgres.fields import ArrayField, HStoreField, JSONField
 from django.core.mail import send_mail
 from django.core.mail import send_mail
 from django.db import IntegrityError, models, transaction
 from django.db import IntegrityError, models, transaction
-from django.db.models import Q
 from django.urls import reverse
 from django.urls import reverse
 from django.utils import timezone
 from django.utils import timezone
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
@@ -130,17 +129,9 @@ class UserManager(BaseUserManager):
         return self.get(email_hash=hash_email(email))
         return self.get(email_hash=hash_email(email))
 
 
     def get_by_username_or_email(self, login):
     def get_by_username_or_email(self, login):
-        email_hash = hash_email(login)
-        slug = slugify(login)
-        
-        users = list(self.filter(Q(slug=slug) | Q(email_hash=email_hash)))
-        for user in users:
-            if user.email_hash == email_hash:
-                return user
-        for user in users:
-            if user.slug == slug:
-                return user
-        raise User.DoesNotExist()
+        if '@' in login:
+            return self.get(email_hash=hash_email(login))
+        return self.get(slug=slugify(login))
 
 
 
 
 class User(AbstractBaseUser, PermissionsMixin):
 class User(AbstractBaseUser, PermissionsMixin):

+ 13 - 1
misago/users/tests/test_auth_views.py

@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
 from django.test import TestCase
 from django.test import TestCase
 from django.urls import reverse
 from django.urls import reverse
 
 
@@ -56,7 +57,18 @@ class AuthViewsTests(TestCase):
         response = self.client.post(
         response = self.client.post(
             reverse('misago:login'),
             reverse('misago:login'),
             data={
             data={
-                'redirect_to': 'canada goose not url',
+                'redirect_to': 'canada goose not url!',
+            },
+        )
+
+        self.assertEqual(response.status_code, 302)
+        self.assertEqual(response['location'], '/')
+
+        # invalid redirect (unicode)
+        response = self.client.post(
+            reverse('misago:login'),
+            data={
+                'redirect_to': u'łelcome!',
             },
             },
         )
         )
 
 

+ 14 - 3
misago/users/tests/test_user_avatar_api.py

@@ -6,6 +6,7 @@ from path import Path
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
 
 
 from misago.acl.testutils import override_acl
 from misago.acl.testutils import override_acl
+from misago.conf import settings
 from misago.users.avatars import gallery, store
 from misago.users.avatars import gallery, store
 from misago.users.models import AvatarGallery
 from misago.users.models import AvatarGallery
 from misago.users.testutils import AuthenticatedUserTestCase
 from misago.users.testutils import AuthenticatedUserTestCase
@@ -23,10 +24,16 @@ class UserAvatarTests(AuthenticatedUserTestCase):
     def setUp(self):
     def setUp(self):
         super(UserAvatarTests, self).setUp()
         super(UserAvatarTests, self).setUp()
         self.link = '/api/users/%s/avatar/' % self.user.pk
         self.link = '/api/users/%s/avatar/' % self.user.pk
+        self.client.post(self.link, data={'avatar': 'generated'})
 
 
     def get_current_user(self):
     def get_current_user(self):
         return UserModel.objects.get(pk=self.user.pk)
         return UserModel.objects.get(pk=self.user.pk)
 
 
+    def assertOldAvatarsAreDeleted(self, user):
+        self.assertEqual(
+            user.avatar_set.count(), len(settings.MISAGO_AVATARS_SIZES)
+        )
+
     def test_avatars_off(self):
     def test_avatars_off(self):
         """custom avatars are not allowed"""
         """custom avatars are not allowed"""
         with self.settings(allow_custom_avatars=False):
         with self.settings(allow_custom_avatars=False):
@@ -109,16 +116,17 @@ class UserAvatarTests(AuthenticatedUserTestCase):
         response = self.client.post(self.link, data={'avatar': 'gravatar'})
         response = self.client.post(self.link, data={'avatar': 'gravatar'})
         self.assertContains(response, "Gravatar was downloaded and set")
         self.assertContains(response, "Gravatar was downloaded and set")
 
 
+        self.assertOldAvatarsAreDeleted(self.user)
+
     def test_generation_request(self):
     def test_generation_request(self):
         """generated avatar is set"""
         """generated avatar is set"""
         response = self.client.post(self.link, data={'avatar': 'generated'})
         response = self.client.post(self.link, data={'avatar': 'generated'})
         self.assertContains(response, "New avatar based on your account")
         self.assertContains(response, "New avatar based on your account")
 
 
+        self.assertOldAvatarsAreDeleted(self.user)
+
     def test_avatar_upload_and_crop(self):
     def test_avatar_upload_and_crop(self):
         """avatar can be uploaded and cropped"""
         """avatar can be uploaded and cropped"""
-        response = self.client.post(self.link, data={'avatar': 'generated'})
-        self.assertEqual(response.status_code, 200)
-
         response = self.client.post(self.link, data={'avatar': 'upload'})
         response = self.client.post(self.link, data={'avatar': 'upload'})
         self.assertContains(response, "No file was sent.", status_code=400)
         self.assertContains(response, "No file was sent.", status_code=400)
 
 
@@ -156,6 +164,7 @@ class UserAvatarTests(AuthenticatedUserTestCase):
         self.assertContains(response, "Uploaded avatar was set.")
         self.assertContains(response, "Uploaded avatar was set.")
 
 
         self.assertFalse(self.get_current_user().avatar_tmp)
         self.assertFalse(self.get_current_user().avatar_tmp)
+        self.assertOldAvatarsAreDeleted(self.user)
 
 
         avatar = Path(self.get_current_user().avatar_src.path)
         avatar = Path(self.get_current_user().avatar_src.path)
         self.assertTrue(avatar.exists())
         self.assertTrue(avatar.exists())
@@ -192,6 +201,7 @@ class UserAvatarTests(AuthenticatedUserTestCase):
             content_type="application/json",
             content_type="application/json",
         )
         )
         self.assertContains(response, "Avatar was re-cropped.")
         self.assertContains(response, "Avatar was re-cropped.")
+        self.assertOldAvatarsAreDeleted(self.user)
 
 
         # delete user avatars, test if it deletes src and tmp
         # delete user avatars, test if it deletes src and tmp
         store.delete_avatar(self.get_current_user())
         store.delete_avatar(self.get_current_user())
@@ -280,6 +290,7 @@ class UserAvatarTests(AuthenticatedUserTestCase):
         )
         )
 
 
         self.assertContains(response, "Avatar from gallery was set.")
         self.assertContains(response, "Avatar from gallery was set.")
+        self.assertOldAvatarsAreDeleted(self.user)
 
 
 
 
 class UserAvatarModerationTests(AuthenticatedUserTestCase):
 class UserAvatarModerationTests(AuthenticatedUserTestCase):

+ 3 - 3
misago/users/views/auth.py

@@ -24,10 +24,10 @@ def login(request):
             if is_redirect_safe:
             if is_redirect_safe:
                 redirect_to_path = urlparse(redirect_to).path
                 redirect_to_path = urlparse(redirect_to).path
                 if '?' not in redirect_to_path:
                 if '?' not in redirect_to_path:
-                    redirect_to_path = '{}?'.format(redirect_to_path)
+                    redirect_to_path = u'{}?'.format(redirect_to_path)
                 else:
                 else:
-                    redirect_to_path = '{}&'.format(redirect_to_path)
-                redirect_to_path = '{}ref=login'.format(redirect_to_path)
+                    redirect_to_path = u'{}&'.format(redirect_to_path)
+                redirect_to_path = u'{}ref=login'.format(redirect_to_path)
                 try:
                 try:
                     return redirect(redirect_to_path)
                     return redirect(redirect_to_path)
                 except NoReverseMatch:
                 except NoReverseMatch: