From cfdb9d5339e79acd2b650f636d3746341034d2c7 Mon Sep 17 00:00:00 2001 From: rafalp Date: Sun, 25 Nov 2018 16:39:30 +0100 Subject: [PATCH 01/86] Create basic misago.cache application --- devproject/settings.py | 1 + .../migrations/0002_acl_version_tracker.py | 12 ++------ misago/acl/migrations/0004_cache_version.py | 16 ++++++++++ misago/cache/__init__.py | 1 + misago/cache/apps.py | 7 +++++ misago/cache/middleware.py | 10 +++++++ misago/cache/migrations/0001_initial.py | 24 +++++++++++++++ misago/cache/migrations/__init__.py | 0 misago/cache/models.py | 8 +++++ misago/cache/operations.py | 29 +++++++++++++++++++ misago/cache/utils.py | 5 ++++ .../migrations/0003_bans_version_tracker.py | 12 ++------ misago/users/migrations/0016_cache_version.py | 16 ++++++++++ 13 files changed, 121 insertions(+), 20 deletions(-) create mode 100644 misago/acl/migrations/0004_cache_version.py create mode 100644 misago/cache/__init__.py create mode 100644 misago/cache/apps.py create mode 100644 misago/cache/middleware.py create mode 100644 misago/cache/migrations/0001_initial.py create mode 100644 misago/cache/migrations/__init__.py create mode 100644 misago/cache/models.py create mode 100644 misago/cache/operations.py create mode 100644 misago/cache/utils.py create mode 100644 misago/users/migrations/0016_cache_version.py diff --git a/devproject/settings.py b/devproject/settings.py index ffebc62ee6..907c1ca3a6 100644 --- a/devproject/settings.py +++ b/devproject/settings.py @@ -188,6 +188,7 @@ # Misago apps 'misago.admin', 'misago.acl', + 'misago.cache', 'misago.core', 'misago.conf', 'misago.markup', diff --git a/misago/acl/migrations/0002_acl_version_tracker.py b/misago/acl/migrations/0002_acl_version_tracker.py index ffbfd319c8..f18598daa1 100644 --- a/misago/acl/migrations/0002_acl_version_tracker.py +++ b/misago/acl/migrations/0002_acl_version_tracker.py @@ -1,20 +1,12 @@ from django.db import migrations -from misago.acl.constants import ACL_CACHEBUSTER -from misago.core.migrationutils import cachebuster_register_cache - - -def register_acl_version_tracker(apps, schema_editor): - cachebuster_register_cache(apps, ACL_CACHEBUSTER) - class Migration(migrations.Migration): + """Superseded by 0004""" dependencies = [ ('misago_acl', '0001_initial'), ('misago_core', '0001_initial'), ] - operations = [ - migrations.RunPython(register_acl_version_tracker), - ] + operations = [] diff --git a/misago/acl/migrations/0004_cache_version.py b/misago/acl/migrations/0004_cache_version.py new file mode 100644 index 0000000000..8407b8fce6 --- /dev/null +++ b/misago/acl/migrations/0004_cache_version.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +from django.db import migrations + +from misago.cache.operations import StartCacheVersioning + + +class Migration(migrations.Migration): + + dependencies = [ + ('misago_acl', '0003_default_roles'), + ('misago_cache', '0001_initial'), + ] + + operations = [ + StartCacheVersioning("acl") + ] \ No newline at end of file diff --git a/misago/cache/__init__.py b/misago/cache/__init__.py new file mode 100644 index 0000000000..491c569199 --- /dev/null +++ b/misago/cache/__init__.py @@ -0,0 +1 @@ +default_app_config = 'misago.cache.apps.MisagoCacheConfig' \ No newline at end of file diff --git a/misago/cache/apps.py b/misago/cache/apps.py new file mode 100644 index 0000000000..0374fe03eb --- /dev/null +++ b/misago/cache/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class MisagoCacheConfig(AppConfig): + name = 'misago.cache' + label = 'misago_cache' + verbose_name = "Misago Cache" diff --git a/misago/cache/middleware.py b/misago/cache/middleware.py new file mode 100644 index 0000000000..7cb912f151 --- /dev/null +++ b/misago/cache/middleware.py @@ -0,0 +1,10 @@ +from django.utils.deprecation import MiddlewareMixin + +from .utils import get_cache_versions + + +def cache_versions_middleware(get_response): + """Sets request.cache_versions attribute with dict cache versions.""" + def middleware(request): + request.cache_versions = get_cache_versions() + return get_response(request) diff --git a/misago/cache/migrations/0001_initial.py b/misago/cache/migrations/0001_initial.py new file mode 100644 index 0000000000..6f798d71ee --- /dev/null +++ b/misago/cache/migrations/0001_initial.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.16 on 2018-11-25 15:15 +from __future__ import unicode_literals + +from django.db import migrations, models +import misago.cache.utils + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='CacheVersion', + fields=[ + ('cache', models.CharField(max_length=128, primary_key=True, serialize=False)), + ('version', models.CharField(default=misago.cache.utils.get_random_version, max_length=8)), + ], + ), + ] diff --git a/misago/cache/migrations/__init__.py b/misago/cache/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/misago/cache/models.py b/misago/cache/models.py new file mode 100644 index 0000000000..ee52fa2eae --- /dev/null +++ b/misago/cache/models.py @@ -0,0 +1,8 @@ +from django.db import models + +from .utils import get_random_version + + +class CacheVersion(models.Model): + cache = models.CharField(max_length=128, primary_key=True) + version = models.CharField(max_length=8, default=get_random_version) diff --git a/misago/cache/operations.py b/misago/cache/operations.py new file mode 100644 index 0000000000..c68ae0651e --- /dev/null +++ b/misago/cache/operations.py @@ -0,0 +1,29 @@ +from django.db.migrations import RunPython + + +class StartCacheVersioning(RunPython): + def __init__(self, cache): + code = start_cache_versioning(cache) + reverse_code = stop_cache_versioning(cache) + super().__init__(code, reverse_code) + + +class StopCacheVersioning(RunPython): + def __init__(self, cache): + code = stop_cache_versioning(cache) + reverse_code = start_cache_versioning(cache) + super().__init__(code, reverse_code) + + +def start_cache_versioning(cache): + def migration_operation(apps, _): + CacheVersion = apps.get_model('misago_cache', 'CacheVersion') + CacheVersion.objects.create(cache=cache) + return migration_operation + + +def stop_cache_versioning(cache): + def migration_operation(apps, _): + CacheVersion = apps.get_model('misago_cache', 'CacheVersion') + CacheVersion.objects.filter(cache=cache).delete() + return migration_operation \ No newline at end of file diff --git a/misago/cache/utils.py b/misago/cache/utils.py new file mode 100644 index 0000000000..ab32bc36a9 --- /dev/null +++ b/misago/cache/utils.py @@ -0,0 +1,5 @@ +from django.utils.crypto import get_random_string + + +def get_random_version(): + return get_random_string(8) diff --git a/misago/users/migrations/0003_bans_version_tracker.py b/misago/users/migrations/0003_bans_version_tracker.py index 2ece5fe996..8620cc8c9b 100644 --- a/misago/users/migrations/0003_bans_version_tracker.py +++ b/misago/users/migrations/0003_bans_version_tracker.py @@ -1,20 +1,12 @@ from django.db import migrations -from misago.core.migrationutils import cachebuster_register_cache -from misago.users.constants import BANS_CACHEBUSTER - - -def register_bans_version_tracker(apps, schema_editor): - cachebuster_register_cache(apps, BANS_CACHEBUSTER) - class Migration(migrations.Migration): + """Migration superseded by 0016""" dependencies = [ ('misago_users', '0002_users_settings'), ('misago_core', '0001_initial'), ] - operations = [ - migrations.RunPython(register_bans_version_tracker), - ] + operations = [] diff --git a/misago/users/migrations/0016_cache_version.py b/misago/users/migrations/0016_cache_version.py new file mode 100644 index 0000000000..ed20be0151 --- /dev/null +++ b/misago/users/migrations/0016_cache_version.py @@ -0,0 +1,16 @@ +# Generated by Django 1.11.16 on 2018-11-25 15:31 +from django.db import migrations + +from misago.cache.operations import StartCacheVersioning + + +class Migration(migrations.Migration): + + dependencies = [ + ('misago_users', '0015_user_agreements'), + ('misago_cache', '0001_initial'), + ] + + operations = [ + StartCacheVersioning("bans") + ] From 4c246418a64a8087b61e8d828cca061589296595 Mon Sep 17 00:00:00 2001 From: rafalp Date: Sun, 25 Nov 2018 22:03:45 +0100 Subject: [PATCH 02/86] Another iteration on utility module --- devproject/settings.py | 1 + misago/cache/__init__.py | 4 +++- misago/cache/middleware.py | 29 +++++++++++++++++++++---- misago/cache/migrations/0001_initial.py | 2 +- misago/cache/models.py | 4 ++-- misago/cache/utils.py | 2 +- 6 files changed, 33 insertions(+), 9 deletions(-) diff --git a/devproject/settings.py b/devproject/settings.py index 907c1ca3a6..e8e8972512 100644 --- a/devproject/settings.py +++ b/devproject/settings.py @@ -224,6 +224,7 @@ 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'misago.cache.middleware.cache_versions_middleware', 'misago.users.middleware.UserMiddleware', 'misago.core.middleware.ExceptionHandlerMiddleware', 'misago.users.middleware.OnlineTrackerMiddleware', diff --git a/misago/cache/__init__.py b/misago/cache/__init__.py index 491c569199..1b1b95d7a5 100644 --- a/misago/cache/__init__.py +++ b/misago/cache/__init__.py @@ -1 +1,3 @@ -default_app_config = 'misago.cache.apps.MisagoCacheConfig' \ No newline at end of file +default_app_config = 'misago.cache.apps.MisagoCacheConfig' + +CACHE_NAME = "cache_versions" \ No newline at end of file diff --git a/misago/cache/middleware.py b/misago/cache/middleware.py index 7cb912f151..8affa020d8 100644 --- a/misago/cache/middleware.py +++ b/misago/cache/middleware.py @@ -1,10 +1,31 @@ -from django.utils.deprecation import MiddlewareMixin +from django.core.cache import cache +from django.utils.functional import SimpleLazyObject -from .utils import get_cache_versions +from . import CACHE_NAME +from .models import CacheVersion def cache_versions_middleware(get_response): - """Sets request.cache_versions attribute with dict cache versions.""" + """Sets request.cache_versions attribute with dict of cache versions.""" def middleware(request): - request.cache_versions = get_cache_versions() + request.cache_versions = SimpleLazyObject(get_cache_versions) return get_response(request) + + return middleware + + +def get_cache_versions(): + cache_versions = get_cache_versions_from_cache() + if cache_versions is None: + cache_versions = get_cache_versions_from_db() + cache.set(CACHE_NAME, cache_versions) + return cache_versions + + +def get_cache_versions_from_cache(): + return cache.get(CACHE_NAME) + + +def get_cache_versions_from_db(): + queryset = CacheVersion.objects.all() + return {i.cache: i.version for i in queryset} \ No newline at end of file diff --git a/misago/cache/migrations/0001_initial.py b/misago/cache/migrations/0001_initial.py index 6f798d71ee..7316c7a103 100644 --- a/misago/cache/migrations/0001_initial.py +++ b/misago/cache/migrations/0001_initial.py @@ -18,7 +18,7 @@ class Migration(migrations.Migration): name='CacheVersion', fields=[ ('cache', models.CharField(max_length=128, primary_key=True, serialize=False)), - ('version', models.CharField(default=misago.cache.utils.get_random_version, max_length=8)), + ('version', models.CharField(default=misago.cache.utils.generate_version_string, max_length=8)), ], ), ] diff --git a/misago/cache/models.py b/misago/cache/models.py index ee52fa2eae..31194b9806 100644 --- a/misago/cache/models.py +++ b/misago/cache/models.py @@ -1,8 +1,8 @@ from django.db import models -from .utils import get_random_version +from .utils import generate_version_string class CacheVersion(models.Model): cache = models.CharField(max_length=128, primary_key=True) - version = models.CharField(max_length=8, default=get_random_version) + version = models.CharField(max_length=8, default=generate_version_string) diff --git a/misago/cache/utils.py b/misago/cache/utils.py index ab32bc36a9..4d94dfcc69 100644 --- a/misago/cache/utils.py +++ b/misago/cache/utils.py @@ -1,5 +1,5 @@ from django.utils.crypto import get_random_string -def get_random_version(): +def generate_version_string(): return get_random_string(8) From 50f918ec96672d4b72141f5f0a157f585860bd13 Mon Sep 17 00:00:00 2001 From: rafalp Date: Sun, 25 Nov 2018 23:13:29 +0100 Subject: [PATCH 03/86] Add more features to misago.cache --- misago/cache/__init__.py | 4 +- misago/cache/management/__init__.py | 0 misago/cache/management/commands/__init__.py | 0 .../commands/invalidateversionedcaches.py | 11 ++++++ misago/cache/middleware.py | 21 +--------- misago/cache/tests/__init__.py | 0 misago/cache/versions.py | 38 +++++++++++++++++++ 7 files changed, 51 insertions(+), 23 deletions(-) create mode 100644 misago/cache/management/__init__.py create mode 100644 misago/cache/management/commands/__init__.py create mode 100644 misago/cache/management/commands/invalidateversionedcaches.py create mode 100644 misago/cache/tests/__init__.py create mode 100644 misago/cache/versions.py diff --git a/misago/cache/__init__.py b/misago/cache/__init__.py index 1b1b95d7a5..491c569199 100644 --- a/misago/cache/__init__.py +++ b/misago/cache/__init__.py @@ -1,3 +1 @@ -default_app_config = 'misago.cache.apps.MisagoCacheConfig' - -CACHE_NAME = "cache_versions" \ No newline at end of file +default_app_config = 'misago.cache.apps.MisagoCacheConfig' \ No newline at end of file diff --git a/misago/cache/management/__init__.py b/misago/cache/management/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/misago/cache/management/commands/__init__.py b/misago/cache/management/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/misago/cache/management/commands/invalidateversionedcaches.py b/misago/cache/management/commands/invalidateversionedcaches.py new file mode 100644 index 0000000000..5a1473099c --- /dev/null +++ b/misago/cache/management/commands/invalidateversionedcaches.py @@ -0,0 +1,11 @@ +from django.core.management.base import BaseCommand + +from misago.cache.versions import invalidate_all + + +class Command(BaseCommand): + help = 'Invalidates versioned caches' + + def handle(self, *args, **options): + invalidate_all() + self.stdout.write("Invalidated versioned caches.") diff --git a/misago/cache/middleware.py b/misago/cache/middleware.py index 8affa020d8..7531230670 100644 --- a/misago/cache/middleware.py +++ b/misago/cache/middleware.py @@ -1,8 +1,6 @@ -from django.core.cache import cache from django.utils.functional import SimpleLazyObject -from . import CACHE_NAME -from .models import CacheVersion +from .versions import get_cache_versions def cache_versions_middleware(get_response): @@ -12,20 +10,3 @@ def middleware(request): return get_response(request) return middleware - - -def get_cache_versions(): - cache_versions = get_cache_versions_from_cache() - if cache_versions is None: - cache_versions = get_cache_versions_from_db() - cache.set(CACHE_NAME, cache_versions) - return cache_versions - - -def get_cache_versions_from_cache(): - return cache.get(CACHE_NAME) - - -def get_cache_versions_from_db(): - queryset = CacheVersion.objects.all() - return {i.cache: i.version for i in queryset} \ No newline at end of file diff --git a/misago/cache/tests/__init__.py b/misago/cache/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/misago/cache/versions.py b/misago/cache/versions.py new file mode 100644 index 0000000000..009d7c6b8e --- /dev/null +++ b/misago/cache/versions.py @@ -0,0 +1,38 @@ +from django.core.cache import cache + +from .models import CacheVersion +from .utils import generate_version_string + +CACHE_NAME = "cache_versions" + + +def get_cache_versions(): + cache_versions = get_cache_versions_from_cache() + if cache_versions is None: + cache_versions = get_cache_versions_from_db() + cache.set(CACHE_NAME, cache_versions) + return cache_versions + + +def get_cache_versions_from_cache(): + return cache.get(CACHE_NAME) + + +def get_cache_versions_from_db(): + queryset = CacheVersion.objects.all() + return {i.cache: i.version for i in queryset} + + +def invalidate(cache): + CacheVersion.objects.filter(cache=cache).update( + version=generate_version_string(), + ) + cache.delete(CACHE_NAME) + + +def invalidate_all(): + for cache in get_cache_versions_from_db().keys(): + CacheVersion.objects.filter(cache=cache).update( + version=generate_version_string(), + ) + cache.delete(CACHE_NAME) From c919604d4ec907bac64c2f1a29831d031c89ecf7 Mon Sep 17 00:00:00 2001 From: rafalp Date: Tue, 27 Nov 2018 20:46:32 +0100 Subject: [PATCH 04/86] Add tests for misago.cache.cache --- misago/cache/{versions.py => cache.py} | 4 +- .../commands/invalidateversionedcaches.py | 6 +-- misago/cache/middleware.py | 2 +- .../tests/test_getting_cache_versions.py | 43 +++++++++++++++++++ 4 files changed, 49 insertions(+), 6 deletions(-) rename misago/cache/{versions.py => cache.py} (94%) create mode 100644 misago/cache/tests/test_getting_cache_versions.py diff --git a/misago/cache/versions.py b/misago/cache/cache.py similarity index 94% rename from misago/cache/versions.py rename to misago/cache/cache.py index 009d7c6b8e..5967433fab 100644 --- a/misago/cache/versions.py +++ b/misago/cache/cache.py @@ -23,14 +23,14 @@ def get_cache_versions_from_db(): return {i.cache: i.version for i in queryset} -def invalidate(cache): +def invalidate_cache(cache): CacheVersion.objects.filter(cache=cache).update( version=generate_version_string(), ) cache.delete(CACHE_NAME) -def invalidate_all(): +def invalidate_all_caches(): for cache in get_cache_versions_from_db().keys(): CacheVersion.objects.filter(cache=cache).update( version=generate_version_string(), diff --git a/misago/cache/management/commands/invalidateversionedcaches.py b/misago/cache/management/commands/invalidateversionedcaches.py index 5a1473099c..0de55700b1 100644 --- a/misago/cache/management/commands/invalidateversionedcaches.py +++ b/misago/cache/management/commands/invalidateversionedcaches.py @@ -1,11 +1,11 @@ from django.core.management.base import BaseCommand -from misago.cache.versions import invalidate_all +from misago.cache.cache import invalidate_all_caches class Command(BaseCommand): help = 'Invalidates versioned caches' def handle(self, *args, **options): - invalidate_all() - self.stdout.write("Invalidated versioned caches.") + invalidate_all_caches() + self.stdout.write("Invalidated all versioned caches.") diff --git a/misago/cache/middleware.py b/misago/cache/middleware.py index 7531230670..524d0379c3 100644 --- a/misago/cache/middleware.py +++ b/misago/cache/middleware.py @@ -1,6 +1,6 @@ from django.utils.functional import SimpleLazyObject -from .versions import get_cache_versions +from .cache import get_cache_versions def cache_versions_middleware(get_response): diff --git a/misago/cache/tests/test_getting_cache_versions.py b/misago/cache/tests/test_getting_cache_versions.py new file mode 100644 index 0000000000..64c19db1a1 --- /dev/null +++ b/misago/cache/tests/test_getting_cache_versions.py @@ -0,0 +1,43 @@ +from unittest.mock import patch +from django.test import TestCase + +from misago.cache.cache import ( + CACHE_NAME, get_cache_versions, get_cache_versions_from_cache, get_cache_versions_from_db +) + + +class CacheVersionsTests(TestCase): + def test_db_getter_returns_cache_versions_from_db(self): + cache_versions = get_cache_versions_from_db() + assert cache_versions + + @patch('django.core.cache.cache.get', return_value=True) + def test_cache_getter_returns_cache_versions_from_cache(self, cache_get): + assert get_cache_versions_from_cache() is True + cache_get.assert_called_once_with(CACHE_NAME) + + @patch('django.core.cache.cache.get', return_value=True) + def test_getter_reads_from_cache(self, cache_get): + assert get_cache_versions() is True + cache_get.assert_called_once_with(CACHE_NAME) + + @patch('django.core.cache.cache.set') + @patch('django.core.cache.cache.get', return_value=None) + def test_getter_reads_from_db_when_cache_is_not_available(self, cache_get, _): + db_caches = get_cache_versions_from_db() + assert get_cache_versions() == db_caches + cache_get.assert_called_once_with(CACHE_NAME) + + @patch('django.core.cache.cache.set') + @patch('django.core.cache.cache.get', return_value=None) + def test_getter_sets_new_cache_if_no_cache_is_set(self, _, cache_set): + assert get_cache_versions() + db_caches = get_cache_versions_from_db() + cache_set.assert_called_once_with(CACHE_NAME, db_caches) + + @patch('django.core.cache.cache.set') + @patch('django.core.cache.cache.get', return_value=True) + def test_getter_is_not_setting_new_cache_if_cache_is_set(self, _, cache_set): + assert get_cache_versions() + db_caches = get_cache_versions_from_db() + cache_set.assert_not_called() \ No newline at end of file From e82261188c5d1ea53f4e9b5747de7367bdaa3ada Mon Sep 17 00:00:00 2001 From: rafalp Date: Tue, 27 Nov 2018 21:17:44 +0100 Subject: [PATCH 05/86] Add tests for invalidating caches --- misago/cache/cache.py | 8 ++-- .../tests/test_getting_cache_versions.py | 1 + .../cache/tests/test_invalidating_caches.py | 40 +++++++++++++++++++ 3 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 misago/cache/tests/test_invalidating_caches.py diff --git a/misago/cache/cache.py b/misago/cache/cache.py index 5967433fab..dfe5e90a92 100644 --- a/misago/cache/cache.py +++ b/misago/cache/cache.py @@ -23,16 +23,16 @@ def get_cache_versions_from_db(): return {i.cache: i.version for i in queryset} -def invalidate_cache(cache): - CacheVersion.objects.filter(cache=cache).update( +def invalidate_cache(cache_name): + CacheVersion.objects.filter(cache=cache_name).update( version=generate_version_string(), ) cache.delete(CACHE_NAME) def invalidate_all_caches(): - for cache in get_cache_versions_from_db().keys(): - CacheVersion.objects.filter(cache=cache).update( + for cache_name in get_cache_versions_from_db().keys(): + CacheVersion.objects.filter(cache=cache_name).update( version=generate_version_string(), ) cache.delete(CACHE_NAME) diff --git a/misago/cache/tests/test_getting_cache_versions.py b/misago/cache/tests/test_getting_cache_versions.py index 64c19db1a1..ced38bde3f 100644 --- a/misago/cache/tests/test_getting_cache_versions.py +++ b/misago/cache/tests/test_getting_cache_versions.py @@ -1,4 +1,5 @@ from unittest.mock import patch + from django.test import TestCase from misago.cache.cache import ( diff --git a/misago/cache/tests/test_invalidating_caches.py b/misago/cache/tests/test_invalidating_caches.py new file mode 100644 index 0000000000..9d926788a2 --- /dev/null +++ b/misago/cache/tests/test_invalidating_caches.py @@ -0,0 +1,40 @@ +from unittest.mock import patch + +from django.test import TestCase + +from misago.cache.cache import ( + CACHE_NAME, get_cache_versions_from_db, invalidate_cache, invalidate_all_caches +) +from misago.cache.models import CacheVersion + + +def cache_version(): + return CacheVersion.objects.create(cache="test_cache") + + +class InvalidatingCacheTests(TestCase): + @patch('django.core.cache.cache.delete') + def test_invalidating_cache_updates_cache_version_in_database(self, _): + test_cache = cache_version() + invalidate_cache(test_cache.cache) + updated_test_cache = CacheVersion.objects.get(cache=test_cache.cache) + assert test_cache.version != updated_test_cache.version + + @patch('django.core.cache.cache.delete') + def test_invalidating_cache_deletes_versions_cache(self, cache_delete): + test_cache = cache_version() + invalidate_cache(test_cache.cache) + cache_delete.assert_called_once_with(CACHE_NAME) + + @patch('django.core.cache.cache.delete') + def test_invalidating_all_caches_updates_cache_version_in_database(self, _): + test_cache = cache_version() + invalidate_all_caches() + updated_test_cache = CacheVersion.objects.get(cache=test_cache.cache) + assert test_cache.version != updated_test_cache.version + + @patch('django.core.cache.cache.delete') + def test_invalidating_all_caches_deletes_versions_cache(self, cache_delete): + cache_version() + invalidate_all_caches() + cache_delete.assert_called_once_with(CACHE_NAME) From 38325a7b7b82d9a66f7144184be4284a4d7f99eb Mon Sep 17 00:00:00 2001 From: rafalp Date: Tue, 27 Nov 2018 22:46:05 +0100 Subject: [PATCH 06/86] Add middleware tests --- misago/cache/tests/conftest.py | 5 ++ .../tests/test_cache_versions_middleware.py | 48 +++++++++++++++++++ ...st_invalidate_caches_management_command.py | 0 .../cache/tests/test_invalidating_caches.py | 4 +- 4 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 misago/cache/tests/conftest.py create mode 100644 misago/cache/tests/test_cache_versions_middleware.py create mode 100644 misago/cache/tests/test_invalidate_caches_management_command.py diff --git a/misago/cache/tests/conftest.py b/misago/cache/tests/conftest.py new file mode 100644 index 0000000000..ab098ec21d --- /dev/null +++ b/misago/cache/tests/conftest.py @@ -0,0 +1,5 @@ +from misago.cache.models import CacheVersion + + +def cache_version(): + return CacheVersion.objects.create(cache="test_cache") \ No newline at end of file diff --git a/misago/cache/tests/test_cache_versions_middleware.py b/misago/cache/tests/test_cache_versions_middleware.py new file mode 100644 index 0000000000..b0c8ceba90 --- /dev/null +++ b/misago/cache/tests/test_cache_versions_middleware.py @@ -0,0 +1,48 @@ +from unittest.mock import Mock, PropertyMock, patch + +from django.test import TestCase +from django.utils.functional import SimpleLazyObject + +from misago.cache.cache import CACHE_NAME +from misago.cache.middleware import cache_versions_middleware + + +class MiddlewareTests(TestCase): + def test_middleware_sets_attr_on_request(self): + get_response = Mock() + request = Mock() + cache_versions = PropertyMock() + type(request).cache_versions = cache_versions + middleware = cache_versions_middleware(get_response) + middleware(request) + cache_versions.assert_called_once() + + def test_attr_set_by_middleware_on_request_is_lazy_object(self): + get_response = Mock() + request = Mock() + cache_versions = PropertyMock() + type(request).cache_versions = cache_versions + middleware = cache_versions_middleware(get_response) + middleware(request) + attr_value = cache_versions.call_args[0][0] + assert isinstance(attr_value, SimpleLazyObject) + + def test_middleware_calls_get_response(self): + get_response = Mock() + request = Mock() + middleware = cache_versions_middleware(get_response) + middleware(request) + get_response.assert_called_once() + + def test_middleware_is_not_making_db_query(self): + get_response = Mock() + request = Mock() + with self.assertNumQueries(0): + middleware = cache_versions_middleware(get_response) + middleware(request) + + def test_middleware_is_not_reading_cache(self): + get_response = Mock() + request = Mock() + middleware = cache_versions_middleware(get_response) + middleware(request) diff --git a/misago/cache/tests/test_invalidate_caches_management_command.py b/misago/cache/tests/test_invalidate_caches_management_command.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/misago/cache/tests/test_invalidating_caches.py b/misago/cache/tests/test_invalidating_caches.py index 9d926788a2..625c604b30 100644 --- a/misago/cache/tests/test_invalidating_caches.py +++ b/misago/cache/tests/test_invalidating_caches.py @@ -7,9 +7,7 @@ ) from misago.cache.models import CacheVersion - -def cache_version(): - return CacheVersion.objects.create(cache="test_cache") +from .conftest import cache_version class InvalidatingCacheTests(TestCase): From 7d6fe8ecaf20fbed90b6c4a183f70797cc2174ac Mon Sep 17 00:00:00 2001 From: rafalp Date: Tue, 27 Nov 2018 22:47:52 +0100 Subject: [PATCH 07/86] Small polishing on middleware tests --- misago/cache/tests/test_cache_versions_middleware.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/misago/cache/tests/test_cache_versions_middleware.py b/misago/cache/tests/test_cache_versions_middleware.py index b0c8ceba90..4dca005a98 100644 --- a/misago/cache/tests/test_cache_versions_middleware.py +++ b/misago/cache/tests/test_cache_versions_middleware.py @@ -34,15 +34,17 @@ def test_middleware_calls_get_response(self): middleware(request) get_response.assert_called_once() - def test_middleware_is_not_making_db_query(self): + def test_middleware_is_not_reading_db(self): get_response = Mock() request = Mock() with self.assertNumQueries(0): middleware = cache_versions_middleware(get_response) middleware(request) - def test_middleware_is_not_reading_cache(self): + @patch('django.core.cache.cache.get') + def test_middleware_is_not_reading_cache(self, cache_get): get_response = Mock() request = Mock() middleware = cache_versions_middleware(get_response) middleware(request) + cache_get.assert_not_called() \ No newline at end of file From 27f5058c1a663709062e61c097dfffa19e73e8b1 Mon Sep 17 00:00:00 2001 From: rafalp Date: Tue, 27 Nov 2018 22:57:40 +0100 Subject: [PATCH 08/86] Add tests for management command --- .../test_invalidate_caches_management_command.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/misago/cache/tests/test_invalidate_caches_management_command.py b/misago/cache/tests/test_invalidate_caches_management_command.py index e69de29bb2..a6a7362197 100644 --- a/misago/cache/tests/test_invalidate_caches_management_command.py +++ b/misago/cache/tests/test_invalidate_caches_management_command.py @@ -0,0 +1,11 @@ +from unittest.mock import Mock, patch + +from django.core.management import call_command +from django.test import TestCase + + +class InvalidateCachesManagementCommandTests(TestCase): + @patch("misago.cache.cache.invalidate_all_caches") + def test_management_command_invalidates_all_caches(self, invalidate_all_caches): + call_command('invalidateversionedcaches', stdout=Mock()) + invalidate_all_caches.assert_called_once() From 6f29825f49958fae9f79b3a9ec21a11ef14596bd Mon Sep 17 00:00:00 2001 From: rafalp Date: Wed, 28 Nov 2018 21:52:38 +0100 Subject: [PATCH 09/86] Rename misago.cache.cache to misago.cache.versions --- misago/cache/management/commands/invalidateversionedcaches.py | 2 +- misago/cache/middleware.py | 2 +- misago/cache/tests/test_cache_versions_middleware.py | 2 +- misago/cache/tests/test_getting_cache_versions.py | 2 +- misago/cache/tests/test_invalidate_caches_management_command.py | 2 +- misago/cache/tests/test_invalidating_caches.py | 2 +- misago/cache/{cache.py => versions.py} | 0 7 files changed, 6 insertions(+), 6 deletions(-) rename misago/cache/{cache.py => versions.py} (100%) diff --git a/misago/cache/management/commands/invalidateversionedcaches.py b/misago/cache/management/commands/invalidateversionedcaches.py index 0de55700b1..91825c12b3 100644 --- a/misago/cache/management/commands/invalidateversionedcaches.py +++ b/misago/cache/management/commands/invalidateversionedcaches.py @@ -1,6 +1,6 @@ from django.core.management.base import BaseCommand -from misago.cache.cache import invalidate_all_caches +from misago.cache.versions import invalidate_all_caches class Command(BaseCommand): diff --git a/misago/cache/middleware.py b/misago/cache/middleware.py index 524d0379c3..7531230670 100644 --- a/misago/cache/middleware.py +++ b/misago/cache/middleware.py @@ -1,6 +1,6 @@ from django.utils.functional import SimpleLazyObject -from .cache import get_cache_versions +from .versions import get_cache_versions def cache_versions_middleware(get_response): diff --git a/misago/cache/tests/test_cache_versions_middleware.py b/misago/cache/tests/test_cache_versions_middleware.py index 4dca005a98..27e284621b 100644 --- a/misago/cache/tests/test_cache_versions_middleware.py +++ b/misago/cache/tests/test_cache_versions_middleware.py @@ -3,7 +3,7 @@ from django.test import TestCase from django.utils.functional import SimpleLazyObject -from misago.cache.cache import CACHE_NAME +from misago.cache.versions import CACHE_NAME from misago.cache.middleware import cache_versions_middleware diff --git a/misago/cache/tests/test_getting_cache_versions.py b/misago/cache/tests/test_getting_cache_versions.py index ced38bde3f..5c165cc604 100644 --- a/misago/cache/tests/test_getting_cache_versions.py +++ b/misago/cache/tests/test_getting_cache_versions.py @@ -2,7 +2,7 @@ from django.test import TestCase -from misago.cache.cache import ( +from misago.cache.versions import ( CACHE_NAME, get_cache_versions, get_cache_versions_from_cache, get_cache_versions_from_db ) diff --git a/misago/cache/tests/test_invalidate_caches_management_command.py b/misago/cache/tests/test_invalidate_caches_management_command.py index a6a7362197..d01363165a 100644 --- a/misago/cache/tests/test_invalidate_caches_management_command.py +++ b/misago/cache/tests/test_invalidate_caches_management_command.py @@ -5,7 +5,7 @@ class InvalidateCachesManagementCommandTests(TestCase): - @patch("misago.cache.cache.invalidate_all_caches") + @patch("misago.cache.versions.invalidate_all_caches") def test_management_command_invalidates_all_caches(self, invalidate_all_caches): call_command('invalidateversionedcaches', stdout=Mock()) invalidate_all_caches.assert_called_once() diff --git a/misago/cache/tests/test_invalidating_caches.py b/misago/cache/tests/test_invalidating_caches.py index 625c604b30..78c60c522e 100644 --- a/misago/cache/tests/test_invalidating_caches.py +++ b/misago/cache/tests/test_invalidating_caches.py @@ -2,7 +2,7 @@ from django.test import TestCase -from misago.cache.cache import ( +from misago.cache.versions import ( CACHE_NAME, get_cache_versions_from_db, invalidate_cache, invalidate_all_caches ) from misago.cache.models import CacheVersion diff --git a/misago/cache/cache.py b/misago/cache/versions.py similarity index 100% rename from misago/cache/cache.py rename to misago/cache/versions.py From 8762f23718e946b96f15f87864a3e55e78c7a1cb Mon Sep 17 00:00:00 2001 From: rafalp Date: Sat, 1 Dec 2018 17:11:37 +0100 Subject: [PATCH 10/86] Revert old cache versioning migrations to make build pass --- misago/acl/migrations/0002_acl_version_tracker.py | 10 +++++++++- misago/users/migrations/0003_bans_version_tracker.py | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/misago/acl/migrations/0002_acl_version_tracker.py b/misago/acl/migrations/0002_acl_version_tracker.py index f18598daa1..e2c0b11891 100644 --- a/misago/acl/migrations/0002_acl_version_tracker.py +++ b/misago/acl/migrations/0002_acl_version_tracker.py @@ -1,6 +1,11 @@ from django.db import migrations +def register_acl_version_tracker(apps, schema_editor): + from misago.core.migrationutils import cachebuster_register_cache + cachebuster_register_cache(apps, "misago_acl") + + class Migration(migrations.Migration): """Superseded by 0004""" @@ -9,4 +14,7 @@ class Migration(migrations.Migration): ('misago_core', '0001_initial'), ] - operations = [] + operations = [ + # FIXME: remove this operation + migrations.RunPython(register_acl_version_tracker), + ] diff --git a/misago/users/migrations/0003_bans_version_tracker.py b/misago/users/migrations/0003_bans_version_tracker.py index 8620cc8c9b..16afe681d6 100644 --- a/misago/users/migrations/0003_bans_version_tracker.py +++ b/misago/users/migrations/0003_bans_version_tracker.py @@ -1,6 +1,11 @@ from django.db import migrations +def register_bans_version_tracker(apps, schema_editor): + from misago.core.migrationutils import cachebuster_register_cache + cachebuster_register_cache(apps, "misago_bans") + + class Migration(migrations.Migration): """Migration superseded by 0016""" @@ -9,4 +14,7 @@ class Migration(migrations.Migration): ('misago_core', '0001_initial'), ] - operations = [] + operations = [ + # FIXME: remove this operation + migrations.RunPython(register_bans_version_tracker), + ] From f96aea9d3c594baa1f364256ce1b3a667c49cf53 Mon Sep 17 00:00:00 2001 From: rafalp Date: Sat, 1 Dec 2018 17:15:45 +0100 Subject: [PATCH 11/86] Cleanup migration --- misago/cache/migrations/0001_initial.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/misago/cache/migrations/0001_initial.py b/misago/cache/migrations/0001_initial.py index 7316c7a103..006bd7f8be 100644 --- a/misago/cache/migrations/0001_initial.py +++ b/misago/cache/migrations/0001_initial.py @@ -1,8 +1,6 @@ -# -*- coding: utf-8 -*- # Generated by Django 1.11.16 on 2018-11-25 15:15 -from __future__ import unicode_literals - from django.db import migrations, models + import misago.cache.utils From b950fc4118a61332c77ddd13f7a872d62e4a512e Mon Sep 17 00:00:00 2001 From: rafalp Date: Sat, 1 Dec 2018 17:29:29 +0100 Subject: [PATCH 12/86] Move user online utils to take request instead of viewer --- misago/threads/api/postendpoints/edits.py | 2 +- misago/threads/api/threadposts.py | 4 +- misago/threads/viewmodels/posts.py | 2 +- misago/users/online/utils.py | 45 ++++++++++++----------- misago/users/tests/test_online_utils.py | 11 ++++-- misago/users/viewmodels/activeposters.py | 2 +- misago/users/viewmodels/followers.py | 2 +- misago/users/viewmodels/rankusers.py | 2 +- 8 files changed, 38 insertions(+), 32 deletions(-) diff --git a/misago/threads/api/postendpoints/edits.py b/misago/threads/api/postendpoints/edits.py index 38ff7e4b4f..262f1e6e86 100644 --- a/misago/threads/api/postendpoints/edits.py +++ b/misago/threads/api/postendpoints/edits.py @@ -74,7 +74,7 @@ def revert_post_endpoint(request, post): add_acl(request.user, post) if post.poster: - make_users_status_aware(request.user, [post.poster]) + make_users_status_aware(request, [post.poster]) return Response(PostSerializer(post, context={'user': request.user}).data) diff --git a/misago/threads/api/threadposts.py b/misago/threads/api/threadposts.py index 656dbfbef1..f5dbe88b52 100644 --- a/misago/threads/api/threadposts.py +++ b/misago/threads/api/threadposts.py @@ -111,7 +111,7 @@ def create(self, request, thread_pk): post.is_new = True post.poster.posts = user_posts + 1 - make_users_status_aware(request.user, [post.poster]) + make_users_status_aware(request, [post.poster]) return Response(PostSerializer(post, context={'user': request.user}).data) else: @@ -141,7 +141,7 @@ def update(self, request, thread_pk, pk=None): post.edits = post_edits + 1 if post.poster: - make_users_status_aware(request.user, [post.poster]) + make_users_status_aware(request, [post.poster]) return Response(PostSerializer(post, context={'user': request.user}).data) else: diff --git a/misago/threads/viewmodels/posts.py b/misago/threads/viewmodels/posts.py index 1210bd3e3d..4dcf11d723 100644 --- a/misago/threads/viewmodels/posts.py +++ b/misago/threads/viewmodels/posts.py @@ -38,7 +38,7 @@ def __init__(self, request, thread, page): if post.poster: posters.append(post.poster) - make_users_status_aware(request.user, posters) + make_users_status_aware(request, posters) if thread.category.acl['can_see_posts_likes']: add_likes_to_posts(request.user, posts) diff --git a/misago/users/online/utils.py b/misago/users/online/utils.py index f1909a80bc..454a971bcc 100644 --- a/misago/users/online/utils.py +++ b/misago/users/online/utils.py @@ -9,7 +9,27 @@ ACTIVITY_CUTOFF = timedelta(minutes=2) -def get_user_status(viewer, user): + +def make_users_status_aware(request, users, fetch_state=False): + users_dict = {} + for user in users: + users_dict[user.pk] = user + + if fetch_state: + # Fill ban cache on users + for ban_cache in BanCache.objects.filter(user__in=users_dict.keys()): + users_dict[ban_cache.user_id].ban_cache = ban_cache + + # Fill user online trackers + for online_tracker in Online.objects.filter(user__in=users_dict.keys()): + users_dict[online_tracker.user_id].online_tracker = online_tracker + + # Fill user states + for user in users: + user.status = get_user_status(request, user) + + +def get_user_status(request, user): user_status = { 'is_banned': False, 'is_hidden': user.is_hiding_presence, @@ -28,7 +48,7 @@ def get_user_status(viewer, user): try: online_tracker = user.online_tracker - is_hidden = user.is_hiding_presence and not viewer.acl_cache['can_see_hidden_users'] + is_hidden = user.is_hiding_presence and not request.user.acl_cache['can_see_hidden_users'] if online_tracker and not is_hidden: if online_tracker.last_click >= timezone.now() - ACTIVITY_CUTOFF: @@ -38,7 +58,7 @@ def get_user_status(viewer, user): pass if user_status['is_hidden']: - if viewer.acl_cache['can_see_hidden_users']: + if request.user.acl_cache['can_see_hidden_users']: user_status['is_hidden'] = False if user_status['is_online']: user_status['is_online_hidden'] = True @@ -55,22 +75,3 @@ def get_user_status(viewer, user): user_status['is_offline'] = True return user_status - - -def make_users_status_aware(viewer, users, fetch_state=False): - users_dict = {} - for user in users: - users_dict[user.pk] = user - - if fetch_state: - # Fill ban cache on users - for ban_cache in BanCache.objects.filter(user__in=users_dict.keys()): - users_dict[ban_cache.user_id].ban_cache = ban_cache - - # Fill user online trackers - for online_tracker in Online.objects.filter(user__in=users_dict.keys()): - users_dict[online_tracker.user_id].online_tracker = online_tracker - - # Fill user states - for user in users: - user.status = get_user_status(viewer, user) diff --git a/misago/users/tests/test_online_utils.py b/misago/users/tests/test_online_utils.py index c2d9ffb5bd..58f079cf7e 100644 --- a/misago/users/tests/test_online_utils.py +++ b/misago/users/tests/test_online_utils.py @@ -1,3 +1,5 @@ +from unittest.mock import Mock + from django.contrib.auth import get_user_model from misago.acl.testutils import override_acl @@ -18,7 +20,8 @@ def test_user_hiding_presence(self): self.other_user.is_hiding_presence = True self.other_user.save() - get_user_status(self.user, self.other_user) + request = Mock(user=self.user, cache_versions={"bans": "abcdfghi"}) + get_user_status(request, self.other_user) def test_user_visible_hidden_presence(self): """get_user_status has no showstopper forvisible hidden user""" @@ -29,8 +32,10 @@ def test_user_visible_hidden_presence(self): 'can_see_hidden_users': True, }) - get_user_status(self.user, self.other_user) + request = Mock(user=self.user, cache_versions={"bans": "abcdfghi"}) + get_user_status(request, self.other_user) def test_user_not_hiding_presence(self): """get_user_status has no showstoppers for non-hidden user""" - get_user_status(self.user, self.other_user) + request = Mock(user=self.user, cache_versions={"bans": "abcdfghi"}) + get_user_status(request, self.other_user) diff --git a/misago/users/viewmodels/activeposters.py b/misago/users/viewmodels/activeposters.py index 935475393b..c874985888 100644 --- a/misago/users/viewmodels/activeposters.py +++ b/misago/users/viewmodels/activeposters.py @@ -7,7 +7,7 @@ class ActivePosters(object): def __init__(self, request): ranking = get_active_posters_ranking() - make_users_status_aware(request.user, ranking['users'], fetch_state=True) + make_users_status_aware(request, ranking['users'], fetch_state=True) self.count = ranking['users_count'] self.tracked_period = settings.MISAGO_RANKING_LENGTH diff --git a/misago/users/viewmodels/followers.py b/misago/users/viewmodels/followers.py index 4cd325ab02..a2451477b3 100644 --- a/misago/users/viewmodels/followers.py +++ b/misago/users/viewmodels/followers.py @@ -21,7 +21,7 @@ def __init__(self, request, profile, page=0, search=None): raise Http404() list_page = paginate(queryset, page, settings.MISAGO_USERS_PER_PAGE, 4) - make_users_status_aware(request.user, list_page.object_list) + make_users_status_aware(request, list_page.object_list) self.users = list_page.object_list self.paginator = pagination_dict(list_page) diff --git a/misago/users/viewmodels/rankusers.py b/misago/users/viewmodels/rankusers.py index 42f581eb12..4c9b4f6e14 100644 --- a/misago/users/viewmodels/rankusers.py +++ b/misago/users/viewmodels/rankusers.py @@ -16,7 +16,7 @@ def __init__(self, request, rank, page=0): queryset = queryset.filter(is_active=True) list_page = paginate(queryset, page, settings.MISAGO_USERS_PER_PAGE, 4) - make_users_status_aware(request.user, list_page.object_list) + make_users_status_aware(request, list_page.object_list) self.users = list_page.object_list self.paginator = pagination_dict(list_page) From cdea0c5e2b2c535c00fc96ef0d8a5fa7c07e16e9 Mon Sep 17 00:00:00 2001 From: rafalp Date: Sat, 1 Dec 2018 23:05:53 +0100 Subject: [PATCH 13/86] Remove global state from bans --- misago/users/api/auth.py | 2 +- misago/users/api/users.py | 4 +-- misago/users/apps.py | 2 +- misago/users/bans.py | 19 +++++------ misago/users/constants.py | 2 +- misago/users/forms/auth.py | 6 +++- .../management/commands/invalidatebans.py | 8 +++-- misago/users/middleware.py | 5 ++- .../0017_move_bans_to_cache_version.py | 32 +++++++++++++++++++ misago/users/models/ban.py | 17 +++++----- misago/users/online/utils.py | 2 +- misago/users/social/pipeline.py | 2 +- misago/users/tests/test_bans.py | 32 +++++++++++-------- misago/users/tests/test_invalidatebans.py | 6 ++-- misago/users/tests/test_social_pipeline.py | 2 ++ misago/users/views/activation.py | 2 +- misago/users/views/forgottenpassword.py | 2 +- misago/users/views/profile.py | 4 +-- 18 files changed, 98 insertions(+), 51 deletions(-) create mode 100644 misago/users/migrations/0017_move_bans_to_cache_version.py diff --git a/misago/users/api/auth.py b/misago/users/api/auth.py index aa6cc3d37b..20b386aced 100644 --- a/misago/users/api/auth.py +++ b/misago/users/api/auth.py @@ -191,7 +191,7 @@ def change_forgotten_password(request, pk, token): if user.requires_activation: raise PasswordChangeFailed(expired_message) - if get_user_ban(user): + if get_user_ban(user, request.cache_versions): raise PasswordChangeFailed(expired_message) except PasswordChangeFailed as e: return Response( diff --git a/misago/users/api/users.py b/misago/users/api/users.py index 291bb5fe6d..1436b4819f 100644 --- a/misago/users/api/users.py +++ b/misago/users/api/users.py @@ -85,7 +85,7 @@ def retrieve(self, request, pk=None): profile = self.get_user(request, pk) add_acl(request.user, profile) - profile.status = get_user_status(request.user, profile) + profile.status = get_user_status(request, profile) serializer = UserProfileSerializer(profile, context={'user': request.user}) profile_json = serializer.data @@ -199,7 +199,7 @@ def ban(self, request, pk=None): profile = self.get_user(request, pk) allow_see_ban_details(request.user, profile) - ban = get_user_ban(profile) + ban = get_user_ban(profile, request.cache_versions) if ban: return Response(BanDetailsSerializer(ban).data) else: diff --git a/misago/users/apps.py b/misago/users/apps.py index 036c7c9355..da416470c9 100644 --- a/misago/users/apps.py +++ b/misago/users/apps.py @@ -80,7 +80,7 @@ def can_see_ban_details(request, profile): if request.user.is_authenticated: if request.user.acl_cache['can_see_ban_details']: from .bans import get_user_ban - return bool(get_user_ban(profile)) + return bool(get_user_ban(profile, request.cache_versions)) else: return False else: diff --git a/misago/users/bans.py b/misago/users/bans.py index 8696c032a7..0fc5110365 100644 --- a/misago/users/bans.py +++ b/misago/users/bans.py @@ -9,13 +9,10 @@ from django.utils import timezone from django.utils.dateparse import parse_datetime -from misago.core import cachebuster - +from .constants import BANS_CACHE from .models import Ban, BanCache - CACHE_SESSION_KEY = 'misago_ip_check' -VERSION_KEY = 'misago_bans' def get_username_ban(username, registration_only=False): @@ -39,7 +36,7 @@ def get_ip_ban(ip, registration_only=False): return None -def get_user_ban(user): +def get_user_ban(user, cache_versions): """ This function checks if user is banned @@ -49,11 +46,11 @@ def get_user_ban(user): """ try: ban_cache = user.ban_cache - if not ban_cache.is_valid: + if not ban_cache.is_valid(cache_versions): _set_user_ban_cache(user) except BanCache.DoesNotExist: user.ban_cache = BanCache(user=user) - user.ban_cache = _set_user_ban_cache(user) + user.ban_cache = _set_user_ban_cache(user, cache_versions) if user.ban_cache.ban: return user.ban_cache @@ -61,9 +58,9 @@ def get_user_ban(user): return None -def _set_user_ban_cache(user): +def _set_user_ban_cache(user, cache_versions): ban_cache = user.ban_cache - ban_cache.bans_version = cachebuster.get_version(VERSION_KEY) + ban_cache.cache_version = cache_versions[BANS_CACHE] try: user_ban = Ban.objects.get_ban( @@ -103,7 +100,7 @@ def get_request_ip_ban(request): found_ban = get_ip_ban(request.user_ip) ban_cache = request.session[CACHE_SESSION_KEY] = { - 'version': cachebuster.get_version(VERSION_KEY), + 'version': request.cache_versions[BANS_CACHE], 'ip': request.user_ip, } @@ -128,7 +125,7 @@ def _get_session_bancache(request): ban_cache = _hydrate_session_cache(ban_cache) if ban_cache['ip'] != request.user_ip: return None - if not cachebuster.is_valid(VERSION_KEY, ban_cache['version']): + if ban_cache['version'] != request.cache_versions[BANS_CACHE]: return None if ban_cache.get('expires_on'): if ban_cache['expires_on'] < timezone.today(): diff --git a/misago/users/constants.py b/misago/users/constants.py index c9dbe210d9..a3c12c6f19 100644 --- a/misago/users/constants.py +++ b/misago/users/constants.py @@ -1 +1 @@ -BANS_CACHEBUSTER = 'misago_bans' +BANS_CACHE = "bans" \ No newline at end of file diff --git a/misago/users/forms/auth.py b/misago/users/forms/auth.py index 41e2db0eaa..a696bed559 100644 --- a/misago/users/forms/auth.py +++ b/misago/users/forms/auth.py @@ -31,7 +31,7 @@ def confirm_user_active(self, user): def confirm_user_not_banned(self, user): if not user.is_staff: - self.user_ban = get_user_ban(user) + self.user_ban = get_user_ban(user, self.request.cache_versions) if self.user_ban: raise ValidationError('', code='banned') @@ -62,6 +62,10 @@ class AuthenticationForm(MisagoAuthMixin, BaseAuthenticationForm): widget=forms.PasswordInput, ) + def __init__(self, *args, request=None, **kwargs): + self.request = request + super().__init__(*args, **kwargs) + def clean(self): username = self.cleaned_data.get('username') password = self.cleaned_data.get('password') diff --git a/misago/users/management/commands/invalidatebans.py b/misago/users/management/commands/invalidatebans.py index db825eabe0..d755e09535 100644 --- a/misago/users/management/commands/invalidatebans.py +++ b/misago/users/management/commands/invalidatebans.py @@ -1,7 +1,9 @@ from django.core.management.base import BaseCommand from django.utils import timezone -from misago.core import cachebuster +from misago.cache.versions import get_cache_versions + +from misago.users.constants import BANS_CACHE from misago.users.models import Ban, BanCache @@ -28,8 +30,8 @@ def handle_bans_caches(self): expired_count = queryset.count() queryset.delete() - bans_version = cachebuster.get_version('misago_bans') - queryset = BanCache.objects.filter(bans_version__lt=bans_version) + cache_versions = get_cache_versions() + queryset = BanCache.objects.exclude(cache_version=cache_versions[BANS_CACHE]) expired_count += queryset.count() queryset.delete() diff --git a/misago/users/middleware.py b/misago/users/middleware.py index 4b7677c23c..ae7ccd4f4d 100644 --- a/misago/users/middleware.py +++ b/misago/users/middleware.py @@ -20,7 +20,10 @@ def process_request(self, request): if request.user.is_anonymous: request.user = AnonymousUser() elif not request.user.is_staff: - if get_request_ip_ban(request) or get_user_ban(request.user): + if ( + get_request_ip_ban(request) or + get_user_ban(request.user, request.cache_versions) + ): logout(request) request.user = AnonymousUser() diff --git a/misago/users/migrations/0017_move_bans_to_cache_version.py b/misago/users/migrations/0017_move_bans_to_cache_version.py new file mode 100644 index 0000000000..784f6bfd3f --- /dev/null +++ b/misago/users/migrations/0017_move_bans_to_cache_version.py @@ -0,0 +1,32 @@ +# Generated by Django 1.11.16 on 2018-11-29 20:28 +from django.db import migrations, models + +from misago.users.constants import BANS_CACHE + + +def populate_cache_version(apps, _): + BanCache = apps.get_model("misago_users", "BanCache") + BanCache.objects.all().delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('misago_users', '0016_cache_version'), + ] + + operations = [ + migrations.RemoveField( + model_name='bancache', + name='bans_version', + ), + migrations.RunPython( + populate_cache_version, + migrations.RunPython.noop, + ), + migrations.AddField( + model_name='bancache', + name='cache_version', + field=models.CharField(max_length=8), + ), + ] \ No newline at end of file diff --git a/misago/users/models/ban.py b/misago/users/models/ban.py index 5a22aab3ea..b532fd0f7a 100644 --- a/misago/users/models/ban.py +++ b/misago/users/models/ban.py @@ -5,8 +5,8 @@ from django.utils import timezone from django.utils.translation import gettext_lazy as _ -from misago.core import cachebuster -from misago.users.constants import BANS_CACHEBUSTER +from misago.cache.versions import invalidate_cache +from misago.users.constants import BANS_CACHE class BansManager(models.Manager): @@ -29,7 +29,7 @@ def get_email_ban(self, email, registration_only=False): ) def invalidate_cache(self): - cachebuster.invalidate(BANS_CACHEBUSTER) + invalidate_cache(BANS_CACHE) def get_ban(self, username=None, email=None, ip=None, registration_only=False): checks = [] @@ -131,7 +131,7 @@ class BanCache(models.Model): blank=True, on_delete=models.SET_NULL, ) - bans_version = models.PositiveIntegerField(default=0) + cache_version = models.CharField(max_length=8) user_message = models.TextField(null=True, blank=True) staff_message = models.TextField(null=True, blank=True) expires_on = models.DateTimeField(null=True, blank=True) @@ -157,9 +157,8 @@ def get_serialized_message(self): def is_banned(self): return bool(self.ban) - @property - def is_valid(self): - version_is_valid = cachebuster.is_valid(BANS_CACHEBUSTER, self.bans_version) - expired = self.expires_on and self.expires_on < timezone.now() + def is_valid(self, cache_versions): + is_versioned = self.cache_version == cache_versions[BANS_CACHE] + is_expired = self.expires_on and self.expires_on < timezone.now() - return version_is_valid and not expired + return is_versioned and not is_expired diff --git a/misago/users/online/utils.py b/misago/users/online/utils.py index 454a971bcc..7ade237a6a 100644 --- a/misago/users/online/utils.py +++ b/misago/users/online/utils.py @@ -41,7 +41,7 @@ def get_user_status(request, user): 'last_click': user.last_login or user.joined_on, } - user_ban = get_user_ban(user) + user_ban = get_user_ban(user, request.cache_versions) if user_ban: user_status['is_banned'] = True user_status['banned_until'] = user_ban.expires_on diff --git a/misago/users/social/pipeline.py b/misago/users/social/pipeline.py index 80c5bffd9b..7a0be49bea 100644 --- a/misago/users/social/pipeline.py +++ b/misago/users/social/pipeline.py @@ -47,7 +47,7 @@ def validate_user_not_banned(strategy, details, backend, user=None, *args, **kwa if not user or user.is_staff: return None - user_ban = get_user_ban(user) + user_ban = get_user_ban(user, strategy.request.cache_versions) if user_ban: raise SocialAuthBanned(backend, user_ban) diff --git a/misago/users/tests/test_bans.py b/misago/users/tests/test_bans.py index 5e517eea0e..7aa8eec7d8 100644 --- a/misago/users/tests/test_bans.py +++ b/misago/users/tests/test_bans.py @@ -4,8 +4,11 @@ from django.test import TestCase from django.utils import timezone +from misago.cache.versions import get_cache_versions_from_db + from misago.users.bans import ( ban_ip, ban_user, get_email_ban, get_ip_ban, get_request_ip_ban, get_user_ban, get_username_ban) +from misago.users.constants import BANS_CACHE from misago.users.models import Ban @@ -40,13 +43,13 @@ def test_get_username_ban(self): ) self.assertEqual(get_username_ban('admiral').pk, valid_ban.pk) - regitration_ban = Ban.objects.create( + registration_ban = Ban.objects.create( banned_value='bob*', expires_on=timezone.now() + timedelta(days=7), registration_only=True, ) self.assertIsNone(get_username_ban('boberson')) - self.assertEqual(get_username_ban('boberson', True).pk, regitration_ban.pk) + self.assertEqual(get_username_ban('boberson', True).pk, registration_ban.pk) def test_get_email_ban(self): """get_email_ban returns valid ban""" @@ -77,14 +80,14 @@ def test_get_email_ban(self): ) self.assertEqual(get_email_ban('banned@mail.ru').pk, valid_ban.pk) - regitration_ban = Ban.objects.create( + registration_ban = Ban.objects.create( banned_value='*.ua', check_type=Ban.EMAIL, expires_on=timezone.now() + timedelta(days=7), registration_only=True, ) self.assertIsNone(get_email_ban('banned@mail.ua')) - self.assertEqual(get_email_ban('banned@mail.ua', True).pk, regitration_ban.pk) + self.assertEqual(get_email_ban('banned@mail.ua', True).pk, registration_ban.pk) def test_get_ip_ban(self): """get_ip_ban returns valid ban""" @@ -115,14 +118,14 @@ def test_get_ip_ban(self): ) self.assertEqual(get_ip_ban('125.0.0.1').pk, valid_ban.pk) - regitration_ban = Ban.objects.create( + registration_ban = Ban.objects.create( banned_value='188.*', check_type=Ban.IP, expires_on=timezone.now() + timedelta(days=7), registration_only=True, ) self.assertIsNone(get_ip_ban('188.12.12.41')) - self.assertEqual(get_ip_ban('188.12.12.41', True).pk, regitration_ban.pk) + self.assertEqual(get_ip_ban('188.12.12.41', True).pk, registration_ban.pk) class UserBansTests(TestCase): @@ -131,7 +134,7 @@ def setUp(self): def test_no_ban(self): """user is not caught by ban""" - self.assertIsNone(get_user_ban(self.user)) + self.assertIsNone(get_user_ban(self.user, get_cache_versions_from_db())) self.assertFalse(self.user.ban_cache.is_banned) def test_permanent_ban(self): @@ -142,7 +145,7 @@ def test_permanent_ban(self): staff_message='Staff reason', ) - user_ban = get_user_ban(self.user) + user_ban = get_user_ban(self.user, get_cache_versions_from_db()) self.assertIsNotNone(user_ban) self.assertEqual(user_ban.user_message, 'User reason') self.assertEqual(user_ban.staff_message, 'Staff reason') @@ -157,7 +160,7 @@ def test_temporary_ban(self): expires_on=timezone.now() + timedelta(days=7), ) - user_ban = get_user_ban(self.user) + user_ban = get_user_ban(self.user, get_cache_versions_from_db()) self.assertIsNotNone(user_ban) self.assertEqual(user_ban.user_message, 'User reason') self.assertEqual(user_ban.staff_message, 'Staff reason') @@ -170,7 +173,7 @@ def test_expired_ban(self): expires_on=timezone.now() - timedelta(days=7), ) - self.assertIsNone(get_user_ban(self.user)) + self.assertIsNone(get_user_ban(self.user, get_cache_versions_from_db())) self.assertFalse(self.user.ban_cache.is_banned) def test_expired_non_flagged_ban(self): @@ -181,7 +184,7 @@ def test_expired_non_flagged_ban(self): ) Ban.objects.update(is_checked=True) - self.assertIsNone(get_user_ban(self.user)) + self.assertIsNone(get_user_ban(self.user, get_cache_versions_from_db())) self.assertFalse(self.user.ban_cache.is_banned) @@ -189,6 +192,9 @@ class MockRequest(object): def __init__(self): self.user_ip = '127.0.0.1' self.session = {} + self.cache_versions = { + BANS_CACHE: "abcdefgh" + } class RequestIPBansTests(TestCase): @@ -255,7 +261,7 @@ def test_ban_user(self): self.assertEqual(ban.user_message, 'User reason') self.assertEqual(ban.staff_message, 'Staff reason') - db_ban = get_user_ban(user) + db_ban = get_user_ban(user, get_cache_versions_from_db()) self.assertEqual(ban.pk, db_ban.ban_id) @@ -267,4 +273,4 @@ def test_ban_ip(self): self.assertEqual(ban.staff_message, 'Staff reason') db_ban = get_ip_ban('127.0.0.1') - self.assertEqual(ban.pk, db_ban.pk) + self.assertEqual(ban.pk, db_ban.pk) \ No newline at end of file diff --git a/misago/users/tests/test_invalidatebans.py b/misago/users/tests/test_invalidatebans.py index d012f4edcd..03f9c9ac86 100644 --- a/misago/users/tests/test_invalidatebans.py +++ b/misago/users/tests/test_invalidatebans.py @@ -6,6 +6,8 @@ from django.test import TestCase from django.utils import timezone +from misago.cache.versions import get_cache_versions + from misago.users import bans from misago.users.management.commands import invalidatebans from misago.users.models import Ban, BanCache @@ -41,7 +43,7 @@ def test_bans_caches_updates(self): # ban user Ban.objects.create(banned_value="bob") - user_ban = bans.get_user_ban(user) + user_ban = bans.get_user_ban(user, get_cache_versions()) self.assertIsNotNone(user_ban) self.assertEqual(Ban.objects.filter(is_checked=True).count(), 1) @@ -74,4 +76,4 @@ def test_bans_caches_updates(self): # see if user is banned anymore user = UserModel.objects.get(id=user.id) - self.assertIsNone(bans.get_user_ban(user)) + self.assertIsNone(bans.get_user_ban(user, get_cache_versions())) diff --git a/misago/users/tests/test_social_pipeline.py b/misago/users/tests/test_social_pipeline.py index f5be0155dd..8e711f5abd 100644 --- a/misago/users/tests/test_social_pipeline.py +++ b/misago/users/tests/test_social_pipeline.py @@ -9,6 +9,7 @@ from misago.core.exceptions import SocialAuthFailed, SocialAuthBanned from misago.legal.models import Agreement +from misago.users.constants import BANS_CACHE from misago.users.models import AnonymousUser, Ban, BanCache from misago.users.social.pipeline import ( associate_by_email, create_user, create_user_with_form, get_username, require_activation, @@ -27,6 +28,7 @@ def create_request(user_ip='0.0.0.0', data=None): else: request = factory.post('/', data=json.dumps(data), content_type='application/json') request.include_frontend_context = True + request.cache_versions = {BANS_CACHE: "abcdefgh"} request.frontend_context = {} request.session = {} request.user = AnonymousUser() diff --git a/misago/users/views/activation.py b/misago/users/views/activation.py index 09dd2f4708..1f3c82f542 100644 --- a/misago/users/views/activation.py +++ b/misago/users/views/activation.py @@ -53,7 +53,7 @@ def activate_by_token(request, pk, token): ) raise ActivationError(message % {'user': inactive_user.username}) - ban = get_user_ban(inactive_user) + ban = get_user_ban(inactive_user, request.cache_versions) if ban: raise Banned(ban) except ActivationStopped as e: diff --git a/misago/users/views/forgottenpassword.py b/misago/users/views/forgottenpassword.py index b94937d7ad..efbd90891a 100644 --- a/misago/users/views/forgottenpassword.py +++ b/misago/users/views/forgottenpassword.py @@ -42,7 +42,7 @@ def reset_password_form(request, pk, token): message = _("%(user)s, your link is invalid. Please try again or request new link.") raise ResetError(message % {'user': requesting_user.username}) - ban = get_user_ban(requesting_user) + ban = get_user_ban(requesting_user, request.cache_versions) if ban: raise Banned(ban) except ResetError as e: diff --git a/misago/users/views/profile.py b/misago/users/views/profile.py index 4965da74e8..27f707ff5f 100644 --- a/misago/users/views/profile.py +++ b/misago/users/views/profile.py @@ -27,7 +27,7 @@ def get(self, request, *args, **kwargs): if not active_section: raise Http404() - profile.status = get_user_status(request.user, profile) + profile.status = get_user_status(request, profile) context_data = self.get_context_data(request, profile) self.complete_frontend_context(request, profile, sections) @@ -184,7 +184,7 @@ class UserBanView(ProfileView): template_name = 'misago/profile/ban_details.html' def get_context_data(self, request, profile): - ban = get_user_ban(profile) + ban = get_user_ban(profile, request.cache_versions) request.frontend_context['PROFILE_BAN'] = BanDetailsSerializer(ban).data From e44ca3fc25fc51fedbdf5be2b6d070438fe0f73b Mon Sep 17 00:00:00 2001 From: rafalp Date: Sat, 1 Dec 2018 23:14:02 +0100 Subject: [PATCH 14/86] Remove bans cache versioning from old cache versioning feature --- misago/users/migrations/0003_bans_version_tracker.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/misago/users/migrations/0003_bans_version_tracker.py b/misago/users/migrations/0003_bans_version_tracker.py index 16afe681d6..8620cc8c9b 100644 --- a/misago/users/migrations/0003_bans_version_tracker.py +++ b/misago/users/migrations/0003_bans_version_tracker.py @@ -1,11 +1,6 @@ from django.db import migrations -def register_bans_version_tracker(apps, schema_editor): - from misago.core.migrationutils import cachebuster_register_cache - cachebuster_register_cache(apps, "misago_bans") - - class Migration(migrations.Migration): """Migration superseded by 0016""" @@ -14,7 +9,4 @@ class Migration(migrations.Migration): ('misago_core', '0001_initial'), ] - operations = [ - # FIXME: remove this operation - migrations.RunPython(register_bans_version_tracker), - ] + operations = [] From 58cbc437efe22079696f258aa3775a39f58c12bc Mon Sep 17 00:00:00 2001 From: rafalp Date: Sun, 9 Dec 2018 03:04:07 +0100 Subject: [PATCH 15/86] Add non-global acl implementation and testing utils --- devproject/settings.py | 1 + misago/acl/api.py | 2 +- misago/acl/{builder.py => buildacl.py} | 0 misago/acl/middleware.py | 12 ++++ misago/acl/test.py | 35 ++++++++++ misago/acl/tests/test_getting_user_acl.py | 70 +++++++++++++++++++ .../tests/test_patching_user_acl_in_tests.py | 42 +++++++++++ misago/acl/tests/test_user_acl_middleware.py | 29 ++++++++ misago/acl/useracl.py | 15 ++++ misago/cache/middleware.py | 4 +- .../tests/test_cache_versions_middleware.py | 32 +-------- .../tests/test_getting_cache_versions.py | 6 +- 12 files changed, 211 insertions(+), 37 deletions(-) rename misago/acl/{builder.py => buildacl.py} (100%) create mode 100644 misago/acl/middleware.py create mode 100644 misago/acl/test.py create mode 100644 misago/acl/tests/test_getting_user_acl.py create mode 100644 misago/acl/tests/test_patching_user_acl_in_tests.py create mode 100644 misago/acl/tests/test_user_acl_middleware.py create mode 100644 misago/acl/useracl.py diff --git a/devproject/settings.py b/devproject/settings.py index e8e8972512..c7cacbe57a 100644 --- a/devproject/settings.py +++ b/devproject/settings.py @@ -226,6 +226,7 @@ 'misago.cache.middleware.cache_versions_middleware', 'misago.users.middleware.UserMiddleware', + 'misago.acl.middleware.user_acl_middleware', 'misago.core.middleware.ExceptionHandlerMiddleware', 'misago.users.middleware.OnlineTrackerMiddleware', 'misago.admin.middleware.AdminAuthMiddleware', diff --git a/misago/acl/api.py b/misago/acl/api.py index d003162cfc..fa92bd4436 100644 --- a/misago/acl/api.py +++ b/misago/acl/api.py @@ -14,7 +14,7 @@ from misago.core.cache import cache from . import version -from .builder import build_acl +from .buildacl import build_acl from .providers import providers diff --git a/misago/acl/builder.py b/misago/acl/buildacl.py similarity index 100% rename from misago/acl/builder.py rename to misago/acl/buildacl.py diff --git a/misago/acl/middleware.py b/misago/acl/middleware.py new file mode 100644 index 0000000000..2e3f732366 --- /dev/null +++ b/misago/acl/middleware.py @@ -0,0 +1,12 @@ +from django.utils.functional import SimpleLazyObject + +from . import useracl + + +def user_acl_middleware(get_response): + """Sets request.cache_versions attribute with dict of cache versions.""" + def middleware(request): + request.user_acl = useracl.get_user_acl(request.user, request.cache_versions) + return get_response(request) + + return middleware diff --git a/misago/acl/test.py b/misago/acl/test.py new file mode 100644 index 0000000000..a49761851f --- /dev/null +++ b/misago/acl/test.py @@ -0,0 +1,35 @@ +from functools import wraps +from unittest.mock import patch + +from .useracl import get_user_acl + + +class PatchUserACL: + def patch_user_acl(self, user, patch): + self.patches[user.id] = patch + + def patched_get_user_acl(self, user, cache_versions): + user_acl = get_user_acl(user, cache_versions) + user_acl.update(self.patches.get(user.id, {})) + return user_acl + + def __enter__(self): + self.patches = {} + + def __exit__(self, *_): + self.patches = {} + + def __call__(self, f): + @wraps(f) + def inner(*args, **kwargs): + with self as context: + with patch( + "misago.acl.useracl.get_user_acl", + side_effect=self.patched_get_user_acl, + ): + return f(*args, self.patch_user_acl, **kwargs) + + return inner + + +patch_user_acl = PatchUserACL() \ No newline at end of file diff --git a/misago/acl/tests/test_getting_user_acl.py b/misago/acl/tests/test_getting_user_acl.py new file mode 100644 index 0000000000..894abd4b72 --- /dev/null +++ b/misago/acl/tests/test_getting_user_acl.py @@ -0,0 +1,70 @@ +from unittest.mock import patch + +from django.contrib.auth import get_user_model +from django.test import TestCase + +from misago.acl.useracl import get_user_acl +from misago.users.models import AnonymousUser + +User = get_user_model() + +cache_versions = {"acl": "abcdefgh"} + + +class GettingUserACLTests(TestCase): + def test_getter_returns_authenticated_user_acl(self): + user = User.objects.create_user('Bob', 'bob@bob.com') + acl = get_user_acl(user, cache_versions) + + assert acl + assert acl["user_id"] == user.id + assert acl["is_authenticated"] is True + assert acl["is_anonymous"] is False + + def test_getter_returns_anonymous_user_acl(self): + user = AnonymousUser() + acl = get_user_acl(user, cache_versions) + + assert acl + assert acl["user_id"] == user.id + assert acl["is_authenticated"] is False + assert acl["is_anonymous"] is True + + @patch('django.core.cache.cache.get', return_value=dict()) + def test_getter_returns_acl_from_cache(self, cache_get): + user = AnonymousUser() + get_user_acl(user, cache_versions) + cache_get.assert_called_once() + + @patch('django.core.cache.cache.set') + @patch('misago.acl.buildacl.build_acl', return_value=dict()) + @patch('django.core.cache.cache.get', return_value=None) + def test_getter_builds_new_acl_when_cache_is_not_available(self, cache_get, *_): + user = AnonymousUser() + get_user_acl(user, cache_versions) + cache_get.assert_called_once() + + @patch('django.core.cache.cache.set') + @patch('misago.acl.buildacl.build_acl', return_value=dict()) + @patch('django.core.cache.cache.get', return_value=None) + def test_getter_sets_new_cache_if_no_cache_is_set(self, cache_set, *_): + user = AnonymousUser() + get_user_acl(user, cache_versions) + cache_set.assert_called_once() + + + @patch('django.core.cache.cache.set') + @patch('misago.acl.buildacl.build_acl', return_value=dict()) + @patch('django.core.cache.cache.get', return_value=None) + def test_acl_cache_name_includes_cache_verssion(self, cache_set, *_): + user = AnonymousUser() + get_user_acl(user, cache_versions) + cache_key = cache_set.call_args[0][0] + assert cache_versions["acl"] in cache_key + + @patch('django.core.cache.cache.set') + @patch('django.core.cache.cache.get', return_value=dict()) + def test_getter_is_not_setting_new_cache_if_cache_is_set(self, _, cache_set): + user = AnonymousUser() + get_user_acl(user, cache_versions) + cache_set.assert_not_called() \ No newline at end of file diff --git a/misago/acl/tests/test_patching_user_acl_in_tests.py b/misago/acl/tests/test_patching_user_acl_in_tests.py new file mode 100644 index 0000000000..579d6ee435 --- /dev/null +++ b/misago/acl/tests/test_patching_user_acl_in_tests.py @@ -0,0 +1,42 @@ +from django.contrib.auth import get_user_model +from django.test import TestCase + +from misago.acl import useracl +from misago.acl.test import patch_user_acl + +User = get_user_model() + +cache_versions = {"acl": "abcdefgh"} + + +class PatchingUserACLInTestsTests(TestCase): + @patch_user_acl + def test_decorator_adds_patching_function_to_test(self, patch_user_acl): + assert patch_user_acl + + @patch_user_acl + def test_patching_function_changes_user_permission_value(self, patch_user_acl): + user = User.objects.create_user("User", "user@example.com") + patch_user_acl(user, {"can_rename_users": 123}) + user_acl = useracl.get_user_acl(user, cache_versions) + assert user_acl["can_rename_users"] == 123 + + @patch_user_acl + def test_patching_function_adds_user_permission(self, patch_user_acl): + user = User.objects.create_user("User", "user@example.com") + patch_user_acl(user, {"new_user_permission": 123}) + user_acl = useracl.get_user_acl(user, cache_versions) + assert user_acl["new_user_permission"] == 123 + + def test_acl_patches_are_removed_after_test(self): + user = User.objects.create_user("User", "user@example.com") + + @patch_user_acl + def test_function(patch_user_acl): + patch_user_acl(user, {"is_patched": True}) + user_acl = useracl.get_user_acl(user, cache_versions) + assert user_acl["is_patched"] + + user_acl = useracl.get_user_acl(user, cache_versions) + assert "is_patched" not in user_acl + \ No newline at end of file diff --git a/misago/acl/tests/test_user_acl_middleware.py b/misago/acl/tests/test_user_acl_middleware.py new file mode 100644 index 0000000000..9c7b04a43d --- /dev/null +++ b/misago/acl/tests/test_user_acl_middleware.py @@ -0,0 +1,29 @@ +from unittest.mock import Mock + +from django.contrib.auth import get_user_model +from django.test import TestCase + +from misago.acl.middleware import user_acl_middleware + +User = get_user_model() + +cache_versions = {"acl": "abcdefgh"} + + +class MiddlewareTests(TestCase): + def test_middleware_sets_attr_on_request(self): + user = User.objects.create_user("User", "user@example.com") + + get_response = Mock() + request = Mock(user=user, cache_versions=cache_versions) + middleware = user_acl_middleware(get_response) + middleware(request) + assert request.user_acl + + def test_middleware_calls_get_response(self): + user = User.objects.create_user("User", "user@example.com") + get_response = Mock() + request = Mock(user=user, cache_versions=cache_versions) + middleware = user_acl_middleware(get_response) + middleware(request) + get_response.assert_called_once() diff --git a/misago/acl/useracl.py b/misago/acl/useracl.py new file mode 100644 index 0000000000..3222577a49 --- /dev/null +++ b/misago/acl/useracl.py @@ -0,0 +1,15 @@ +from django.core.cache import cache + +from . import buildacl + + +def get_user_acl(user, cache_versions): + cache_name = 'acl_%s_%s' % (user.acl_key, cache_versions["acl"]) + user_acl = cache.get(cache_name) + if user_acl is None: + user_acl = buildacl.build_acl(user.get_roles()) + cache.set(cache_name, user_acl) + user_acl["user_id"] = user.id + user_acl["is_authenticated"] = bool(user.is_authenticated) + user_acl["is_anonymous"] = bool(user.is_anonymous) + return user_acl diff --git a/misago/cache/middleware.py b/misago/cache/middleware.py index 7531230670..579c399b17 100644 --- a/misago/cache/middleware.py +++ b/misago/cache/middleware.py @@ -1,12 +1,10 @@ -from django.utils.functional import SimpleLazyObject - from .versions import get_cache_versions def cache_versions_middleware(get_response): """Sets request.cache_versions attribute with dict of cache versions.""" def middleware(request): - request.cache_versions = SimpleLazyObject(get_cache_versions) + request.cache_versions = get_cache_versions() return get_response(request) return middleware diff --git a/misago/cache/tests/test_cache_versions_middleware.py b/misago/cache/tests/test_cache_versions_middleware.py index 27e284621b..7115e120a0 100644 --- a/misago/cache/tests/test_cache_versions_middleware.py +++ b/misago/cache/tests/test_cache_versions_middleware.py @@ -1,7 +1,6 @@ -from unittest.mock import Mock, PropertyMock, patch +from unittest.mock import Mock from django.test import TestCase -from django.utils.functional import SimpleLazyObject from misago.cache.versions import CACHE_NAME from misago.cache.middleware import cache_versions_middleware @@ -11,21 +10,9 @@ class MiddlewareTests(TestCase): def test_middleware_sets_attr_on_request(self): get_response = Mock() request = Mock() - cache_versions = PropertyMock() - type(request).cache_versions = cache_versions middleware = cache_versions_middleware(get_response) middleware(request) - cache_versions.assert_called_once() - - def test_attr_set_by_middleware_on_request_is_lazy_object(self): - get_response = Mock() - request = Mock() - cache_versions = PropertyMock() - type(request).cache_versions = cache_versions - middleware = cache_versions_middleware(get_response) - middleware(request) - attr_value = cache_versions.call_args[0][0] - assert isinstance(attr_value, SimpleLazyObject) + assert request.cache_versions def test_middleware_calls_get_response(self): get_response = Mock() @@ -33,18 +20,3 @@ def test_middleware_calls_get_response(self): middleware = cache_versions_middleware(get_response) middleware(request) get_response.assert_called_once() - - def test_middleware_is_not_reading_db(self): - get_response = Mock() - request = Mock() - with self.assertNumQueries(0): - middleware = cache_versions_middleware(get_response) - middleware(request) - - @patch('django.core.cache.cache.get') - def test_middleware_is_not_reading_cache(self, cache_get): - get_response = Mock() - request = Mock() - middleware = cache_versions_middleware(get_response) - middleware(request) - cache_get.assert_not_called() \ No newline at end of file diff --git a/misago/cache/tests/test_getting_cache_versions.py b/misago/cache/tests/test_getting_cache_versions.py index 5c165cc604..720d78f5dd 100644 --- a/misago/cache/tests/test_getting_cache_versions.py +++ b/misago/cache/tests/test_getting_cache_versions.py @@ -32,13 +32,13 @@ def test_getter_reads_from_db_when_cache_is_not_available(self, cache_get, _): @patch('django.core.cache.cache.set') @patch('django.core.cache.cache.get', return_value=None) def test_getter_sets_new_cache_if_no_cache_is_set(self, _, cache_set): - assert get_cache_versions() + get_cache_versions() db_caches = get_cache_versions_from_db() cache_set.assert_called_once_with(CACHE_NAME, db_caches) @patch('django.core.cache.cache.set') @patch('django.core.cache.cache.get', return_value=True) def test_getter_is_not_setting_new_cache_if_cache_is_set(self, _, cache_set): - assert get_cache_versions() - db_caches = get_cache_versions_from_db() + get_cache_versions() + get_cache_versions_from_db() cache_set.assert_not_called() \ No newline at end of file From 8ef7c589195216da443f1ba3b46690c994ac7f55 Mon Sep 17 00:00:00 2001 From: rafalp Date: Sun, 9 Dec 2018 03:11:18 +0100 Subject: [PATCH 16/86] Include user staff and superuser status flags in user_acl --- misago/acl/tests/test_getting_user_acl.py | 16 ++++++++++++++++ misago/acl/useracl.py | 2 ++ 2 files changed, 18 insertions(+) diff --git a/misago/acl/tests/test_getting_user_acl.py b/misago/acl/tests/test_getting_user_acl.py index 894abd4b72..79d4b816c1 100644 --- a/misago/acl/tests/test_getting_user_acl.py +++ b/misago/acl/tests/test_getting_user_acl.py @@ -21,6 +21,14 @@ def test_getter_returns_authenticated_user_acl(self): assert acl["is_authenticated"] is True assert acl["is_anonymous"] is False + def test_user_acl_includes_staff_and_superuser_false_status(self): + user = User.objects.create_user('Bob', 'bob@bob.com') + acl = get_user_acl(user, cache_versions) + + assert acl + assert acl["is_staff"] is False + assert acl["is_superuser"] is False + def test_getter_returns_anonymous_user_acl(self): user = AnonymousUser() acl = get_user_acl(user, cache_versions) @@ -30,6 +38,14 @@ def test_getter_returns_anonymous_user_acl(self): assert acl["is_authenticated"] is False assert acl["is_anonymous"] is True + def test_superuser_acl_includes_staff_and_superuser_true_status(self): + user = User.objects.create_superuser('Bob', 'bob@bob.com', 'Pass.123') + acl = get_user_acl(user, cache_versions) + + assert acl + assert acl["is_staff"] is True + assert acl["is_superuser"] is True + @patch('django.core.cache.cache.get', return_value=dict()) def test_getter_returns_acl_from_cache(self, cache_get): user = AnonymousUser() diff --git a/misago/acl/useracl.py b/misago/acl/useracl.py index 3222577a49..0423e86fd4 100644 --- a/misago/acl/useracl.py +++ b/misago/acl/useracl.py @@ -12,4 +12,6 @@ def get_user_acl(user, cache_versions): user_acl["user_id"] = user.id user_acl["is_authenticated"] = bool(user.is_authenticated) user_acl["is_anonymous"] = bool(user.is_anonymous) + user_acl["is_staff"] = user.is_staff + user_acl["is_superuser"] = user.is_superuser return user_acl From 1e2df6e9cf91c9b69bb7852ab864599f2e7a9816 Mon Sep 17 00:00:00 2001 From: rafalp Date: Sun, 9 Dec 2018 05:37:59 +0100 Subject: [PATCH 17/86] Make tests pass for misago.users --- misago/acl/api.py | 10 +- misago/acl/middleware.py | 2 +- misago/acl/panels.py | 2 +- misago/acl/test.py | 5 +- misago/categories/permissions.py | 14 +- misago/search/api.py | 2 +- misago/search/context_processors.py | 2 +- misago/search/views.py | 4 +- misago/threads/api/attachments.py | 6 +- misago/threads/api/pollvotecreateendpoint.py | 2 +- misago/threads/api/postendpoints/edits.py | 2 +- misago/threads/api/postendpoints/merge.py | 2 +- .../threads/api/postendpoints/patch_event.py | 2 +- .../threads/api/postendpoints/patch_post.py | 2 +- misago/threads/api/threadendpoints/editor.py | 4 +- misago/threads/api/threadendpoints/merge.py | 2 +- misago/threads/api/threadendpoints/patch.py | 4 +- misago/threads/api/threadpoll.py | 4 +- misago/threads/api/threadposts.py | 2 +- misago/threads/api/threads.py | 2 +- misago/threads/middleware.py | 2 +- misago/threads/permissions/attachments.py | 8 +- misago/threads/permissions/bestanswers.py | 58 ++-- misago/threads/permissions/polls.py | 74 ++--- misago/threads/permissions/privatethreads.py | 50 +-- misago/threads/permissions/threads.py | 302 +++++++++--------- misago/threads/search.py | 2 +- misago/threads/viewmodels/category.py | 4 +- misago/threads/viewmodels/post.py | 2 +- misago/threads/viewmodels/posts.py | 2 +- misago/threads/viewmodels/thread.py | 12 +- misago/threads/viewmodels/threads.py | 15 +- misago/threads/views/attachment.py | 2 +- misago/users/api/userendpoints/signature.py | 6 +- misago/users/api/usernamechanges.py | 2 +- misago/users/api/users.py | 18 +- misago/users/apps.py | 4 +- misago/users/models/user.py | 18 +- misago/users/online/utils.py | 4 +- misago/users/permissions/decorators.py | 24 +- misago/users/permissions/delete.py | 14 +- misago/users/permissions/moderation.py | 55 ++-- misago/users/permissions/profiles.py | 25 +- misago/users/profilefields/default.py | 2 +- misago/users/profilefields/serializers.py | 2 +- misago/users/search.py | 2 +- misago/users/serializers/user.py | 9 +- .../users/tests/test_joinip_profilefield.py | 12 +- misago/users/tests/test_lists_views.py | 13 +- misago/users/tests/test_online_utils.py | 28 +- misago/users/tests/test_profile_views.py | 12 +- misago/users/tests/test_search.py | 7 +- misago/users/tests/test_user_avatar_api.py | 33 +- misago/users/tests/test_user_details_api.py | 10 +- .../users/tests/test_user_editdetails_api.py | 10 +- misago/users/tests/test_user_signature_api.py | 32 +- misago/users/tests/test_user_username_api.py | 37 +-- .../users/tests/test_usernamechanges_api.py | 27 +- misago/users/tests/test_users_api.py | 62 ++-- misago/users/viewmodels/posts.py | 2 +- misago/users/viewmodels/threads.py | 6 +- misago/users/views/lists.py | 4 +- misago/users/views/profile.py | 6 +- 63 files changed, 556 insertions(+), 538 deletions(-) diff --git a/misago/acl/api.py b/misago/acl/api.py index fa92bd4436..e854dc8322 100644 --- a/misago/acl/api.py +++ b/misago/acl/api.py @@ -38,21 +38,21 @@ def get_user_acl(user): return new_acl -def add_acl(user, target): +def add_acl(user_acl, target): """add valid ACL to target (iterable of objects or single object)""" if hasattr(target, '__iter__'): for item in target: - _add_acl_to_target(user, item) + _add_acl_to_target(user_acl, item) else: - _add_acl_to_target(user, target) + _add_acl_to_target(user_acl, target) -def _add_acl_to_target(user, target): +def _add_acl_to_target(user_acl, target): """add valid ACL to single target, helper for add_acl function""" target.acl = {} for annotator in providers.get_obj_type_annotators(target): - annotator(user, target) + annotator(user_acl, target) def serialize_acl(target): diff --git a/misago/acl/middleware.py b/misago/acl/middleware.py index 2e3f732366..f948c166ec 100644 --- a/misago/acl/middleware.py +++ b/misago/acl/middleware.py @@ -4,7 +4,7 @@ def user_acl_middleware(get_response): - """Sets request.cache_versions attribute with dict of cache versions.""" + """Sets request.user_acl attribute with dict containing current user acl.""" def middleware(request): request.user_acl = useracl.get_user_acl(request.user, request.cache_versions) return get_response(request) diff --git a/misago/acl/panels.py b/misago/acl/panels.py index 19d346ca95..01c3ddbc94 100644 --- a/misago/acl/panels.py +++ b/misago/acl/panels.py @@ -24,7 +24,7 @@ def process_response(self, request, response): misago_user = None try: - misago_acl = misago_user.acl_cache + misago_acl = request.user_acl except AttributeError: misago_acl = {} diff --git a/misago/acl/test.py b/misago/acl/test.py index a49761851f..2d7af04f06 100644 --- a/misago/acl/test.py +++ b/misago/acl/test.py @@ -3,6 +3,8 @@ from .useracl import get_user_acl +__all__ = ["patch_user_acl"] + class PatchUserACL: def patch_user_acl(self, user, patch): @@ -27,7 +29,8 @@ def inner(*args, **kwargs): "misago.acl.useracl.get_user_acl", side_effect=self.patched_get_user_acl, ): - return f(*args, self.patch_user_acl, **kwargs) + new_args = args + (self.patch_user_acl,) + return f(*new_args, **kwargs) return inner diff --git a/misago/categories/permissions.py b/misago/categories/permissions.py index 40ddc4885b..9e786ba83d 100644 --- a/misago/categories/permissions.py +++ b/misago/categories/permissions.py @@ -85,9 +85,9 @@ def build_category_acl(acl, category, categories_roles, key_name): acl['browseable_categories'].append(category.pk) -def add_acl_to_category(user, target): - target.acl['can_see'] = can_see_category(user, target) - target.acl['can_browse'] = can_browse_category(user, target) +def add_acl_to_category(user_acl, target): + target.acl['can_see'] = can_see_category(user_acl, target) + target.acl['can_browse'] = can_browse_category(user_acl, target) def serialize_categories_acls(serialized_acl): @@ -112,21 +112,21 @@ def register_with(registry): registry.acl_serializer(AnonymousUser, serialize_categories_acls) -def allow_see_category(user, target): +def allow_see_category(user_acl, target): try: category_id = target.pk except AttributeError: category_id = int(target) - if not category_id in user.acl_cache['visible_categories']: + if not category_id in user_acl['visible_categories']: raise Http404() can_see_category = return_boolean(allow_see_category) -def allow_browse_category(user, target): - target_acl = user.acl_cache['categories'].get(target.id, {'can_browse': False}) +def allow_browse_category(user_acl, target): + target_acl = user_acl['categories'].get(target.id, {'can_browse': False}) if not target_acl['can_browse']: message = _('You don\'t have permission to browse "%(category)s" contents.') raise PermissionDenied(message % {'category': target.name}) diff --git a/misago/search/api.py b/misago/search/api.py index 763ec8d65e..5109e33eba 100644 --- a/misago/search/api.py +++ b/misago/search/api.py @@ -15,7 +15,7 @@ @api_view() def search(request, search_provider=None): allowed_providers = searchproviders.get_allowed_providers(request) - if not request.user.acl_cache['can_search'] or not allowed_providers: + if not request.user_acl['can_search'] or not allowed_providers: raise PermissionDenied(_("You don't have permission to search site.")) search_query = get_search_query(request) diff --git a/misago/search/context_processors.py b/misago/search/context_processors.py index 4a30f2307a..b46e056063 100644 --- a/misago/search/context_processors.py +++ b/misago/search/context_processors.py @@ -8,7 +8,7 @@ def search_providers(request): allowed_providers = [] try: - if request.user.acl_cache['can_search']: + if request.user_acl['can_search']: allowed_providers = searchproviders.get_allowed_providers(request) except AttributeError: # is user has no acl_cache attribute, cease entire middleware diff --git a/misago/search/views.py b/misago/search/views.py index 12e56ab6b5..3b9759f797 100644 --- a/misago/search/views.py +++ b/misago/search/views.py @@ -9,7 +9,7 @@ def landing(request): allowed_providers = searchproviders.get_allowed_providers(request) - if not request.user.acl_cache['can_search'] or not allowed_providers: + if not request.user_acl['can_search'] or not allowed_providers: raise PermissionDenied(_("You don't have permission to search site.")) default_provider = allowed_providers[0] @@ -18,7 +18,7 @@ def landing(request): def search(request, search_provider): all_providers = searchproviders.get_providers(request) - if not request.user.acl_cache['can_search'] or not all_providers: + if not request.user_acl['can_search'] or not all_providers: raise PermissionDenied(_("You don't have permission to search site.")) for provider in all_providers: diff --git a/misago/threads/api/attachments.py b/misago/threads/api/attachments.py index 48542ecdf7..d1a32338aa 100644 --- a/misago/threads/api/attachments.py +++ b/misago/threads/api/attachments.py @@ -16,7 +16,7 @@ class AttachmentViewSet(viewsets.ViewSet): def create(self, request): - if not request.user.acl_cache['max_attachment_size']: + if not request.user_acl['max_attachment_size']: raise PermissionDenied(_("You don't have permission to upload new files.")) try: @@ -31,7 +31,7 @@ def create_attachment(self, request): user_roles = set(r.pk for r in request.user.get_roles()) filetype = validate_filetype(upload, user_roles) - validate_filesize(upload, filetype, request.user.acl_cache['max_attachment_size']) + validate_filesize(upload, filetype, request.user_acl['max_attachment_size']) attachment = Attachment( secret=Attachment.generate_new_secret(), @@ -52,7 +52,7 @@ def create_attachment(self, request): attachment.set_file(upload) attachment.save() - add_acl(request.user, attachment) + add_acl(request.user_acl, attachment) create_audit_trail(request, attachment) diff --git a/misago/threads/api/pollvotecreateendpoint.py b/misago/threads/api/pollvotecreateendpoint.py index 940d4872d0..40280ae8ab 100644 --- a/misago/threads/api/pollvotecreateendpoint.py +++ b/misago/threads/api/pollvotecreateendpoint.py @@ -33,7 +33,7 @@ def poll_vote_create(request, thread, poll): remove_user_votes(request.user, poll, serializer.data['choices']) set_new_votes(request, poll, serializer.data['choices']) - add_acl(request.user, poll) + add_acl(request.user_acl, poll) serialized_poll = PollSerializer(poll).data poll.choices = list(map(presave_clean_choice, deepcopy(poll.choices))) diff --git a/misago/threads/api/postendpoints/edits.py b/misago/threads/api/postendpoints/edits.py index 262f1e6e86..30bb94b14b 100644 --- a/misago/threads/api/postendpoints/edits.py +++ b/misago/threads/api/postendpoints/edits.py @@ -71,7 +71,7 @@ def revert_post_endpoint(request, post): post.is_new = False post.edits = post_edits + 1 - add_acl(request.user, post) + add_acl(request.user_acl, post) if post.poster: make_users_status_aware(request, [post.poster]) diff --git a/misago/threads/api/postendpoints/merge.py b/misago/threads/api/postendpoints/merge.py index 1e29d3136e..adb3f4e49d 100644 --- a/misago/threads/api/postendpoints/merge.py +++ b/misago/threads/api/postendpoints/merge.py @@ -55,6 +55,6 @@ def posts_merge_endpoint(request, thread): first_post.thread = thread first_post.category = thread.category - add_acl(request.user, first_post) + add_acl(request.user_acl, first_post) return Response(PostSerializer(first_post, context={'user': request.user}).data) diff --git a/misago/threads/api/postendpoints/patch_event.py b/misago/threads/api/postendpoints/patch_event.py index 3e9ed41770..9c7c938ebc 100644 --- a/misago/threads/api/postendpoints/patch_event.py +++ b/misago/threads/api/postendpoints/patch_event.py @@ -13,7 +13,7 @@ def patch_acl(request, event, value): """useful little op that updates event acl to current state""" if value: - add_acl(request.user, event) + add_acl(request.user_acl, event) return {'acl': event.acl} else: return {'acl': None} diff --git a/misago/threads/api/postendpoints/patch_post.py b/misago/threads/api/postendpoints/patch_post.py index 504d836a34..5383b37a1e 100644 --- a/misago/threads/api/postendpoints/patch_post.py +++ b/misago/threads/api/postendpoints/patch_post.py @@ -23,7 +23,7 @@ def patch_acl(request, post, value): """useful little op that updates post acl to current state""" if value: - add_acl(request.user, post) + add_acl(request.user_acl, post) return {'acl': post.acl} else: return {'acl': None} diff --git a/misago/threads/api/threadendpoints/editor.py b/misago/threads/api/threadendpoints/editor.py index e4da8c2a01..f10ee1ea20 100644 --- a/misago/threads/api/threadendpoints/editor.py +++ b/misago/threads/api/threadendpoints/editor.py @@ -19,12 +19,12 @@ def thread_start_editor(request): categories = [] queryset = Category.objects.filter( - pk__in=request.user.acl_cache['browseable_categories'], + pk__in=request.user_acl['browseable_categories'], tree_id=trees_map.get_tree_id_for_root(THREADS_ROOT_NAME) ).order_by('-lft') for category in queryset: - add_acl(request.user, category) + add_acl(request.user_acl, category) post = False if can_start_thread(request.user, category): diff --git a/misago/threads/api/threadendpoints/merge.py b/misago/threads/api/threadendpoints/merge.py index 1b6407d667..cab462bd5d 100644 --- a/misago/threads/api/threadendpoints/merge.py +++ b/misago/threads/api/threadendpoints/merge.py @@ -191,5 +191,5 @@ def merge_threads(request, validated_data, threads, merge_conflict): new_thread.is_read = False new_thread.subscription = None - add_acl(request.user, new_thread) + add_acl(request.user_acl, new_thread) return new_thread diff --git a/misago/threads/api/threadendpoints/patch.py b/misago/threads/api/threadendpoints/patch.py index dd52230a08..103c61ad36 100644 --- a/misago/threads/api/threadendpoints/patch.py +++ b/misago/threads/api/threadendpoints/patch.py @@ -37,7 +37,7 @@ def patch_acl(request, thread, value): """useful little op that updates thread acl to current state""" if value: - add_acl(request.user, thread) + add_acl(request.user_acl, thread) return {'acl': thread.acl} else: return {'acl': None} @@ -96,7 +96,7 @@ def patch_move(request, thread, value): Category.objects.all_categories().select_related('parent'), pk=category_pk ) - add_acl(request.user, new_category) + add_acl(request.user_acl, new_category) allow_see_category(request.user, new_category) allow_browse_category(request.user, new_category) allow_start_thread(request.user, new_category) diff --git a/misago/threads/api/threadpoll.py b/misago/threads/api/threadpoll.py index 4b8fc79710..dd2fa3912e 100644 --- a/misago/threads/api/threadpoll.py +++ b/misago/threads/api/threadpoll.py @@ -68,7 +68,7 @@ def create(self, request, thread_pk): serializer.save() - add_acl(request.user, instance) + add_acl(request.user_acl, instance) for choice in instance.choices: choice['selected'] = False @@ -91,7 +91,7 @@ def update(self, request, thread_pk, pk=None): serializer.save() - add_acl(request.user, instance) + add_acl(request.user_acl, instance) instance.make_choices_votes_aware(request.user) create_audit_trail(request, instance) diff --git a/misago/threads/api/threadposts.py b/misago/threads/api/threadposts.py index f5dbe88b52..ae6ce5d9a4 100644 --- a/misago/threads/api/threadposts.py +++ b/misago/threads/api/threadposts.py @@ -192,7 +192,7 @@ def post_editor(self, request, thread_pk, pk=None): attachments = [] for attachment in post.attachment_set.order_by('-id'): - add_acl(request.user, attachment) + add_acl(request.user_acl, attachment) attachments.append(attachment) attachments_json = AttachmentSerializer( attachments, many=True, context={'user': request.user} diff --git a/misago/threads/api/threads.py b/misago/threads/api/threads.py index b3bb42eb68..272610e329 100644 --- a/misago/threads/api/threads.py +++ b/misago/threads/api/threads.py @@ -119,7 +119,7 @@ def list(self, request): @transaction.atomic def create(self, request): allow_use_private_threads(request.user) - if not request.user.acl_cache['can_start_private_threads']: + if not request.user_acl['can_start_private_threads']: raise PermissionDenied(_("You can't start private threads.")) request.user.lock() diff --git a/misago/threads/middleware.py b/misago/threads/middleware.py index 3f37a454bd..5a0a10867d 100644 --- a/misago/threads/middleware.py +++ b/misago/threads/middleware.py @@ -11,7 +11,7 @@ def process_request(self, request): if request.user.is_anonymous: return - if not request.user.acl_cache['can_use_private_threads']: + if not request.user_acl['can_use_private_threads']: return if not request.user.sync_unread_private_threads: diff --git a/misago/threads/permissions/attachments.py b/misago/threads/permissions/attachments.py index df49623843..f83a5af43e 100644 --- a/misago/threads/permissions/attachments.py +++ b/misago/threads/permissions/attachments.py @@ -58,15 +58,15 @@ def build_acl(acl, roles, key_name): ) -def add_acl_to_attachment(user, attachment): - if user.is_authenticated and user.id == attachment.uploader_id: +def add_acl_to_attachment(user_acl, attachment): + if user_acl["is_authenticated"] and user_acl["user_id"] == attachment.uploader_id: attachment.acl.update({ 'can_delete': True, }) else: - user_can_delete = user.acl_cache['can_delete_other_users_attachments'] + user_can_delete = user_acl['can_delete_other_users_attachments'] attachment.acl.update({ - 'can_delete': user.is_authenticated and user_can_delete, + 'can_delete': user_acl["is_authenticated"] and user_can_delete, }) diff --git a/misago/threads/permissions/bestanswers.py b/misago/threads/permissions/bestanswers.py index cf51149f6f..fe2993bf83 100644 --- a/misago/threads/permissions/bestanswers.py +++ b/misago/threads/permissions/bestanswers.py @@ -108,19 +108,19 @@ def build_category_acl(acl, category, categories_roles, key_name): return final_acl -def add_acl_to_thread(user, thread): +def add_acl_to_thread(user_acl, thread): thread.acl.update({ - 'can_mark_best_answer': can_mark_best_answer(user, thread), - 'can_change_best_answer': can_change_best_answer(user, thread), - 'can_unmark_best_answer': can_unmark_best_answer(user, thread), + 'can_mark_best_answer': can_mark_best_answer(user_acl, thread), + 'can_change_best_answer': can_change_best_answer(user_acl, thread), + 'can_unmark_best_answer': can_unmark_best_answer(user_acl, thread), }) -def add_acl_to_post(user, post): +def add_acl_to_post(user_acl, post): post.acl.update({ - 'can_mark_as_best_answer': can_mark_as_best_answer(user, post), - 'can_hide_best_answer': can_hide_best_answer(user, post), - 'can_delete_best_answer': can_delete_best_answer(user, post), + 'can_mark_as_best_answer': can_mark_as_best_answer(user_acl, post), + 'can_hide_best_answer': can_hide_best_answer(user_acl, post), + 'can_delete_best_answer': can_delete_best_answer(user_acl, post), }) @@ -129,11 +129,11 @@ def register_with(registry): registry.acl_annotator(Post, add_acl_to_post) -def allow_mark_best_answer(user, target): - if user.is_anonymous: +def allow_mark_best_answer(user_acl, target): + if user_acl["is_anonymous"]: raise PermissionDenied(_("You have to sign in to mark best answers.")) - category_acl = user.acl_cache['categories'].get(target.category_id, {}) + category_acl = user_acl['categories'].get(target.category_id, {}) if not category_acl.get('can_mark_best_answers'): raise PermissionDenied( @@ -144,7 +144,7 @@ def allow_mark_best_answer(user, target): } ) - if category_acl['can_mark_best_answers'] == 1 and target.starter_id != user.id: + if category_acl['can_mark_best_answers'] == 1 and user_acl["user_id"] != target.starter_id: raise PermissionDenied( _( "You don't have permission to mark best answer in this thread because you didn't " @@ -174,11 +174,11 @@ def allow_mark_best_answer(user, target): can_mark_best_answer = return_boolean(allow_mark_best_answer) -def allow_change_best_answer(user, target): +def allow_change_best_answer(user_acl, target): if not target.has_best_answer: return # shortcircut permission test - category_acl = user.acl_cache['categories'].get(target.category_id, {}) + category_acl = user_acl['categories'].get(target.category_id, {}) if not category_acl.get('can_change_marked_answers'): raise PermissionDenied( @@ -191,14 +191,14 @@ def allow_change_best_answer(user, target): ) if category_acl['can_change_marked_answers'] == 1: - if target.starter_id != user.id: + if user_acl["user_id"] != target.starter_id: raise PermissionDenied( _( "You don't have permission to change this thread's marked answer because you " "are not a thread starter." ) ) - if not has_time_to_change_answer(user, target): + if not has_time_to_change_answer(user_acl, target): raise PermissionDenied( ngettext( ( @@ -227,14 +227,14 @@ def allow_change_best_answer(user, target): can_change_best_answer = return_boolean(allow_change_best_answer) -def allow_unmark_best_answer(user, target): - if user.is_anonymous: +def allow_unmark_best_answer(user_acl, target): + if user_acl["is_anonymous"]: raise PermissionDenied(_("You have to sign in to unmark best answers.")) if not target.has_best_answer: return # shortcircut test - category_acl = user.acl_cache['categories'].get(target.category_id, {}) + category_acl = user_acl['categories'].get(target.category_id, {}) if not category_acl.get('can_change_marked_answers'): raise PermissionDenied( @@ -247,14 +247,14 @@ def allow_unmark_best_answer(user, target): ) if category_acl['can_change_marked_answers'] == 1: - if target.starter_id != user.id: + if user_acl["user_id"] != target.starter_id: raise PermissionDenied( _( "You don't have permission to unmark this best answer because you are not a " "thread starter." ) ) - if not has_time_to_change_answer(user, target): + if not has_time_to_change_answer(user_acl, target): raise PermissionDenied( ngettext( ( @@ -301,14 +301,14 @@ def allow_unmark_best_answer(user, target): can_unmark_best_answer = return_boolean(allow_unmark_best_answer) -def allow_mark_as_best_answer(user, target): - if user.is_anonymous: +def allow_mark_as_best_answer(user_acl, target): + if user_acl["is_anonymous"]: raise PermissionDenied(_("You have to sign in to mark best answers.")) if target.is_event: raise PermissionDenied(_("Events can't be marked as best answers.")) - category_acl = user.acl_cache['categories'].get(target.category_id, {}) + category_acl = user_acl['categories'].get(target.category_id, {}) if not category_acl.get('can_mark_best_answers'): raise PermissionDenied( @@ -319,7 +319,7 @@ def allow_mark_as_best_answer(user, target): } ) - if category_acl['can_mark_best_answers'] == 1 and target.thread.starter_id != user.id: + if category_acl['can_mark_best_answers'] == 1 and user_acl["user_id"] != target.thread.starter_id: raise PermissionDenied( _( "You don't have permission to mark best answer in this thread because you " @@ -348,7 +348,7 @@ def allow_mark_as_best_answer(user, target): can_mark_as_best_answer = return_boolean(allow_mark_as_best_answer) -def allow_hide_best_answer(user, target): +def allow_hide_best_answer(user_acl, target): if target.is_best_answer: raise PermissionDenied( _("You can't hide this post because its marked as best answer.") @@ -358,7 +358,7 @@ def allow_hide_best_answer(user, target): can_hide_best_answer = return_boolean(allow_hide_best_answer) -def allow_delete_best_answer(user, target): +def allow_delete_best_answer(user_acl, target): if target.is_best_answer: raise PermissionDenied( _("You can't delete this post because its marked as best answer.") @@ -368,8 +368,8 @@ def allow_delete_best_answer(user, target): can_delete_best_answer = return_boolean(allow_delete_best_answer) -def has_time_to_change_answer(user, target): - category_acl = user.acl_cache['categories'].get(target.category_id, {}) +def has_time_to_change_answer(user_acl, target): + category_acl = user_acl['categories'].get(target.category_id, {}) change_time = category_acl.get('best_answer_change_time', 0) if change_time: diff --git a/misago/threads/permissions/polls.py b/misago/threads/permissions/polls.py index c8f3c9348f..7b4e6922ec 100644 --- a/misago/threads/permissions/polls.py +++ b/misago/threads/permissions/polls.py @@ -98,18 +98,18 @@ def build_acl(acl, roles, key_name): ) -def add_acl_to_poll(user, poll): +def add_acl_to_poll(user_acl, poll): poll.acl.update({ - 'can_vote': can_vote_poll(user, poll), - 'can_edit': can_edit_poll(user, poll), - 'can_delete': can_delete_poll(user, poll), - 'can_see_votes': can_see_poll_votes(user, poll), + 'can_vote': can_vote_poll(user_acl, poll), + 'can_edit': can_edit_poll(user_acl, poll), + 'can_delete': can_delete_poll(user_acl, poll), + 'can_see_votes': can_see_poll_votes(user_acl, poll), }) -def add_acl_to_thread(user, thread): +def add_acl_to_thread(user_acl, thread): thread.acl.update({ - 'can_start_poll': can_start_poll(user, thread), + 'can_start_poll': can_start_poll(user_acl, thread), }) @@ -118,19 +118,19 @@ def register_with(registry): registry.acl_annotator(Thread, add_acl_to_thread) -def allow_start_poll(user, target): - if user.is_anonymous: +def allow_start_poll(user_acl, target): + if user_acl["is_anonymous"]: raise PermissionDenied(_("You have to sign in to start polls.")) - category_acl = user.acl_cache['categories'].get( + category_acl = user_acl['categories'].get( target.category_id, { 'can_close_threads': False, } ) - if not user.acl_cache.get('can_start_polls'): + if not user_acl.get('can_start_polls'): raise PermissionDenied(_("You can't start polls.")) - if user.acl_cache.get('can_start_polls') < 2 and user.pk != target.starter_id: + if user_acl.get('can_start_polls') < 2 and user_acl["user_id"] != target.starter_id: raise PermissionDenied(_("You can't start polls in other users threads.")) if not category_acl.get('can_close_threads'): @@ -143,29 +143,29 @@ def allow_start_poll(user, target): can_start_poll = return_boolean(allow_start_poll) -def allow_edit_poll(user, target): - if user.is_anonymous: +def allow_edit_poll(user_acl, target): + if user_acl["is_anonymous"]: raise PermissionDenied(_("You have to sign in to edit polls.")) - category_acl = user.acl_cache['categories'].get( + category_acl = user_acl['categories'].get( target.category_id, { 'can_close_threads': False, } ) - if not user.acl_cache.get('can_edit_polls'): + if not user_acl.get('can_edit_polls'): raise PermissionDenied(_("You can't edit polls.")) - if user.acl_cache.get('can_edit_polls') < 2: - if user.pk != target.poster_id: + if user_acl.get('can_edit_polls') < 2: + if user_acl["user_id"] != target.poster_id: raise PermissionDenied(_("You can't edit other users polls in this category.")) - if not has_time_to_edit_poll(user, target): + if not has_time_to_edit_poll(user_acl, target): message = ngettext( "You can't edit polls that are older than %(minutes)s minute.", "You can't edit polls that are older than %(minutes)s minutes.", - user.acl_cache['poll_edit_time'] + user_acl['poll_edit_time'] ) - raise PermissionDenied(message % {'minutes': user.acl_cache['poll_edit_time']}) + raise PermissionDenied(message % {'minutes': user_acl['poll_edit_time']}) if target.is_over: raise PermissionDenied(_("This poll is over. You can't edit it.")) @@ -180,29 +180,29 @@ def allow_edit_poll(user, target): can_edit_poll = return_boolean(allow_edit_poll) -def allow_delete_poll(user, target): - if user.is_anonymous: +def allow_delete_poll(user_acl, target): + if user_acl["is_anonymous"]: raise PermissionDenied(_("You have to sign in to delete polls.")) - category_acl = user.acl_cache['categories'].get( + category_acl = user_acl['categories'].get( target.category_id, { 'can_close_threads': False, } ) - if not user.acl_cache.get('can_delete_polls'): + if not user_acl.get('can_delete_polls'): raise PermissionDenied(_("You can't delete polls.")) - if user.acl_cache.get('can_delete_polls') < 2: - if user.pk != target.poster_id: + if user_acl.get('can_delete_polls') < 2: + if user_acl["user_id"] != target.poster_id: raise PermissionDenied(_("You can't delete other users polls in this category.")) - if not has_time_to_edit_poll(user, target): + if not has_time_to_edit_poll(user_acl, target): message = ngettext( "You can't delete polls that are older than %(minutes)s minute.", "You can't delete polls that are older than %(minutes)s minutes.", - user.acl_cache['poll_edit_time'] + user_acl['poll_edit_time'] ) - raise PermissionDenied(message % {'minutes': user.acl_cache['poll_edit_time']}) + raise PermissionDenied(message % {'minutes': user_acl['poll_edit_time']}) if target.is_over: raise PermissionDenied(_("This poll is over. You can't delete it.")) @@ -216,8 +216,8 @@ def allow_delete_poll(user, target): can_delete_poll = return_boolean(allow_delete_poll) -def allow_vote_poll(user, target): - if user.is_anonymous: +def allow_vote_poll(user_acl, target): + if user_acl["is_anonymous"]: raise PermissionDenied(_("You have to sign in to vote in polls.")) if target.has_selected_choices and not target.allow_revotes: @@ -225,7 +225,7 @@ def allow_vote_poll(user, target): if target.is_over: raise PermissionDenied(_("This poll is over. You can't vote in it.")) - category_acl = user.acl_cache['categories'].get( + category_acl = user_acl['categories'].get( target.category_id, { 'can_close_threads': False, } @@ -241,16 +241,16 @@ def allow_vote_poll(user, target): can_vote_poll = return_boolean(allow_vote_poll) -def allow_see_poll_votes(user, target): - if not target.is_public and not user.acl_cache['can_always_see_poll_voters']: +def allow_see_poll_votes(user_acl, target): + if not target.is_public and not user_acl['can_always_see_poll_voters']: raise PermissionDenied(_("You dont have permission to this poll's voters.")) can_see_poll_votes = return_boolean(allow_see_poll_votes) -def has_time_to_edit_poll(user, target): - edit_time = user.acl_cache['poll_edit_time'] +def has_time_to_edit_poll(user_acl, target): + edit_time = user_acl['poll_edit_time'] if edit_time: diff = timezone.now() - target.posted_on diff_minutes = int(diff.total_seconds() / 60) diff --git a/misago/threads/permissions/privatethreads.py b/misago/threads/permissions/privatethreads.py index bef5404d48..81b8793c61 100644 --- a/misago/threads/permissions/privatethreads.py +++ b/misago/threads/permissions/privatethreads.py @@ -152,7 +152,7 @@ def build_acl(acl, roles, key_name): return new_acl -def add_acl_to_thread(user, thread): +def add_acl_to_thread(user_acl, thread): if thread.thread_type.root_name != PRIVATE_THREADS_ROOT_NAME: return @@ -162,8 +162,8 @@ def add_acl_to_thread(user, thread): thread.acl.update({ 'can_start_poll': False, - 'can_change_owner': can_change_owner(user, thread), - 'can_add_participants': can_add_participants(user, thread), + 'can_change_owner': can_change_owner(user_acl, thread), + 'can_add_participants': can_add_participants(user_acl, thread), }) @@ -171,23 +171,23 @@ def register_with(registry): registry.acl_annotator(Thread, add_acl_to_thread) -def allow_use_private_threads(user): - if user.is_anonymous: +def allow_use_private_threads(user_acl): + if user_acl["is_anonymous"]: raise PermissionDenied(_("You have to sign in to use private threads.")) - if not user.acl_cache['can_use_private_threads']: + if not user_acl['can_use_private_threads']: raise PermissionDenied(_("You can't use private threads.")) can_use_private_threads = return_boolean(allow_use_private_threads) -def allow_see_private_thread(user, target): - if user.acl_cache['can_moderate_private_threads']: +def allow_see_private_thread(user_acl, target): + if user_acl['can_moderate_private_threads']: can_see_reported = target.has_reported_posts else: can_see_reported = False - can_see_participating = user in [p.user for p in target.participants_list] + can_see_participating = user_acl["user_id"] in [p.user_id for p in target.participants_list] if not (can_see_participating or can_see_reported): raise Http404() @@ -196,8 +196,8 @@ def allow_see_private_thread(user, target): can_see_private_thread = return_boolean(allow_see_private_thread) -def allow_change_owner(user, target): - is_moderator = user.acl_cache['can_moderate_private_threads'] +def allow_change_owner(user_acl, target): + is_moderator = user_acl['can_moderate_private_threads'] is_owner = target.participant and target.participant.is_owner if not (is_owner or is_moderator): @@ -210,8 +210,8 @@ def allow_change_owner(user, target): can_change_owner = return_boolean(allow_change_owner) -def allow_add_participants(user, target): - is_moderator = user.acl_cache['can_moderate_private_threads'] +def allow_add_participants(user_acl, target): + is_moderator = user_acl['can_moderate_private_threads'] if not is_moderator: if not target.participant or not target.participant.is_owner: @@ -220,7 +220,7 @@ def allow_add_participants(user, target): if target.is_closed: raise PermissionDenied(_("Only moderators can add participants to closed threads.")) - max_participants = user.acl_cache['max_private_thread_participants'] + max_participants = user_acl['max_private_thread_participants'] current_participants = len(target.participants_list) - 1 if current_participants >= max_participants: @@ -230,11 +230,11 @@ def allow_add_participants(user, target): can_add_participants = return_boolean(allow_add_participants) -def allow_remove_participant(user, thread, target): - if user.acl_cache['can_moderate_private_threads']: +def allow_remove_participant(user_acl, thread, target): + if user_acl['can_moderate_private_threads']: return - if user == target: + if user_acl["user_id"] == target.id: return # we can always remove ourselves if thread.is_closed: @@ -247,7 +247,7 @@ def allow_remove_participant(user, thread, target): can_remove_participant = return_boolean(allow_remove_participant) -def allow_add_participant(user, target): +def allow_add_participant(user_acl, target): message_format = {'user': target.username} if not can_use_private_threads(target): @@ -255,10 +255,11 @@ def allow_add_participant(user, target): _("%(user)s can't participate in private threads.") % message_format ) - if user.acl_cache['can_add_everyone_to_private_threads']: + if user_acl['can_add_everyone_to_private_threads']: return - if user.acl_cache['can_be_blocked'] and target.is_blocking(user): + # FIXME: User.is_blocking() needs to work with ids + if user_acl['can_be_blocked'] and target.is_blocking(user_acl["user_id"]): raise PermissionDenied(_("%(user)s is blocking you.") % message_format) if target.can_be_messaged_by_nobody: @@ -266,7 +267,8 @@ def allow_add_participant(user, target): _("%(user)s is not allowing invitations to private threads.") % message_format ) - if target.can_be_messaged_by_followed and not target.is_following(user): + # FIXME: User.is_following() needs to work with ids + if target.can_be_messaged_by_followed and not target.is_following(user_acl["user_id"]): message = _("%(user)s limits invitations to private threads to followed users.") raise PermissionDenied(message % message_format) @@ -274,9 +276,9 @@ def allow_add_participant(user, target): can_add_participant = return_boolean(allow_add_participant) -def allow_message_user(user, target): - allow_use_private_threads(user) - allow_add_participant(user, target) +def allow_message_user(user_acl, target): + allow_use_private_threads(user_acl) + allow_add_participant(user_acl, target) can_message_user = return_boolean(allow_message_user) diff --git a/misago/threads/permissions/threads.py b/misago/threads/permissions/threads.py index a51ddf0335..e2785a3f2f 100644 --- a/misago/threads/permissions/threads.py +++ b/misago/threads/permissions/threads.py @@ -370,8 +370,8 @@ def build_category_acl(acl, category, categories_roles, key_name): return final_acl -def add_acl_to_category(user, category): - category_acl = user.acl_cache['categories'].get(category.pk, {}) +def add_acl_to_category(user_acl, category): + category_acl = user_acl['categories'].get(category.pk, {}) category.acl.update({ 'can_see_all_threads': 0, @@ -411,7 +411,7 @@ def add_acl_to_category(user, category): can_see_posts_likes=algebra.greater, ) - if user.is_authenticated: + if user_acl["is_authenticated"]: algebra.sum_acls( category.acl, acls=[category_acl], @@ -442,7 +442,7 @@ def add_acl_to_category(user, category): can_hide_events=algebra.greater, ) - if user.acl_cache['can_approve_content']: + if user_acl['can_approve_content']: category.acl.update({ 'require_threads_approval': 0, 'require_replies_approval': 0, @@ -452,23 +452,23 @@ def add_acl_to_category(user, category): category.acl['can_see_own_threads'] = not category.acl['can_see_all_threads'] -def add_acl_to_thread(user, thread): - category_acl = user.acl_cache['categories'].get(thread.category_id, {}) +def add_acl_to_thread(user_acl, thread): + category_acl = user_acl['categories'].get(thread.category_id, {}) thread.acl.update({ - 'can_reply': can_reply_thread(user, thread), - 'can_edit': can_edit_thread(user, thread), - 'can_pin': can_pin_thread(user, thread), + 'can_reply': can_reply_thread(user_acl, thread), + 'can_edit': can_edit_thread(user_acl, thread), + 'can_pin': can_pin_thread(user_acl, thread), 'can_pin_globally': False, - 'can_hide': can_hide_thread(user, thread), - 'can_unhide': can_unhide_thread(user, thread), - 'can_delete': can_delete_thread(user, thread), + 'can_hide': can_hide_thread(user_acl, thread), + 'can_unhide': can_unhide_thread(user_acl, thread), + 'can_delete': can_delete_thread(user_acl, thread), 'can_close': category_acl.get('can_close_threads', False), - 'can_move': can_move_thread(user, thread), - 'can_merge': can_merge_thread(user, thread), + 'can_move': can_move_thread(user_acl, thread), + 'can_merge': can_merge_thread(user_acl, thread), 'can_move_posts': category_acl.get('can_move_posts', False), 'can_merge_posts': category_acl.get('can_merge_posts', False), - 'can_approve': can_approve_thread(user, thread), + 'can_approve': can_approve_thread(user_acl, thread), 'can_see_reports': category_acl.get('can_see_reports', False), }) @@ -476,18 +476,18 @@ def add_acl_to_thread(user, thread): thread.acl['can_pin_globally'] = True -def add_acl_to_post(user, post): +def add_acl_to_post(user_acl, post): if post.is_event: - add_acl_to_event(user, post) + add_acl_to_event(user_acl, post) else: - add_acl_to_reply(user, post) + add_acl_to_reply(user_acl, post) -def add_acl_to_event(user, event): +def add_acl_to_event(user_acl, event): can_hide_events = 0 - if user.is_authenticated: - category_acl = user.acl_cache['categories'].get( + if user_acl["is_authenticated"]: + category_acl = user_acl['categories'].get( event.category_id, { 'can_hide_events': 0, } @@ -497,25 +497,25 @@ def add_acl_to_event(user, event): event.acl.update({ 'can_see_hidden': can_hide_events > 0, - 'can_hide': can_hide_event(user, event), - 'can_delete': can_delete_event(user, event), + 'can_hide': can_hide_event(user_acl, event), + 'can_delete': can_delete_event(user_acl, event), }) -def add_acl_to_reply(user, post): - category_acl = user.acl_cache['categories'].get(post.category_id, {}) +def add_acl_to_reply(user_acl, post): + category_acl = user_acl['categories'].get(post.category_id, {}) post.acl.update({ - 'can_reply': can_reply_thread(user, post.thread), - 'can_edit': can_edit_post(user, post), + 'can_reply': can_reply_thread(user_acl, post.thread), + 'can_edit': can_edit_post(user_acl, post), 'can_see_hidden': post.is_first_post or category_acl.get('can_hide_posts'), - 'can_unhide': can_unhide_post(user, post), - 'can_hide': can_hide_post(user, post), - 'can_delete': can_delete_post(user, post), - 'can_protect': can_protect_post(user, post), - 'can_approve': can_approve_post(user, post), - 'can_move': can_move_post(user, post), - 'can_merge': can_merge_post(user, post), + 'can_unhide': can_unhide_post(user_acl, post), + 'can_hide': can_hide_post(user_acl, post), + 'can_delete': can_delete_post(user_acl, post), + 'can_protect': can_protect_post(user_acl, post), + 'can_approve': can_approve_post(user_acl, post), + 'can_move': can_move_post(user_acl, post), + 'can_merge': can_merge_post(user_acl, post), 'can_report': category_acl.get('can_report_content', False), 'can_see_reports': category_acl.get('can_see_reports', False), 'can_see_likes': category_acl.get('can_see_posts_likes', 0), @@ -524,7 +524,7 @@ def add_acl_to_reply(user, post): if not post.acl['can_see_hidden']: post.acl['can_see_hidden'] = post.id == post.thread.first_post_id - if user.is_authenticated and post.acl['can_see_likes']: + if user_acl["is_authenticated"] and post.acl['can_see_likes']: post.acl['can_like'] = category_acl.get('can_like_posts', False) @@ -534,8 +534,8 @@ def register_with(registry): registry.acl_annotator(Post, add_acl_to_post) -def allow_see_thread(user, target): - category_acl = user.acl_cache['categories'].get( +def allow_see_thread(user_acl, target): + category_acl = user_acl['categories'].get( target.category_id, { 'can_see': False, 'can_browse': False, @@ -545,10 +545,10 @@ def allow_see_thread(user, target): if not (category_acl['can_see'] and category_acl['can_browse']): raise Http404() - if target.is_hidden and (user.is_anonymous or not category_acl['can_hide_threads']): + if target.is_hidden and (user_acl["is_anonymous"] or not category_acl['can_hide_threads']): raise Http404() - if user.is_anonymous or user.pk != target.starter_id: + if user_acl["is_anonymous"] or user_acl["user_id"] != target.starter_id: if not category_acl['can_see_all_threads']: raise Http404() @@ -559,11 +559,11 @@ def allow_see_thread(user, target): can_see_thread = return_boolean(allow_see_thread) -def allow_start_thread(user, target): - if user.is_anonymous: +def allow_start_thread(user_acl, target): + if user_acl["is_anonymous"]: raise PermissionDenied(_("You have to sign in to start threads.")) - category_acl = user.acl_cache['categories'].get( + category_acl = user_acl['categories'].get( target.pk, { 'can_start_threads': False, } @@ -581,11 +581,11 @@ def allow_start_thread(user, target): can_start_thread = return_boolean(allow_start_thread) -def allow_reply_thread(user, target): - if user.is_anonymous: +def allow_reply_thread(user_acl, target): + if user_acl["is_anonymous"]: raise PermissionDenied(_("You have to sign in to reply threads.")) - category_acl = user.acl_cache['categories'].get( + category_acl = user_acl['categories'].get( target.category_id, { 'can_reply_threads': False, } @@ -604,11 +604,11 @@ def allow_reply_thread(user, target): can_reply_thread = return_boolean(allow_reply_thread) -def allow_edit_thread(user, target): - if user.is_anonymous: +def allow_edit_thread(user_acl, target): + if user_acl["is_anonymous"]: raise PermissionDenied(_("You have to sign in to edit threads.")) - category_acl = user.acl_cache['categories'].get( + category_acl = user_acl['categories'].get( target.category_id, { 'can_edit_threads': False, } @@ -618,10 +618,10 @@ def allow_edit_thread(user, target): raise PermissionDenied(_("You can't edit threads in this category.")) if category_acl['can_edit_threads'] == 1: - if target.starter_id != user.pk: + if user_acl["user_id"] != target.starter_id: raise PermissionDenied(_("You can't edit other users threads in this category.")) - if not has_time_to_edit_thread(user, target): + if not has_time_to_edit_thread(user_acl, target): message = ngettext( "You can't edit threads that are older than %(minutes)s minute.", "You can't edit threads that are older than %(minutes)s minutes.", @@ -639,11 +639,11 @@ def allow_edit_thread(user, target): can_edit_thread = return_boolean(allow_edit_thread) -def allow_pin_thread(user, target): - if user.is_anonymous: +def allow_pin_thread(user_acl, target): + if user_acl["is_anonymous"]: raise PermissionDenied(_("You have to sign in to change threads weights.")) - category_acl = user.acl_cache['categories'].get( + category_acl = user_acl['categories'].get( target.category_id, { 'can_pin_threads': 0, } @@ -662,11 +662,11 @@ def allow_pin_thread(user, target): can_pin_thread = return_boolean(allow_pin_thread) -def allow_unhide_thread(user, target): - if user.is_anonymous: +def allow_unhide_thread(user_acl, target): + if user_acl["is_anonymous"]: raise PermissionDenied(_("You have to sign in to hide threads.")) - category_acl = user.acl_cache['categories'].get( + category_acl = user_acl['categories'].get( target.category_id, { 'can_close_threads': False, } @@ -682,11 +682,11 @@ def allow_unhide_thread(user, target): can_unhide_thread = return_boolean(allow_unhide_thread) -def allow_hide_thread(user, target): - if user.is_anonymous: +def allow_hide_thread(user_acl, target): + if user_acl["is_anonymous"]: raise PermissionDenied(_("You have to sign in to hide threads.")) - category_acl = user.acl_cache['categories'].get( + category_acl = user_acl['categories'].get( target.category_id, { 'can_hide_threads': 0, 'can_hide_own_threads': 0, @@ -697,10 +697,10 @@ def allow_hide_thread(user, target): raise PermissionDenied(_("You can't hide threads in this category.")) if not category_acl['can_hide_threads'] and category_acl['can_hide_own_threads']: - if user.id != target.starter_id: + if user_acl["user_id"] != target.starter_id: raise PermissionDenied(_("You can't hide other users theads in this category.")) - if not has_time_to_edit_thread(user, target): + if not has_time_to_edit_thread(user_acl, target): message = ngettext( "You can't hide threads that are older than %(minutes)s minute.", "You can't hide threads that are older than %(minutes)s minutes.", @@ -718,11 +718,11 @@ def allow_hide_thread(user, target): can_hide_thread = return_boolean(allow_hide_thread) -def allow_delete_thread(user, target): - if user.is_anonymous: +def allow_delete_thread(user_acl, target): + if user_acl["is_anonymous"]: raise PermissionDenied(_("You have to sign in to delete threads.")) - category_acl = user.acl_cache['categories'].get( + category_acl = user_acl['categories'].get( target.category_id, { 'can_hide_threads': 0, 'can_hide_own_threads': 0, @@ -733,10 +733,10 @@ def allow_delete_thread(user, target): raise PermissionDenied(_("You can't delete threads in this category.")) if category_acl['can_hide_threads'] != 2 and category_acl['can_hide_own_threads'] == 2: - if user.id != target.starter_id: + if user_acl["user_id"] != target.starter_id: raise PermissionDenied(_("You can't delete other users theads in this category.")) - if not has_time_to_edit_thread(user, target): + if not has_time_to_edit_thread(user_acl, target): message = ngettext( "You can't delete threads that are older than %(minutes)s minute.", "You can't delete threads that are older than %(minutes)s minutes.", @@ -754,11 +754,11 @@ def allow_delete_thread(user, target): can_delete_thread = return_boolean(allow_delete_thread) -def allow_move_thread(user, target): - if user.is_anonymous: +def allow_move_thread(user_acl, target): + if user_acl["is_anonymous"]: raise PermissionDenied(_("You have to sign in to move threads.")) - category_acl = user.acl_cache['categories'].get( + category_acl = user_acl['categories'].get( target.category_id, { 'can_move_threads': 0, } @@ -777,11 +777,11 @@ def allow_move_thread(user, target): can_move_thread = return_boolean(allow_move_thread) -def allow_merge_thread(user, target, otherthread=False): - if user.is_anonymous: +def allow_merge_thread(user_acl, target, otherthread=False): + if user_acl["is_anonymous"]: raise PermissionDenied(_("You have to sign in to merge threads.")) - category_acl = user.acl_cache['categories'].get( + category_acl = user_acl['categories'].get( target.category_id, { 'can_merge_threads': 0, } @@ -806,11 +806,11 @@ def allow_merge_thread(user, target, otherthread=False): can_merge_thread = return_boolean(allow_merge_thread) -def allow_approve_thread(user, target): - if user.is_anonymous: +def allow_approve_thread(user_acl, target): + if user_acl["is_anonymous"]: raise PermissionDenied(_("You have to sign in to approve threads.")) - category_acl = user.acl_cache['categories'].get( + category_acl = user_acl['categories'].get( target.category_id, { 'can_approve_content': 0, } @@ -829,8 +829,8 @@ def allow_approve_thread(user, target): can_approve_thread = return_boolean(allow_approve_thread) -def allow_see_post(user, target): - category_acl = user.acl_cache['categories'].get( +def allow_see_post(user_acl, target): + category_acl = user_acl['categories'].get( target.category_id, { 'can_approve_content': False, 'can_hide_events': False, @@ -838,10 +838,10 @@ def allow_see_post(user, target): ) if not target.is_event and target.is_unapproved: - if user.is_anonymous: + if user_acl["is_anonymous"]: raise Http404() - if not category_acl['can_approve_content'] and user.id != target.poster_id: + if not category_acl['can_approve_content'] and user_acl["user_id"] != target.poster_id: raise Http404() if target.is_event and target.is_hidden and not category_acl['can_hide_events']: @@ -851,14 +851,14 @@ def allow_see_post(user, target): can_see_post = return_boolean(allow_see_post) -def allow_edit_post(user, target): - if user.is_anonymous: +def allow_edit_post(user_acl, target): + if user_acl["is_anonymous"]: raise PermissionDenied(_("You have to sign in to edit posts.")) if target.is_event: raise PermissionDenied(_("Events can't be edited.")) - category_acl = user.acl_cache['categories'].get(target.category_id, {'can_edit_posts': False}) + category_acl = user_acl['categories'].get(target.category_id, {'can_edit_posts': False}) if not category_acl['can_edit_posts']: raise PermissionDenied(_("You can't edit posts in this category.")) @@ -867,13 +867,13 @@ def allow_edit_post(user, target): raise PermissionDenied(_("This post is hidden, you can't edit it.")) if category_acl['can_edit_posts'] == 1: - if target.poster_id != user.pk: + if target.poster_id != user_acl["user_id"]: raise PermissionDenied(_("You can't edit other users posts in this category.")) if target.is_protected and not category_acl['can_protect_posts']: raise PermissionDenied(_("This post is protected. You can't edit it.")) - if not has_time_to_edit_post(user, target): + if not has_time_to_edit_post(user_acl, target): message = ngettext( "You can't edit posts that are older than %(minutes)s minute.", "You can't edit posts that are older than %(minutes)s minutes.", @@ -891,11 +891,11 @@ def allow_edit_post(user, target): can_edit_post = return_boolean(allow_edit_post) -def allow_unhide_post(user, target): - if user.is_anonymous: +def allow_unhide_post(user_acl, target): + if user_acl["is_anonymous"]: raise PermissionDenied(_("You have to sign in to reveal posts.")) - category_acl = user.acl_cache['categories'].get( + category_acl = user_acl['categories'].get( target.category_id, { 'can_hide_posts': 0, 'can_hide_own_posts': 0, @@ -906,13 +906,13 @@ def allow_unhide_post(user, target): if not category_acl['can_hide_own_posts']: raise PermissionDenied(_("You can't reveal posts in this category.")) - if user.id != target.poster_id: + if user_acl["user_id"] != target.poster_id: raise PermissionDenied(_("You can't reveal other users posts in this category.")) if target.is_protected and not category_acl['can_protect_posts']: raise PermissionDenied(_("This post is protected. You can't reveal it.")) - if not has_time_to_edit_post(user, target): + if not has_time_to_edit_post(user_acl, target): message = ngettext( "You can't reveal posts that are older than %(minutes)s minute.", "You can't reveal posts that are older than %(minutes)s minutes.", @@ -933,11 +933,11 @@ def allow_unhide_post(user, target): can_unhide_post = return_boolean(allow_unhide_post) -def allow_hide_post(user, target): - if user.is_anonymous: +def allow_hide_post(user_acl, target): + if user_acl["is_anonymous"]: raise PermissionDenied(_("You have to sign in to hide posts.")) - category_acl = user.acl_cache['categories'].get( + category_acl = user_acl['categories'].get( target.category_id, { 'can_hide_posts': 0, 'can_hide_own_posts': 0, @@ -948,13 +948,13 @@ def allow_hide_post(user, target): if not category_acl['can_hide_own_posts']: raise PermissionDenied(_("You can't hide posts in this category.")) - if user.id != target.poster_id: + if user_acl["user_id"] != target.poster_id: raise PermissionDenied(_("You can't hide other users posts in this category.")) if target.is_protected and not category_acl['can_protect_posts']: raise PermissionDenied(_("This post is protected. You can't hide it.")) - if not has_time_to_edit_post(user, target): + if not has_time_to_edit_post(user_acl, target): message = ngettext( "You can't hide posts that are older than %(minutes)s minute.", "You can't hide posts that are older than %(minutes)s minutes.", @@ -975,11 +975,11 @@ def allow_hide_post(user, target): can_hide_post = return_boolean(allow_hide_post) -def allow_delete_post(user, target): - if user.is_anonymous: +def allow_delete_post(user_acl, target): + if user_acl["is_anonymous"]: raise PermissionDenied(_("You have to sign in to delete posts.")) - category_acl = user.acl_cache['categories'].get( + category_acl = user_acl['categories'].get( target.category_id, { 'can_hide_posts': 0, 'can_hide_own_posts': 0, @@ -990,13 +990,13 @@ def allow_delete_post(user, target): if category_acl['can_hide_own_posts'] != 2: raise PermissionDenied(_("You can't delete posts in this category.")) - if user.id != target.poster_id: + if user_acl["user_id"] != target.poster_id: raise PermissionDenied(_("You can't delete other users posts in this category.")) if target.is_protected and not category_acl['can_protect_posts']: raise PermissionDenied(_("This post is protected. You can't delete it.")) - if not has_time_to_edit_post(user, target): + if not has_time_to_edit_post(user_acl, target): message = ngettext( "You can't delete posts that are older than %(minutes)s minute.", "You can't delete posts that are older than %(minutes)s minutes.", @@ -1017,28 +1017,28 @@ def allow_delete_post(user, target): can_delete_post = return_boolean(allow_delete_post) -def allow_protect_post(user, target): - if user.is_anonymous: +def allow_protect_post(user_acl, target): + if user_acl["is_anonymous"]: raise PermissionDenied(_("You have to sign in to protect posts.")) - category_acl = user.acl_cache['categories'].get( + category_acl = user_acl['categories'].get( target.category_id, {'can_protect_posts': False} ) if not category_acl['can_protect_posts']: raise PermissionDenied(_("You can't protect posts in this category.")) - if not can_edit_post(user, target): + if not can_edit_post(user_acl, target): raise PermissionDenied(_("You can't protect posts you can't edit.")) can_protect_post = return_boolean(allow_protect_post) -def allow_approve_post(user, target): - if user.is_anonymous: +def allow_approve_post(user_acl, target): + if user_acl["is_anonymous"]: raise PermissionDenied(_("You have to sign in to approve posts.")) - category_acl = user.acl_cache['categories'].get( + category_acl = user_acl['categories'].get( target.category_id, {'can_approve_content': False} ) @@ -1059,11 +1059,11 @@ def allow_approve_post(user, target): can_approve_post = return_boolean(allow_approve_post) -def allow_move_post(user, target): - if user.is_anonymous: +def allow_move_post(user_acl, target): + if user_acl["is_anonymous"]: raise PermissionDenied(_("You have to sign in to move posts.")) - category_acl = user.acl_cache['categories'].get( + category_acl = user_acl['categories'].get( target.category_id, { 'can_move_posts': False, } @@ -1088,11 +1088,11 @@ def allow_move_post(user, target): can_move_post = return_boolean(allow_move_post) -def allow_merge_post(user, target): - if user.is_anonymous: +def allow_merge_post(user_acl, target): + if user_acl["is_anonymous"]: raise PermissionDenied(_("You have to sign in to merge posts.")) - category_acl = user.acl_cache['categories'].get( + category_acl = user_acl['categories'].get( target.category_id, { 'can_merge_posts': False, } @@ -1115,11 +1115,11 @@ def allow_merge_post(user, target): can_merge_post = return_boolean(allow_merge_post) -def allow_split_post(user, target): - if user.is_anonymous: +def allow_split_post(user_acl, target): + if user_acl["is_anonymous"]: raise PermissionDenied(_("You have to sign in to split posts.")) - category_acl = user.acl_cache['categories'].get( + category_acl = user_acl['categories'].get( target.category_id, { 'can_move_posts': False, } @@ -1143,11 +1143,11 @@ def allow_split_post(user, target): can_split_post = return_boolean(allow_split_post) -def allow_unhide_event(user, target): - if user.is_anonymous: +def allow_unhide_event(user_acl, target): + if user_acl["is_anonymous"]: raise PermissionDenied(_("You have to sign in to reveal events.")) - category_acl = user.acl_cache['categories'].get( + category_acl = user_acl['categories'].get( target.category_id, { 'can_hide_events': 0, } @@ -1166,11 +1166,11 @@ def allow_unhide_event(user, target): can_unhide_event = return_boolean(allow_unhide_event) -def allow_hide_event(user, target): - if user.is_anonymous: +def allow_hide_event(user_acl, target): + if user_acl["is_anonymous"]: raise PermissionDenied(_("You have to sign in to hide events.")) - category_acl = user.acl_cache['categories'].get( + category_acl = user_acl['categories'].get( target.category_id, { 'can_hide_events': 0, } @@ -1189,11 +1189,11 @@ def allow_hide_event(user, target): can_hide_event = return_boolean(allow_hide_event) -def allow_delete_event(user, target): - if user.is_anonymous: +def allow_delete_event(user_acl, target): + if user_acl["is_anonymous"]: raise PermissionDenied(_("You have to sign in to delete events.")) - category_acl = user.acl_cache['categories'].get( + category_acl = user_acl['categories'].get( target.category_id, { 'can_hide_events': 0, } @@ -1212,18 +1212,18 @@ def allow_delete_event(user, target): can_delete_event = return_boolean(allow_delete_event) -def can_change_owned_thread(user, target): - if user.is_anonymous or user.pk != target.starter_id: +def can_change_owned_thread(user_acl, target): + if user_acl["is_anonymous"] or user_acl["user_id"] != target.starter_id: return False if target.category.is_closed or target.is_closed: return False - return has_time_to_edit_thread(user, target) + return has_time_to_edit_thread(user_acl, target) -def has_time_to_edit_thread(user, target): - edit_time = user.acl_cache['categories'].get(target.category_id, {}).get('thread_edit_time', 0) +def has_time_to_edit_thread(user_acl, target): + edit_time = user_acl['categories'].get(target.category_id, {}).get('thread_edit_time', 0) if edit_time: diff = timezone.now() - target.started_on diff_minutes = int(diff.total_seconds() / 60) @@ -1232,8 +1232,8 @@ def has_time_to_edit_thread(user, target): return True -def has_time_to_edit_post(user, target): - edit_time = user.acl_cache['categories'].get(target.category_id, {}).get('post_edit_time', 0) +def has_time_to_edit_post(user_acl, target): + edit_time = user_acl['categories'].get(target.category_id, {}).get('post_edit_time', 0) if edit_time: diff = timezone.now() - target.posted_on diff_minutes = int(diff.total_seconds() / 60) @@ -1242,7 +1242,7 @@ def has_time_to_edit_post(user, target): return True -def exclude_invisible_threads(user, categories, queryset): +def exclude_invisible_threads(user_acl, categories, queryset): show_all = [] show_accepted_visible = [] show_accepted = [] @@ -1251,7 +1251,7 @@ def exclude_invisible_threads(user, categories, queryset): show_owned_visible = [] for category in categories: - add_acl(user, category) + add_acl(user_acl, category) if not (category.acl['can_see'] and category.acl['can_browse']): continue @@ -1262,7 +1262,7 @@ def exclude_invisible_threads(user, categories, queryset): if can_mod and can_hide: show_all.append(category) - elif user.is_authenticated: + elif user_acl["is_authenticated"]: if not can_mod and not can_hide: show_accepted_visible.append(category) elif not can_mod: @@ -1271,7 +1271,7 @@ def exclude_invisible_threads(user, categories, queryset): show_visible.append(category) else: show_accepted_visible.append(category) - elif user.is_authenticated: + elif user_acl["is_authenticated"]: if can_hide: show_owned.append(category) else: @@ -1282,9 +1282,9 @@ def exclude_invisible_threads(user, categories, queryset): conditions = Q(category__in=show_all) if show_accepted_visible: - if user.is_authenticated: + if user_acl["is_authenticated"]: condition = Q( - Q(starter=user) | Q(is_unapproved=False), + Q(starter_id=user_acl["user_id"]) | Q(is_unapproved=False), category__in=show_accepted_visible, is_hidden=False, ) @@ -1302,7 +1302,7 @@ def exclude_invisible_threads(user, categories, queryset): if show_accepted: condition = Q( - Q(starter=user) | Q(is_unapproved=False), + Q(starter_id=user_acl["user_id"]) | Q(is_unapproved=False), category__in=show_accepted, ) @@ -1320,7 +1320,7 @@ def exclude_invisible_threads(user, categories, queryset): conditions = condition if show_owned: - condition = Q(category__in=show_owned, starter=user) + condition = Q(category__in=show_owned, starter_id=user_acl["user_id"]) if conditions: conditions = conditions | condition @@ -1330,7 +1330,7 @@ def exclude_invisible_threads(user, categories, queryset): if show_owned_visible: condition = Q( category__in=show_owned_visible, - starter=user, + starter_id=user_acl["user_id"], is_hidden=False, ) @@ -1345,14 +1345,14 @@ def exclude_invisible_threads(user, categories, queryset): return Thread.objects.none() -def exclude_invisible_posts(user, categories, queryset): +def exclude_invisible_posts(user_acl, categories, queryset): if hasattr(categories, '__iter__'): - return exclude_invisible_posts_in_categories(user, categories, queryset) + return exclude_invisible_posts_in_categories(user_acl, categories, queryset) else: - return exclude_invisible_posts_in_category(user, categories, queryset) + return exclude_invisible_posts_in_category(user_acl, categories, queryset) -def exclude_invisible_posts_in_categories(user, categories, queryset): +def exclude_invisible_posts_in_categories(user_acl, categories, queryset): show_all = [] show_approved = [] show_approved_owned = [] @@ -1360,12 +1360,12 @@ def exclude_invisible_posts_in_categories(user, categories, queryset): hide_invisible_events = [] for category in categories: - add_acl(user, category) + add_acl(user_acl, category) if category.acl['can_approve_content']: show_all.append(category.pk) else: - if user.is_authenticated: + if user_acl["is_authenticated"]: show_approved_owned.append(category.pk) else: show_approved.append(category.pk) @@ -1390,7 +1390,7 @@ def exclude_invisible_posts_in_categories(user, categories, queryset): if show_approved_owned: condition = Q( - Q(poster=user) | Q(is_unapproved=False), + Q(poster_id=user_acl["user_id"]) | Q(is_unapproved=False), category__in=show_approved_owned, ) @@ -1412,12 +1412,12 @@ def exclude_invisible_posts_in_categories(user, categories, queryset): return Post.objects.none() -def exclude_invisible_posts_in_category(user, category, queryset): - add_acl(user, category) +def exclude_invisible_posts_in_category(user_acl, category, queryset): + add_acl(user_acl, category) if not category.acl['can_approve_content']: - if user.is_authenticated: - queryset = queryset.filter(Q(is_unapproved=False) | Q(poster=user)) + if user_acl["is_authenticated"]: + queryset = queryset.filter(Q(is_unapproved=False) | Q(poster_id=user_acl["user_id"])) else: queryset = queryset.exclude(is_unapproved=True) diff --git a/misago/threads/search.py b/misago/threads/search.py index cb1df4a213..1d0dcb9f48 100644 --- a/misago/threads/search.py +++ b/misago/threads/search.py @@ -27,7 +27,7 @@ def search(self, query, page=1): if len(query) > 2: visible_threads = exclude_invisible_threads( - self.request.user, threads_categories, Thread.objects + self.request.user_acl, threads_categories, Thread.objects ) results = search_threads(self.request, query, visible_threads) else: diff --git a/misago/threads/viewmodels/category.py b/misago/threads/viewmodels/category.py index d4cbcaed2c..5ca0d74641 100644 --- a/misago/threads/viewmodels/category.py +++ b/misago/threads/viewmodels/category.py @@ -15,7 +15,7 @@ class ViewModel(BaseViewModel): def __init__(self, request, **kwargs): self._categories = self.get_categories(request) - add_acl(request.user, self._categories) + add_acl(request.user_acl, self._categories) self._model = self.get_category(request, self._categories, **kwargs) @@ -51,7 +51,7 @@ class ThreadsRootCategory(ViewModel): def get_categories(self, request): return [Category.objects.root_category()] + list( Category.objects.all_categories().filter( - id__in=request.user.acl_cache['visible_categories'], + id__in=request.user_acl['visible_categories'], ).select_related('parent') ) diff --git a/misago/threads/viewmodels/post.py b/misago/threads/viewmodels/post.py index de3fcca0cd..26f42fe118 100644 --- a/misago/threads/viewmodels/post.py +++ b/misago/threads/viewmodels/post.py @@ -12,7 +12,7 @@ class ViewModel(BaseViewModel): def __init__(self, request, thread, pk): model = self.get_post(request, thread, pk) - add_acl(request.user, model) + add_acl(request.user_acl, model) self._model = model diff --git a/misago/threads/viewmodels/posts.py b/misago/threads/viewmodels/posts.py index 4dcf11d723..763a4c7b4b 100644 --- a/misago/threads/viewmodels/posts.py +++ b/misago/threads/viewmodels/posts.py @@ -61,7 +61,7 @@ def __init__(self, request, thread, page): posts.sort(key=lambda p: p.pk) # make posts and events ACL and reads aware - add_acl(request.user, posts) + add_acl(request.user_acl, posts) make_read_aware(request.user, posts) self._user = request.user diff --git a/misago/threads/viewmodels/thread.py b/misago/threads/viewmodels/thread.py index 53b148d251..aaa2d0da0e 100644 --- a/misago/threads/viewmodels/thread.py +++ b/misago/threads/viewmodels/thread.py @@ -44,8 +44,8 @@ def __init__( if path_aware: model.path = self.get_thread_path(model.category) - add_acl(request.user, model.category) - add_acl(request.user, model) + add_acl(request.user_acl, model.category) + add_acl(request.user_acl, model) if read_aware: make_read_aware(request.user, model) @@ -56,7 +56,7 @@ def __init__( try: self._poll = model.poll - add_acl(request.user, self._poll) + add_acl(request.user_acl, self._poll) if poll_votes_aware: self._poll.make_choices_votes_aware(request.user) @@ -109,7 +109,7 @@ def get_thread(self, request, pk, slug=None): category__tree_id=trees_map.get_tree_id_for_root(THREADS_ROOT_NAME), ) - allow_see_thread(request.user, thread) + allow_see_thread(request.user_acl, thread) if slug: validate_slug(thread, slug) return thread @@ -123,7 +123,7 @@ def get_frontend_context(self): class PrivateThread(ViewModel): def get_thread(self, request, pk, slug=None): - allow_use_private_threads(request.user) + allow_use_private_threads(request.user_acl) thread = get_object_or_404( Thread.objects.select_related(*BASE_RELATIONS), @@ -132,7 +132,7 @@ def get_thread(self, request, pk, slug=None): ) make_participants_aware(request.user, thread) - allow_see_private_thread(request.user, thread) + allow_see_private_thread(request.user_acl, thread) if slug: validate_slug(thread, slug) diff --git a/misago/threads/viewmodels/threads.py b/misago/threads/viewmodels/threads.py index e96df66994..694da97be0 100644 --- a/misago/threads/viewmodels/threads.py +++ b/misago/threads/viewmodels/threads.py @@ -19,7 +19,6 @@ from misago.threads.subscriptions import make_subscription_aware from misago.threads.utils import add_categories_to_items - __all__ = ['ForumThreads', 'PrivateThreads', 'filter_read_threads_queryset'] LISTS_NAMES = { @@ -69,7 +68,7 @@ def __init__(self, request, category, list_type, page): threads = list(pinned_threads) + list(list_page.object_list) add_categories_to_items(category_model, category.categories, threads) - add_acl(request.user, threads) + add_acl(request.user_acl, threads) make_subscription_aware(request.user, threads) if list_type in ('new', 'unread'): @@ -96,7 +95,7 @@ def allow_see_list(self, request, category, list_type): if list_type in LIST_DENIED_MESSAGES: raise PermissionDenied(LIST_DENIED_MESSAGES[list_type]) else: - has_permission = request.user.acl_cache['can_see_unapproved_content_lists'] + has_permission = request.user_acl['can_see_unapproved_content_lists'] if list_type == 'unapproved' and not has_permission: raise PermissionDenied( _("You don't have permission to see unapproved content lists.") @@ -107,7 +106,7 @@ def get_list_name(self, list_type): def get_base_queryset(self, request, threads_categories, list_type): return get_threads_queryset( - request.user, + request, threads_categories, list_type, ).order_by('-last_post_id') @@ -169,7 +168,7 @@ def get_base_queryset(self, request, threads_categories, list_type): # limit queryset to threads we are participant of participated_threads = request.user.threadparticipant_set.values('thread_id') - if request.user.acl_cache['can_moderate_private_threads']: + if request.user_acl['can_moderate_private_threads']: queryset = queryset.filter(Q(id__in=participated_threads) | Q(has_reported_posts=True)) else: queryset = queryset.filter(id__in=participated_threads) @@ -183,13 +182,13 @@ def filter_threads(self, request, threads): make_participants_aware(request.user, threads) -def get_threads_queryset(user, categories, list_type): - queryset = exclude_invisible_threads(user, categories, Thread.objects) +def get_threads_queryset(request, categories, list_type): + queryset = exclude_invisible_threads(request.user_acl, categories, Thread.objects) if list_type == 'all': return queryset else: - return filter_threads_queryset(user, categories, list_type, queryset) + return filter_threads_queryset(request.user, categories, list_type, queryset) def filter_threads_queryset(user, categories, list_type, queryset): diff --git a/misago/threads/views/attachment.py b/misago/threads/views/attachment.py index 06d7f18383..cb0c95914d 100644 --- a/misago/threads/views/attachment.py +++ b/misago/threads/views/attachment.py @@ -50,7 +50,7 @@ def allow_file_download(request, attachment): if not is_authenticated or request.user.id != attachment.uploader_id: if not attachment.post_id: raise Http404() - if not request.user.acl_cache['can_download_other_users_attachments']: + if not request.user_acl['can_download_other_users_attachments']: raise PermissionDenied() allowed_roles = set(r.pk for r in attachment.filetype.limit_downloads_to.all()) diff --git a/misago/users/api/userendpoints/signature.py b/misago/users/api/userendpoints/signature.py index 54b6fca7d9..50e0afe153 100644 --- a/misago/users/api/userendpoints/signature.py +++ b/misago/users/api/userendpoints/signature.py @@ -11,11 +11,11 @@ def signature_endpoint(request): - user = request.user - - if not user.acl_cache['can_have_signature']: + if not request.user_acl['can_have_signature']: raise PermissionDenied(_("You don't have permission to change signature.")) + user = request.user + if user.is_signature_locked: if user.signature_lock_user_message: reason = format_plaintext_for_html(user.signature_lock_user_message) diff --git a/misago/users/api/usernamechanges.py b/misago/users/api/usernamechanges.py index e9f62bd19a..27f22e27df 100644 --- a/misago/users/api/usernamechanges.py +++ b/misago/users/api/usernamechanges.py @@ -26,7 +26,7 @@ def has_permission(self, request, view): if user_pk == request.user.pk: return True - elif not request.user.acl_cache.get('can_see_users_name_history'): + elif not request.user_acl.get('can_see_users_name_history'): raise PermissionDenied(_("You don't have permission to see other users name history.")) return True diff --git a/misago/users/api/users.py b/misago/users/api/users.py index 1436b4819f..f65fde9b66 100644 --- a/misago/users/api/users.py +++ b/misago/users/api/users.py @@ -75,7 +75,7 @@ def get_user(self, request, pk): return user def list(self, request): - allow_browse_users_list(request.user) + allow_browse_users_list(request.user_acl) return list_endpoint(request) def create(self, request): @@ -84,10 +84,10 @@ def create(self, request): def retrieve(self, request, pk=None): profile = self.get_user(request, pk) - add_acl(request.user, profile) + add_acl(request.user_acl, profile) profile.status = get_user_status(request, profile) - serializer = UserProfileSerializer(profile, context={'user': request.user}) + serializer = UserProfileSerializer(profile, context={'request': request}) profile_json = serializer.data if not profile.is_active: @@ -153,7 +153,7 @@ def details(self, request, pk=None): @detail_route(methods=['get', 'post']) def edit_details(self, request, pk=None): profile = self.get_user(request, pk) - allow_edit_profile_details(request.user, profile) + allow_edit_profile_details(request.user_acl, profile) return edit_details_endpoint(request, profile) @detail_route(methods=['post']) @@ -169,7 +169,7 @@ def delete_own_account(self, request, pk=None): @detail_route(methods=['post']) def follow(self, request, pk=None): profile = self.get_user(request, pk) - allow_follow_user(request.user, profile) + allow_follow_user(request.user_acl, profile) profile_followers = profile.followers @@ -197,7 +197,7 @@ def follow(self, request, pk=None): @detail_route() def ban(self, request, pk=None): profile = self.get_user(request, pk) - allow_see_ban_details(request.user, profile) + allow_see_ban_details(request.user_acl, profile) ban = get_user_ban(profile, request.cache_versions) if ban: @@ -208,14 +208,14 @@ def ban(self, request, pk=None): @detail_route(methods=['get', 'post']) def moderate_avatar(self, request, pk=None): profile = self.get_user(request, pk) - allow_moderate_avatar(request.user, profile) + allow_moderate_avatar(request.user_acl, profile) return moderate_avatar_endpoint(request, profile) @detail_route(methods=['get', 'post']) def moderate_username(self, request, pk=None): profile = self.get_user(request, pk) - allow_rename_user(request.user, profile) + allow_rename_user(request.user_acl, profile) return moderate_username_endpoint(request, profile) @@ -238,7 +238,7 @@ def request_data_download(self, request, pk=None): @detail_route(methods=['get', 'post']) def delete(self, request, pk=None): profile = self.get_user(request, pk) - allow_delete_user(request.user, profile) + allow_delete_user(request.user_acl, profile) if request.method == 'POST': with transaction.atomic(): diff --git a/misago/users/apps.py b/misago/users/apps.py index da416470c9..c4e908e0be 100644 --- a/misago/users/apps.py +++ b/misago/users/apps.py @@ -71,14 +71,14 @@ def register_default_user_profile_pages(self): def can_see_names_history(request, profile): if request.user.is_authenticated: is_account_owner = profile.pk == request.user.pk - has_permission = request.user.acl_cache['can_see_users_name_history'] + has_permission = request.user_acl['can_see_users_name_history'] return is_account_owner or has_permission else: return False def can_see_ban_details(request, profile): if request.user.is_authenticated: - if request.user.acl_cache['can_see_ban_details']: + if request.user_acl['can_see_ban_details']: from .bans import get_user_ban return bool(get_user_ban(profile, request.cache_versions)) else: diff --git a/misago/users/models/user.py b/misago/users/models/user.py index de831833e7..a15cb8cd69 100644 --- a/misago/users/models/user.py +++ b/misago/users/models/user.py @@ -453,16 +453,26 @@ def email_user(self, subject, message, from_email=None, **kwargs): """sends an email to this user (for compat with Django)""" send_mail(subject, message, from_email, [self.email], **kwargs) - def is_following(self, user): + def is_following(self, user_or_id): try: - self.follows.get(pk=user.pk) + user_id = user_or_id.id + except AttributeError: + user_id = user_or_id + + try: + self.follows.get(id=user_id) return True except User.DoesNotExist: return False - def is_blocking(self, user): + def is_blocking(self, user_or_id): + try: + user_id = user_or_id.id + except AttributeError: + user_id = user_or_id + try: - self.blocks.get(pk=user.pk) + self.blocks.get(id=user_id) return True except User.DoesNotExist: return False diff --git a/misago/users/online/utils.py b/misago/users/online/utils.py index 7ade237a6a..31a0a4b22a 100644 --- a/misago/users/online/utils.py +++ b/misago/users/online/utils.py @@ -48,7 +48,7 @@ def get_user_status(request, user): try: online_tracker = user.online_tracker - is_hidden = user.is_hiding_presence and not request.user.acl_cache['can_see_hidden_users'] + is_hidden = user.is_hiding_presence and not request.user_acl['can_see_hidden_users'] if online_tracker and not is_hidden: if online_tracker.last_click >= timezone.now() - ACTIVITY_CUTOFF: @@ -58,7 +58,7 @@ def get_user_status(request, user): pass if user_status['is_hidden']: - if request.user.acl_cache['can_see_hidden_users']: + if request.user_acl['can_see_hidden_users']: user_status['is_hidden'] = False if user_status['is_online']: user_status['is_online_hidden'] = True diff --git a/misago/users/permissions/decorators.py b/misago/users/permissions/decorators.py index 06047bf214..299a6c7552 100644 --- a/misago/users/permissions/decorators.py +++ b/misago/users/permissions/decorators.py @@ -9,22 +9,24 @@ def authenticated_only(f): - def perm_decorator(user, target): - if user.is_authenticated: - return f(user, target) - else: - messsage = _("You have to sig in to perform this action.") - raise PermissionDenied(messsage) + def perm_decorator(user_acl, target): + if user_acl["is_authenticated"]: + return f(user_acl, target) + else: + raise PermissionDenied( + _("You have to sig in to perform this action.") + ) return perm_decorator def anonymous_only(f): - def perm_decorator(user, target): - if user.is_anonymous: - return f(user, target) + def perm_decorator(user_acl, target): + if user_acl["is_anonymous"]: + return f(user_acl, target) else: - messsage = _("Only guests can perform this action.") - raise PermissionDenied(messsage) + raise PermissionDenied( + _("Only guests can perform this action.") + ) return perm_decorator diff --git a/misago/users/permissions/delete.py b/misago/users/permissions/delete.py index eed6b0da5e..724b7c0001 100644 --- a/misago/users/permissions/delete.py +++ b/misago/users/permissions/delete.py @@ -61,8 +61,8 @@ def build_acl(acl, roles, key_name): ) -def add_acl_to_user(user, target): - target.acl['can_delete'] = can_delete_user(user, target) +def add_acl_to_user(user_acl, target): + target.acl['can_delete'] = can_delete_user(user_acl, target) if target.acl['can_delete']: target.acl['can_moderate'] = True @@ -71,13 +71,13 @@ def register_with(registry): registry.acl_annotator(get_user_model(), add_acl_to_user) -def allow_delete_user(user, target): - newer_than = user.acl_cache['can_delete_users_newer_than'] - less_posts_than = user.acl_cache['can_delete_users_with_less_posts_than'] +def allow_delete_user(user_acl, target): + newer_than = user_acl['can_delete_users_newer_than'] + less_posts_than = user_acl['can_delete_users_with_less_posts_than'] if not newer_than and not less_posts_than: raise PermissionDenied(_("You can't delete users.")) - if user.pk == target.pk: + if user_acl["user_id"] == target.id: raise PermissionDenied(_("You can't delete your account.")) if target.is_staff or target.is_superuser: raise PermissionDenied(_("You can't delete administrators.")) @@ -106,7 +106,7 @@ def allow_delete_user(user, target): def allow_delete_own_account(user, target): if not settings.MISAGO_ENABLE_DELETE_OWN_ACCOUNT and not user.is_deleting_account: raise PermissionDenied(_("You can't delete your account.")) - if user.pk != target.pk: + if user.id != target.id: raise PermissionDenied(_("You can't delete other users accounts.")) if user.is_staff or user.is_superuser: raise PermissionDenied( diff --git a/misago/users/permissions/moderation.py b/misago/users/permissions/moderation.py index 08d885c6dc..48d0e2a3a7 100644 --- a/misago/users/permissions/moderation.py +++ b/misago/users/permissions/moderation.py @@ -88,14 +88,14 @@ def build_acl(acl, roles, key_name): ) -def add_acl_to_user(user, target): - target.acl['can_rename'] = can_rename_user(user, target) - target.acl['can_moderate_avatar'] = can_moderate_avatar(user, target) - target.acl['can_moderate_signature'] = can_moderate_signature(user, target) - target.acl['can_edit_profile_details'] = can_edit_profile_details(user, target) - target.acl['can_ban'] = can_ban_user(user, target) - target.acl['max_ban_length'] = user.acl_cache['max_ban_length'] - target.acl['can_lift_ban'] = can_lift_ban(user, target) +def add_acl_to_user(user_acl, target): + target.acl['can_rename'] = can_rename_user(user_acl, target) + target.acl['can_moderate_avatar'] = can_moderate_avatar(user_acl, target) + target.acl['can_moderate_signature'] = can_moderate_signature(user_acl, target) + target.acl['can_edit_profile_details'] = can_edit_profile_details(user_acl, target) + target.acl['can_ban'] = can_ban_user(user_acl, target) + target.acl['max_ban_length'] = user_acl['max_ban_length'] + target.acl['can_lift_ban'] = can_lift_ban(user_acl, target) mod_permissions = [ 'can_rename', @@ -113,30 +113,30 @@ def register_with(registry): registry.acl_annotator(get_user_model(), add_acl_to_user) -def allow_rename_user(user, target): - if not user.acl_cache['can_rename_users']: +def allow_rename_user(user_acl, target): + if not user_acl['can_rename_users']: raise PermissionDenied(_("You can't rename users.")) - if not user.is_superuser and (target.is_staff or target.is_superuser): + if not user_acl["is_superuser"] and (target.is_staff or target.is_superuser): raise PermissionDenied(_("You can't rename administrators.")) can_rename_user = return_boolean(allow_rename_user) -def allow_moderate_avatar(user, target): - if not user.acl_cache['can_moderate_avatars']: +def allow_moderate_avatar(user_acl, target): + if not user_acl['can_moderate_avatars']: raise PermissionDenied(_("You can't moderate avatars.")) - if not user.is_superuser and (target.is_staff or target.is_superuser): + if not user_acl["is_superuser"] and (target.is_staff or target.is_superuser): raise PermissionDenied(_("You can't moderate administrators avatars.")) can_moderate_avatar = return_boolean(allow_moderate_avatar) -def allow_moderate_signature(user, target): - if not user.acl_cache['can_moderate_signatures']: +def allow_moderate_signature(user_acl, target): + if not user_acl['can_moderate_signatures']: raise PermissionDenied(_("You can't moderate signatures.")) - if not user.is_superuser and (target.is_staff or target.is_superuser): + if not user_acl["is_superuser"] and (target.is_staff or target.is_superuser): message = _("You can't moderate administrators signatures.") raise PermissionDenied(message) @@ -144,12 +144,12 @@ def allow_moderate_signature(user, target): can_moderate_signature = return_boolean(allow_moderate_signature) -def allow_edit_profile_details(user, target): - if user.is_anonymous: +def allow_edit_profile_details(user_acl, target): + if user_acl["is_anonymous"]: raise PermissionDenied(_("You have to sign in to edit profile details.")) - if user != target and not user.acl_cache['can_moderate_profile_details']: + if user_acl["user_id"] != target.id and not user_acl['can_moderate_profile_details']: raise PermissionDenied(_("You can't edit other users details.")) - if not user.is_superuser and (target.is_staff or target.is_superuser): + if not user_acl["is_superuser"] and (target.is_staff or target.is_superuser): message = _("You can't edit administrators details.") raise PermissionDenied(message) @@ -157,8 +157,8 @@ def allow_edit_profile_details(user, target): can_edit_profile_details = return_boolean(allow_edit_profile_details) -def allow_ban_user(user, target): - if not user.acl_cache['can_ban_users']: +def allow_ban_user(user_acl, target): + if not user_acl['can_ban_users']: raise PermissionDenied(_("You can't ban users.")) if target.is_staff or target.is_superuser: raise PermissionDenied(_("You can't ban administrators.")) @@ -167,14 +167,15 @@ def allow_ban_user(user, target): can_ban_user = return_boolean(allow_ban_user) -def allow_lift_ban(user, target): - if not user.acl_cache['can_lift_bans']: +def allow_lift_ban(user_acl, target): + if not user_acl['can_lift_bans']: raise PermissionDenied(_("You can't lift bans.")) + # FIXME: this will require cache version delegation ban = get_user_ban(target) if not ban: raise PermissionDenied(_("This user is not banned.")) - if user.acl_cache['max_lifted_ban_length']: - expiration_limit = timedelta(days=user.acl_cache['max_lifted_ban_length']) + if user_acl['max_lifted_ban_length']: + expiration_limit = timedelta(days=user_acl['max_lifted_ban_length']) lift_cutoff = (timezone.now() + expiration_limit).date() if not ban.valid_until: raise PermissionDenied(_("You can't lift permanent bans.")) diff --git a/misago/users/permissions/profiles.py b/misago/users/permissions/profiles.py index 3e338ef752..911e5d7b1c 100644 --- a/misago/users/permissions/profiles.py +++ b/misago/users/permissions/profiles.py @@ -92,10 +92,10 @@ def build_acl(acl, roles, key_name): ) -def add_acl_to_user(user, target): +def add_acl_to_user(user_acl, target): target.acl['can_have_attitude'] = False - target.acl['can_follow'] = can_follow_user(user, target) - target.acl['can_block'] = can_block_user(user, target) + target.acl['can_follow'] = can_follow_user(user_acl, target) + target.acl['can_block'] = can_block_user(user_acl, target) mod_permissions = ('can_have_attitude', 'can_follow', 'can_block', ) @@ -109,8 +109,8 @@ def register_with(registry): registry.acl_annotator(get_user_model(), add_acl_to_user) -def allow_browse_users_list(user): - if not user.acl_cache['can_browse_users_list']: +def allow_browse_users_list(user_acl): + if not user_acl['can_browse_users_list']: raise PermissionDenied(_("You can't browse users list.")) @@ -118,10 +118,10 @@ def allow_browse_users_list(user): @authenticated_only -def allow_follow_user(user, target): - if not user.acl_cache['can_follow_users']: +def allow_follow_user(user_acl, target): + if not user_acl['can_follow_users']: raise PermissionDenied(_("You can't follow other users.")) - if user.pk == target.pk: + if user_acl["user_id"] == target.id: raise PermissionDenied(_("You can't add yourself to followed.")) @@ -129,11 +129,12 @@ def allow_follow_user(user, target): @authenticated_only -def allow_block_user(user, target): +def allow_block_user(user_acl, target): if target.is_staff or target.is_superuser: raise PermissionDenied(_("You can't block administrators.")) - if user.pk == target.pk: + if user_acl["user_id"] == target.id: raise PermissionDenied(_("You can't block yourself.")) + # FIXME: this will require changes in ACL checking if not target.acl_cache['can_be_blocked'] or target.is_superuser: message = _("%(user)s can't be blocked.") % {'user': target.username} raise PermissionDenied(message) @@ -143,8 +144,8 @@ def allow_block_user(user, target): @authenticated_only -def allow_see_ban_details(user, target): - if not user.acl_cache['can_see_ban_details']: +def allow_see_ban_details(user_acl, target): + if not user_acl['can_see_ban_details']: raise PermissionDenied(_("You can't see users bans details.")) diff --git a/misago/users/profilefields/default.py b/misago/users/profilefields/default.py index 92cf73c8b5..7c24e920db 100644 --- a/misago/users/profilefields/default.py +++ b/misago/users/profilefields/default.py @@ -84,7 +84,7 @@ class JoinIpField(basefields.TextProfileField): readonly = True def get_value_display_data(self, request, user, value): - if not request.user.acl_cache.get('can_see_users_ips'): + if not request.user_acl.get('can_see_users_ips'): return None if not user.joined_from_ip: diff --git a/misago/users/profilefields/serializers.py b/misago/users/profilefields/serializers.py index a10ec7d2f1..4596af55bb 100644 --- a/misago/users/profilefields/serializers.py +++ b/misago/users/profilefields/serializers.py @@ -8,7 +8,7 @@ def serialize_profilefields_data(request, profilefields, user): 'edit': False, } - can_edit = can_edit_profile_details(request.user, user) + can_edit = can_edit_profile_details(request.user_acl, user) has_editable_fields = False for group in profilefields.get_fields_groups(): diff --git a/misago/users/search.py b/misago/users/search.py index 54133454d4..940e83b618 100644 --- a/misago/users/search.py +++ b/misago/users/search.py @@ -20,7 +20,7 @@ class SearchUsers(SearchProvider): url = 'users' def allow_search(self): - if not self.request.user.acl_cache['can_search_users']: + if not self.request.user_acl['can_search_users']: raise PermissionDenied(_("You don't have permission to search users.")) def search(self, query, page=1): diff --git a/misago/users/serializers/user.py b/misago/users/serializers/user.py index c23b620a1e..2c6a9431d6 100644 --- a/misago/users/serializers/user.py +++ b/misago/users/serializers/user.py @@ -74,20 +74,23 @@ def get_acl(self, obj): return obj.acl def get_email(self, obj): - if (obj == self.context['user'] or self.context['user'].acl_cache['can_see_users_emails']): + request = self.context['request'] + if (obj == request.user or request.user_acl['can_see_users_emails']): return obj.email else: return None def get_is_followed(self, obj): + request = self.context['request'] if obj.acl['can_follow']: - return self.context['user'].is_following(obj) + return request.user.is_following(obj) else: return False def get_is_blocked(self, obj): + request = self.context['request'] if obj.acl['can_block']: - return self.context['user'].is_blocking(obj) + return request.user.is_blocking(obj) else: return False diff --git a/misago/users/tests/test_joinip_profilefield.py b/misago/users/tests/test_joinip_profilefield.py index 5a1fca31ce..7fe530865c 100644 --- a/misago/users/tests/test_joinip_profilefield.py +++ b/misago/users/tests/test_joinip_profilefield.py @@ -1,8 +1,8 @@ from django.contrib.auth import get_user_model from django.urls import reverse +from misago.acl.test import patch_user_acl from misago.admin.testutils import AdminTestCase -from misago.acl.testutils import override_acl UserModel = get_user_model() @@ -74,7 +74,8 @@ def test_field_display(self): self.assertContains(response, "Join IP") self.assertContains(response, "127.0.0.1") - def test_field_hidden_no_permission(self): + @patch_user_acl + def test_field_hidden_no_permission(self, patch_user_acl): """field is hidden on user profile if user has no permission""" test_link = reverse( 'misago:user-details', @@ -84,7 +85,7 @@ def test_field_hidden_no_permission(self): }, ) - override_acl(self.user, { + patch_user_acl(self.user, { 'can_see_users_ips': 0 }) @@ -132,11 +133,12 @@ def test_field_display_json(self): ] ) - def test_field_hidden_no_permission_json(self): + @patch_user_acl + def test_field_hidden_no_permission_json(self, patch_user_acl): """field is not included in display json if user has no permission""" test_link = reverse('misago:api:user-details', kwargs={'pk': self.user.pk}) - override_acl(self.user, { + patch_user_acl(self.user, { 'can_see_users_ips': 0 }) diff --git a/misago/users/tests/test_lists_views.py b/misago/users/tests/test_lists_views.py index 0e6c300706..4247b4fe59 100644 --- a/misago/users/tests/test_lists_views.py +++ b/misago/users/tests/test_lists_views.py @@ -1,7 +1,7 @@ from django.contrib.auth import get_user_model from django.urls import reverse -from misago.acl.testutils import override_acl +from misago.acl.test import patch_user_acl from misago.categories.models import Category from misago.threads.testutils import post_thread from misago.users.activepostersranking import build_active_posters_ranking @@ -13,17 +13,14 @@ class UsersListTestCase(AuthenticatedUserTestCase): - def setUp(self): - super().setUp() - override_acl(self.user, { - 'can_browse_users_list': 1, - }) + pass class UsersListLanderTests(UsersListTestCase): - def test_lander_no_permission(self): + @patch_user_acl + def test_lander_no_permission(self, patch_user_acl): """lander returns 403 if user has no permission""" - override_acl(self.user, { + patch_user_acl(self.user, { 'can_browse_users_list': 0, }) diff --git a/misago/users/tests/test_online_utils.py b/misago/users/tests/test_online_utils.py index 58f079cf7e..eee371b04f 100644 --- a/misago/users/tests/test_online_utils.py +++ b/misago/users/tests/test_online_utils.py @@ -2,25 +2,27 @@ from django.contrib.auth import get_user_model -from misago.acl.testutils import override_acl from misago.users.online.utils import get_user_status from misago.users.testutils import AuthenticatedUserTestCase - -UserModel = get_user_model() +User = get_user_model() class GetUserStatusTests(AuthenticatedUserTestCase): def setUp(self): super().setUp() - self.other_user = UserModel.objects.create_user('Tyrael', 't123@test.com', 'pass123') + self.other_user = User.objects.create_user('Tyrael', 't123@test.com', 'pass123') def test_user_hiding_presence(self): """get_user_status has no showstopper for hidden user""" self.other_user.is_hiding_presence = True self.other_user.save() - request = Mock(user=self.user, cache_versions={"bans": "abcdfghi"}) + request = Mock( + user=self.user, + user_acl={'can_see_hidden_users': False}, + cache_versions={"bans": "abcdefgh"}, + ) get_user_status(request, self.other_user) def test_user_visible_hidden_presence(self): @@ -28,14 +30,18 @@ def test_user_visible_hidden_presence(self): self.other_user.is_hiding_presence = True self.other_user.save() - override_acl(self.user, { - 'can_see_hidden_users': True, - }) - - request = Mock(user=self.user, cache_versions={"bans": "abcdfghi"}) + request = Mock( + user=self.user, + user_acl={'can_see_hidden_users': True}, + cache_versions={"bans": "abcdefgh"}, + ) get_user_status(request, self.other_user) def test_user_not_hiding_presence(self): """get_user_status has no showstoppers for non-hidden user""" - request = Mock(user=self.user, cache_versions={"bans": "abcdfghi"}) + request = Mock( + user=self.user, + user_acl={'can_see_hidden_users': False}, + cache_versions={"bans": "abcdefgh"}, + ) get_user_status(request, self.other_user) diff --git a/misago/users/tests/test_profile_views.py b/misago/users/tests/test_profile_views.py index 6a77cc9b18..16f46429d0 100644 --- a/misago/users/tests/test_profile_views.py +++ b/misago/users/tests/test_profile_views.py @@ -1,7 +1,7 @@ from django.contrib.auth import get_user_model from django.urls import reverse -from misago.acl.testutils import override_acl +from misago.acl.test import patch_user_acl from misago.categories.models import Category from misago.threads import testutils from misago.users.models import Ban @@ -182,9 +182,10 @@ def test_username_history_list(self): self.assertContains(response, "TestUser") self.assertContains(response, "RenamedAdmin") - def test_user_ban_details(self): + @patch_user_acl + def test_user_ban_details(self, patch_user_acl): """user ban details page has no showstoppers""" - override_acl(self.user, { + patch_user_acl(self.user, { 'can_see_ban_details': 0, }) @@ -197,7 +198,7 @@ def test_user_ban_details(self): )) self.assertEqual(response.status_code, 404) - override_acl(self.user, { + patch_user_acl(self.user, { 'can_see_ban_details': 1, }) @@ -207,9 +208,6 @@ def test_user_ban_details(self): )) self.assertEqual(response.status_code, 404) - override_acl(self.user, { - 'can_see_ban_details': 1, - }) test_user.ban_cache.delete() Ban.objects.create( diff --git a/misago/users/tests/test_search.py b/misago/users/tests/test_search.py index 0d1c80483e..0989732822 100644 --- a/misago/users/tests/test_search.py +++ b/misago/users/tests/test_search.py @@ -1,7 +1,7 @@ from django.contrib.auth import get_user_model from django.urls import reverse -from misago.acl.testutils import override_acl +from misago.acl.test import patch_user_acl from misago.users.testutils import AuthenticatedUserTestCase @@ -14,9 +14,10 @@ def setUp(self): self.api_link = reverse('misago:api:search') - def test_no_permission(self): + @patch_user_acl + def test_no_permission(self, patch_user_acl): """api respects permission to search users""" - override_acl(self.user, {'can_search_users': 0}) + patch_user_acl(self.user, {'can_search_users': 0}) response = self.client.get(self.api_link) self.assertEqual(response.status_code, 200) diff --git a/misago/users/tests/test_user_avatar_api.py b/misago/users/tests/test_user_avatar_api.py index 7511a05198..5c8cef055e 100644 --- a/misago/users/tests/test_user_avatar_api.py +++ b/misago/users/tests/test_user_avatar_api.py @@ -4,7 +4,7 @@ from django.contrib.auth import get_user_model -from misago.acl.testutils import override_acl +from misago.acl.test import patch_user_acl from misago.conf import settings from misago.users.avatars import gallery, store from misago.users.models import AvatarGallery @@ -351,9 +351,10 @@ def setUp(self): self.link = '/api/users/%s/moderate-avatar/' % self.other_user.pk - def test_no_permission(self): + @patch_user_acl + def test_no_permission(self, patch_user_acl): """no permission to moderate avatar""" - override_acl(self.user, { + patch_user_acl(self.user, { 'can_moderate_avatars': 0, }) @@ -363,9 +364,10 @@ def test_no_permission(self): "detail": "You can't moderate avatars.", }) - def test_moderate_avatar(self): + @patch_user_acl + def test_moderate_avatar(self, patch_user_acl): """moderate avatar""" - override_acl(self.user, { + patch_user_acl(self.user, { 'can_moderate_avatars': 1, }) @@ -381,10 +383,6 @@ def test_moderate_avatar(self): options['avatar_lock_staff_message'], self.other_user.avatar_lock_staff_message ) - override_acl(self.user, { - 'can_moderate_avatars': 1, - }) - response = self.client.post( self.link, json.dumps({ @@ -410,10 +408,6 @@ def test_moderate_avatar(self): options['avatar_lock_staff_message'], other_user.avatar_lock_staff_message ) - override_acl(self.user, { - 'can_moderate_avatars': 1, - }) - response = self.client.post( self.link, json.dumps({ @@ -438,10 +432,6 @@ def test_moderate_avatar(self): options['avatar_lock_staff_message'], other_user.avatar_lock_staff_message ) - override_acl(self.user, { - 'can_moderate_avatars': 1, - }) - response = self.client.post( self.link, json.dumps({ @@ -466,10 +456,6 @@ def test_moderate_avatar(self): options['avatar_lock_staff_message'], other_user.avatar_lock_staff_message ) - override_acl(self.user, { - 'can_moderate_avatars': 1, - }) - response = self.client.post( self.link, json.dumps({ @@ -492,9 +478,10 @@ def test_moderate_avatar(self): options['avatar_lock_staff_message'], other_user.avatar_lock_staff_message ) - def test_moderate_own_avatar(self): + @patch_user_acl + def test_moderate_own_avatar(self, patch_user_acl): """moderate own avatar""" - override_acl(self.user, { + patch_user_acl(self.user, { 'can_moderate_avatars': 1, }) diff --git a/misago/users/tests/test_user_details_api.py b/misago/users/tests/test_user_details_api.py index ccd2a73045..8d8d0a72cb 100644 --- a/misago/users/tests/test_user_details_api.py +++ b/misago/users/tests/test_user_details_api.py @@ -1,8 +1,7 @@ from django.contrib.auth import get_user_model from django.urls import reverse -from misago.acl.testutils import override_acl - +from misago.acl.test import patch_user_acl from misago.users.testutils import AuthenticatedUserTestCase @@ -44,7 +43,8 @@ def test_api_has_no_showstoppers_old_user(self): self.assertEqual(response.status_code, 200) self.assertTrue(response.json()['edit']) - def test_other_user(self): + @patch_user_acl + def test_other_user(self, patch_user_acl): """api handles scenario when its other user looking at profile""" test_user = UserModel.objects.create_user('BobBoberson', 'bob@test.com', 'bob123456') @@ -56,7 +56,7 @@ def test_other_user(self): ) # moderator has permission to edit details - override_acl(self.user, { + patch_user_acl(self.user, { 'can_moderate_profile_details': True, }) @@ -65,7 +65,7 @@ def test_other_user(self): self.assertTrue(response.json()['edit']) # non-moderator has no permission to edit details - override_acl(self.user, { + patch_user_acl(self.user, { 'can_moderate_profile_details': False, }) diff --git a/misago/users/tests/test_user_editdetails_api.py b/misago/users/tests/test_user_editdetails_api.py index 7e96e99703..4d029750a9 100644 --- a/misago/users/tests/test_user_editdetails_api.py +++ b/misago/users/tests/test_user_editdetails_api.py @@ -1,8 +1,7 @@ from django.contrib.auth import get_user_model from django.urls import reverse -from misago.acl.testutils import override_acl - +from misago.acl.test import patch_user_acl from misago.users.testutils import AuthenticatedUserTestCase @@ -33,7 +32,8 @@ def test_api_has_no_showstoppers_old_user(self): response = self.client.get(self.api_link) self.assertEqual(response.status_code, 200) - def test_other_user(self): + @patch_user_acl + def test_other_user(self, patch_user_acl): """api handles scenario when its other user looking at profile""" test_user = UserModel.objects.create_user('BobBoberson', 'bob@test.com', 'bob123456') @@ -45,7 +45,7 @@ def test_other_user(self): ) # moderator has permission to edit details - override_acl(self.user, { + patch_user_acl(self.user, { 'can_moderate_profile_details': True, }) @@ -53,7 +53,7 @@ def test_other_user(self): self.assertEqual(response.status_code, 200) # non-moderator has no permission to edit details - override_acl(self.user, { + patch_user_acl(self.user, { 'can_moderate_profile_details': False, }) diff --git a/misago/users/tests/test_user_signature_api.py b/misago/users/tests/test_user_signature_api.py index f627668ebf..1d8edecac1 100644 --- a/misago/users/tests/test_user_signature_api.py +++ b/misago/users/tests/test_user_signature_api.py @@ -1,4 +1,4 @@ -from misago.acl.testutils import override_acl +from misago.acl.test import patch_user_acl from misago.users.testutils import AuthenticatedUserTestCase @@ -9,9 +9,10 @@ def setUp(self): super().setUp() self.link = '/api/users/%s/signature/' % self.user.pk - def test_signature_no_permission(self): + @patch_user_acl + def test_signature_no_permission(self, patch_user_acl): """edit signature api with no ACL returns 403""" - override_acl(self.user, { + patch_user_acl(self.user, { 'can_have_signature': 0, }) @@ -21,9 +22,10 @@ def test_signature_no_permission(self): "detail": "You don't have permission to change signature.", }) - def test_signature_locked(self): + @patch_user_acl + def test_signature_locked(self, patch_user_acl): """locked edit signature returns 403""" - override_acl(self.user, { + patch_user_acl(self.user, { 'can_have_signature': 1, }) @@ -38,9 +40,10 @@ def test_signature_locked(self): "reason": "

Your siggy is banned.

", }) - def test_get_signature(self): + @patch_user_acl + def test_get_signature(self, patch_user_acl): """GET to api returns json with no signature""" - override_acl(self.user, { + patch_user_acl(self.user, { 'can_have_signature': 1, }) @@ -52,9 +55,10 @@ def test_get_signature(self): self.assertFalse(response.json()['signature']) - def test_post_empty_signature(self): + @patch_user_acl + def test_post_empty_signature(self, patch_user_acl): """empty POST empties user signature""" - override_acl(self.user, { + patch_user_acl(self.user, { 'can_have_signature': 1, }) @@ -71,9 +75,10 @@ def test_post_empty_signature(self): self.assertFalse(response.json()['signature']) - def test_post_too_long_signature(self): + @patch_user_acl + def test_post_too_long_signature(self, patch_user_acl): """too long new signature errors""" - override_acl(self.user, { + patch_user_acl(self.user, { 'can_have_signature': 1, }) @@ -91,9 +96,10 @@ def test_post_too_long_signature(self): "detail": "Signature is too long.", }) - def test_post_good_signature(self): + @patch_user_acl + def test_post_good_signature(self, patch_user_acl): """POST with good signature changes user signature""" - override_acl(self.user, { + patch_user_acl(self.user, { 'can_have_signature': 1, }) diff --git a/misago/users/tests/test_user_username_api.py b/misago/users/tests/test_user_username_api.py index 39995d74c2..9d584f8075 100644 --- a/misago/users/tests/test_user_username_api.py +++ b/misago/users/tests/test_user_username_api.py @@ -2,7 +2,7 @@ from django.contrib.auth import get_user_model -from misago.acl.testutils import override_acl +from misago.acl.test import patch_user_acl from misago.conf import settings from misago.users.testutils import AuthenticatedUserTestCase @@ -117,9 +117,10 @@ def setUp(self): self.link = '/api/users/%s/moderate-username/' % self.other_user.pk - def test_no_permission(self): + @patch_user_acl + def test_no_permission(self, patch_user_acl): """no permission to moderate avatar""" - override_acl(self.user, { + patch_user_acl(self.user, { 'can_rename_users': 0, }) @@ -129,19 +130,16 @@ def test_no_permission(self): "detail": "You can't rename users.", }) - override_acl(self.user, { - 'can_rename_users': 0, - }) - response = self.client.post(self.link) self.assertEqual(response.status_code, 403) self.assertEqual(response.json(), { "detail": "You can't rename users.", }) - def test_moderate_username(self): + @patch_user_acl + def test_moderate_username(self, patch_user_acl): """moderate username""" - override_acl(self.user, { + patch_user_acl(self.user, { 'can_rename_users': 1, }) @@ -152,10 +150,6 @@ def test_moderate_username(self): self.assertEqual(options['length_min'], settings.username_length_min) self.assertEqual(options['length_max'], settings.username_length_max) - override_acl(self.user, { - 'can_rename_users': 1, - }) - response = self.client.post( self.link, json.dumps({ @@ -168,10 +162,6 @@ def test_moderate_username(self): "detail": "Enter new username.", }) - override_acl(self.user, { - 'can_rename_users': 1, - }) - response = self.client.post( self.link, json.dumps({ @@ -184,10 +174,6 @@ def test_moderate_username(self): "detail": "Username can only contain latin alphabet letters and digits.", }) - override_acl(self.user, { - 'can_rename_users': 1, - }) - response = self.client.post( self.link, json.dumps({ @@ -200,10 +186,6 @@ def test_moderate_username(self): "detail": "Username must be at least 3 characters long.", }) - override_acl(self.user, { - 'can_rename_users': 1, - }) - response = self.client.post( self.link, json.dumps({ @@ -223,9 +205,10 @@ def test_moderate_username(self): self.assertEqual(options['username'], other_user.username) self.assertEqual(options['slug'], other_user.slug) - def test_moderate_own_username(self): + @patch_user_acl + def test_moderate_own_username(self, patch_user_acl): """moderate own username""" - override_acl(self.user, { + patch_user_acl(self.user, { 'can_rename_users': 1, }) diff --git a/misago/users/tests/test_usernamechanges_api.py b/misago/users/tests/test_usernamechanges_api.py index a77d316cff..988bd23de1 100644 --- a/misago/users/tests/test_usernamechanges_api.py +++ b/misago/users/tests/test_usernamechanges_api.py @@ -1,4 +1,4 @@ -from misago.acl.testutils import override_acl +from misago.acl.test import patch_user_acl from misago.users.testutils import AuthenticatedUserTestCase @@ -7,39 +7,43 @@ def setUp(self): super().setUp() self.link = '/api/username-changes/' - def test_user_can_always_see_his_name_changes(self): + @patch_user_acl + def test_user_can_always_see_his_name_changes(self, patch_user_acl): """list returns own username changes""" self.user.set_username('NewUsername', self.user) - override_acl(self.user, {'can_see_users_name_history': False}) + patch_user_acl(self.user, {'can_see_users_name_history': False}) response = self.client.get('%s?user=%s' % (self.link, self.user.pk)) self.assertEqual(response.status_code, 200) self.assertContains(response, self.user.username) - def test_list_handles_invalid_filter(self): + @patch_user_acl + def test_list_handles_invalid_filter(self, patch_user_acl): """list raises 404 for invalid filter""" self.user.set_username('NewUsername', self.user) - override_acl(self.user, {'can_see_users_name_history': True}) + patch_user_acl(self.user, {'can_see_users_name_history': True}) response = self.client.get('%s?user=abcd' % self.link) self.assertEqual(response.status_code, 404) - def test_list_handles_nonexisting_user(self): + @patch_user_acl + def test_list_handles_nonexisting_user(self, patch_user_acl): """list raises 404 for invalid user id""" self.user.set_username('NewUsername', self.user) - override_acl(self.user, {'can_see_users_name_history': True}) + patch_user_acl(self.user, {'can_see_users_name_history': True}) response = self.client.get('%s?user=142141' % self.link) self.assertEqual(response.status_code, 404) - def test_list_handles_search(self): + @patch_user_acl + def test_list_handles_search(self, patch_user_acl): """list returns found username changes""" self.user.set_username('NewUsername', self.user) - override_acl(self.user, {'can_see_users_name_history': False}) + patch_user_acl(self.user, {'can_see_users_name_history': False}) response = self.client.get('%s?user=%s&search=new' % (self.link, self.user.pk)) self.assertEqual(response.status_code, 200) @@ -49,9 +53,10 @@ def test_list_handles_search(self): self.assertEqual(response.status_code, 200) self.assertEqual(response.json()["count"], 0) - def test_list_denies_permission(self): + @patch_user_acl + def test_list_denies_permission(self, patch_user_acl): """list denies permission for other user (or all) if no access""" - override_acl(self.user, {'can_see_users_name_history': False}) + patch_user_acl(self.user, {'can_see_users_name_history': False}) response = self.client.get('%s?user=%s' % (self.link, self.user.pk + 1)) self.assertEqual(response.status_code, 403) diff --git a/misago/users/tests/test_users_api.py b/misago/users/tests/test_users_api.py index 39c9f71e40..6b31ad0a7f 100644 --- a/misago/users/tests/test_users_api.py +++ b/misago/users/tests/test_users_api.py @@ -6,7 +6,7 @@ from django.urls import reverse from django.utils.encoding import smart_str -from misago.acl.testutils import override_acl +from misago.acl.test import patch_user_acl from misago.categories.models import Category from misago.core import threadstore from misago.core.cache import cache @@ -421,9 +421,10 @@ def test_follow_myself(self): "detail": "You can't add yourself to followed.", }) - def test_cant_follow(self): + @patch_user_acl + def test_cant_follow(self, patch_user_acl): """no permission to follow users""" - override_acl(self.user, { + patch_user_acl(self.user, { 'can_follow_users': 0, }) @@ -476,9 +477,10 @@ def setUp(self): self.link = '/api/users/%s/ban/' % self.other_user.pk - def test_no_permission(self): + @patch_user_acl + def test_no_permission(self, patch_user_acl): """user has no permission to access ban""" - override_acl(self.user, {'can_see_ban_details': 0}) + patch_user_acl(self.user, {'can_see_ban_details': 0}) response = self.client.get(self.link) self.assertEqual(response.status_code, 403) @@ -486,17 +488,19 @@ def test_no_permission(self): "detail": "You can't see users bans details.", }) - def test_no_ban(self): + @patch_user_acl + def test_no_ban(self, patch_user_acl): """api returns empty json""" - override_acl(self.user, {'can_see_ban_details': 1}) + patch_user_acl(self.user, {'can_see_ban_details': 1}) response = self.client.get(self.link) self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), {}) - def test_ban_details(self): + @patch_user_acl + def test_ban_details(self, patch_user_acl): """api returns ban json""" - override_acl(self.user, {'can_see_ban_details': 1}) + patch_user_acl(self.user, {'can_see_ban_details': 1}) Ban.objects.create( check_type=Ban.USERNAME, @@ -604,9 +608,10 @@ def setUp(self): self.other_user.threads = 1 self.other_user.save() - def test_delete_no_permission(self): + @patch_user_acl + def test_delete_no_permission(self, patch_user_acl): """raises 403 error when no permission to delete""" - override_acl( + patch_user_acl( self.user, { 'can_delete_users_newer_than': 0, 'can_delete_users_with_less_posts_than': 0, @@ -619,9 +624,10 @@ def test_delete_no_permission(self): 'detail': "You can't delete users.", }) - def test_delete_too_many_posts(self): + @patch_user_acl + def test_delete_too_many_posts(self, patch_user_acl): """raises 403 error when user has too many posts""" - override_acl( + patch_user_acl( self.user, { 'can_delete_users_newer_than': 0, 'can_delete_users_with_less_posts_than': 5, @@ -637,9 +643,10 @@ def test_delete_too_many_posts(self): 'detail': "You can't delete users that made more than 5 posts.", }) - def test_delete_too_old_member(self): + @patch_user_acl + def test_delete_too_old_member(self, patch_user_acl): """raises 403 error when user is too old""" - override_acl( + patch_user_acl( self.user, { 'can_delete_users_newer_than': 5, 'can_delete_users_with_less_posts_than': 0, @@ -656,9 +663,10 @@ def test_delete_too_old_member(self): 'detail': "You can't delete users that are members for more than 5 days.", }) - def test_delete_self(self): + @patch_user_acl + def test_delete_self(self, patch_user_acl): """raises 403 error when attempting to delete oneself""" - override_acl( + patch_user_acl( self.user, { 'can_delete_users_newer_than': 10, 'can_delete_users_with_less_posts_than': 10, @@ -671,9 +679,10 @@ def test_delete_self(self): 'detail': "You can't delete your account.", }) - def test_delete_admin(self): + @patch_user_acl + def test_delete_admin(self, patch_user_acl): """raises 403 error when attempting to delete admin""" - override_acl( + patch_user_acl( self.user, { 'can_delete_users_newer_than': 10, 'can_delete_users_with_less_posts_than': 10, @@ -689,9 +698,10 @@ def test_delete_admin(self): 'detail': "You can't delete administrators.", }) - def test_delete_superadmin(self): + @patch_user_acl + def test_delete_superadmin(self, patch_user_acl): """raises 403 error when attempting to delete superadmin""" - override_acl( + patch_user_acl( self.user, { 'can_delete_users_newer_than': 10, 'can_delete_users_with_less_posts_than': 10, @@ -707,9 +717,10 @@ def test_delete_superadmin(self): 'detail': "You can't delete administrators.", }) - def test_delete_with_content(self): + @patch_user_acl + def test_delete_with_content(self, patch_user_acl): """returns 200 and deletes user with content""" - override_acl( + patch_user_acl( self.user, { 'can_delete_users_newer_than': 10, 'can_delete_users_with_less_posts_than': 10, @@ -731,9 +742,10 @@ def test_delete_with_content(self): self.assertEqual(Thread.objects.count(), self.threads) self.assertEqual(Post.objects.count(), self.posts) - def test_delete_without_content(self): + @patch_user_acl + def test_delete_without_content(self, patch_user_acl): """returns 200 and deletes user without content""" - override_acl( + patch_user_acl( self.user, { 'can_delete_users_newer_than': 10, 'can_delete_users_with_less_posts_than': 10, diff --git a/misago/users/viewmodels/posts.py b/misago/users/viewmodels/posts.py index a60bac3fff..f475438580 100644 --- a/misago/users/viewmodels/posts.py +++ b/misago/users/viewmodels/posts.py @@ -6,7 +6,7 @@ class UserPosts(UserThreads): def get_threads_queryset(self, request, threads_categories, profile): - return exclude_invisible_threads(request.user, threads_categories, Thread.objects) + return exclude_invisible_threads(request.user_acl, threads_categories, Thread.objects) def get_posts_queryset(self, user, profile, threads_queryset): return profile.post_set.select_related('thread', 'poster').filter( diff --git a/misago/users/viewmodels/threads.py b/misago/users/viewmodels/threads.py index 420b78155a..3bff85ec6c 100644 --- a/misago/users/viewmodels/threads.py +++ b/misago/users/viewmodels/threads.py @@ -33,8 +33,8 @@ def __init__(self, request, profile, page=0): add_categories_to_items(root_category.unwrap(), threads_categories, posts + threads) - add_acl(request.user, threads) - add_acl(request.user, posts) + add_acl(request.user_acl, threads) + add_acl(request.user_acl, posts) self._user = request.user @@ -42,7 +42,7 @@ def __init__(self, request, profile, page=0): self.paginator = paginator def get_threads_queryset(self, request, threads_categories, profile): - return exclude_invisible_threads(request.user, threads_categories, profile.thread_set) + return exclude_invisible_threads(request.user_acl, threads_categories, profile.thread_set) def get_posts_queryset(self, user, profile, threads_queryset): return profile.post_set.select_related('thread', 'poster').filter( diff --git a/misago/users/views/lists.py b/misago/users/views/lists.py index a8e87a4e10..bba248df5f 100644 --- a/misago/users/views/lists.py +++ b/misago/users/views/lists.py @@ -11,7 +11,7 @@ class ListView(View): def get(self, request, *args, **kwargs): - allow_browse_users_list(request.user) + allow_browse_users_list(request.user_acl) context_data = self.get_context_data(request, *args, **kwargs) @@ -62,7 +62,7 @@ def get_context_data(self, request, *args, **kwargs): def landing(request): - allow_browse_users_list(request.user) + allow_browse_users_list(request.user_acl) return redirect(users_list.get_default_link()) diff --git a/misago/users/views/profile.py b/misago/users/views/profile.py index 27f707ff5f..7e65de5734 100644 --- a/misago/users/views/profile.py +++ b/misago/users/views/profile.py @@ -44,7 +44,7 @@ def get_profile(self, request, pk, slug): raise Http404() validate_slug(profile, slug) - add_acl(request.user, profile) + add_acl(request.user_acl, profile) return profile @@ -67,7 +67,7 @@ def complete_frontend_context(self, request, profile, sections): }) request.frontend_context['PROFILE'] = UserProfileSerializer( - profile, context={'user': request.user} + profile, context={'request': request} ).data if not profile.is_active: @@ -92,7 +92,7 @@ def complete_context_data(self, request, profile, sections, context): }) if not context['show_email']: - context['show_email'] = request.user.acl_cache['can_see_users_emails'] + context['show_email'] = request.user_acl['can_see_users_emails'] else: context.update({ 'is_authenticated_user': False, From 0354f43cb0e22d578adcd41a1a96d9bd8c9cb4fd Mon Sep 17 00:00:00 2001 From: rafalp Date: Sun, 9 Dec 2018 15:41:17 +0100 Subject: [PATCH 18/86] Improve get_user_status tests --- misago/users/tests/test_online_utils.py | 47 ------------------------- 1 file changed, 47 deletions(-) delete mode 100644 misago/users/tests/test_online_utils.py diff --git a/misago/users/tests/test_online_utils.py b/misago/users/tests/test_online_utils.py deleted file mode 100644 index eee371b04f..0000000000 --- a/misago/users/tests/test_online_utils.py +++ /dev/null @@ -1,47 +0,0 @@ -from unittest.mock import Mock - -from django.contrib.auth import get_user_model - -from misago.users.online.utils import get_user_status -from misago.users.testutils import AuthenticatedUserTestCase - -User = get_user_model() - - -class GetUserStatusTests(AuthenticatedUserTestCase): - def setUp(self): - super().setUp() - self.other_user = User.objects.create_user('Tyrael', 't123@test.com', 'pass123') - - def test_user_hiding_presence(self): - """get_user_status has no showstopper for hidden user""" - self.other_user.is_hiding_presence = True - self.other_user.save() - - request = Mock( - user=self.user, - user_acl={'can_see_hidden_users': False}, - cache_versions={"bans": "abcdefgh"}, - ) - get_user_status(request, self.other_user) - - def test_user_visible_hidden_presence(self): - """get_user_status has no showstopper forvisible hidden user""" - self.other_user.is_hiding_presence = True - self.other_user.save() - - request = Mock( - user=self.user, - user_acl={'can_see_hidden_users': True}, - cache_versions={"bans": "abcdefgh"}, - ) - get_user_status(request, self.other_user) - - def test_user_not_hiding_presence(self): - """get_user_status has no showstoppers for non-hidden user""" - request = Mock( - user=self.user, - user_acl={'can_see_hidden_users': False}, - cache_versions={"bans": "abcdefgh"}, - ) - get_user_status(request, self.other_user) From 8e63ac7ae0b96f8fad124f88a478f1bbcb0968bd Mon Sep 17 00:00:00 2001 From: rafalp Date: Sun, 9 Dec 2018 16:13:55 +0100 Subject: [PATCH 19/86] Improve patch_user_acl testing utility and get_user_status tests --- misago/acl/test.py | 39 +++++++--- misago/acl/tests/test_patching_user_acl.py | 71 +++++++++++++++++++ .../tests/test_patching_user_acl_in_tests.py | 42 ----------- .../users/tests/test_getting_user_status.py | 45 ++++++++++++ 4 files changed, 146 insertions(+), 51 deletions(-) create mode 100644 misago/acl/tests/test_patching_user_acl.py delete mode 100644 misago/acl/tests/test_patching_user_acl_in_tests.py create mode 100644 misago/users/tests/test_getting_user_status.py diff --git a/misago/acl/test.py b/misago/acl/test.py index 2d7af04f06..1227286eae 100644 --- a/misago/acl/test.py +++ b/misago/acl/test.py @@ -6,25 +6,49 @@ __all__ = ["patch_user_acl"] -class PatchUserACL: +class patch_user_acl: + """Testing utility that patches get_user_acl results + + Patch should be a dict or callable. + Can be used as decorator or context manager. + """ + _global_patch = None + _user_patches = {} + + def __init__(self, global_patch=None): + self._global_patch = global_patch + def patch_user_acl(self, user, patch): - self.patches[user.id] = patch + self._user_patches[user.id] = patch def patched_get_user_acl(self, user, cache_versions): user_acl = get_user_acl(user, cache_versions) - user_acl.update(self.patches.get(user.id, {})) + self.apply_acl_patches(user, user_acl) return user_acl + def apply_acl_patches(self, user, user_acl): + if self._global_patch: + self.apply_acl_patch(user, user_acl, self._global_patch) + if user.id in self._user_patches: + user_acl_patch = self._user_patches[user.id] + self.apply_acl_patch(user, user_acl, user_acl_patch) + + def apply_acl_patch(self, user, user_acl, acl_patch): + if callable(acl_patch): + acl_patch(user, user_acl) + else: + user_acl.update(acl_patch) + def __enter__(self): - self.patches = {} + return self def __exit__(self, *_): - self.patches = {} + self._user_patches = {} def __call__(self, f): @wraps(f) def inner(*args, **kwargs): - with self as context: + with self: with patch( "misago.acl.useracl.get_user_acl", side_effect=self.patched_get_user_acl, @@ -33,6 +57,3 @@ def inner(*args, **kwargs): return f(*new_args, **kwargs) return inner - - -patch_user_acl = PatchUserACL() \ No newline at end of file diff --git a/misago/acl/tests/test_patching_user_acl.py b/misago/acl/tests/test_patching_user_acl.py new file mode 100644 index 0000000000..8cf127e7b9 --- /dev/null +++ b/misago/acl/tests/test_patching_user_acl.py @@ -0,0 +1,71 @@ +from django.contrib.auth import get_user_model +from django.test import TestCase + +from misago.acl import useracl +from misago.acl.test import patch_user_acl + +User = get_user_model() + +cache_versions = {"acl": "abcdefgh"} + + +def callable_acl_patch(user, user_acl): + user_acl["patched_for_user_id"] = user.id + + +class PatchingUserACLInTestsTests(TestCase): + @patch_user_acl() + def test_decorator_adds_patching_function_to_test(self, patch_user_acl): + assert patch_user_acl + + @patch_user_acl() + def test_patching_function_changes_user_permission_value(self, patch_user_acl): + user = User.objects.create_user("User", "user@example.com") + patch_user_acl(user, {"can_rename_users": 123}) + user_acl = useracl.get_user_acl(user, cache_versions) + assert user_acl["can_rename_users"] == 123 + + @patch_user_acl() + def test_patching_function_adds_user_permission(self, patch_user_acl): + user = User.objects.create_user("User", "user@example.com") + patch_user_acl(user, {"new_user_permission": 123}) + user_acl = useracl.get_user_acl(user, cache_versions) + assert user_acl["new_user_permission"] == 123 + + def test_acl_patches_are_removed_after_test(self): + user = User.objects.create_user("User", "user@example.com") + + @patch_user_acl() + def test_function(patch_user_acl): + patch_user_acl(user, {"is_patched": True}) + user_acl = useracl.get_user_acl(user, cache_versions) + assert user_acl["is_patched"] + + user_acl = useracl.get_user_acl(user, cache_versions) + assert "is_patched" not in user_acl + + @patch_user_acl({"is_patched": True}) + def test_decorator_given_global_patch_patches_all_users_acls(self, _): + user = User.objects.create_user("User", "user@example.com") + user_acl = useracl.get_user_acl(user, cache_versions) + assert user_acl["is_patched"] + + @patch_user_acl(callable_acl_patch) + def test_callable_global_patch_is_called_with_user_and_user_acl(self, _): + user = User.objects.create_user("User", "user@example.com") + user_acl = useracl.get_user_acl(user, cache_versions) + assert user_acl["patched_for_user_id"] == user.id + + @patch_user_acl({"is_patched": True}) + def test_patching_function_overrides_global_patch(self, patch_user_acl): + user = User.objects.create_user("User", "user@example.com") + patch_user_acl(user, {"is_patched": 123}) + user_acl = useracl.get_user_acl(user, cache_versions) + assert user_acl["is_patched"] == 123 + + @patch_user_acl() + def test_callable_user_patch_is_called_with_user_and_user_acl(self, patch_user_acl): + user = User.objects.create_user("User", "user@example.com") + patch_user_acl(user, callable_acl_patch) + user_acl = useracl.get_user_acl(user, cache_versions) + assert user_acl["patched_for_user_id"] == user.id diff --git a/misago/acl/tests/test_patching_user_acl_in_tests.py b/misago/acl/tests/test_patching_user_acl_in_tests.py deleted file mode 100644 index 579d6ee435..0000000000 --- a/misago/acl/tests/test_patching_user_acl_in_tests.py +++ /dev/null @@ -1,42 +0,0 @@ -from django.contrib.auth import get_user_model -from django.test import TestCase - -from misago.acl import useracl -from misago.acl.test import patch_user_acl - -User = get_user_model() - -cache_versions = {"acl": "abcdefgh"} - - -class PatchingUserACLInTestsTests(TestCase): - @patch_user_acl - def test_decorator_adds_patching_function_to_test(self, patch_user_acl): - assert patch_user_acl - - @patch_user_acl - def test_patching_function_changes_user_permission_value(self, patch_user_acl): - user = User.objects.create_user("User", "user@example.com") - patch_user_acl(user, {"can_rename_users": 123}) - user_acl = useracl.get_user_acl(user, cache_versions) - assert user_acl["can_rename_users"] == 123 - - @patch_user_acl - def test_patching_function_adds_user_permission(self, patch_user_acl): - user = User.objects.create_user("User", "user@example.com") - patch_user_acl(user, {"new_user_permission": 123}) - user_acl = useracl.get_user_acl(user, cache_versions) - assert user_acl["new_user_permission"] == 123 - - def test_acl_patches_are_removed_after_test(self): - user = User.objects.create_user("User", "user@example.com") - - @patch_user_acl - def test_function(patch_user_acl): - patch_user_acl(user, {"is_patched": True}) - user_acl = useracl.get_user_acl(user, cache_versions) - assert user_acl["is_patched"] - - user_acl = useracl.get_user_acl(user, cache_versions) - assert "is_patched" not in user_acl - \ No newline at end of file diff --git a/misago/users/tests/test_getting_user_status.py b/misago/users/tests/test_getting_user_status.py new file mode 100644 index 0000000000..ac5307fff0 --- /dev/null +++ b/misago/users/tests/test_getting_user_status.py @@ -0,0 +1,45 @@ +from unittest.mock import Mock + +from django.contrib.auth import get_user_model + +from misago.users.online.utils import get_user_status +from misago.users.testutils import AuthenticatedUserTestCase + +User = get_user_model() + + +class GetUserStatusTests(AuthenticatedUserTestCase): + def setUp(self): + super().setUp() + self.other_user = User.objects.create_user('Tyrael', 't123@test.com', 'pass123') + + def test_get_visible_user_status_returns_online(self): + request = Mock( + user=self.user, + user_acl={'can_see_hidden_users': False}, + cache_versions={"bans": "abcdefgh"}, + ) + assert get_user_status(request, self.other_user)["is_online"] + + def test_get_hidden_user_status_without_seeing_hidden_permission_returns_offline(self): + """get_user_status has no showstopper for hidden user""" + self.other_user.is_hiding_presence = True + self.other_user.save() + + request = Mock( + user=self.user, + user_acl={'can_see_hidden_users': False}, + cache_versions={"bans": "abcdefgh"}, + ) + assert get_user_status(request, self.other_user)["is_hidden"] + + def test_get_hidden_user_status_with_seeing_hidden_permission_returns_online_hidden(self): + self.other_user.is_hiding_presence = True + self.other_user.save() + + request = Mock( + user=self.user, + user_acl={'can_see_hidden_users': True}, + cache_versions={"bans": "abcdefgh"}, + ) + assert get_user_status(request, self.other_user)["is_online_hidden"] From 55156a85be9e5ff35a03202d5ff58108817b9a29 Mon Sep 17 00:00:00 2001 From: rafalp Date: Sun, 9 Dec 2018 17:48:08 +0100 Subject: [PATCH 20/86] Improve test util to work as decorator or context processor --- misago/acl/test.py | 42 ++++++++----- misago/acl/tests/test_patching_user_acl.py | 72 +++++++++++----------- 2 files changed, 65 insertions(+), 49 deletions(-) diff --git a/misago/acl/test.py b/misago/acl/test.py index 1227286eae..a4750634f7 100644 --- a/misago/acl/test.py +++ b/misago/acl/test.py @@ -1,3 +1,4 @@ +from contextlib import ExitStack from functools import wraps from unittest.mock import patch @@ -6,20 +7,31 @@ __all__ = ["patch_user_acl"] -class patch_user_acl: +class patch_user_acl(ExitStack): """Testing utility that patches get_user_acl results - Patch should be a dict or callable. Can be used as decorator or context manager. + + Accepts one or two arguments: + - patch_user_acl(acl_patch) + - patch_user_acl(user, acl_patch) + + Patch should be a dict or callable. """ - _global_patch = None - _user_patches = {} - def __init__(self, global_patch=None): - self._global_patch = global_patch + def __init__(self, *args): + super().__init__() + + self._global_patch = None + self._user_patches = {} - def patch_user_acl(self, user, patch): - self._user_patches[user.id] = patch + if len(args) == 2: + user, patch = args + self._user_patches[user.id] = patch + elif len(args) == 1: + self._global_patch = args[0] + else: + raise ValueError("patch_user_acl takes one or two arguments.") def patched_get_user_acl(self, user, cache_versions): user_acl = get_user_acl(user, cache_versions) @@ -40,10 +52,13 @@ def apply_acl_patch(self, user, user_acl, acl_patch): user_acl.update(acl_patch) def __enter__(self): - return self - - def __exit__(self, *_): - self._user_patches = {} + super().__enter__() + self.enter_context( + patch( + "misago.acl.useracl.get_user_acl", + side_effect=self.patched_get_user_acl, + ) + ) def __call__(self, f): @wraps(f) @@ -53,7 +68,6 @@ def inner(*args, **kwargs): "misago.acl.useracl.get_user_acl", side_effect=self.patched_get_user_acl, ): - new_args = args + (self.patch_user_acl,) - return f(*new_args, **kwargs) + return f(*args, **kwargs) return inner diff --git a/misago/acl/tests/test_patching_user_acl.py b/misago/acl/tests/test_patching_user_acl.py index 8cf127e7b9..aed4c8c48e 100644 --- a/misago/acl/tests/test_patching_user_acl.py +++ b/misago/acl/tests/test_patching_user_acl.py @@ -13,59 +13,61 @@ def callable_acl_patch(user, user_acl): user_acl["patched_for_user_id"] = user.id -class PatchingUserACLInTestsTests(TestCase): - @patch_user_acl() - def test_decorator_adds_patching_function_to_test(self, patch_user_acl): - assert patch_user_acl - - @patch_user_acl() - def test_patching_function_changes_user_permission_value(self, patch_user_acl): - user = User.objects.create_user("User", "user@example.com") - patch_user_acl(user, {"can_rename_users": 123}) - user_acl = useracl.get_user_acl(user, cache_versions) - assert user_acl["can_rename_users"] == 123 - - @patch_user_acl() - def test_patching_function_adds_user_permission(self, patch_user_acl): +class PatchingUserACLTests(TestCase): + @patch_user_acl({"is_patched": True}) + def test_decorator_patches_all_users_acls_in_test(self): user = User.objects.create_user("User", "user@example.com") - patch_user_acl(user, {"new_user_permission": 123}) user_acl = useracl.get_user_acl(user, cache_versions) - assert user_acl["new_user_permission"] == 123 + assert user_acl["is_patched"] - def test_acl_patches_are_removed_after_test(self): + def test_decorator_removes_patches_after_test(self): user = User.objects.create_user("User", "user@example.com") - @patch_user_acl() + @patch_user_acl({"is_patched": True}) def test_function(patch_user_acl): - patch_user_acl(user, {"is_patched": True}) user_acl = useracl.get_user_acl(user, cache_versions) assert user_acl["is_patched"] user_acl = useracl.get_user_acl(user, cache_versions) assert "is_patched" not in user_acl - - @patch_user_acl({"is_patched": True}) - def test_decorator_given_global_patch_patches_all_users_acls(self, _): + + def test_context_manager_patches_all_users_acls_in_test(self): user = User.objects.create_user("User", "user@example.com") - user_acl = useracl.get_user_acl(user, cache_versions) - assert user_acl["is_patched"] + with patch_user_acl({"can_rename_users": "patched"}): + user_acl = useracl.get_user_acl(user, cache_versions) + assert user_acl["can_rename_users"] == "patched" - @patch_user_acl(callable_acl_patch) - def test_callable_global_patch_is_called_with_user_and_user_acl(self, _): + def test_context_manager_removes_patches_after_exit(self): user = User.objects.create_user("User", "user@example.com") + + with patch_user_acl({"is_patched": True}): + user_acl = useracl.get_user_acl(user, cache_versions) + assert user_acl["is_patched"] + user_acl = useracl.get_user_acl(user, cache_versions) - assert user_acl["patched_for_user_id"] == user.id + assert "is_patched" not in user_acl - @patch_user_acl({"is_patched": True}) - def test_patching_function_overrides_global_patch(self, patch_user_acl): + def test_context_manager_patches_specified_user_acl(self): user = User.objects.create_user("User", "user@example.com") - patch_user_acl(user, {"is_patched": 123}) - user_acl = useracl.get_user_acl(user, cache_versions) - assert user_acl["is_patched"] == 123 + with patch_user_acl(user, {"can_rename_users": "patched"}): + user_acl = useracl.get_user_acl(user, cache_versions) + assert user_acl["can_rename_users"] == "patched" + + def test_other_user_acl_is_not_changed_by_user_specific_context_manager(self): + patched_user = User.objects.create_user("User", "user@example.com") + other_user = User.objects.create_user("User2", "user2@example.com") + with patch_user_acl(patched_user, {"can_rename_users": "patched"}): + other_user_acl = useracl.get_user_acl(other_user, cache_versions) + assert other_user_acl["can_rename_users"] != "patched" - @patch_user_acl() - def test_callable_user_patch_is_called_with_user_and_user_acl(self, patch_user_acl): + @patch_user_acl(callable_acl_patch) + def test_callable_patch_is_called_with_user_and_acl_by_decorator(self): user = User.objects.create_user("User", "user@example.com") - patch_user_acl(user, callable_acl_patch) user_acl = useracl.get_user_acl(user, cache_versions) assert user_acl["patched_for_user_id"] == user.id + + def test_callable_patch_is_called_with_user_and_acl_by_context_manager(self): + user = User.objects.create_user("User", "user@example.com") + with patch_user_acl(callable_acl_patch): + user_acl = useracl.get_user_acl(user, cache_versions) + assert user_acl["patched_for_user_id"] == user.id From 263dff949ce579152ae85eb74e019fbb039f0a7d Mon Sep 17 00:00:00 2001 From: rafalp Date: Sun, 9 Dec 2018 17:48:35 +0100 Subject: [PATCH 21/86] Update misago.users tests to use new testing utilities --- .../users/tests/test_joinip_profilefield.py | 16 +- misago/users/tests/test_lists_views.py | 8 +- misago/users/tests/test_profile_views.py | 53 +++---- misago/users/tests/test_search.py | 54 ++++--- misago/users/tests/test_user_avatar_api.py | 24 +-- misago/users/tests/test_user_details_api.py | 25 ++-- .../users/tests/test_user_editdetails_api.py | 21 +-- misago/users/tests/test_user_signature_api.py | 48 ++---- misago/users/tests/test_user_username_api.py | 26 +--- .../users/tests/test_usernamechanges_api.py | 33 ++--- misago/users/tests/test_users_api.py | 138 ++++++------------ 11 files changed, 154 insertions(+), 292 deletions(-) diff --git a/misago/users/tests/test_joinip_profilefield.py b/misago/users/tests/test_joinip_profilefield.py index 7fe530865c..94388eb842 100644 --- a/misago/users/tests/test_joinip_profilefield.py +++ b/misago/users/tests/test_joinip_profilefield.py @@ -74,8 +74,8 @@ def test_field_display(self): self.assertContains(response, "Join IP") self.assertContains(response, "127.0.0.1") - @patch_user_acl - def test_field_hidden_no_permission(self, patch_user_acl): + @patch_user_acl({'can_see_users_ips': 0}) + def test_field_hidden_no_permission(self): """field is hidden on user profile if user has no permission""" test_link = reverse( 'misago:user-details', @@ -85,10 +85,6 @@ def test_field_hidden_no_permission(self, patch_user_acl): }, ) - patch_user_acl(self.user, { - 'can_see_users_ips': 0 - }) - response = self.client.get(test_link) self.assertNotContains(response, "IP address") self.assertNotContains(response, "Join IP") @@ -133,15 +129,11 @@ def test_field_display_json(self): ] ) - @patch_user_acl - def test_field_hidden_no_permission_json(self, patch_user_acl): + @patch_user_acl({'can_see_users_ips': 0}) + def test_field_hidden_no_permission_json(self): """field is not included in display json if user has no permission""" test_link = reverse('misago:api:user-details', kwargs={'pk': self.user.pk}) - patch_user_acl(self.user, { - 'can_see_users_ips': 0 - }) - response = self.client.get(test_link) self.assertEqual(response.json()['groups'], []) diff --git a/misago/users/tests/test_lists_views.py b/misago/users/tests/test_lists_views.py index 4247b4fe59..5c10e69beb 100644 --- a/misago/users/tests/test_lists_views.py +++ b/misago/users/tests/test_lists_views.py @@ -17,13 +17,9 @@ class UsersListTestCase(AuthenticatedUserTestCase): class UsersListLanderTests(UsersListTestCase): - @patch_user_acl - def test_lander_no_permission(self, patch_user_acl): + @patch_user_acl({'can_browse_users_list': 0}) + def test_lander_no_permission(self): """lander returns 403 if user has no permission""" - patch_user_acl(self.user, { - 'can_browse_users_list': 0, - }) - response = self.client.get(reverse('misago:users')) self.assertEqual(response.status_code, 403) diff --git a/misago/users/tests/test_profile_views.py b/misago/users/tests/test_profile_views.py index 16f46429d0..e3ebd4da7f 100644 --- a/misago/users/tests/test_profile_views.py +++ b/misago/users/tests/test_profile_views.py @@ -182,46 +182,39 @@ def test_username_history_list(self): self.assertContains(response, "TestUser") self.assertContains(response, "RenamedAdmin") - @patch_user_acl - def test_user_ban_details(self, patch_user_acl): + def test_user_ban_details(self): """user ban details page has no showstoppers""" - patch_user_acl(self.user, { - 'can_see_ban_details': 0, - }) - test_user = UserModel.objects.create_user("Bob", "bob@bob.com", 'pass.123') link_kwargs = {'slug': test_user.slug, 'pk': test_user.pk} - response = self.client.get(reverse( - 'misago:user-ban', - kwargs=link_kwargs, - )) - self.assertEqual(response.status_code, 404) - - patch_user_acl(self.user, { - 'can_see_ban_details': 1, - }) + with patch_user_acl({'can_see_ban_details': 0}): + response = self.client.get(reverse( + 'misago:user-ban', + kwargs=link_kwargs, + )) + self.assertEqual(response.status_code, 404) - response = self.client.get(reverse( - 'misago:user-ban', - kwargs=link_kwargs, - )) - self.assertEqual(response.status_code, 404) - - test_user.ban_cache.delete() + with patch_user_acl({'can_see_ban_details': 1}): + response = self.client.get(reverse( + 'misago:user-ban', + kwargs=link_kwargs, + )) + self.assertEqual(response.status_code, 404) Ban.objects.create( banned_value=test_user.username, user_message="User m3ss4ge.", staff_message="Staff m3ss4ge.", is_checked=True, - ) + ) + test_user.ban_cache.delete() - response = self.client.get(reverse( - 'misago:user-ban', - kwargs=link_kwargs, - )) + with patch_user_acl({'can_see_ban_details': 1}): + response = self.client.get(reverse( + 'misago:user-ban', + kwargs=link_kwargs, + )) - self.assertEqual(response.status_code, 200) - self.assertContains(response, 'User m3ss4ge') - self.assertContains(response, 'Staff m3ss4ge') + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'User m3ss4ge') + self.assertContains(response, 'Staff m3ss4ge') diff --git a/misago/users/tests/test_search.py b/misago/users/tests/test_search.py index 0989732822..1b4fbdd1da 100644 --- a/misago/users/tests/test_search.py +++ b/misago/users/tests/test_search.py @@ -14,11 +14,9 @@ def setUp(self): self.api_link = reverse('misago:api:search') - @patch_user_acl - def test_no_permission(self, patch_user_acl): + @patch_user_acl({'can_search_users': 0}) + def test_no_permission(self): """api respects permission to search users""" - patch_user_acl(self.user, {'can_search_users': 0}) - response = self.client.get(self.api_link) self.assertEqual(response.status_code, 200) self.assertNotIn('users', [p['id'] for p in response.json()]) @@ -28,10 +26,10 @@ def test_no_query(self): response = self.client.get(self.api_link) self.assertEqual(response.status_code, 200) - reponse_json = response.json() - self.assertIn('users', [p['id'] for p in reponse_json]) + response_json = response.json() + self.assertIn('users', [p['id'] for p in response_json]) - for provider in reponse_json: + for provider in response_json: if provider['id'] == 'users': self.assertEqual(provider['results']['results'], []) @@ -40,10 +38,10 @@ def test_empty_query(self): response = self.client.get('%s?q=' % self.api_link) self.assertEqual(response.status_code, 200) - reponse_json = response.json() - self.assertIn('users', [p['id'] for p in reponse_json]) + response_json = response.json() + self.assertIn('users', [p['id'] for p in response_json]) - for provider in reponse_json: + for provider in response_json: if provider['id'] == 'users': self.assertEqual(provider['results']['results'], []) @@ -52,10 +50,10 @@ def test_short_query(self): response = self.client.get('%s?q=%s' % (self.api_link, self.user.username[0])) self.assertEqual(response.status_code, 200) - reponse_json = response.json() - self.assertIn('users', [p['id'] for p in reponse_json]) + response_json = response.json() + self.assertIn('users', [p['id'] for p in response_json]) - for provider in reponse_json: + for provider in response_json: if provider['id'] == 'users': results = provider['results']['results'] self.assertEqual(len(results), 1) @@ -66,10 +64,10 @@ def test_exact_match(self): response = self.client.get('%s?q=%s' % (self.api_link, self.user.username)) self.assertEqual(response.status_code, 200) - reponse_json = response.json() - self.assertIn('users', [p['id'] for p in reponse_json]) + response_json = response.json() + self.assertIn('users', [p['id'] for p in response_json]) - for provider in reponse_json: + for provider in response_json: if provider['id'] == 'users': results = provider['results']['results'] self.assertEqual(len(results), 1) @@ -80,10 +78,10 @@ def test_tail_match(self): response = self.client.get('%s?q=%s' % (self.api_link, self.user.username[-3:])) self.assertEqual(response.status_code, 200) - reponse_json = response.json() - self.assertIn('users', [p['id'] for p in reponse_json]) + response_json = response.json() + self.assertIn('users', [p['id'] for p in response_json]) - for provider in reponse_json: + for provider in response_json: if provider['id'] == 'users': results = provider['results']['results'] self.assertEqual(len(results), 1) @@ -94,10 +92,10 @@ def test_no_match(self): response = self.client.get('%s?q=BobBoberson' % self.api_link) self.assertEqual(response.status_code, 200) - reponse_json = response.json() - self.assertIn('users', [p['id'] for p in reponse_json]) + response_json = response.json() + self.assertIn('users', [p['id'] for p in response_json]) - for provider in reponse_json: + for provider in response_json: if provider['id'] == 'users': self.assertEqual(provider['results']['results'], []) @@ -113,10 +111,10 @@ def test_search_disabled(self): response = self.client.get('%s?q=DisabledUser' % self.api_link) self.assertEqual(response.status_code, 200) - reponse_json = response.json() - self.assertIn('users', [p['id'] for p in reponse_json]) + response_json = response.json() + self.assertIn('users', [p['id'] for p in response_json]) - for provider in reponse_json: + for provider in response_json: if provider['id'] == 'users': self.assertEqual(provider['results']['results'], []) @@ -127,10 +125,10 @@ def test_search_disabled(self): response = self.client.get('%s?q=DisabledUser' % self.api_link) self.assertEqual(response.status_code, 200) - reponse_json = response.json() - self.assertIn('users', [p['id'] for p in reponse_json]) + response_json = response.json() + self.assertIn('users', [p['id'] for p in response_json]) - for provider in reponse_json: + for provider in response_json: if provider['id'] == 'users': results = provider['results']['results'] self.assertEqual(len(results), 1) diff --git a/misago/users/tests/test_user_avatar_api.py b/misago/users/tests/test_user_avatar_api.py index 5c8cef055e..eb4f699fc1 100644 --- a/misago/users/tests/test_user_avatar_api.py +++ b/misago/users/tests/test_user_avatar_api.py @@ -351,26 +351,18 @@ def setUp(self): self.link = '/api/users/%s/moderate-avatar/' % self.other_user.pk - @patch_user_acl - def test_no_permission(self, patch_user_acl): + @patch_user_acl({'can_moderate_avatars': 0}) + def test_no_permission(self): """no permission to moderate avatar""" - patch_user_acl(self.user, { - 'can_moderate_avatars': 0, - }) - response = self.client.get(self.link) self.assertEqual(response.status_code, 403) self.assertEqual(response.json(), { "detail": "You can't moderate avatars.", }) - @patch_user_acl - def test_moderate_avatar(self, patch_user_acl): + @patch_user_acl({'can_moderate_avatars': 1}) + def test_moderate_avatar(self): """moderate avatar""" - patch_user_acl(self.user, { - 'can_moderate_avatars': 1, - }) - response = self.client.get(self.link) self.assertEqual(response.status_code, 200) @@ -478,12 +470,8 @@ def test_moderate_avatar(self, patch_user_acl): options['avatar_lock_staff_message'], other_user.avatar_lock_staff_message ) - @patch_user_acl - def test_moderate_own_avatar(self, patch_user_acl): + @patch_user_acl({'can_moderate_avatars': 1}) + def test_moderate_own_avatar(self): """moderate own avatar""" - patch_user_acl(self.user, { - 'can_moderate_avatars': 1, - }) - response = self.client.get('/api/users/%s/moderate-avatar/' % self.user.pk) self.assertEqual(response.status_code, 200) diff --git a/misago/users/tests/test_user_details_api.py b/misago/users/tests/test_user_details_api.py index 8d8d0a72cb..c74fd84995 100644 --- a/misago/users/tests/test_user_details_api.py +++ b/misago/users/tests/test_user_details_api.py @@ -43,8 +43,7 @@ def test_api_has_no_showstoppers_old_user(self): self.assertEqual(response.status_code, 200) self.assertTrue(response.json()['edit']) - @patch_user_acl - def test_other_user(self, patch_user_acl): + def test_other_user(self): """api handles scenario when its other user looking at profile""" test_user = UserModel.objects.create_user('BobBoberson', 'bob@test.com', 'bob123456') @@ -56,22 +55,16 @@ def test_other_user(self, patch_user_acl): ) # moderator has permission to edit details - patch_user_acl(self.user, { - 'can_moderate_profile_details': True, - }) - - response = self.client.get(api_link) - self.assertEqual(response.status_code, 200) - self.assertTrue(response.json()['edit']) + with patch_user_acl(self.user, {'can_moderate_profile_details': True}): + response = self.client.get(api_link) + self.assertEqual(response.status_code, 200) + self.assertTrue(response.json()['edit']) # non-moderator has no permission to edit details - patch_user_acl(self.user, { - 'can_moderate_profile_details': False, - }) - - response = self.client.get(api_link) - self.assertEqual(response.status_code, 200) - self.assertFalse(response.json()['edit']) + with patch_user_acl(self.user, {'can_moderate_profile_details': False}): + response = self.client.get(api_link) + self.assertEqual(response.status_code, 200) + self.assertFalse(response.json()['edit']) def test_nonexistant_user(self): """api handles nonexistant users""" diff --git a/misago/users/tests/test_user_editdetails_api.py b/misago/users/tests/test_user_editdetails_api.py index 4d029750a9..d42fb47c86 100644 --- a/misago/users/tests/test_user_editdetails_api.py +++ b/misago/users/tests/test_user_editdetails_api.py @@ -32,8 +32,7 @@ def test_api_has_no_showstoppers_old_user(self): response = self.client.get(self.api_link) self.assertEqual(response.status_code, 200) - @patch_user_acl - def test_other_user(self, patch_user_acl): + def test_other_user(self): """api handles scenario when its other user looking at profile""" test_user = UserModel.objects.create_user('BobBoberson', 'bob@test.com', 'bob123456') @@ -45,20 +44,14 @@ def test_other_user(self, patch_user_acl): ) # moderator has permission to edit details - patch_user_acl(self.user, { - 'can_moderate_profile_details': True, - }) - - response = self.client.get(api_link) - self.assertEqual(response.status_code, 200) + with patch_user_acl(self.user, {'can_moderate_profile_details': True}): + response = self.client.get(api_link) + self.assertEqual(response.status_code, 200) # non-moderator has no permission to edit details - patch_user_acl(self.user, { - 'can_moderate_profile_details': False, - }) - - response = self.client.get(api_link) - self.assertEqual(response.status_code, 403) + with patch_user_acl(self.user, {'can_moderate_profile_details': False}): + response = self.client.get(api_link) + self.assertEqual(response.status_code, 403) def test_nonexistant_user(self): """api handles nonexistant users""" diff --git a/misago/users/tests/test_user_signature_api.py b/misago/users/tests/test_user_signature_api.py index 1d8edecac1..8e5072a030 100644 --- a/misago/users/tests/test_user_signature_api.py +++ b/misago/users/tests/test_user_signature_api.py @@ -9,26 +9,18 @@ def setUp(self): super().setUp() self.link = '/api/users/%s/signature/' % self.user.pk - @patch_user_acl - def test_signature_no_permission(self, patch_user_acl): + @patch_user_acl({'can_have_signature': 0}) + def test_signature_no_permission(self): """edit signature api with no ACL returns 403""" - patch_user_acl(self.user, { - 'can_have_signature': 0, - }) - response = self.client.get(self.link) self.assertEqual(response.status_code, 403) self.assertEqual(response.json(), { "detail": "You don't have permission to change signature.", }) - @patch_user_acl - def test_signature_locked(self, patch_user_acl): + @patch_user_acl({'can_have_signature': 1}) + def test_signature_locked(self): """locked edit signature returns 403""" - patch_user_acl(self.user, { - 'can_have_signature': 1, - }) - self.user.is_signature_locked = True self.user.signature_lock_user_message = 'Your siggy is banned.' self.user.save() @@ -40,13 +32,9 @@ def test_signature_locked(self, patch_user_acl): "reason": "

Your siggy is banned.

", }) - @patch_user_acl - def test_get_signature(self, patch_user_acl): + @patch_user_acl({'can_have_signature': 1}) + def test_get_signature(self): """GET to api returns json with no signature""" - patch_user_acl(self.user, { - 'can_have_signature': 1, - }) - self.user.is_signature_locked = False self.user.save() @@ -55,13 +43,9 @@ def test_get_signature(self, patch_user_acl): self.assertFalse(response.json()['signature']) - @patch_user_acl - def test_post_empty_signature(self, patch_user_acl): + @patch_user_acl({'can_have_signature': 1}) + def test_post_empty_signature(self): """empty POST empties user signature""" - patch_user_acl(self.user, { - 'can_have_signature': 1, - }) - self.user.is_signature_locked = False self.user.save() @@ -75,13 +59,9 @@ def test_post_empty_signature(self, patch_user_acl): self.assertFalse(response.json()['signature']) - @patch_user_acl - def test_post_too_long_signature(self, patch_user_acl): + @patch_user_acl({'can_have_signature': 1}) + def test_post_too_long_signature(self): """too long new signature errors""" - patch_user_acl(self.user, { - 'can_have_signature': 1, - }) - self.user.is_signature_locked = False self.user.save() @@ -96,13 +76,9 @@ def test_post_too_long_signature(self, patch_user_acl): "detail": "Signature is too long.", }) - @patch_user_acl - def test_post_good_signature(self, patch_user_acl): + @patch_user_acl({'can_have_signature': 1}) + def test_post_good_signature(self): """POST with good signature changes user signature""" - patch_user_acl(self.user, { - 'can_have_signature': 1, - }) - self.user.is_signature_locked = False self.user.save() diff --git a/misago/users/tests/test_user_username_api.py b/misago/users/tests/test_user_username_api.py index 9d584f8075..7ed51b830c 100644 --- a/misago/users/tests/test_user_username_api.py +++ b/misago/users/tests/test_user_username_api.py @@ -117,13 +117,9 @@ def setUp(self): self.link = '/api/users/%s/moderate-username/' % self.other_user.pk - @patch_user_acl - def test_no_permission(self, patch_user_acl): - """no permission to moderate avatar""" - patch_user_acl(self.user, { - 'can_rename_users': 0, - }) - + @patch_user_acl({'can_rename_users': 0}) + def test_no_permission(self): + """no permission to moderate username""" response = self.client.get(self.link) self.assertEqual(response.status_code, 403) self.assertEqual(response.json(), { @@ -136,13 +132,9 @@ def test_no_permission(self, patch_user_acl): "detail": "You can't rename users.", }) - @patch_user_acl - def test_moderate_username(self, patch_user_acl): + @patch_user_acl({'can_rename_users': 1}) + def test_moderate_username(self): """moderate username""" - patch_user_acl(self.user, { - 'can_rename_users': 1, - }) - response = self.client.get(self.link) self.assertEqual(response.status_code, 200) @@ -205,12 +197,8 @@ def test_moderate_username(self, patch_user_acl): self.assertEqual(options['username'], other_user.username) self.assertEqual(options['slug'], other_user.slug) - @patch_user_acl - def test_moderate_own_username(self, patch_user_acl): + @patch_user_acl({'can_rename_users': 1}) + def test_moderate_own_username(self): """moderate own username""" - patch_user_acl(self.user, { - 'can_rename_users': 1, - }) - response = self.client.get('/api/users/%s/moderate-username/' % self.user.pk) self.assertEqual(response.status_code, 200) diff --git a/misago/users/tests/test_usernamechanges_api.py b/misago/users/tests/test_usernamechanges_api.py index 988bd23de1..cb932a5304 100644 --- a/misago/users/tests/test_usernamechanges_api.py +++ b/misago/users/tests/test_usernamechanges_api.py @@ -7,44 +7,33 @@ def setUp(self): super().setUp() self.link = '/api/username-changes/' - @patch_user_acl - def test_user_can_always_see_his_name_changes(self, patch_user_acl): + @patch_user_acl({'can_see_users_name_history': False}) + def test_user_can_always_see_his_name_changes(self): """list returns own username changes""" self.user.set_username('NewUsername', self.user) - - patch_user_acl(self.user, {'can_see_users_name_history': False}) - response = self.client.get('%s?user=%s' % (self.link, self.user.pk)) self.assertEqual(response.status_code, 200) self.assertContains(response, self.user.username) - @patch_user_acl - def test_list_handles_invalid_filter(self, patch_user_acl): + @patch_user_acl({'can_see_users_name_history': True}) + def test_list_handles_invalid_filter(self): """list raises 404 for invalid filter""" self.user.set_username('NewUsername', self.user) - - patch_user_acl(self.user, {'can_see_users_name_history': True}) - response = self.client.get('%s?user=abcd' % self.link) self.assertEqual(response.status_code, 404) - @patch_user_acl - def test_list_handles_nonexisting_user(self, patch_user_acl): + @patch_user_acl({'can_see_users_name_history': True}) + def test_list_handles_nonexisting_user(self): """list raises 404 for invalid user id""" self.user.set_username('NewUsername', self.user) - - patch_user_acl(self.user, {'can_see_users_name_history': True}) - response = self.client.get('%s?user=142141' % self.link) self.assertEqual(response.status_code, 404) - @patch_user_acl - def test_list_handles_search(self, patch_user_acl): + @patch_user_acl({'can_see_users_name_history': False}) + def test_list_handles_search(self): """list returns found username changes""" self.user.set_username('NewUsername', self.user) - patch_user_acl(self.user, {'can_see_users_name_history': False}) - response = self.client.get('%s?user=%s&search=new' % (self.link, self.user.pk)) self.assertEqual(response.status_code, 200) self.assertContains(response, self.user.username) @@ -53,11 +42,9 @@ def test_list_handles_search(self, patch_user_acl): self.assertEqual(response.status_code, 200) self.assertEqual(response.json()["count"], 0) - @patch_user_acl - def test_list_denies_permission(self, patch_user_acl): + @patch_user_acl({'can_see_users_name_history': False}) + def test_list_denies_permission(self): """list denies permission for other user (or all) if no access""" - patch_user_acl(self.user, {'can_see_users_name_history': False}) - response = self.client.get('%s?user=%s' % (self.link, self.user.pk + 1)) self.assertEqual(response.status_code, 403) self.assertEqual(response.json(), { diff --git a/misago/users/tests/test_users_api.py b/misago/users/tests/test_users_api.py index 6b31ad0a7f..79cffccd10 100644 --- a/misago/users/tests/test_users_api.py +++ b/misago/users/tests/test_users_api.py @@ -421,13 +421,9 @@ def test_follow_myself(self): "detail": "You can't add yourself to followed.", }) - @patch_user_acl - def test_cant_follow(self, patch_user_acl): + @patch_user_acl({'can_follow_users': 0}) + def test_cant_follow(self): """no permission to follow users""" - patch_user_acl(self.user, { - 'can_follow_users': 0, - }) - response = self.client.post(self.link) self.assertEqual(response.status_code, 403) self.assertEqual(response.json(), { @@ -477,31 +473,25 @@ def setUp(self): self.link = '/api/users/%s/ban/' % self.other_user.pk - @patch_user_acl - def test_no_permission(self, patch_user_acl): + @patch_user_acl({'can_see_ban_details': 0}) + def test_no_permission(self): """user has no permission to access ban""" - patch_user_acl(self.user, {'can_see_ban_details': 0}) - response = self.client.get(self.link) self.assertEqual(response.status_code, 403) self.assertEqual(response.json(), { "detail": "You can't see users bans details.", }) - @patch_user_acl - def test_no_ban(self, patch_user_acl): + @patch_user_acl({'can_see_ban_details': 1}) + def test_no_ban(self): """api returns empty json""" - patch_user_acl(self.user, {'can_see_ban_details': 1}) - response = self.client.get(self.link) self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), {}) - @patch_user_acl - def test_ban_details(self, patch_user_acl): + @patch_user_acl({'can_see_ban_details': 1}) + def test_ban_details(self): """api returns ban json""" - patch_user_acl(self.user, {'can_see_ban_details': 1}) - Ban.objects.create( check_type=Ban.USERNAME, banned_value=self.other_user.username, @@ -608,32 +598,24 @@ def setUp(self): self.other_user.threads = 1 self.other_user.save() - @patch_user_acl - def test_delete_no_permission(self, patch_user_acl): + @patch_user_acl({ + 'can_delete_users_newer_than': 0, + 'can_delete_users_with_less_posts_than': 0, + }) + def test_delete_no_permission(self): """raises 403 error when no permission to delete""" - patch_user_acl( - self.user, { - 'can_delete_users_newer_than': 0, - 'can_delete_users_with_less_posts_than': 0, - } - ) - response = self.client.post(self.link) self.assertEqual(response.status_code, 403) self.assertEqual(response.json(), { 'detail': "You can't delete users.", }) - @patch_user_acl - def test_delete_too_many_posts(self, patch_user_acl): + @patch_user_acl({ + 'can_delete_users_newer_than': 0, + 'can_delete_users_with_less_posts_than': 5, + }) + def test_delete_too_many_posts(self): """raises 403 error when user has too many posts""" - patch_user_acl( - self.user, { - 'can_delete_users_newer_than': 0, - 'can_delete_users_with_less_posts_than': 5, - } - ) - self.other_user.posts = 6 self.other_user.save() @@ -643,16 +625,12 @@ def test_delete_too_many_posts(self, patch_user_acl): 'detail': "You can't delete users that made more than 5 posts.", }) - @patch_user_acl - def test_delete_too_old_member(self, patch_user_acl): + @patch_user_acl({ + 'can_delete_users_newer_than': 5, + 'can_delete_users_with_less_posts_than': 0, + }) + def test_delete_too_old_member(self): """raises 403 error when user is too old""" - patch_user_acl( - self.user, { - 'can_delete_users_newer_than': 5, - 'can_delete_users_with_less_posts_than': 0, - } - ) - self.other_user.joined_on -= timedelta(days=6) self.other_user.save() @@ -663,32 +641,24 @@ def test_delete_too_old_member(self, patch_user_acl): 'detail': "You can't delete users that are members for more than 5 days.", }) - @patch_user_acl - def test_delete_self(self, patch_user_acl): + @patch_user_acl({ + 'can_delete_users_newer_than': 10, + 'can_delete_users_with_less_posts_than': 10, + }) + def test_delete_self(self): """raises 403 error when attempting to delete oneself""" - patch_user_acl( - self.user, { - 'can_delete_users_newer_than': 10, - 'can_delete_users_with_less_posts_than': 10, - } - ) - response = self.client.post('/api/users/%s/delete/' % self.user.pk) self.assertEqual(response.status_code, 403) self.assertEqual(response.json(), { 'detail': "You can't delete your account.", }) - @patch_user_acl - def test_delete_admin(self, patch_user_acl): + @patch_user_acl({ + 'can_delete_users_newer_than': 10, + 'can_delete_users_with_less_posts_than': 10, + }) + def test_delete_admin(self): """raises 403 error when attempting to delete admin""" - patch_user_acl( - self.user, { - 'can_delete_users_newer_than': 10, - 'can_delete_users_with_less_posts_than': 10, - } - ) - self.other_user.is_staff = True self.other_user.save() @@ -698,16 +668,12 @@ def test_delete_admin(self, patch_user_acl): 'detail': "You can't delete administrators.", }) - @patch_user_acl - def test_delete_superadmin(self, patch_user_acl): + @patch_user_acl({ + 'can_delete_users_newer_than': 10, + 'can_delete_users_with_less_posts_than': 10, + }) + def test_delete_superadmin(self): """raises 403 error when attempting to delete superadmin""" - patch_user_acl( - self.user, { - 'can_delete_users_newer_than': 10, - 'can_delete_users_with_less_posts_than': 10, - } - ) - self.other_user.is_superuser = True self.other_user.save() @@ -717,16 +683,12 @@ def test_delete_superadmin(self, patch_user_acl): 'detail': "You can't delete administrators.", }) - @patch_user_acl - def test_delete_with_content(self, patch_user_acl): + @patch_user_acl({ + 'can_delete_users_newer_than': 10, + 'can_delete_users_with_less_posts_than': 10, + }) + def test_delete_with_content(self): """returns 200 and deletes user with content""" - patch_user_acl( - self.user, { - 'can_delete_users_newer_than': 10, - 'can_delete_users_with_less_posts_than': 10, - } - ) - response = self.client.post( self.link, json.dumps({ @@ -742,16 +704,12 @@ def test_delete_with_content(self, patch_user_acl): self.assertEqual(Thread.objects.count(), self.threads) self.assertEqual(Post.objects.count(), self.posts) - @patch_user_acl - def test_delete_without_content(self, patch_user_acl): + @patch_user_acl({ + 'can_delete_users_newer_than': 10, + 'can_delete_users_with_less_posts_than': 10, + }) + def test_delete_without_content(self): """returns 200 and deletes user without content""" - patch_user_acl( - self.user, { - 'can_delete_users_newer_than': 10, - 'can_delete_users_with_less_posts_than': 10, - } - ) - response = self.client.post( self.link, json.dumps({ From cc47a3bb71fe4d26d4e98fe27a8dbf929021e2fe Mon Sep 17 00:00:00 2001 From: rafalp Date: Sun, 9 Dec 2018 21:03:24 +0100 Subject: [PATCH 22/86] Update readtracker to use user_acl --- misago/readtracker/categoriestracker.py | 6 +-- .../tests/test_categoriestracker.py | 47 ++++++++++--------- .../readtracker/tests/test_threadstracker.py | 41 ++++++++-------- misago/readtracker/threadstracker.py | 4 +- 4 files changed, 52 insertions(+), 46 deletions(-) diff --git a/misago/readtracker/categoriestracker.py b/misago/readtracker/categoriestracker.py index 7c2172b590..4664d2c4c4 100644 --- a/misago/readtracker/categoriestracker.py +++ b/misago/readtracker/categoriestracker.py @@ -4,7 +4,7 @@ from .dates import get_cutoff_date -def make_read_aware(user, categories): +def make_read_aware(user, user_acl, categories): if not categories: return @@ -17,7 +17,7 @@ def make_read_aware(user, categories): return threads = Thread.objects.filter(category__in=categories) - threads = exclude_invisible_threads(user, categories, threads) + threads = exclude_invisible_threads(user_acl, categories, threads) queryset = Post.objects.filter( category__in=categories, @@ -26,7 +26,7 @@ def make_read_aware(user, categories): ).values_list('category', flat=True).distinct() queryset = queryset.exclude(id__in=user.postread_set.values('post')) - queryset = exclude_invisible_posts(user, categories, queryset) + queryset = exclude_invisible_posts(user_acl, categories, queryset) unread_categories = list(queryset) diff --git a/misago/readtracker/tests/test_categoriestracker.py b/misago/readtracker/tests/test_categoriestracker.py index 1538af6ce5..a3d1061197 100644 --- a/misago/readtracker/tests/test_categoriestracker.py +++ b/misago/readtracker/tests/test_categoriestracker.py @@ -4,6 +4,7 @@ from django.test import TestCase from django.utils import timezone +from misago.acl.useracl import get_user_acl from misago.categories.models import Category from misago.conf import settings from misago.core import cache, threadstore @@ -11,8 +12,9 @@ from misago.readtracker.models import PostRead from misago.threads import testutils +User = get_user_model() -UserModel = get_user_model() +cache_versions = {"acl": "abcdefgh"} class AnonymousUser(object): @@ -25,21 +27,22 @@ def setUp(self): cache.cache.clear() threadstore.clear() - self.user = UserModel.objects.create_user("UserA", "testa@user.com", 'Pass.123') + self.user = User.objects.create_user("UserA", "testa@user.com", 'Pass.123') + self.user_acl = get_user_acl(self.user, cache_versions) self.category = Category.objects.get(slug='first-category') def test_falsy_value(self): """passing falsy value to readtracker causes no errors""" - categoriestracker.make_read_aware(self.user, None) - categoriestracker.make_read_aware(self.user, False) - categoriestracker.make_read_aware(self.user, []) + categoriestracker.make_read_aware(self.user, self.user_acl, None) + categoriestracker.make_read_aware(self.user, self.user_acl, False) + categoriestracker.make_read_aware(self.user, self.user_acl, []) def test_anon_thread_before_cutoff(self): """non-tracked thread is marked as read for anonymous users""" started_on = timezone.now() - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF) testutils.post_thread(self.category, started_on=started_on) - categoriestracker.make_read_aware(AnonymousUser(), self.category) + categoriestracker.make_read_aware(AnonymousUser(), None, self.category) self.assertTrue(self.category.is_read) self.assertFalse(self.category.is_new) @@ -47,7 +50,7 @@ def test_anon_thread_after_cutoff(self): """tracked thread is marked as read for anonymous users""" testutils.post_thread(self.category, started_on=timezone.now()) - categoriestracker.make_read_aware(AnonymousUser(), self.category) + categoriestracker.make_read_aware(AnonymousUser(), None, self.category) self.assertTrue(self.category.is_read) self.assertFalse(self.category.is_new) @@ -56,7 +59,7 @@ def test_user_thread_before_cutoff(self): started_on = timezone.now() - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF) testutils.post_thread(self.category, started_on=started_on) - categoriestracker.make_read_aware(self.user, self.category) + categoriestracker.make_read_aware(self.user, self.user_acl, self.category) self.assertTrue(self.category.is_read) self.assertFalse(self.category.is_new) @@ -64,7 +67,7 @@ def test_user_unread_thread(self): """tracked thread is marked as unread for authenticated users""" testutils.post_thread(self.category, started_on=timezone.now()) - categoriestracker.make_read_aware(self.user, self.category) + categoriestracker.make_read_aware(self.user, self.user_acl, self.category) self.assertFalse(self.category.is_read) self.assertTrue(self.category.is_new) @@ -73,7 +76,7 @@ def test_user_created_after_thread(self): started_on = timezone.now() - timedelta(days=1) testutils.post_thread(self.category, started_on=started_on) - categoriestracker.make_read_aware(self.user, self.category) + categoriestracker.make_read_aware(self.user, self.user_acl, self.category) self.assertTrue(self.category.is_read) self.assertFalse(self.category.is_new) @@ -83,7 +86,7 @@ def test_user_read_post(self): poststracker.save_read(self.user, thread.first_post) - categoriestracker.make_read_aware(self.user, self.category) + categoriestracker.make_read_aware(self.user, self.user_acl, self.category) self.assertTrue(self.category.is_read) self.assertFalse(self.category.is_new) @@ -94,7 +97,7 @@ def test_user_first_unread_last_read_post(self): post = testutils.reply_thread(thread, posted_on=timezone.now()) poststracker.save_read(self.user, post) - categoriestracker.make_read_aware(self.user, self.category) + categoriestracker.make_read_aware(self.user, self.user_acl, self.category) self.assertFalse(self.category.is_read) self.assertTrue(self.category.is_new) @@ -105,7 +108,7 @@ def test_user_first_read_post_unread_event(self): testutils.reply_thread(thread, posted_on=timezone.now(), is_event=True) - categoriestracker.make_read_aware(self.user, self.category) + categoriestracker.make_read_aware(self.user, self.user_acl, self.category) self.assertFalse(self.category.is_read) self.assertTrue(self.category.is_new) @@ -120,7 +123,7 @@ def test_user_hidden_event(self): is_hidden=True, ) - categoriestracker.make_read_aware(self.user, self.category) + categoriestracker.make_read_aware(self.user, self.user_acl, self.category) self.assertFalse(self.category.is_read) self.assertTrue(self.category.is_new) @@ -136,7 +139,7 @@ def test_user_first_read_post_hidden_event(self): is_hidden=True, ) - categoriestracker.make_read_aware(self.user, self.category) + categoriestracker.make_read_aware(self.user, self.user_acl, self.category) self.assertTrue(self.category.is_read) self.assertFalse(self.category.is_new) @@ -145,7 +148,7 @@ def test_user_thread_before_cutoff_unread_post(self): started_on = timezone.now() - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF) testutils.post_thread(self.category, started_on=started_on) - categoriestracker.make_read_aware(self.user, self.category) + categoriestracker.make_read_aware(self.user, self.user_acl, self.category) self.assertTrue(self.category.is_read) self.assertFalse(self.category.is_new) @@ -160,7 +163,7 @@ def test_user_first_read_post_unapproved_post(self): is_unapproved=True, ) - categoriestracker.make_read_aware(self.user, self.category) + categoriestracker.make_read_aware(self.user, self.user_acl, self.category) self.assertTrue(self.category.is_read) self.assertFalse(self.category.is_new) @@ -176,7 +179,7 @@ def test_user_first_read_post_unapproved_own_post(self): is_unapproved=True, ) - categoriestracker.make_read_aware(self.user, self.category) + categoriestracker.make_read_aware(self.user, self.user_acl, self.category) self.assertFalse(self.category.is_read) self.assertTrue(self.category.is_new) @@ -192,7 +195,7 @@ def test_user_first_read_post_unapproved_own_post(self): is_unapproved=True, ) - categoriestracker.make_read_aware(self.user, self.category) + categoriestracker.make_read_aware(self.user, self.user_acl, self.category) self.assertFalse(self.category.is_read) self.assertTrue(self.category.is_new) @@ -204,7 +207,7 @@ def test_user_unapproved_thread_unread_post(self): is_unapproved=True, ) - categoriestracker.make_read_aware(self.user, self.category) + categoriestracker.make_read_aware(self.user, self.user_acl, self.category) self.assertTrue(self.category.is_read) self.assertFalse(self.category.is_new) @@ -217,7 +220,7 @@ def test_user_unapproved_own_thread_unread_post(self): is_unapproved=True, ) - categoriestracker.make_read_aware(self.user, self.category) + categoriestracker.make_read_aware(self.user, self.user_acl, self.category) self.assertFalse(self.category.is_read) self.assertTrue(self.category.is_new) @@ -229,6 +232,6 @@ def test_user_hidden_thread_unread_post(self): is_hidden=True, ) - categoriestracker.make_read_aware(self.user, self.category) + categoriestracker.make_read_aware(self.user, self.user_acl, self.category) self.assertTrue(self.category.is_read) self.assertFalse(self.category.is_new) diff --git a/misago/readtracker/tests/test_threadstracker.py b/misago/readtracker/tests/test_threadstracker.py index 2a33d72c04..da96627f6f 100644 --- a/misago/readtracker/tests/test_threadstracker.py +++ b/misago/readtracker/tests/test_threadstracker.py @@ -5,6 +5,7 @@ from django.utils import timezone from misago.acl import add_acl +from misago.acl.useracl import get_user_acl from misago.categories.models import Category from misago.conf import settings from misago.core import cache, threadstore @@ -12,8 +13,9 @@ from misago.readtracker.models import PostRead from misago.threads import testutils +User = get_user_model() -UserModel = get_user_model() +cache_versions = {"acl": "abcdefgh"} class AnonymousUser(object): @@ -26,23 +28,24 @@ def setUp(self): cache.cache.clear() threadstore.clear() - self.user = UserModel.objects.create_user("UserA", "testa@user.com", 'Pass.123') + self.user = User.objects.create_user("UserA", "testa@user.com", 'Pass.123') + self.user_acl = get_user_acl(self.user, cache_versions) self.category = Category.objects.get(slug='first-category') - add_acl(self.user, self.category) + add_acl(self.user_acl, self.category) def test_falsy_value(self): """passing falsy value to readtracker causes no errors""" - threadstracker.make_read_aware(self.user, None) - threadstracker.make_read_aware(self.user, False) - threadstracker.make_read_aware(self.user, []) + threadstracker.make_read_aware(self.user, self.user_acl, None) + threadstracker.make_read_aware(self.user, self.user_acl, False) + threadstracker.make_read_aware(self.user, self.user_acl, []) def test_anon_thread_before_cutoff(self): """non-tracked thread is marked as read for anonymous users""" started_on = timezone.now() - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF) thread = testutils.post_thread(self.category, started_on=started_on) - threadstracker.make_read_aware(AnonymousUser(), thread) + threadstracker.make_read_aware(AnonymousUser(), None, thread) self.assertTrue(thread.is_read) self.assertFalse(thread.is_new) @@ -50,7 +53,7 @@ def test_anon_thread_after_cutoff(self): """tracked thread is marked as read for anonymous users""" thread = testutils.post_thread(self.category, started_on=timezone.now()) - threadstracker.make_read_aware(AnonymousUser(), thread) + threadstracker.make_read_aware(AnonymousUser(), None, thread) self.assertTrue(thread.is_read) self.assertFalse(thread.is_new) @@ -59,7 +62,7 @@ def test_user_thread_before_cutoff(self): started_on = timezone.now() - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF) thread = testutils.post_thread(self.category, started_on=started_on) - threadstracker.make_read_aware(self.user, thread) + threadstracker.make_read_aware(self.user, self.user_acl, thread) self.assertTrue(thread.is_read) self.assertFalse(thread.is_new) @@ -67,7 +70,7 @@ def test_user_unread_thread(self): """tracked thread is marked as unread for authenticated users""" thread = testutils.post_thread(self.category, started_on=timezone.now()) - threadstracker.make_read_aware(self.user, thread) + threadstracker.make_read_aware(self.user, self.user_acl, thread) self.assertFalse(thread.is_read) self.assertTrue(thread.is_new) @@ -76,7 +79,7 @@ def test_user_created_after_thread(self): started_on = timezone.now() - timedelta(days=1) thread = testutils.post_thread(self.category, started_on=started_on) - threadstracker.make_read_aware(self.user, thread) + threadstracker.make_read_aware(self.user, self.user_acl, thread) self.assertTrue(thread.is_read) self.assertFalse(thread.is_new) @@ -86,7 +89,7 @@ def test_user_read_post(self): poststracker.save_read(self.user, thread.first_post) - threadstracker.make_read_aware(self.user, thread) + threadstracker.make_read_aware(self.user, self.user_acl, thread) self.assertTrue(thread.is_read) self.assertFalse(thread.is_new) @@ -97,7 +100,7 @@ def test_user_first_unread_last_read_post(self): post = testutils.reply_thread(thread, posted_on=timezone.now()) poststracker.save_read(self.user, post) - threadstracker.make_read_aware(self.user, thread) + threadstracker.make_read_aware(self.user, self.user_acl, thread) self.assertFalse(thread.is_read) self.assertTrue(thread.is_new) @@ -108,7 +111,7 @@ def test_user_first_read_post_unread_event(self): testutils.reply_thread(thread, posted_on=timezone.now(), is_event=True) - threadstracker.make_read_aware(self.user, thread) + threadstracker.make_read_aware(self.user, self.user_acl, thread) self.assertFalse(thread.is_read) self.assertTrue(thread.is_new) @@ -123,7 +126,7 @@ def test_user_hidden_event(self): is_hidden=True, ) - threadstracker.make_read_aware(self.user, thread) + threadstracker.make_read_aware(self.user, self.user_acl, thread) self.assertFalse(thread.is_read) self.assertTrue(thread.is_new) @@ -139,7 +142,7 @@ def test_user_first_read_post_hidden_event(self): is_hidden=True, ) - threadstracker.make_read_aware(self.user, thread) + threadstracker.make_read_aware(self.user, self.user_acl, thread) self.assertTrue(thread.is_read) self.assertFalse(thread.is_new) @@ -148,7 +151,7 @@ def test_user_thread_before_cutoff_unread_post(self): started_on = timezone.now() - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF) thread = testutils.post_thread(self.category, started_on=started_on) - threadstracker.make_read_aware(self.user, thread) + threadstracker.make_read_aware(self.user, self.user_acl, thread) self.assertTrue(thread.is_read) self.assertFalse(thread.is_new) @@ -163,7 +166,7 @@ def test_user_first_read_post_unapproved_post(self): is_unapproved=True, ) - threadstracker.make_read_aware(self.user, thread) + threadstracker.make_read_aware(self.user, self.user_acl, thread) self.assertTrue(thread.is_read) self.assertFalse(thread.is_new) @@ -179,6 +182,6 @@ def test_user_first_read_post_unapproved_own_post(self): is_unapproved=True, ) - threadstracker.make_read_aware(self.user, thread) + threadstracker.make_read_aware(self.user, self.user_acl, thread) self.assertFalse(thread.is_read) self.assertTrue(thread.is_new) diff --git a/misago/readtracker/threadstracker.py b/misago/readtracker/threadstracker.py index 05595562cf..31b7338031 100644 --- a/misago/readtracker/threadstracker.py +++ b/misago/readtracker/threadstracker.py @@ -4,7 +4,7 @@ from .dates import get_cutoff_date -def make_read_aware(user, threads): +def make_read_aware(user, user_acl, threads): if not threads: return @@ -24,7 +24,7 @@ def make_read_aware(user, threads): ).values_list('thread', flat=True).distinct() queryset = queryset.exclude(id__in=user.postread_set.values('post')) - queryset = exclude_invisible_posts(user, categories, queryset) + queryset = exclude_invisible_posts(user_acl, categories, queryset) unread_threads = list(queryset) From aebcf97564e1ea1118e4004ef51558c54e9551ea Mon Sep 17 00:00:00 2001 From: rafalp Date: Sun, 9 Dec 2018 21:04:39 +0100 Subject: [PATCH 23/86] Remove cache and global state dependency from readtracker tests --- misago/readtracker/tests/test_categoriestracker.py | 4 ---- misago/readtracker/tests/test_threadstracker.py | 4 ---- misago/threads/tests/test_threadview.py | 2 ++ 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/misago/readtracker/tests/test_categoriestracker.py b/misago/readtracker/tests/test_categoriestracker.py index a3d1061197..9bea2726ce 100644 --- a/misago/readtracker/tests/test_categoriestracker.py +++ b/misago/readtracker/tests/test_categoriestracker.py @@ -7,7 +7,6 @@ from misago.acl.useracl import get_user_acl from misago.categories.models import Category from misago.conf import settings -from misago.core import cache, threadstore from misago.readtracker import poststracker, categoriestracker from misago.readtracker.models import PostRead from misago.threads import testutils @@ -24,9 +23,6 @@ class AnonymousUser(object): class CategoriesTrackerTests(TestCase): def setUp(self): - cache.cache.clear() - threadstore.clear() - self.user = User.objects.create_user("UserA", "testa@user.com", 'Pass.123') self.user_acl = get_user_acl(self.user, cache_versions) self.category = Category.objects.get(slug='first-category') diff --git a/misago/readtracker/tests/test_threadstracker.py b/misago/readtracker/tests/test_threadstracker.py index da96627f6f..e9e8a68ba7 100644 --- a/misago/readtracker/tests/test_threadstracker.py +++ b/misago/readtracker/tests/test_threadstracker.py @@ -8,7 +8,6 @@ from misago.acl.useracl import get_user_acl from misago.categories.models import Category from misago.conf import settings -from misago.core import cache, threadstore from misago.readtracker import poststracker, threadstracker from misago.readtracker.models import PostRead from misago.threads import testutils @@ -25,9 +24,6 @@ class AnonymousUser(object): class ThreadsTrackerTests(TestCase): def setUp(self): - cache.cache.clear() - threadstore.clear() - self.user = User.objects.create_user("UserA", "testa@user.com", 'Pass.123') self.user_acl = get_user_acl(self.user, cache_versions) self.category = Category.objects.get(slug='first-category') diff --git a/misago/threads/tests/test_threadview.py b/misago/threads/tests/test_threadview.py index e1b3e71339..c40dc39d91 100644 --- a/misago/threads/tests/test_threadview.py +++ b/misago/threads/tests/test_threadview.py @@ -15,6 +15,8 @@ def __init__(self, user): self.user_ip = '127.0.0.1' +def patch_category_acl + class ThreadViewTestCase(AuthenticatedUserTestCase): def setUp(self): super().setUp() From d2be86e9053263c27fc7eb95eabb87a12aefeb42 Mon Sep 17 00:00:00 2001 From: rafalp Date: Sun, 9 Dec 2018 21:43:21 +0100 Subject: [PATCH 24/86] Fix tests for thread view --- misago/threads/tests/test_threadview.py | 305 +++++++++++------------- misago/threads/viewmodels/post.py | 2 +- misago/threads/viewmodels/posts.py | 4 +- misago/threads/viewmodels/thread.py | 2 +- 4 files changed, 147 insertions(+), 166 deletions(-) diff --git a/misago/threads/tests/test_threadview.py b/misago/threads/tests/test_threadview.py index c40dc39d91..db8efde6c7 100644 --- a/misago/threads/tests/test_threadview.py +++ b/misago/threads/tests/test_threadview.py @@ -1,4 +1,7 @@ -from misago.acl.testutils import override_acl +from unittest.mock import Mock + +from misago.acl import useracl +from misago.acl.test import patch_user_acl from misago.categories.models import Category from misago.conf import settings from misago.threads import testutils @@ -8,24 +11,15 @@ from misago.threads.moderation import hide_post from misago.users.testutils import AuthenticatedUserTestCase +cache_versions = {"acl": "abcdefgh"} -class MockRequest(object): - def __init__(self, user): - self.user = user - self.user_ip = '127.0.0.1' - - -def patch_category_acl - -class ThreadViewTestCase(AuthenticatedUserTestCase): - def setUp(self): - super().setUp() - self.category = Category.objects.get(slug='first-category') - self.thread = testutils.post_thread(category=self.category) +def patch_category_acl(new_acl=None): + def patch_acl(_, user_acl): + category = Category.objects.get(slug='first-category') + category_acl = user_acl['categories'][category.id] - def override_acl(self, acl=None): - category_acl = self.user.acl_cache['categories'][self.category.pk] + # reset category ACL to single predictable state category_acl.update({ 'can_see': 1, 'can_browse': 1, @@ -41,14 +35,18 @@ def override_acl(self, acl=None): 'can_hide_events': 0, }) - if acl: - category_acl.update(acl) + if new_acl: + category_acl.update(new_acl) - override_acl(self.user, { - 'categories': { - self.category.pk: category_acl, - }, - }) + return patch_user_acl(patch_acl) + + +class ThreadViewTestCase(AuthenticatedUserTestCase): + def setUp(self): + super().setUp() + + self.category = Category.objects.get(slug='first-category') + self.thread = testutils.post_thread(category=self.category) class ThreadVisibilityTests(ThreadViewTestCase): @@ -59,66 +57,57 @@ def test_thread_displays(self): def test_view_shows_owner_thread(self): """view handles "owned threads" only""" - self.override_acl({'can_see_all_threads': 0}) - - response = self.client.get(self.thread.get_absolute_url()) - self.assertEqual(response.status_code, 404) - - self.thread.starter = self.user - self.thread.save() + with patch_category_acl({'can_see_all_threads': 0}): + response = self.client.get(self.thread.get_absolute_url()) + self.assertEqual(response.status_code, 404) - self.override_acl({'can_see_all_threads': 0}) + self.thread.starter = self.user + self.thread.save() - response = self.client.get(self.thread.get_absolute_url()) - self.assertContains(response, self.thread.title) + response = self.client.get(self.thread.get_absolute_url()) + self.assertContains(response, self.thread.title) def test_view_validates_category_permissions(self): """view validates category visiblity""" - self.override_acl({'can_see': 0}) - - response = self.client.get(self.thread.get_absolute_url()) - self.assertEqual(response.status_code, 404) - - self.override_acl({'can_browse': 0}) + with patch_category_acl({'can_see': 0}): + response = self.client.get(self.thread.get_absolute_url()) + self.assertEqual(response.status_code, 404) - response = self.client.get(self.thread.get_absolute_url()) - self.assertEqual(response.status_code, 404) + with patch_category_acl({'can_browse': 0}): + response = self.client.get(self.thread.get_absolute_url()) + self.assertEqual(response.status_code, 404) def test_view_shows_unapproved_thread(self): """view handles unapproved thread""" - self.override_acl({'can_approve_content': 0}) - - self.thread.is_unapproved = True - self.thread.save() + with patch_category_acl({'can_approve_content': 0}): + self.thread.is_unapproved = True + self.thread.save() - response = self.client.get(self.thread.get_absolute_url()) - self.assertEqual(response.status_code, 404) + response = self.client.get(self.thread.get_absolute_url()) + self.assertEqual(response.status_code, 404) # grant permission to see unapproved content - self.override_acl({'can_approve_content': 1}) - - response = self.client.get(self.thread.get_absolute_url()) - self.assertContains(response, self.thread.title) - - # make test user thread's owner and remove permission to see unapproved - # user should be able to see thread as its author anyway - self.thread.starter = self.user - self.thread.save() + with patch_category_acl({'can_approve_content': 1}): + response = self.client.get(self.thread.get_absolute_url()) + self.assertContains(response, self.thread.title) - self.override_acl({'can_approve_content': 0}) + # make test user thread's owner and remove permission to see unapproved + # user should be able to see thread as its author anyway + self.thread.starter = self.user + self.thread.save() - response = self.client.get(self.thread.get_absolute_url()) - self.assertContains(response, self.thread.title) + with patch_category_acl({'can_approve_content': 0}): + response = self.client.get(self.thread.get_absolute_url()) + self.assertContains(response, self.thread.title) def test_view_shows_hidden_thread(self): """view handles hidden thread""" - self.override_acl({'can_hide_threads': 0}) - - self.thread.is_hidden = True - self.thread.save() + with patch_category_acl({'can_hide_threads': 0}): + self.thread.is_hidden = True + self.thread.save() - response = self.client.get(self.thread.get_absolute_url()) - self.assertEqual(response.status_code, 404) + response = self.client.get(self.thread.get_absolute_url()) + self.assertEqual(response.status_code, 404) # threads owners are not extempt from hidden threads check self.thread.starter = self.user @@ -128,10 +117,9 @@ def test_view_shows_hidden_thread(self): self.assertEqual(response.status_code, 404) # grant permission to see hidden content - self.override_acl({'can_hide_threads': 1}) - - response = self.client.get(self.thread.get_absolute_url()) - self.assertContains(response, self.thread.title) + with patch_category_acl({'can_hide_threads': 1}): + response = self.client.get(self.thread.get_absolute_url()) + self.assertContains(response, self.thread.title) class ThreadPostsVisibilityTests(ThreadViewTestCase): @@ -174,23 +162,21 @@ def test_hidden_post_visibility(self): self.assertNotContains(response, post.parsed) # permission to hide own posts isn't enought to see post content - self.override_acl({'can_hide_own_posts': 1}) - - response = self.client.get(self.thread.get_absolute_url()) - self.assertContains(response, post.get_absolute_url()) - self.assertContains(response, "This post is hidden. You cannot not see its contents.") - self.assertNotContains(response, post.parsed) + with patch_category_acl({'can_hide_own_posts': 1}): + response = self.client.get(self.thread.get_absolute_url()) + self.assertContains(response, post.get_absolute_url()) + self.assertContains(response, "This post is hidden. You cannot not see its contents.") + self.assertNotContains(response, post.parsed) # post's content is displayed after permission to see posts is granted - self.override_acl({'can_hide_posts': 1}) - - response = self.client.get(self.thread.get_absolute_url()) - self.assertContains(response, post.get_absolute_url()) - self.assertContains( - response, "This post is hidden. Only users with permission may see its contents." - ) - self.assertNotContains(response, "This post is hidden. You cannot not see its contents.") - self.assertContains(response, post.parsed) + with patch_category_acl({'can_hide_posts': 1}): + response = self.client.get(self.thread.get_absolute_url()) + self.assertContains(response, post.get_absolute_url()) + self.assertContains( + response, "This post is hidden. Only users with permission may see its contents." + ) + self.assertNotContains(response, "This post is hidden. You cannot not see its contents.") + self.assertContains(response, post.parsed) def test_unapproved_post_visibility(self): """unapproved post renders for its author and users with perm to approve content""" @@ -201,23 +187,21 @@ def test_unapproved_post_visibility(self): self.assertNotContains(response, post.get_absolute_url()) # post displays because we have permission to approve unapproved content - self.override_acl({'can_approve_content': 1}) - - response = self.client.get(self.thread.get_absolute_url()) - self.assertContains(response, post.get_absolute_url()) - self.assertContains(response, "This post is unapproved.") - self.assertContains(response, post.parsed) + with patch_category_acl({'can_approve_content': 1}): + response = self.client.get(self.thread.get_absolute_url()) + self.assertContains(response, post.get_absolute_url()) + self.assertContains(response, "This post is unapproved.") + self.assertContains(response, post.parsed) # post displays because we are its author - post.poster = self.user - post.save() - - self.override_acl({'can_approve_content': 0}) + with patch_category_acl({'can_approve_content': 0}): + post.poster = self.user + post.save() - response = self.client.get(self.thread.get_absolute_url()) - self.assertContains(response, post.get_absolute_url()) - self.assertContains(response, "This post is unapproved.") - self.assertContains(response, post.parsed) + response = self.client.get(self.thread.get_absolute_url()) + self.assertContains(response, post.get_absolute_url()) + self.assertContains(response, "This post is unapproved.") + self.assertContains(response, post.parsed) class ThreadEventVisibilityTests(ThreadViewTestCase): @@ -238,51 +222,50 @@ def test_thread_events_render(self): self.thread.save() for action, message in TEST_ACTIONS: - self.override_acl({'can_approve_content': 1, 'can_hide_threads': 1}) - self.thread.post_set.filter(is_event=True).delete() - action(MockRequest(self.user), self.thread) - event = self.thread.post_set.filter(is_event=True)[0] + with patch_category_acl({'can_approve_content': 1, 'can_hide_threads': 1}): + user_acl = useracl.get_user_acl(self.user, cache_versions) + request = Mock(user=self.user, user_acl=user_acl, user_ip="127.0.0.1") + action(request, self.thread) - # event renders - response = self.client.get(self.thread.get_absolute_url()) - self.assertContains(response, event.get_absolute_url()) - self.assertContains(response, message) + event = self.thread.post_set.filter(is_event=True)[0] - # hidden events don't render without permission - hide_post(self.user, event) - self.override_acl({'can_approve_content': 1, 'can_hide_threads': 1}) + # event renders + response = self.client.get(self.thread.get_absolute_url()) + self.assertContains(response, event.get_absolute_url()) + self.assertContains(response, message) - response = self.client.get(self.thread.get_absolute_url()) - self.assertNotContains(response, event.get_absolute_url()) - self.assertNotContains(response, message) + # hidden events don't render without permission + with patch_category_acl({'can_approve_content': 1, 'can_hide_threads': 1}): + hide_post(self.user, event) + response = self.client.get(self.thread.get_absolute_url()) + self.assertNotContains(response, event.get_absolute_url()) + self.assertNotContains(response, message) # hidden event renders with permission - hide_post(self.user, event) - self.override_acl({ + with patch_category_acl({ 'can_approve_content': 1, 'can_hide_threads': 1, 'can_hide_events': 1, - }) - - response = self.client.get(self.thread.get_absolute_url()) - self.assertContains(response, event.get_absolute_url()) - self.assertContains(response, message) - self.assertContains(response, "Hidden by") + }): + hide_post(self.user, event) + response = self.client.get(self.thread.get_absolute_url()) + self.assertContains(response, event.get_absolute_url()) + self.assertContains(response, message) + self.assertContains(response, "Hidden by") # Event is only loaded if thread has events flag - self.thread.has_events = False - self.thread.save() - - self.override_acl({ + with patch_category_acl({ 'can_approve_content': 1, 'can_hide_threads': 1, 'can_hide_events': 1, - }) + }): + self.thread.has_events = False + self.thread.save() - response = self.client.get(self.thread.get_absolute_url()) - self.assertNotContains(response, event.get_absolute_url()) + response = self.client.get(self.thread.get_absolute_url()) + self.assertNotContains(response, event.get_absolute_url()) def test_events_limit(self): """forum will trim oldest events if theres more than allowed by config""" @@ -290,7 +273,8 @@ def test_events_limit(self): events = [] for _ in range(events_limit + 5): - event = record_event(MockRequest(self.user), self.thread, 'closed') + request = Mock(user=self.user, user_ip="127.0.0.1") + event = record_event(request, self.thread, 'closed') events.append(event) # test that only events within limits were rendered @@ -308,7 +292,8 @@ def test_events_dont_take_space(self): events = [] for _ in range(events_limit + 5): - event = record_event(MockRequest(self.user), self.thread, 'closed') + request = Mock(user=self.user, user_ip="127.0.0.1") + event = record_event(request, self.thread, 'closed') events.append(event) posts = [] @@ -328,7 +313,8 @@ def test_events_dont_take_space(self): for _ in range(posts_limit): post = testutils.reply_thread(self.thread) for _ in range(events_limit): - event = record_event(MockRequest(self.user), self.thread, 'closed') + request = Mock(user=self.user, user_ip="127.0.0.1") + event = record_event(request, self.thread, 'closed') events.append(event) # see first page @@ -348,8 +334,9 @@ def test_events_dont_take_space(self): def test_changed_thread_title_event_renders(self): """changed thread title event renders""" + request = Mock(user=self.user, user_ip="127.0.0.1") threads_moderation.change_thread_title( - MockRequest(self.user), self.thread, "Lorem renamed ipsum!" + request, self.thread, "Lorem renamed ipsum!" ) event = self.thread.post_set.filter(is_event=True)[0] @@ -366,7 +353,8 @@ def test_thread_move_event_renders(self): self.thread.category = self.thread.category.parent self.thread.save() - threads_moderation.move_thread(MockRequest(self.user), self.thread, self.category) + request = Mock(user=self.user, user_ip="127.0.0.1") + threads_moderation.move_thread(request, self.thread, self.category) event = self.thread.post_set.filter(is_event=True)[0] self.assertEqual(event.event_type, 'moved') @@ -378,8 +366,9 @@ def test_thread_move_event_renders(self): def test_thread_merged_event_renders(self): """merged thread event renders""" + request = Mock(user=self.user, user_ip="127.0.0.1") other_thread = testutils.post_thread(category=self.category) - threads_moderation.merge_thread(MockRequest(self.user), self.thread, other_thread) + threads_moderation.merge_thread(request, self.thread, other_thread) event = self.thread.post_set.filter(is_event=True)[0] self.assertEqual(event.event_type, 'merged') @@ -496,21 +485,22 @@ def test_liked_posts_no_permission(self): """ testutils.like_post(self.thread.first_post, self.user) - self.override_acl({'can_see_posts_likes': 0}) - - response = self.client.get(self.thread.get_absolute_url()) - self.assertNotContains(response, '"is_liked": true') - self.assertNotContains(response, '"is_liked": false') - self.assertContains(response, '"is_liked": null') + with patch_category_acl({'can_see_posts_likes': 0}): + response = self.client.get(self.thread.get_absolute_url()) + self.assertNotContains(response, '"is_liked": true') + self.assertNotContains(response, '"is_liked": false') + self.assertContains(response, '"is_liked": null') class ThreadAnonViewTests(ThreadViewTestCase): def test_anonymous_user_view_no_showstoppers_display(self): """kitchensink thread view has no showstoppers for anons""" + request = Mock(user=self.user, user_ip="127.0.0.1") + poll = testutils.post_poll(self.thread, self.user) - event = record_event(MockRequest(self.user), self.thread, 'closed') + event = record_event(request, self.thread, 'closed') - hidden_event = record_event(MockRequest(self.user), self.thread, 'opened') + hidden_event = record_event(request, self.thread, 'opened') hide_post(self.user, hidden_event) unapproved_post = testutils.reply_thread(self.thread, is_unapproved=True) @@ -530,26 +520,21 @@ class ThreadUnicodeSupportTests(ThreadViewTestCase): def test_category_name(self): """unicode in category name causes no showstopper""" self.category.name = 'Łódź' - self.category.slug = 'Lodz' - self.category.save() - self.override_acl() - - response = self.client.get(self.thread.get_absolute_url()) - self.assertEqual(response.status_code, 200) + with patch_category_acl(): + response = self.client.get(self.thread.get_absolute_url()) + self.assertEqual(response.status_code, 200) def test_thread_title(self): """unicode in thread title causes no showstopper""" self.thread.title = 'Łódź' self.thread.slug = 'Lodz' - self.thread.save() - self.override_acl() - - response = self.client.get(self.thread.get_absolute_url()) - self.assertEqual(response.status_code, 200) + with patch_category_acl(): + response = self.client.get(self.thread.get_absolute_url()) + self.assertEqual(response.status_code, 200) def test_post_content(self): """unicode in thread title causes no showstopper""" @@ -557,24 +542,20 @@ def test_post_content(self): self.thread.first_post.parsed = '

Łódź

' update_post_checksum(self.thread.first_post) - self.thread.first_post.save() - self.override_acl() - - response = self.client.get(self.thread.get_absolute_url()) - self.assertEqual(response.status_code, 200) + with patch_category_acl(): + response = self.client.get(self.thread.get_absolute_url()) + self.assertEqual(response.status_code, 200) def test_user_rank(self): """unicode in user rank causes no showstopper""" self.user.title = 'Łódź' self.user.rank.name = 'Łódź' self.user.rank.title = 'Łódź' - self.user.rank.save() self.user.save() - self.override_acl() - - response = self.client.get(self.thread.get_absolute_url()) - self.assertEqual(response.status_code, 200) + with patch_category_acl(): + response = self.client.get(self.thread.get_absolute_url()) + self.assertEqual(response.status_code, 200) diff --git a/misago/threads/viewmodels/post.py b/misago/threads/viewmodels/post.py index 26f42fe118..7c21f94e5b 100644 --- a/misago/threads/viewmodels/post.py +++ b/misago/threads/viewmodels/post.py @@ -36,7 +36,7 @@ def get_post(self, request, thread, pk): return post def get_queryset(self, request, thread): - return exclude_invisible_posts(request.user, thread.category, thread.post_set) + return exclude_invisible_posts(request.user_acl, thread.category, thread.post_set) class ThreadPost(ViewModel): diff --git a/misago/threads/viewmodels/posts.py b/misago/threads/viewmodels/posts.py index 763a4c7b4b..4dbf9831bc 100644 --- a/misago/threads/viewmodels/posts.py +++ b/misago/threads/viewmodels/posts.py @@ -77,7 +77,7 @@ def get_posts_queryset(self, request, thread): 'poster__ban_cache', 'poster__online_tracker', ).filter(is_event=False).order_by('id') - return exclude_invisible_posts(request.user, thread.category, queryset) + return exclude_invisible_posts(request.user_acl, thread.category, queryset) def get_events_queryset(self, request, thread, limit, first_post=None, last_post=None): queryset = thread.post_set.select_related( @@ -93,7 +93,7 @@ def get_events_queryset(self, request, thread, limit, first_post=None, last_post if last_post: queryset = queryset.filter(pk__lt=last_post.pk) - queryset = exclude_invisible_posts(request.user, thread.category, queryset) + queryset = exclude_invisible_posts(request.user_acl, thread.category, queryset) return list(queryset.order_by('-id')[:limit]) def get_frontend_context(self): diff --git a/misago/threads/viewmodels/thread.py b/misago/threads/viewmodels/thread.py index aaa2d0da0e..11125b4871 100644 --- a/misago/threads/viewmodels/thread.py +++ b/misago/threads/viewmodels/thread.py @@ -48,7 +48,7 @@ def __init__( add_acl(request.user_acl, model) if read_aware: - make_read_aware(request.user, model) + make_read_aware(request.user, request.user_acl, model) if subscription_aware: make_subscription_aware(request.user, model) From 0115195d17c6eb829f5c607d1599fc05c5c4d8ec Mon Sep 17 00:00:00 2001 From: rafalp Date: Sun, 9 Dec 2018 23:33:19 +0100 Subject: [PATCH 25/86] Move more tests to patch_user_acl --- misago/categories/api.py | 2 +- misago/categories/utils.py | 8 +- misago/categories/views/categorieslist.py | 2 +- misago/threads/tests/test_threadslists.py | 412 +++++++--------------- misago/threads/viewmodels/category.py | 6 +- misago/threads/viewmodels/threads.py | 18 +- 6 files changed, 143 insertions(+), 305 deletions(-) diff --git a/misago/categories/api.py b/misago/categories/api.py index a01b3d6aec..b0ddf70c65 100644 --- a/misago/categories/api.py +++ b/misago/categories/api.py @@ -7,5 +7,5 @@ class CategoryViewSet(viewsets.ViewSet): def list(self, request): - categories_tree = get_categories_tree(request.user, join_posters=True) + categories_tree = get_categories_tree(request.user, request.user_acl, join_posters=True) return Response(CategorySerializer(categories_tree, many=True).data) diff --git a/misago/categories/utils.py b/misago/categories/utils.py index bae3ecfe9b..6e514736f2 100644 --- a/misago/categories/utils.py +++ b/misago/categories/utils.py @@ -4,7 +4,7 @@ from .models import Category -def get_categories_tree(user, parent=None, join_posters=False): +def get_categories_tree(user, user_acl, parent=None, join_posters=False): if not user.acl_cache['visible_categories']: return [] @@ -13,7 +13,7 @@ def get_categories_tree(user, parent=None, join_posters=False): else: queryset = Category.objects.all_categories() - queryset_with_acl = queryset.filter(id__in=user.acl_cache['visible_categories']) + queryset_with_acl = queryset.filter(id__in=user_acl['visible_categories']) if join_posters: queryset_with_acl = queryset_with_acl.select_related('last_poster') @@ -32,8 +32,8 @@ def get_categories_tree(user, parent=None, join_posters=False): if category.parent_id and category.level > parent_level: categories_dict[category.parent_id].subcategories.append(category) - add_acl(user, categories_list) - categoriestracker.make_read_aware(user, categories_list) + add_acl(user_acl, categories_list) + categoriestracker.make_read_aware(user, user_acl, categories_list) for category in reversed(visible_categories): if category.acl['can_browse']: diff --git a/misago/categories/views/categorieslist.py b/misago/categories/views/categorieslist.py index 19012111f1..7603f094ca 100644 --- a/misago/categories/views/categorieslist.py +++ b/misago/categories/views/categorieslist.py @@ -6,7 +6,7 @@ def categories(request): - categories_tree = get_categories_tree(request.user, join_posters=True) + categories_tree = get_categories_tree(request.user, request.user_acl, join_posters=True) request.frontend_context.update({ 'CATEGORIES': CategorySerializer(categories_tree, many=True).data, diff --git a/misago/threads/tests/test_threadslists.py b/misago/threads/tests/test_threadslists.py index f5e34d9ae0..36287668a3 100644 --- a/misago/threads/tests/test_threadslists.py +++ b/misago/threads/tests/test_threadslists.py @@ -4,7 +4,7 @@ from django.utils import timezone from django.utils.encoding import smart_str -from misago.acl.testutils import override_acl +from misago.acl.test import patch_user_acl from misago.categories.models import Category from misago.conf import settings from misago.readtracker import poststracker @@ -12,10 +12,48 @@ from misago.users.models import AnonymousUser from misago.users.testutils import AuthenticatedUserTestCase - LISTS_URLS = ('', 'my/', 'new/', 'unread/', 'subscribed/', ) +def patch_categories_acl(category_acl=None, base_acl=None): + def patch_acl(_, user_acl): + first_category = Category.objects.get(slug='first-category') + first_category_acl = user_acl['categories'][first_category.id].copy() + + user_acl.update({ + 'categories': {}, + 'visible_categories': [], + 'browseable_categories': [], + 'can_approve_content': [], + }) + + # copy first category's acl to other categories to make base for overrides + for category in Category.objects.all_categories(): + user_acl['categories'][category.id] = first_category_acl + + if base_acl: + user_acl.update(base_acl) + + for category in Category.objects.all_categories(): + user_acl['visible_categories'].append(category.id) + user_acl['browseable_categories'].append(category.id) + user_acl['categories'][category.id].update({ + 'can_see': 1, + 'can_browse': 1, + 'can_see_all_threads': 1, + 'can_see_own_threads': 0, + 'can_hide_threads': 0, + 'can_approve_content': 0, + }) + + if category_acl: + user_acl['categories'][category.id].update(category_acl) + if category_acl.get('can_approve_content'): + user_acl['can_approve_content'].append(category.id) + + return patch_user_acl(patch_acl) + + class ThreadsListTestCase(AuthenticatedUserTestCase): def setUp(self): """ @@ -120,46 +158,6 @@ def setUp(self): self.category_e = Category.objects.get(slug='category-e') self.category_f = Category.objects.get(slug='category-f') - self.access_all_categories() - - def access_all_categories(self, category_acl=None, base_acl=None): - self.clear_state() - - categories_acl = { - 'categories': {}, - 'visible_categories': [], - 'browseable_categories': [], - 'can_approve_content': [], - } - - # copy first category's acl to other categories to make base for overrides - first_category_acl = self.user.acl_cache['categories'][self.first_category.pk].copy() - for category in Category.objects.all_categories(): - categories_acl['categories'][category.pk] = first_category_acl - - if base_acl: - categories_acl.update(base_acl) - - for category in Category.objects.all_categories(): - categories_acl['visible_categories'].append(category.pk) - categories_acl['browseable_categories'].append(category.pk) - categories_acl['categories'][category.pk].update({ - 'can_see': 1, - 'can_browse': 1, - 'can_see_all_threads': 1, - 'can_see_own_threads': 0, - 'can_hide_threads': 0, - 'can_approve_content': 0, - }) - - if category_acl: - categories_acl['categories'][category.pk].update(category_acl) - if category_acl.get('can_approve_content'): - categories_acl['can_approve_content'].append(category.pk) - - override_acl(self.user, categories_acl) - return categories_acl - def assertContainsThread(self, response, thread): self.assertContains(response, ' href="%s"' % thread.get_absolute_url()) @@ -185,11 +183,10 @@ def test_invalid_list_type(self): class AllThreadsListTests(ThreadsListTestCase): + @patch_categories_acl() def test_list_renders_empty(self): """empty threads list renders""" for url in LISTS_URLS: - self.access_all_categories() - response = self.client.get('/' + url) self.assertEqual(response.status_code, 200) self.assertContains(response, "empty-message") @@ -198,8 +195,6 @@ def test_list_renders_empty(self): else: self.assertContains(response, "There are no threads on this forum") - self.access_all_categories() - response = self.client.get(self.category_b.get_absolute_url() + url) self.assertEqual(response.status_code, 200) self.assertContains(response, self.category_b.name) @@ -209,8 +204,6 @@ def test_list_renders_empty(self): else: self.assertContains(response, "There are no threads in this category") - self.access_all_categories() - response = self.client.get('%s?list=%s' % (self.api_link, url.strip('/') or 'all')) self.assertEqual(response.status_code, 200) @@ -221,46 +214,34 @@ def test_list_renders_empty(self): self.logout_user() self.user = self.get_anonymous_user() - self.access_all_categories() - response = self.client.get('/') self.assertEqual(response.status_code, 200) self.assertContains(response, "empty-message") self.assertContains(response, "There are no threads on this forum") - self.access_all_categories() - response = self.client.get(self.category_b.get_absolute_url()) self.assertEqual(response.status_code, 200) self.assertContains(response, self.category_b.name) self.assertContains(response, "empty-message") self.assertContains(response, "There are no threads in this category") - self.access_all_categories() - response = self.client.get('%s?list=all' % self.api_link) self.assertEqual(response.status_code, 200) response_json = response.json() self.assertEqual(len(response_json['results']), 0) + @patch_categories_acl() def test_list_authenticated_only_views(self): """authenticated only views return 403 for guests""" for url in LISTS_URLS: - self.access_all_categories() - response = self.client.get('/' + url) self.assertEqual(response.status_code, 200) - self.access_all_categories() - response = self.client.get(self.category_b.get_absolute_url() + url) self.assertEqual(response.status_code, 200) self.assertContains(response, self.category_b.name) - self.access_all_categories() - - self.access_all_categories() response = self.client.get( '%s?category=%s&list=%s' % (self.api_link, self.category_b.pk, url.strip('/') or 'all', ) @@ -270,22 +251,19 @@ def test_list_authenticated_only_views(self): self.logout_user() self.user = self.get_anonymous_user() for url in LISTS_URLS[1:]: - self.access_all_categories() - response = self.client.get('/' + url) self.assertEqual(response.status_code, 403) - self.access_all_categories() response = self.client.get(self.category_b.get_absolute_url() + url) self.assertEqual(response.status_code, 403) - self.access_all_categories() response = self.client.get( '%s?category=%s&list=%s' % (self.api_link, self.category_b.pk, url.strip('/') or 'all', ) ) self.assertEqual(response.status_code, 403) + @patch_categories_acl() def test_list_renders_categories_picker(self): """categories picker renders valid categories""" Category( @@ -316,7 +294,6 @@ def test_list_renders_categories_picker(self): # hidden category self.assertNotContains(response, 'subcategory-%s' % test_category.css_class) - self.access_all_categories() response = self.client.get(self.api_link) self.assertEqual(response.status_code, 200) @@ -325,11 +302,8 @@ def test_list_renders_categories_picker(self): self.assertNotIn(self.category_b.pk, response_json['subcategories']) # test category view - self.access_all_categories() - response = self.client.get(self.category_a.get_absolute_url()) self.assertEqual(response.status_code, 200) - self.assertContains(response, 'subcategory-%s' % self.category_b.css_class) # readable categories, but non-accessible directly @@ -337,7 +311,6 @@ def test_list_renders_categories_picker(self): self.assertNotContains(response, 'subcategory-%s' % self.category_d.css_class) self.assertNotContains(response, 'subcategory-%s' % self.category_f.css_class) - self.access_all_categories() response = self.client.get('%s?category=%s' % (self.api_link, self.category_a.pk)) self.assertEqual(response.status_code, 200) @@ -462,7 +435,7 @@ def test_access_hidden_category(self): response = self.client.get(test_category.get_absolute_url() + url) self.assertEqual(response.status_code, 404) - response = self.client.get('%s?category=%s' % (self.api_link, test_category.pk)) + response = self.client.get('%s?category=%s' % (self.api_link, test_category.id)) self.assertEqual(response.status_code, 404) def test_access_protected_category(self): @@ -478,37 +451,23 @@ def test_access_protected_category(self): test_category = Category.objects.get(slug='hidden-category') for url in LISTS_URLS: - override_acl( - self.user, { - 'visible_categories': [test_category.pk], - 'browseable_categories': [], - 'categories': { - test_category.pk: { - 'can_see': 1, - 'can_browse': 0, - }, + with patch_user_acl({ + 'visible_categories': [test_category.id], + 'browseable_categories': [], + 'categories': { + test_category.id: { + 'can_see': 1, + 'can_browse': 0, }, - } - ) - response = self.client.get(test_category.get_absolute_url() + url) - self.assertEqual(response.status_code, 403) + }, + }): + response = self.client.get(test_category.get_absolute_url() + url) + self.assertEqual(response.status_code, 403) - override_acl( - self.user, { - 'visible_categories': [test_category.pk], - 'browseable_categories': [], - 'categories': { - test_category.pk: { - 'can_see': 1, - 'can_browse': 0, - }, - }, - } - ) - response = self.client.get( - '%s?category=%s&list=%s' % (self.api_link, test_category.pk, url.strip('/'), ) - ) - self.assertEqual(response.status_code, 403) + response = self.client.get( + '%s?category=%s&list=%s' % (self.api_link, test_category.id, url.strip('/')) + ) + self.assertEqual(response.status_code, 403) def test_display_pinned_threads(self): """ @@ -550,7 +509,7 @@ def test_display_pinned_threads(self): self.assertTrue(positions['s'] > positions['g']) # API behaviour is identic - response = self.client.get('/api/threads/?category=%s' % self.first_category.pk) + response = self.client.get('/api/threads/?category=%s' % self.first_category.id) self.assertEqual(response.status_code, 200) content = smart_str(response.content) @@ -574,6 +533,7 @@ def test_display_pinned_threads(self): class ThreadsVisibilityTests(ThreadsListTestCase): + @patch_categories_acl() def test_list_renders_test_thread(self): """list renders test thread with valid top category""" test_thread = testutils.post_thread( @@ -592,7 +552,6 @@ def test_list_renders_test_thread(self): self.assertContains(response, 'thread-detail-category-%s' % self.category_c.css_class) # api displays same data - self.access_all_categories() response = self.client.get(self.api_link) self.assertEqual(response.status_code, 200) @@ -602,7 +561,6 @@ def test_list_renders_test_thread(self): self.assertIn(self.category_a.pk, response_json['subcategories']) # test category view - self.access_all_categories() response = self.client.get(self.category_b.get_absolute_url()) self.assertEqual(response.status_code, 200) @@ -613,7 +571,6 @@ def test_list_renders_test_thread(self): self.assertContains(response, 'thread-detail-category-%s' % self.category_c.css_class) # api displays same data - self.access_all_categories() response = self.client.get('%s?category=%s' % (self.api_link, self.category_b.pk)) self.assertEqual(response.status_code, 200) @@ -664,6 +621,7 @@ def test_api_hides_hidden_thread(self): response_json = response.json() self.assertEqual(len(response_json['results']), 0) + @patch_categories_acl() def test_list_user_see_own_unapproved_thread(self): """list renders unapproved thread that belongs to viewer""" test_thread = testutils.post_thread( @@ -677,13 +635,13 @@ def test_list_user_see_own_unapproved_thread(self): self.assertContainsThread(response, test_thread) # test api - self.access_all_categories() response = self.client.get(self.api_link) self.assertEqual(response.status_code, 200) response_json = response.json() self.assertEqual(response_json['results'][0]['id'], test_thread.pk) + @patch_categories_acl() def test_list_user_cant_see_unapproved_thread(self): """list hides unapproved thread that belongs to other user""" test_thread = testutils.post_thread( @@ -696,13 +654,13 @@ def test_list_user_cant_see_unapproved_thread(self): self.assertNotContainsThread(response, test_thread) # test api - self.access_all_categories() response = self.client.get(self.api_link) self.assertEqual(response.status_code, 200) response_json = response.json() self.assertEqual(len(response_json['results']), 0) + @patch_categories_acl() def test_list_user_cant_see_hidden_thread(self): """list hides hidden thread that belongs to other user""" test_thread = testutils.post_thread( @@ -715,13 +673,13 @@ def test_list_user_cant_see_hidden_thread(self): self.assertNotContainsThread(response, test_thread) # test api - self.access_all_categories() response = self.client.get(self.api_link) self.assertEqual(response.status_code, 200) response_json = response.json() self.assertEqual(len(response_json['results']), 0) + @patch_categories_acl() def test_list_user_cant_see_own_hidden_thread(self): """list hides hidden thread that belongs to viewer""" test_thread = testutils.post_thread( @@ -735,13 +693,13 @@ def test_list_user_cant_see_own_hidden_thread(self): self.assertNotContainsThread(response, test_thread) # test api - self.access_all_categories() response = self.client.get(self.api_link) self.assertEqual(response.status_code, 200) response_json = response.json() self.assertEqual(len(response_json['results']), 0) + @patch_categories_acl({'can_hide_threads': 1}) def test_list_user_can_see_own_hidden_thread(self): """list shows hidden thread that belongs to viewer due to permission""" test_thread = testutils.post_thread( @@ -750,21 +708,18 @@ def test_list_user_can_see_own_hidden_thread(self): is_hidden=True, ) - self.access_all_categories({'can_hide_threads': 1}) - response = self.client.get('/') self.assertEqual(response.status_code, 200) self.assertContainsThread(response, test_thread) # test api - self.access_all_categories({'can_hide_threads': 1}) - response = self.client.get(self.api_link) self.assertEqual(response.status_code, 200) response_json = response.json() self.assertEqual(response_json['results'][0]['id'], test_thread.pk) + @patch_categories_acl({'can_hide_threads': 1}) def test_list_user_can_see_hidden_thread(self): """list shows hidden thread that belongs to other user due to permission""" test_thread = testutils.post_thread( @@ -772,21 +727,18 @@ def test_list_user_can_see_hidden_thread(self): is_hidden=True, ) - self.access_all_categories({'can_hide_threads': 1}) - response = self.client.get('/') self.assertEqual(response.status_code, 200) self.assertContainsThread(response, test_thread) # test api - self.access_all_categories({'can_hide_threads': 1}) - response = self.client.get(self.api_link) self.assertEqual(response.status_code, 200) response_json = response.json() self.assertEqual(response_json['results'][0]['id'], test_thread.pk) + @patch_categories_acl({'can_approve_content': 1}) def test_list_user_can_see_unapproved_thread(self): """list shows hidden thread that belongs to other user due to permission""" test_thread = testutils.post_thread( @@ -794,15 +746,11 @@ def test_list_user_can_see_unapproved_thread(self): is_unapproved=True, ) - self.access_all_categories({'can_approve_content': 1}) - response = self.client.get('/') self.assertEqual(response.status_code, 200) self.assertContainsThread(response, test_thread) # test api - self.access_all_categories({'can_approve_content': 1}) - response = self.client.get(self.api_link) self.assertEqual(response.status_code, 200) @@ -811,34 +759,30 @@ def test_list_user_can_see_unapproved_thread(self): class MyThreadsListTests(ThreadsListTestCase): + @patch_categories_acl() def test_list_renders_empty(self): """list renders empty""" - self.access_all_categories() - response = self.client.get('/my/') self.assertEqual(response.status_code, 200) self.assertContains(response, "empty-message") - self.access_all_categories() - response = self.client.get(self.category_a.get_absolute_url() + 'my/') self.assertEqual(response.status_code, 200) self.assertContains(response, "empty-message") # test api - self.access_all_categories() response = self.client.get('%s?list=my' % self.api_link) self.assertEqual(response.status_code, 200) response_json = response.json() self.assertEqual(len(response_json['results']), 0) - self.access_all_categories() response = self.client.get('%s?list=my&category=%s' % (self.api_link, self.category_a.pk)) response_json = response.json() self.assertEqual(len(response_json['results']), 0) + @patch_categories_acl() def test_list_renders_test_thread(self): """list renders only threads posted by user""" test_thread = testutils.post_thread( @@ -848,22 +792,17 @@ def test_list_renders_test_thread(self): other_thread = testutils.post_thread(category=self.category_a) - self.access_all_categories() - response = self.client.get('/my/') self.assertEqual(response.status_code, 200) self.assertContainsThread(response, test_thread) self.assertNotContainsThread(response, other_thread) - self.access_all_categories() - response = self.client.get(self.category_a.get_absolute_url() + 'my/') self.assertEqual(response.status_code, 200) self.assertContainsThread(response, test_thread) self.assertNotContainsThread(response, other_thread) # test api - self.access_all_categories() response = self.client.get('%s?list=my' % self.api_link) self.assertEqual(response.status_code, 200) @@ -871,7 +810,6 @@ def test_list_renders_test_thread(self): self.assertEqual(len(response_json['results']), 1) self.assertEqual(response_json['results'][0]['id'], test_thread.pk) - self.access_all_categories() response = self.client.get('%s?list=my&category=%s' % (self.api_link, self.category_a.pk)) self.assertEqual(response.status_code, 200) @@ -881,52 +819,43 @@ def test_list_renders_test_thread(self): class NewThreadsListTests(ThreadsListTestCase): + @patch_categories_acl() def test_list_renders_empty(self): """list renders empty""" - self.access_all_categories() - response = self.client.get('/new/') self.assertEqual(response.status_code, 200) self.assertContains(response, "empty-message") - self.access_all_categories() - response = self.client.get(self.category_a.get_absolute_url() + 'new/') self.assertEqual(response.status_code, 200) self.assertContains(response, "empty-message") # test api - self.access_all_categories() response = self.client.get('%s?list=new' % self.api_link) self.assertEqual(response.status_code, 200) response_json = response.json() self.assertEqual(len(response_json['results']), 0) - self.access_all_categories() response = self.client.get('%s?list=new&category=%s' % (self.api_link, self.category_a.pk)) response_json = response.json() self.assertEqual(len(response_json['results']), 0) + @patch_categories_acl() def test_list_renders_new_thread(self): """list renders new thread""" test_thread = testutils.post_thread(category=self.category_a) - self.access_all_categories() - response = self.client.get('/new/') self.assertEqual(response.status_code, 200) self.assertContainsThread(response, test_thread) - self.access_all_categories() - response = self.client.get(self.category_a.get_absolute_url() + 'new/') self.assertEqual(response.status_code, 200) self.assertContainsThread(response, test_thread) # test api - self.access_all_categories() response = self.client.get('%s?list=new' % self.api_link) self.assertEqual(response.status_code, 200) @@ -934,7 +863,6 @@ def test_list_renders_new_thread(self): self.assertEqual(len(response_json['results']), 1) self.assertEqual(response_json['results'][0]['id'], test_thread.pk) - self.access_all_categories() response = self.client.get('%s?list=new&category=%s' % (self.api_link, self.category_a.pk)) self.assertEqual(response.status_code, 200) @@ -942,6 +870,7 @@ def test_list_renders_new_thread(self): self.assertEqual(len(response_json['results']), 1) self.assertEqual(response_json['results'][0]['id'], test_thread.pk) + @patch_categories_acl() def test_list_renders_thread_bumped_after_user_cutoff(self): """list renders new thread bumped after user cutoff""" self.user.joined_on = timezone.now() - timedelta(days=10) @@ -957,20 +886,15 @@ def test_list_renders_thread_bumped_after_user_cutoff(self): posted_on=self.user.joined_on + timedelta(days=4), ) - self.access_all_categories() - response = self.client.get('/new/') self.assertEqual(response.status_code, 200) self.assertContainsThread(response, test_thread) - self.access_all_categories() - response = self.client.get(self.category_a.get_absolute_url() + 'new/') self.assertEqual(response.status_code, 200) self.assertContainsThread(response, test_thread) # test api - self.access_all_categories() response = self.client.get('%s?list=new' % self.api_link) self.assertEqual(response.status_code, 200) @@ -978,7 +902,6 @@ def test_list_renders_thread_bumped_after_user_cutoff(self): self.assertEqual(len(response_json['results']), 1) self.assertEqual(response_json['results'][0]['id'], test_thread.pk) - self.access_all_categories() response = self.client.get('%s?list=new&category=%s' % (self.api_link, self.category_a.pk)) self.assertEqual(response.status_code, 200) @@ -986,6 +909,7 @@ def test_list_renders_thread_bumped_after_user_cutoff(self): self.assertEqual(len(response_json['results']), 1) self.assertEqual(response_json['results'][0]['id'], test_thread.pk) + @patch_categories_acl() def test_list_hides_global_cutoff_thread(self): """list hides thread started before global cutoff""" self.user.joined_on = timezone.now() - timedelta(days=10) @@ -996,33 +920,28 @@ def test_list_hides_global_cutoff_thread(self): started_on=timezone.now() - timedelta(days=settings.MISAGO_READTRACKER_CUTOFF + 1), ) - self.access_all_categories() - response = self.client.get('/new/') self.assertEqual(response.status_code, 200) self.assertNotContainsThread(response, test_thread) - self.access_all_categories() - response = self.client.get(self.category_a.get_absolute_url() + 'new/') self.assertEqual(response.status_code, 200) self.assertNotContainsThread(response, test_thread) # test api - self.access_all_categories() response = self.client.get('%s?list=new' % self.api_link) self.assertEqual(response.status_code, 200) response_json = response.json() self.assertEqual(len(response_json['results']), 0) - self.access_all_categories() response = self.client.get('%s?list=new&category=%s' % (self.api_link, self.category_a.pk)) self.assertEqual(response.status_code, 200) response_json = response.json() self.assertEqual(len(response_json['results']), 0) + @patch_categories_acl() def test_list_hides_user_cutoff_thread(self): """list hides thread started before users cutoff""" self.user.joined_on = timezone.now() - timedelta(days=5) @@ -1033,63 +952,51 @@ def test_list_hides_user_cutoff_thread(self): started_on=self.user.joined_on - timedelta(minutes=1), ) - self.access_all_categories() - response = self.client.get('/new/') self.assertEqual(response.status_code, 200) self.assertNotContainsThread(response, test_thread) - self.access_all_categories() - response = self.client.get(self.category_a.get_absolute_url() + 'new/') self.assertEqual(response.status_code, 200) self.assertNotContainsThread(response, test_thread) # test api - self.access_all_categories() response = self.client.get('%s?list=new' % self.api_link) self.assertEqual(response.status_code, 200) response_json = response.json() self.assertEqual(len(response_json['results']), 0) - self.access_all_categories() response = self.client.get('%s?list=new&category=%s' % (self.api_link, self.category_a.pk)) self.assertEqual(response.status_code, 200) response_json = response.json() self.assertEqual(len(response_json['results']), 0) + @patch_categories_acl() def test_list_hides_user_read_thread(self): """list hides thread already read by user""" self.user.joined_on = timezone.now() - timedelta(days=5) self.user.save() test_thread = testutils.post_thread(category=self.category_a) - poststracker.save_read(self.user, test_thread.first_post) - self.access_all_categories() - response = self.client.get('/new/') self.assertEqual(response.status_code, 200) self.assertNotContainsThread(response, test_thread) - self.access_all_categories() - response = self.client.get(self.category_a.get_absolute_url() + 'new/') self.assertEqual(response.status_code, 200) self.assertNotContainsThread(response, test_thread) # test api - self.access_all_categories() response = self.client.get('%s?list=new' % self.api_link) self.assertEqual(response.status_code, 200) response_json = response.json() self.assertEqual(len(response_json['results']), 0) - self.access_all_categories() response = self.client.get('%s?list=new&category=%s' % (self.api_link, self.category_a.pk)) self.assertEqual(response.status_code, 200) @@ -1098,29 +1005,24 @@ def test_list_hides_user_read_thread(self): class UnreadThreadsListTests(ThreadsListTestCase): + @patch_categories_acl() def test_list_renders_empty(self): """list renders empty""" - self.access_all_categories() - response = self.client.get('/unread/') self.assertEqual(response.status_code, 200) self.assertContains(response, "empty-message") - self.access_all_categories() - response = self.client.get(self.category_a.get_absolute_url() + 'unread/') self.assertEqual(response.status_code, 200) self.assertContains(response, "empty-message") # test api - self.access_all_categories() response = self.client.get('%s?list=unread' % self.api_link) self.assertEqual(response.status_code, 200) response_json = response.json() self.assertEqual(len(response_json['results']), 0) - self.access_all_categories() response = self.client.get( '%s?list=unread&category=%s' % (self.api_link, self.category_a.pk) ) @@ -1129,31 +1031,25 @@ def test_list_renders_empty(self): response_json = response.json() self.assertEqual(len(response_json['results']), 0) + @patch_categories_acl() def test_list_renders_unread_thread(self): """list renders thread with unread posts""" self.user.joined_on = timezone.now() - timedelta(days=5) self.user.save() test_thread = testutils.post_thread(category=self.category_a) - poststracker.save_read(self.user, test_thread.first_post) - testutils.reply_thread(test_thread) - self.access_all_categories() - response = self.client.get('/unread/') self.assertEqual(response.status_code, 200) self.assertContainsThread(response, test_thread) - self.access_all_categories() - response = self.client.get(self.category_a.get_absolute_url() + 'unread/') self.assertEqual(response.status_code, 200) self.assertContainsThread(response, test_thread) # test api - self.access_all_categories() response = self.client.get('%s?list=unread' % self.api_link) self.assertEqual(response.status_code, 200) @@ -1161,7 +1057,6 @@ def test_list_renders_unread_thread(self): self.assertEqual(len(response_json['results']), 1) self.assertEqual(response_json['results'][0]['id'], test_thread.pk) - self.access_all_categories() response = self.client.get( '%s?list=unread&category=%s' % (self.api_link, self.category_a.pk) ) @@ -1171,6 +1066,7 @@ def test_list_renders_unread_thread(self): self.assertEqual(len(response_json['results']), 1) self.assertEqual(response_json['results'][0]['id'], test_thread.pk) + @patch_categories_acl() def test_list_hides_never_read_thread(self): """list hides never read thread""" self.user.joined_on = timezone.now() - timedelta(days=5) @@ -1178,27 +1074,21 @@ def test_list_hides_never_read_thread(self): test_thread = testutils.post_thread(category=self.category_a) - self.access_all_categories() - response = self.client.get('/unread/') self.assertEqual(response.status_code, 200) self.assertNotContainsThread(response, test_thread) - self.access_all_categories() - response = self.client.get(self.category_a.get_absolute_url() + 'unread/') self.assertEqual(response.status_code, 200) self.assertNotContainsThread(response, test_thread) # test api - self.access_all_categories() response = self.client.get('%s?list=unread' % self.api_link) self.assertEqual(response.status_code, 200) response_json = response.json() self.assertEqual(len(response_json['results']), 0) - self.access_all_categories() response = self.client.get( '%s?list=unread&category=%s' % (self.api_link, self.category_a.pk) ) @@ -1207,36 +1097,30 @@ def test_list_hides_never_read_thread(self): response_json = response.json() self.assertEqual(len(response_json['results']), 0) + @patch_categories_acl() def test_list_hides_read_thread(self): """list hides read thread""" self.user.joined_on = timezone.now() - timedelta(days=5) self.user.save() test_thread = testutils.post_thread(category=self.category_a) - poststracker.save_read(self.user, test_thread.first_post) - self.access_all_categories() - response = self.client.get('/unread/') self.assertEqual(response.status_code, 200) self.assertNotContainsThread(response, test_thread) - self.access_all_categories() - response = self.client.get(self.category_a.get_absolute_url() + 'unread/') self.assertEqual(response.status_code, 200) self.assertNotContainsThread(response, test_thread) # test api - self.access_all_categories() response = self.client.get('%s?list=unread' % self.api_link) self.assertEqual(response.status_code, 200) response_json = response.json() self.assertEqual(len(response_json['results']), 0) - self.access_all_categories() response = self.client.get( '%s?list=unread&category=%s' % (self.api_link, self.category_a.pk) ) @@ -1245,6 +1129,7 @@ def test_list_hides_read_thread(self): response_json = response.json() self.assertEqual(len(response_json['results']), 0) + @patch_categories_acl() def test_list_hides_global_cutoff_thread(self): """list hides thread replied before global cutoff""" self.user.joined_on = timezone.now() - timedelta(days=10) @@ -1256,30 +1141,23 @@ def test_list_hides_global_cutoff_thread(self): ) poststracker.save_read(self.user, test_thread.first_post) - testutils.reply_thread(test_thread, posted_on=test_thread.started_on + timedelta(days=1)) - self.access_all_categories() - response = self.client.get('/unread/') self.assertEqual(response.status_code, 200) self.assertNotContainsThread(response, test_thread) - self.access_all_categories() - response = self.client.get(self.category_a.get_absolute_url() + 'unread/') self.assertEqual(response.status_code, 200) self.assertNotContainsThread(response, test_thread) # test api - self.access_all_categories() response = self.client.get('%s?list=unread' % self.api_link) self.assertEqual(response.status_code, 200) response_json = response.json() self.assertEqual(len(response_json['results']), 0) - self.access_all_categories() response = self.client.get( '%s?list=unread&category=%s' % (self.api_link, self.category_a.pk) ) @@ -1288,6 +1166,7 @@ def test_list_hides_global_cutoff_thread(self): response_json = response.json() self.assertEqual(len(response_json['results']), 0) + @patch_categories_acl() def test_list_hides_user_cutoff_thread(self): """list hides thread replied before user cutoff""" self.user.joined_on = timezone.now() - timedelta(days=10) @@ -1305,27 +1184,21 @@ def test_list_hides_user_cutoff_thread(self): posted_on=test_thread.started_on + timedelta(days=1), ) - self.access_all_categories() - response = self.client.get('/unread/') self.assertEqual(response.status_code, 200) self.assertNotContainsThread(response, test_thread) - self.access_all_categories() - response = self.client.get(self.category_a.get_absolute_url() + 'unread/') self.assertEqual(response.status_code, 200) self.assertNotContainsThread(response, test_thread) # test api - self.access_all_categories() response = self.client.get('%s?list=unread' % self.api_link) self.assertEqual(response.status_code, 200) response_json = response.json() self.assertEqual(len(response_json['results']), 0) - self.access_all_categories() response = self.client.get( '%s?list=unread&category=%s' % (self.api_link, self.category_a.pk) ) @@ -1336,6 +1209,7 @@ def test_list_hides_user_cutoff_thread(self): class SubscribedThreadsListTests(ThreadsListTestCase): + @patch_categories_acl() def test_list_shows_subscribed_thread(self): """list shows subscribed thread""" test_thread = testutils.post_thread(category=self.category_a) @@ -1345,20 +1219,15 @@ def test_list_shows_subscribed_thread(self): last_read_on=test_thread.last_post_on, ) - self.access_all_categories() - response = self.client.get('/subscribed/') self.assertEqual(response.status_code, 200) self.assertContainsThread(response, test_thread) - self.access_all_categories() - response = self.client.get(self.category_a.get_absolute_url() + 'subscribed/') self.assertEqual(response.status_code, 200) self.assertContainsThread(response, test_thread) # test api - self.access_all_categories() response = self.client.get('%s?list=subscribed' % self.api_link) self.assertEqual(response.status_code, 200) @@ -1366,7 +1235,6 @@ def test_list_shows_subscribed_thread(self): self.assertEqual(len(response_json['results']), 1) self.assertContains(response, test_thread.get_absolute_url()) - self.access_all_categories() response = self.client.get( '%s?list=subscribed&category=%s' % (self.api_link, self.category_a.pk) ) @@ -1376,24 +1244,20 @@ def test_list_shows_subscribed_thread(self): self.assertEqual(len(response_json['results']), 1) self.assertContains(response, test_thread.get_absolute_url()) + @patch_categories_acl() def test_list_hides_unsubscribed_thread(self): """list shows subscribed thread""" test_thread = testutils.post_thread(category=self.category_a) - self.access_all_categories() - response = self.client.get('/subscribed/') self.assertEqual(response.status_code, 200) self.assertNotContainsThread(response, test_thread) - self.access_all_categories() - response = self.client.get(self.category_a.get_absolute_url() + 'subscribed/') self.assertEqual(response.status_code, 200) self.assertNotContainsThread(response, test_thread) # test api - self.access_all_categories() response = self.client.get('%s?list=subscribed' % self.api_link) self.assertEqual(response.status_code, 200) @@ -1401,7 +1265,6 @@ def test_list_hides_unsubscribed_thread(self): self.assertEqual(len(response_json['results']), 0) self.assertNotContainsThread(response, test_thread) - self.access_all_categories() response = self.client.get( '%s?list=subscribed&category=%s' % (self.api_link, self.category_a.pk) ) @@ -1420,29 +1283,29 @@ def test_list_errors_without_permission(self): '%s?list=unapproved' % self.api_link, ) - for test_url in TEST_URLS: - self.access_all_categories() - response = self.client.get(test_url) - self.assertEqual(response.status_code, 403) + with patch_categories_acl(): + for test_url in TEST_URLS: + response = self.client.get(test_url) + self.assertEqual(response.status_code, 403) # approval perm has no influence on visibility - for test_url in TEST_URLS: - self.access_all_categories({'can_approve_content': True}) - - self.access_all_categories() - response = self.client.get(test_url) - self.assertEqual(response.status_code, 403) + with patch_categories_acl({'can_approve_content': True}): + for test_url in TEST_URLS: + response = self.client.get(test_url) + self.assertEqual(response.status_code, 403) # approval perm has no influence on visibility - for test_url in TEST_URLS: - self.access_all_categories(base_acl={ - 'can_see_unapproved_content_lists': True, - }) - - self.access_all_categories() - response = self.client.get(test_url) - self.assertEqual(response.status_code, 200) - + with patch_categories_acl(base_acl={ + 'can_see_unapproved_content_lists': True, + }): + for test_url in TEST_URLS: + response = self.client.get(test_url) + self.assertEqual(response.status_code, 200) + + @patch_categories_acl( + {'can_approve_content': True}, + {'can_see_unapproved_content_lists': True}, + ) def test_list_shows_all_threads_for_approving_user(self): """list shows all threads with unapproved posts when user has perm""" visible_thread = testutils.post_thread( @@ -1455,40 +1318,23 @@ def test_list_shows_all_threads_for_approving_user(self): is_unapproved=False, ) - self.access_all_categories({ - 'can_approve_content': True, - }, { - 'can_see_unapproved_content_lists': True, - }) - response = self.client.get('/unapproved/') self.assertEqual(response.status_code, 200) self.assertContainsThread(response, visible_thread) self.assertNotContainsThread(response, hidden_thread) - self.access_all_categories({ - 'can_approve_content': True - }, { - 'can_see_unapproved_content_lists': True, - }) - response = self.client.get(self.category_a.get_absolute_url() + 'unapproved/') self.assertEqual(response.status_code, 200) self.assertContainsThread(response, visible_thread) self.assertNotContainsThread(response, hidden_thread) # test api - self.access_all_categories({ - 'can_approve_content': True - }, { - 'can_see_unapproved_content_lists': True, - }) - response = self.client.get('%s?list=unapproved' % self.api_link) self.assertEqual(response.status_code, 200) self.assertContains(response, visible_thread.get_absolute_url()) self.assertNotContains(response, hidden_thread.get_absolute_url()) + @patch_categories_acl(base_acl={'can_see_unapproved_content_lists': True}) def test_list_shows_owned_threads_for_unapproving_user(self): """list shows owned threads with unapproved posts for user without perm""" visible_thread = testutils.post_thread( @@ -1502,49 +1348,41 @@ def test_list_shows_owned_threads_for_unapproving_user(self): is_unapproved=True, ) - self.access_all_categories(base_acl={ - 'can_see_unapproved_content_lists': True, - }) response = self.client.get('/unapproved/') self.assertEqual(response.status_code, 200) self.assertContainsThread(response, visible_thread) self.assertNotContainsThread(response, hidden_thread) - self.access_all_categories(base_acl={ - 'can_see_unapproved_content_lists': True, - }) response = self.client.get(self.category_a.get_absolute_url() + 'unapproved/') self.assertEqual(response.status_code, 200) self.assertContainsThread(response, visible_thread) self.assertNotContainsThread(response, hidden_thread) # test api - self.access_all_categories(base_acl={ - 'can_see_unapproved_content_lists': True, - }) response = self.client.get('%s?list=unapproved' % self.api_link) self.assertEqual(response.status_code, 200) self.assertContains(response, visible_thread.get_absolute_url()) self.assertNotContains(response, hidden_thread.get_absolute_url()) +def patch_category_see_all_threads_acl(): + def patch_acl(_, user_acl): + category = Category.objects.get(slug='first-category') + category_acl = user_acl['categories'][category.id].copy() + category_acl.update({'can_see_all_threads': 0}) + user_acl['categories'][category.id] = category_acl + + return patch_user_acl(patch_acl) + + class OwnerOnlyThreadsVisibilityTests(AuthenticatedUserTestCase): def setUp(self): super().setUp() self.category = Category.objects.get(slug='first-category') - def override_acl(self, user): - category_acl = user.acl_cache['categories'][self.category.pk].copy() - category_acl.update({'can_see_all_threads': 0}) - user.acl_cache['categories'][self.category.pk] = category_acl - - override_acl(user, user.acl_cache) - def test_owned_threads_visibility(self): """only user-posted threads are visible in category""" - self.override_acl(self.user) - visible_thread = testutils.post_thread( poster=self.user, category=self.category, @@ -1556,18 +1394,16 @@ def test_owned_threads_visibility(self): is_unapproved=True, ) - response = self.client.get(self.category.get_absolute_url()) - - self.assertEqual(response.status_code, 200) - self.assertContains(response, visible_thread.get_absolute_url()) - self.assertNotContains(response, hidden_thread.get_absolute_url()) + with patch_category_see_all_threads_acl(): + response = self.client.get(self.category.get_absolute_url()) + self.assertEqual(response.status_code, 200) + self.assertContains(response, visible_thread.get_absolute_url()) + self.assertNotContains(response, hidden_thread.get_absolute_url()) def test_owned_threads_visibility_anonymous(self): """anons can't see any threads in limited visibility category""" self.logout_user() - self.override_acl(AnonymousUser()) - user_thread = testutils.post_thread( poster=self.user, category=self.category, @@ -1579,8 +1415,8 @@ def test_owned_threads_visibility_anonymous(self): is_unapproved=True, ) - response = self.client.get(self.category.get_absolute_url()) - - self.assertEqual(response.status_code, 200) - self.assertNotContains(response, user_thread.get_absolute_url()) - self.assertNotContains(response, guest_thread.get_absolute_url()) + with patch_category_see_all_threads_acl(): + response = self.client.get(self.category.get_absolute_url()) + self.assertEqual(response.status_code, 200) + self.assertNotContains(response, user_thread.get_absolute_url()) + self.assertNotContains(response, guest_thread.get_absolute_url()) diff --git a/misago/threads/viewmodels/category.py b/misago/threads/viewmodels/category.py index 5ca0d74641..0175df97e4 100644 --- a/misago/threads/viewmodels/category.py +++ b/misago/threads/viewmodels/category.py @@ -66,8 +66,8 @@ def get_category(self, request, categories, **kwargs): if category.pk == int(kwargs['pk']): if not category.special_role: # check permissions for non-special categories - allow_see_category(request.user, category) - allow_browse_category(request.user, category) + allow_see_category(request.user_acl, category) + allow_browse_category(request.user_acl, category) if 'slug' in kwargs: validate_slug(category, kwargs['slug']) @@ -81,7 +81,7 @@ def get_categories(self, request): return [Category.objects.private_threads()] def get_category(self, request, categories, **kwargs): - allow_use_private_threads(request.user) + allow_use_private_threads(request.user_acl) return categories[0] diff --git a/misago/threads/viewmodels/threads.py b/misago/threads/viewmodels/threads.py index 694da97be0..b79267d659 100644 --- a/misago/threads/viewmodels/threads.py +++ b/misago/threads/viewmodels/threads.py @@ -77,7 +77,7 @@ def __init__(self, request, category, list_type, page): thread.is_read = False thread.is_new = True else: - threadstracker.make_read_aware(request.user, threads) + threadstracker.make_read_aware(request.user, request.user_acl, threads) self.filter_threads(request, threads) @@ -188,29 +188,31 @@ def get_threads_queryset(request, categories, list_type): if list_type == 'all': return queryset else: - return filter_threads_queryset(request.user, categories, list_type, queryset) + return filter_threads_queryset(request, categories, list_type, queryset) -def filter_threads_queryset(user, categories, list_type, queryset): +def filter_threads_queryset(request, categories, list_type, queryset): if list_type == 'my': - return queryset.filter(starter=user) + return queryset.filter(starter=request.user) elif list_type == 'subscribed': - subscribed_threads = user.subscription_set.values('thread_id') + subscribed_threads = request.user.subscription_set.values('thread_id') return queryset.filter(id__in=subscribed_threads) elif list_type == 'unapproved': return queryset.filter(has_unapproved_posts=True) elif list_type in ('new', 'unread'): - return filter_read_threads_queryset(user, categories, list_type, queryset) + return filter_read_threads_queryset(request, categories, list_type, queryset) else: return queryset -def filter_read_threads_queryset(user, categories, list_type, queryset): +def filter_read_threads_queryset(request, categories, list_type, queryset): # grab cutoffs for categories + user = request.user + cutoff_date = get_cutoff_date(user) visible_posts = Post.objects.filter(posted_on__gt=cutoff_date) - visible_posts = exclude_invisible_posts(user, categories, visible_posts) + visible_posts = exclude_invisible_posts(request.user_acl, categories, visible_posts) queryset = queryset.filter(id__in=visible_posts.distinct().values('thread')) From 7a38bd6a64d1a8e0ed27464e9109c990657679f4 Mon Sep 17 00:00:00 2001 From: rafalp Date: Mon, 10 Dec 2018 02:07:44 +0100 Subject: [PATCH 26/86] Make misago.categories tests pass --- misago/categories/tests/test_utils.py | 28 +++++++---- misago/categories/tests/test_views.py | 72 ++++++++++----------------- 2 files changed, 44 insertions(+), 56 deletions(-) diff --git a/misago/categories/tests/test_utils.py b/misago/categories/tests/test_utils.py index e0f4b98c22..f6edc64efd 100644 --- a/misago/categories/tests/test_utils.py +++ b/misago/categories/tests/test_utils.py @@ -1,9 +1,21 @@ -from misago.acl.testutils import override_acl +from misago.acl.useracl import get_user_acl from misago.categories.models import Category from misago.categories.utils import get_categories_tree, get_category_path from misago.core import threadstore from misago.users.testutils import AuthenticatedUserTestCase +cache_versions = {"acl": "abcdefgh"} + + +def get_patched_user_acl(user): + user_acl = get_user_acl(user, cache_versions) + categories_acl = {'categories': {}, 'visible_categories': []} + for category in Category.objects.all_categories(): + categories_acl['visible_categories'].append(category.id) + categories_acl['categories'][category.id] = {'can_see': 1, 'can_browse': 1} + user_acl.update(categories_acl) + return user_acl + class CategoriesUtilsTests(AuthenticatedUserTestCase): def setUp(self): @@ -84,15 +96,11 @@ def setUp(self): save=True, ) - categories_acl = {'categories': {}, 'visible_categories': []} - for category in Category.objects.all_categories(): - categories_acl['visible_categories'].append(category.pk) - categories_acl['categories'][category.pk] = {'can_see': 1, 'can_browse': 1} - override_acl(self.user, categories_acl) + self.user_acl = get_patched_user_acl(self.user) def test_root_categories_tree_no_parent(self): """get_categories_tree returns all children of root nodes""" - categories_tree = get_categories_tree(self.user) + categories_tree = get_categories_tree(self.user, self.user_acl) self.assertEqual(len(categories_tree), 3) self.assertEqual(categories_tree[0], Category.objects.get(slug='first-category')) @@ -101,19 +109,19 @@ def test_root_categories_tree_no_parent(self): def test_root_categories_tree_with_parent(self): """get_categories_tree returns all children of given node""" - categories_tree = get_categories_tree(self.user, self.category_a) + categories_tree = get_categories_tree(self.user, self.user_acl, self.category_a) self.assertEqual(len(categories_tree), 1) self.assertEqual(categories_tree[0], Category.objects.get(slug='category-b')) def test_root_categories_tree_with_leaf(self): """get_categories_tree returns all children of given node""" categories_tree = get_categories_tree( - self.user, Category.objects.get(slug='subcategory-f') + self.user, self.user_acl, Category.objects.get(slug='subcategory-f') ) self.assertEqual(len(categories_tree), 0) def test_get_category_path(self): """get_categories_tree returns all children of root nodes""" - for node in get_categories_tree(self.user): + for node in get_categories_tree(self.user, self.user_acl): parent_nodes = len(get_category_path(node)) self.assertEqual(parent_nodes, node.level) diff --git a/misago/categories/tests/test_views.py b/misago/categories/tests/test_views.py index 5d920df764..60c279f761 100644 --- a/misago/categories/tests/test_views.py +++ b/misago/categories/tests/test_views.py @@ -1,53 +1,47 @@ +import json + from django.urls import reverse -from misago.acl.testutils import override_acl +from misago.acl.test import patch_user_acl from misago.categories.models import Category -from misago.categories.utils import get_categories_tree from misago.users.testutils import AuthenticatedUserTestCase class CategoryViewsTests(AuthenticatedUserTestCase): + def setUp(self): + super().setUp() + + self.category = Category.objects.get(slug='first-category') + def test_index_renders(self): """categories list renders for authenticated""" response = self.client.get(reverse('misago:categories')) - - for node in get_categories_tree(self.user): - self.assertContains(response, node.name) - if node.level > 1: - self.assertContains(response, node.get_absolute_url()) + self.assertContains(response, self.category.name) + self.assertContains(response, self.category.get_absolute_url()) def test_index_renders_for_guest(self): """categories list renders for guest""" self.logout_user() response = self.client.get(reverse('misago:categories')) + self.assertContains(response, self.category.name) + self.assertContains(response, self.category.get_absolute_url()) - for node in get_categories_tree(self.user): - self.assertContains(response, node.name) - if node.level > 1: - self.assertContains(response, node.get_absolute_url()) - + @patch_user_acl({'visible_categories': []}) def test_index_no_perms_renders(self): """categories list renders no visible categories for authenticated""" - override_acl(self.user, {'visible_categories': []}) response = self.client.get(reverse('misago:categories')) + self.assertNotContains(response, self.category.name) + self.assertNotContains(response, self.category.get_absolute_url()) - for node in get_categories_tree(self.user): - self.assertNotIn(node.name, response.content) - if node.level > 1: - self.assertNotContains(response, node.get_absolute_url()) - + @patch_user_acl({'visible_categories': []}) def test_index_no_perms_renders_for_guest(self): """categories list renders no visible categories for guest""" self.logout_user() - override_acl(self.user, {'visible_categories': []}) response = self.client.get(reverse('misago:categories')) - - for node in get_categories_tree(self.user): - self.assertNotIn(node.name, response.content) - if node.level > 1: - self.assertNotContains(response, node.get_absolute_url()) + self.assertNotContains(response, self.category.name) + self.assertNotContains(response, self.category.get_absolute_url()) class CategoryAPIViewsTests(AuthenticatedUserTestCase): @@ -59,41 +53,27 @@ def setUp(self): def test_list_renders(self): """api returns categories for authenticated""" response = self.client.get(reverse('misago:api:category-list')) - - for node in get_categories_tree(self.user): - self.assertContains(response, node.name) - if node.level > 1: - self.assertNotContains(response, node.get_absolute_url()) + self.assertContains(response, self.category.name) + self.assertContains(response, self.category.get_absolute_url()) def test_list_renders_for_guest(self): """api returns categories for guest""" self.logout_user() response = self.client.get(reverse('misago:api:category-list')) + self.assertContains(response, self.category.name) + self.assertContains(response, self.category.get_absolute_url()) - for node in get_categories_tree(self.user): - self.assertContains(response, node.name) - if node.level > 1: - self.assertNotContains(response, node.get_absolute_url()) - + @patch_user_acl({'visible_categories': []}) def test_list_no_perms_renders(self): """api returns no categories for authenticated""" - override_acl(self.user, {'visible_categories': []}) response = self.client.get(reverse('misago:api:category-list')) + assert json.loads(response.content) == [] - for node in get_categories_tree(self.user): - self.assertNotIn(node.name, response.content) - if node.level > 1: - self.assertNotContains(response, node.get_absolute_url()) - + @patch_user_acl({'visible_categories': []}) def test_list_no_perms_renders_for_guest(self): """api returns no categories for guest""" self.logout_user() - override_acl(self.user, {'visible_categories': []}) response = self.client.get(reverse('misago:api:category-list')) - - for node in get_categories_tree(self.user): - self.assertNotContains(response, node.name) - if node.level > 1: - self.assertNotContains(response, node.get_absolute_url()) + assert json.loads(response.content) == [] From 7e79a26512134e23c6927ee552b97e0c0507eed0 Mon Sep 17 00:00:00 2001 From: rafalp Date: Tue, 11 Dec 2018 21:14:07 +0100 Subject: [PATCH 27/86] Redo patch_user_acl test util --- misago/acl/test.py | 21 ++++------------- misago/acl/tests/test_patching_user_acl.py | 27 +++++++++++----------- 2 files changed, 18 insertions(+), 30 deletions(-) diff --git a/misago/acl/test.py b/misago/acl/test.py index a4750634f7..316da78100 100644 --- a/misago/acl/test.py +++ b/misago/acl/test.py @@ -19,19 +19,9 @@ class patch_user_acl(ExitStack): Patch should be a dict or callable. """ - def __init__(self, *args): + def __init__(self, *patches): super().__init__() - - self._global_patch = None - self._user_patches = {} - - if len(args) == 2: - user, patch = args - self._user_patches[user.id] = patch - elif len(args) == 1: - self._global_patch = args[0] - else: - raise ValueError("patch_user_acl takes one or two arguments.") + self._patches = patches def patched_get_user_acl(self, user, cache_versions): user_acl = get_user_acl(user, cache_versions) @@ -39,11 +29,8 @@ def patched_get_user_acl(self, user, cache_versions): return user_acl def apply_acl_patches(self, user, user_acl): - if self._global_patch: - self.apply_acl_patch(user, user_acl, self._global_patch) - if user.id in self._user_patches: - user_acl_patch = self._user_patches[user.id] - self.apply_acl_patch(user, user_acl, user_acl_patch) + for acl_patch in self._patches: + self.apply_acl_patch(user, user_acl, acl_patch) def apply_acl_patch(self, user, user_acl, acl_patch): if callable(acl_patch): diff --git a/misago/acl/tests/test_patching_user_acl.py b/misago/acl/tests/test_patching_user_acl.py index aed4c8c48e..fef1621ddd 100644 --- a/misago/acl/tests/test_patching_user_acl.py +++ b/misago/acl/tests/test_patching_user_acl.py @@ -47,19 +47,6 @@ def test_context_manager_removes_patches_after_exit(self): user_acl = useracl.get_user_acl(user, cache_versions) assert "is_patched" not in user_acl - def test_context_manager_patches_specified_user_acl(self): - user = User.objects.create_user("User", "user@example.com") - with patch_user_acl(user, {"can_rename_users": "patched"}): - user_acl = useracl.get_user_acl(user, cache_versions) - assert user_acl["can_rename_users"] == "patched" - - def test_other_user_acl_is_not_changed_by_user_specific_context_manager(self): - patched_user = User.objects.create_user("User", "user@example.com") - other_user = User.objects.create_user("User2", "user2@example.com") - with patch_user_acl(patched_user, {"can_rename_users": "patched"}): - other_user_acl = useracl.get_user_acl(other_user, cache_versions) - assert other_user_acl["can_rename_users"] != "patched" - @patch_user_acl(callable_acl_patch) def test_callable_patch_is_called_with_user_and_acl_by_decorator(self): user = User.objects.create_user("User", "user@example.com") @@ -71,3 +58,17 @@ def test_callable_patch_is_called_with_user_and_acl_by_context_manager(self): with patch_user_acl(callable_acl_patch): user_acl = useracl.get_user_acl(user, cache_versions) assert user_acl["patched_for_user_id"] == user.id + + @patch_user_acl(callable_acl_patch, {"other_acl_path": True}) + def test_multiple_acl_patches_are_applied_by_decorator(self): + user = User.objects.create_user("User", "user@example.com") + user_acl = useracl.get_user_acl(user, cache_versions) + assert user_acl["patched_for_user_id"] == user.id + assert user_acl["other_acl_path"] + + def test_multiple_acl_patches_are_applied_by_context_manager(self): + user = User.objects.create_user("User", "user@example.com") + with patch_user_acl(callable_acl_patch, {"other_acl_path": True}): + user_acl = useracl.get_user_acl(user, cache_versions) + assert user_acl["patched_for_user_id"] == user.id + assert user_acl["other_acl_path"] From 95f9ff504609982b827e5dce9d7c259a9d610ced Mon Sep 17 00:00:00 2001 From: rafalp Date: Tue, 11 Dec 2018 22:43:04 +0100 Subject: [PATCH 28/86] Make threads patch api tests pass --- misago/threads/api/threadendpoints/patch.py | 36 +- misago/threads/test.py | 83 +++ misago/threads/tests/test_thread_patch_api.py | 596 +++++++----------- 3 files changed, 329 insertions(+), 386 deletions(-) create mode 100644 misago/threads/test.py diff --git a/misago/threads/api/threadendpoints/patch.py b/misago/threads/api/threadendpoints/patch.py index 103c61ad36..fce18a11e4 100644 --- a/misago/threads/api/threadendpoints/patch.py +++ b/misago/threads/api/threadendpoints/patch.py @@ -57,7 +57,7 @@ def patch_title(request, thread, value): except ValidationError as e: raise PermissionDenied(e.args[0]) - allow_edit_thread(request.user, thread) + allow_edit_thread(request.user_acl, thread) moderation.change_thread_title(request, thread, value_cleaned) return {'title': thread.title} @@ -67,7 +67,7 @@ def patch_title(request, thread, value): def patch_weight(request, thread, value): - allow_pin_thread(request.user, thread) + allow_pin_thread(request.user_acl, thread) if not thread.acl.get('can_pin_globally') and thread.weight == 2: raise PermissionDenied(_("You can't change globally pinned threads weights in this category.")) @@ -89,7 +89,7 @@ def patch_weight(request, thread, value): def patch_move(request, thread, value): - allow_move_thread(request.user, thread) + allow_move_thread(request.user_acl, thread) category_pk = get_int_or_404(value) new_category = get_object_or_404( @@ -97,9 +97,9 @@ def patch_move(request, thread, value): ) add_acl(request.user_acl, new_category) - allow_see_category(request.user, new_category) - allow_browse_category(request.user, new_category) - allow_start_thread(request.user, new_category) + allow_see_category(request.user_acl, new_category) + allow_browse_category(request.user_acl, new_category) + allow_start_thread(request.user_acl, new_category) if new_category == thread.category: raise PermissionDenied(_("You can't move thread to the category it's already in.")) @@ -123,7 +123,7 @@ def patch_flatten_categories(request, thread, value): def patch_is_unapproved(request, thread, value): - allow_approve_thread(request.user, thread) + allow_approve_thread(request.user_acl, thread) if value: raise PermissionDenied(_("Content approval can't be reversed.")) @@ -159,10 +159,10 @@ def patch_is_closed(request, thread, value): def patch_is_hidden(request, thread, value): if value: - allow_hide_thread(request.user, thread) + allow_hide_thread(request.user_acl, thread) moderation.hide_thread(request, thread) else: - allow_unhide_thread(request.user, thread) + allow_unhide_thread(request.user_acl, thread) moderation.unhide_thread(request, thread) return {'is_hidden': thread.is_hidden} @@ -205,20 +205,20 @@ def patch_best_answer(request, thread, value): except (TypeError, ValueError): raise PermissionDenied(_("A valid integer is required.")) - allow_mark_best_answer(request.user, thread) + allow_mark_best_answer(request.user_acl, thread) post = get_object_or_404(thread.post_set, id=post_id) post.category = thread.category post.thread = thread - allow_see_post(request.user, post) - allow_mark_as_best_answer(request.user, post) + allow_see_post(request.user_acl, post) + allow_mark_as_best_answer(request.user_acl, post) if post.is_best_answer: raise PermissionDenied(_("This post is already marked as thread's best answer.")) if thread.has_best_answer: - allow_change_best_answer(request.user, thread) + allow_change_best_answer(request.user_acl, thread) thread.set_best_answer(request.user, post) thread.save() @@ -250,7 +250,7 @@ def patch_unmark_best_answer(request, thread, value): raise PermissionDenied( _("This post can't be unmarked because it's not currently marked as best answer.")) - allow_unmark_best_answer(request.user, thread) + allow_unmark_best_answer(request.user_acl, thread) thread.clear_best_answer() thread.save() @@ -268,7 +268,7 @@ def patch_unmark_best_answer(request, thread, value): def patch_add_participant(request, thread, value): - allow_add_participants(request.user, thread) + allow_add_participants(request.user_acl, thread) try: username = str(value).strip().lower() @@ -281,7 +281,7 @@ def patch_add_participant(request, thread, value): if participant in [p.user for p in thread.participants_list]: raise PermissionDenied(_("This user is already thread participant.")) - allow_add_participant(request.user, participant) + allow_add_participant(request.user_acl, participant) add_participant(request, thread, participant) make_participants_aware(request.user, thread) @@ -305,7 +305,7 @@ def patch_remove_participant(request, thread, value): else: raise PermissionDenied(_("Participant doesn't exist.")) - allow_remove_participant(request.user, thread, participant.user) + allow_remove_participant(request.user_acl, thread, participant.user) remove_participant(request, thread, participant.user) if len(thread.participants_list) == 1: @@ -338,7 +338,7 @@ def patch_replace_owner(request, thread, value): else: raise PermissionDenied(_("Participant doesn't exist.")) - allow_change_owner(request.user, thread) + allow_change_owner(request.user_acl, thread) change_owner(request, thread, participant.user) make_participants_aware(request.user, thread) diff --git a/misago/threads/test.py b/misago/threads/test.py new file mode 100644 index 0000000000..59feba8db2 --- /dev/null +++ b/misago/threads/test.py @@ -0,0 +1,83 @@ +from misago.acl.test import patch_user_acl +from misago.categories.models import Category + +default_category_acl = { + 'can_see': 1, + 'can_browse': 1, + 'can_see_all_threads': 1, + 'can_see_own_threads': 0, + 'can_hide_threads': 0, + 'can_approve_content': 0, + 'can_edit_posts': 0, + 'can_hide_posts': 0, + 'can_hide_own_posts': 0, + 'can_merge_threads': 0, + 'can_close_threads': 0, +} + + +def patch_category_acl(acl_patch): + def patch_acl(_, user_acl): + category = Category.objects.get(slug="first-category") + category_acl = user_acl['categories'][category.id] + category_acl.update(default_category_acl) + if acl_patch: + category_acl.update(acl_patch) + cleanup_patched_acl(user_acl, category_acl, category) + + return patch_user_acl(patch_acl) + + +def patch_categories_acl_for_move(src_acl_patch=None, dst_acl_patch=None): + def patch_acl(_, user_acl): + src = Category.objects.get(slug="first-category") + dst = Category.objects.get(slug="other-category") + + src_acl = user_acl['categories'][src.id] + dst_acl = src_acl.copy() + user_acl['categories'][dst.id] = dst_acl + + src_acl.update(default_category_acl) + dst_acl.update(default_category_acl) + + if src_acl_patch: + src_acl.update(src_acl_patch) + if dst_acl_patch: + dst_acl.update(dst_acl_patch) + + cleanup_patched_acl(user_acl, src_acl, src) + cleanup_patched_acl(user_acl, dst_acl, dst) + + return patch_user_acl(patch_acl) + + +def create_category_acl_patch(category_slug, acl_patch): + def created_category_acl_patch(_, user_acl): + category = Category.objects.get(slug=category_slug) + category_acl = user_acl['categories'].get(category.id, {}) + category_acl.update(default_category_acl) + if acl_patch: + category_acl.update(acl_patch) + cleanup_patched_acl(user_acl, category_acl, category) + + return created_category_acl_patch + + +def cleanup_patched_acl(user_acl, category_acl, category): + visible_categories = user_acl['visible_categories'] + browseable_categories = user_acl['browseable_categories'] + + if not category_acl['can_see'] and category.id in visible_categories: + visible_categories.remove(category.id) + + if not category_acl['can_see'] and category.id in browseable_categories: + browseable_categories.remove(category.id) + + if not category_acl['can_browse'] and category.id in browseable_categories: + browseable_categories.remove(category.id) + + if category_acl['can_see'] and category.id not in visible_categories: + visible_categories.append(category.id) + + if category_acl['can_browse'] and category.id not in browseable_categories: + browseable_categories.append(category.id) diff --git a/misago/threads/tests/test_thread_patch_api.py b/misago/threads/tests/test_thread_patch_api.py index 63d641b6df..bb0b1f7ef7 100644 --- a/misago/threads/tests/test_thread_patch_api.py +++ b/misago/threads/tests/test_thread_patch_api.py @@ -3,10 +3,10 @@ from django.utils import timezone -from misago.acl.testutils import override_acl from misago.categories.models import Category from misago.readtracker import poststracker from misago.threads import testutils +from misago.threads.test import patch_category_acl, patch_categories_acl_for_move from misago.threads.models import Thread from .test_threads_api import ThreadsApiTestCase @@ -48,10 +48,9 @@ def test_add_acl_false(self): class ThreadChangeTitleApiTests(ThreadPatchApiTestCase): + @patch_category_acl({'can_edit_threads': 2}) def test_change_thread_title(self): """api makes it possible to change thread title""" - self.override_acl({'can_edit_threads': 2}) - response = self.patch( self.api_link, [ { @@ -69,10 +68,9 @@ def test_change_thread_title(self): thread_json = self.get_thread_json() self.assertEqual(thread_json['title'], "Lorem ipsum change!") + @patch_category_acl({'can_edit_threads': 0}) def test_change_thread_title_no_permission(self): """api validates permission to change title""" - self.override_acl({'can_edit_threads': 0}) - response = self.patch( self.api_link, [ { @@ -87,13 +85,9 @@ def test_change_thread_title_no_permission(self): response_json = response.json() self.assertEqual(response_json['detail'][0], "You can't edit threads in this category.") + @patch_category_acl({'can_edit_threads': 2, 'can_close_threads': 0}) def test_change_thread_title_closed_category_no_permission(self): """api test permission to edit thread title in closed category""" - self.override_acl({ - 'can_edit_threads': 2, - 'can_close_threads': 0 - }) - self.category.is_closed = True self.category.save() @@ -113,13 +107,9 @@ def test_change_thread_title_closed_category_no_permission(self): response_json['detail'][0], "This category is closed. You can't edit threads in it." ) + @patch_category_acl({'can_edit_threads': 2, 'can_close_threads': 0}) def test_change_thread_title_closed_thread_no_permission(self): """api test permission to edit closed thread title""" - self.override_acl({ - 'can_edit_threads': 2, - 'can_close_threads': 0 - }) - self.thread.is_closed = True self.thread.save() @@ -139,10 +129,9 @@ def test_change_thread_title_closed_thread_no_permission(self): response_json['detail'][0], "This thread is closed. You can't edit it." ) + @patch_category_acl({'can_edit_threads': 1, 'thread_edit_time': 1}) def test_change_thread_title_after_edit_time(self): """api cleans, validates and rejects too short title""" - self.override_acl({'thread_edit_time': 1, 'can_edit_threads': 1}) - self.thread.started_on = timezone.now() - timedelta(minutes=10) self.thread.starter = self.user self.thread.save() @@ -163,10 +152,9 @@ def test_change_thread_title_after_edit_time(self): response_json['detail'][0], "You can't edit threads that are older than 1 minute." ) + @patch_category_acl({'can_edit_threads': 2}) def test_change_thread_title_invalid(self): """api cleans, validates and rejects too short title""" - self.override_acl({'can_edit_threads': 2}) - response = self.patch( self.api_link, [ { @@ -186,10 +174,9 @@ def test_change_thread_title_invalid(self): class ThreadPinGloballyApiTests(ThreadPatchApiTestCase): + @patch_category_acl({'can_pin_threads': 2}) def test_pin_thread(self): """api makes it possible to pin globally thread""" - self.override_acl({'can_pin_threads': 2}) - response = self.patch( self.api_link, [ { @@ -207,13 +194,9 @@ def test_pin_thread(self): thread_json = self.get_thread_json() self.assertEqual(thread_json['weight'], 2) + @patch_category_acl({'can_pin_threads': 2, 'can_close_threads': 0}) def test_pin_thread_closed_category_no_permission(self): """api checks if category is closed""" - self.override_acl({ - 'can_pin_threads': 2, - 'can_close_threads': 0, - }) - self.category.is_closed = True self.category.save() @@ -233,13 +216,9 @@ def test_pin_thread_closed_category_no_permission(self): response_json['detail'][0], "This category is closed. You can't change threads weights in it." ) + @patch_category_acl({'can_pin_threads': 2, 'can_close_threads': 0}) def test_pin_thread_closed_no_permission(self): """api checks if thread is closed""" - self.override_acl({ - 'can_pin_threads': 2, - 'can_close_threads': 0, - }) - self.thread.is_closed = True self.thread.save() @@ -259,6 +238,7 @@ def test_pin_thread_closed_no_permission(self): response_json['detail'][0], "This thread is closed. You can't change its weight." ) + @patch_category_acl({'can_pin_threads': 2}) def test_unpin_thread(self): """api makes it possible to unpin thread""" self.thread.weight = 2 @@ -267,8 +247,6 @@ def test_unpin_thread(self): thread_json = self.get_thread_json() self.assertEqual(thread_json['weight'], 2) - self.override_acl({'can_pin_threads': 2}) - response = self.patch( self.api_link, [ { @@ -286,10 +264,9 @@ def test_unpin_thread(self): thread_json = self.get_thread_json() self.assertEqual(thread_json['weight'], 0) + @patch_category_acl({'can_pin_threads': 1}) def test_pin_thread_no_permission(self): """api pin thread globally with no permission fails""" - self.override_acl({'can_pin_threads': 1}) - response = self.patch( self.api_link, [ { @@ -309,6 +286,7 @@ def test_pin_thread_no_permission(self): thread_json = self.get_thread_json() self.assertEqual(thread_json['weight'], 0) + @patch_category_acl({'can_pin_threads': 1}) def test_unpin_thread_no_permission(self): """api unpin thread with no permission fails""" self.thread.weight = 2 @@ -317,8 +295,6 @@ def test_unpin_thread_no_permission(self): thread_json = self.get_thread_json() self.assertEqual(thread_json['weight'], 2) - self.override_acl({'can_pin_threads': 1}) - response = self.patch( self.api_link, [ { @@ -340,10 +316,9 @@ def test_unpin_thread_no_permission(self): class ThreadPinLocallyApiTests(ThreadPatchApiTestCase): + @patch_category_acl({'can_pin_threads': 1}) def test_pin_thread(self): """api makes it possible to pin locally thread""" - self.override_acl({'can_pin_threads': 1}) - response = self.patch( self.api_link, [ { @@ -361,6 +336,7 @@ def test_pin_thread(self): thread_json = self.get_thread_json() self.assertEqual(thread_json['weight'], 1) + @patch_category_acl({'can_pin_threads': 1}) def test_unpin_thread(self): """api makes it possible to unpin thread""" self.thread.weight = 1 @@ -369,8 +345,6 @@ def test_unpin_thread(self): thread_json = self.get_thread_json() self.assertEqual(thread_json['weight'], 1) - self.override_acl({'can_pin_threads': 1}) - response = self.patch( self.api_link, [ { @@ -388,10 +362,9 @@ def test_unpin_thread(self): thread_json = self.get_thread_json() self.assertEqual(thread_json['weight'], 0) + @patch_category_acl({'can_pin_threads': 0}) def test_pin_thread_no_permission(self): """api pin thread locally with no permission fails""" - self.override_acl({'can_pin_threads': 0}) - response = self.patch( self.api_link, [ { @@ -411,6 +384,7 @@ def test_pin_thread_no_permission(self): thread_json = self.get_thread_json() self.assertEqual(thread_json['weight'], 0) + @patch_category_acl({'can_pin_threads': 0}) def test_unpin_thread_no_permission(self): """api unpin thread with no permission fails""" self.thread.weight = 1 @@ -419,8 +393,6 @@ def test_unpin_thread_no_permission(self): thread_json = self.get_thread_json() self.assertEqual(thread_json['weight'], 1) - self.override_acl({'can_pin_threads': 0}) - response = self.patch( self.api_link, [ { @@ -446,57 +418,31 @@ def setUp(self): super().setUp() Category( - name='Category B', - slug='category-b', + name='Other category', + slug='other-category', ).insert_at( self.category, position='last-child', save=True, ) - self.category_b = Category.objects.get(slug='category-b') - - def override_other_acl(self, acl): - other_category_acl = self.user.acl_cache['categories'][self.category.pk].copy() - other_category_acl.update({ - 'can_see': 1, - 'can_browse': 1, - 'can_see_all_threads': 1, - 'can_see_own_threads': 0, - 'can_hide_threads': 0, - 'can_approve_content': 0, - }) - other_category_acl.update(acl) - - categories_acl = self.user.acl_cache['categories'] - categories_acl[self.category_b.pk] = other_category_acl - - visible_categories = [self.category.pk] - if other_category_acl['can_see']: - visible_categories.append(self.category_b.pk) - - override_acl( - self.user, { - 'visible_categories': visible_categories, - 'categories': categories_acl, - } - ) + self.dst_category = Category.objects.get(slug='other-category') + @patch_categories_acl_for_move( + {'can_move_threads': True}, {'can_start_threads': 2} + ) def test_move_thread_no_top(self): """api moves thread to other category, sets no top category""" - self.override_acl({'can_move_threads': True}) - self.override_other_acl({'can_start_threads': 2}) - response = self.patch( self.api_link, [ { 'op': 'replace', 'path': 'category', - 'value': self.category_b.pk, + 'value': self.dst_category.pk, }, { 'op': 'add', 'path': 'top-category', - 'value': self.category_b.pk, + 'value': self.dst_category.pk, }, { 'op': 'replace', @@ -508,24 +454,22 @@ def test_move_thread_no_top(self): self.assertEqual(response.status_code, 200) reponse_json = response.json() - self.assertEqual(reponse_json['category'], self.category_b.pk) - - self.override_other_acl({}) + self.assertEqual(reponse_json['category'], self.dst_category.pk) thread_json = self.get_thread_json() - self.assertEqual(thread_json['category']['id'], self.category_b.pk) + self.assertEqual(thread_json['category']['id'], self.dst_category.pk) + @patch_categories_acl_for_move( + {'can_move_threads': True}, {'can_start_threads': 2} + ) def test_move_thread_with_top(self): """api moves thread to other category, sets top""" - self.override_acl({'can_move_threads': True}) - self.override_other_acl({'can_start_threads': 2}) - response = self.patch( self.api_link, [ { 'op': 'replace', 'path': 'category', - 'value': self.category_b.pk, + 'value': self.dst_category.pk, }, { 'op': 'add', @@ -542,18 +486,16 @@ def test_move_thread_with_top(self): self.assertEqual(response.status_code, 200) reponse_json = response.json() - self.assertEqual(reponse_json['category'], self.category_b.pk) - - self.override_other_acl({}) + self.assertEqual(reponse_json['category'], self.dst_category.pk) thread_json = self.get_thread_json() - self.assertEqual(thread_json['category']['id'], self.category_b.pk) + self.assertEqual(thread_json['category']['id'], self.dst_category.pk) + @patch_categories_acl_for_move( + {'can_move_threads': True}, {'can_start_threads': 2} + ) def test_move_thread_reads(self): """api moves thread reads together with thread""" - self.override_acl({'can_move_threads': True}) - self.override_other_acl({'can_start_threads': 2}) - poststracker.save_read(self.user, self.thread.first_post) self.assertEqual(self.user.postread_set.count(), 1) @@ -564,12 +506,12 @@ def test_move_thread_reads(self): { 'op': 'replace', 'path': 'category', - 'value': self.category_b.pk, + 'value': self.dst_category.pk, }, { 'op': 'add', 'path': 'top-category', - 'value': self.category_b.pk, + 'value': self.dst_category.pk, }, { 'op': 'replace', @@ -584,13 +526,13 @@ def test_move_thread_reads(self): postreads = self.user.postread_set.filter(post__is_event=False).order_by('id') self.assertEqual(postreads.count(), 1) - postreads.get(category=self.category_b) + postreads.get(category=self.dst_category) + @patch_categories_acl_for_move( + {'can_move_threads': True}, {'can_start_threads': 2} + ) def test_move_thread_subscriptions(self): """api moves thread subscriptions together with thread""" - self.override_acl({'can_move_threads': True}) - self.override_other_acl({'can_start_threads': 2}) - self.user.subscription_set.create( thread=self.thread, category=self.thread.category, @@ -606,12 +548,12 @@ def test_move_thread_subscriptions(self): { 'op': 'replace', 'path': 'category', - 'value': self.category_b.pk, + 'value': self.dst_category.pk, }, { 'op': 'add', 'path': 'top-category', - 'value': self.category_b.pk, + 'value': self.dst_category.pk, }, { 'op': 'replace', @@ -624,19 +566,17 @@ def test_move_thread_subscriptions(self): # thread read was moved to new category self.assertEqual(self.user.subscription_set.count(), 1) - self.user.subscription_set.get(category=self.category_b) + self.user.subscription_set.get(category=self.dst_category) + @patch_categories_acl_for_move({'can_move_threads': False}) def test_move_thread_no_permission(self): """api move thread to other category with no permission fails""" - self.override_acl({'can_move_threads': False}) - self.override_other_acl({}) - response = self.patch( self.api_link, [ { 'op': 'replace', 'path': 'category', - 'value': self.category_b.pk, + 'value': self.dst_category.pk, }, ] ) @@ -647,19 +587,12 @@ def test_move_thread_no_permission(self): response_json['detail'][0], "You can't move threads in this category." ) - self.override_other_acl({}) - thread_json = self.get_thread_json() self.assertEqual(thread_json['category']['id'], self.category.pk) + @patch_category_acl({'can_move_threads': True, 'can_close_threads': False}) def test_move_thread_closed_category_no_permission(self): """api move thread from closed category with no permission fails""" - self.override_acl({ - 'can_move_threads': True, - 'can_close_threads': False, - }) - self.override_other_acl({}) - self.category.is_closed = True self.category.save() @@ -668,7 +601,7 @@ def test_move_thread_closed_category_no_permission(self): { 'op': 'replace', 'path': 'category', - 'value': self.category_b.pk, + 'value': self.dst_category.pk, }, ] ) @@ -679,14 +612,9 @@ def test_move_thread_closed_category_no_permission(self): response_json['detail'][0], "This category is closed. You can't move it's threads." ) + @patch_category_acl({'can_move_threads': True, 'can_close_threads': False}) def test_move_closed_thread_no_permission(self): """api move closed thread with no permission fails""" - self.override_acl({ - 'can_move_threads': True, - 'can_close_threads': False, - }) - self.override_other_acl({}) - self.thread.is_closed = True self.thread.save() @@ -695,7 +623,7 @@ def test_move_closed_thread_no_permission(self): { 'op': 'replace', 'path': 'category', - 'value': self.category_b.pk, + 'value': self.dst_category.pk, }, ] ) @@ -706,17 +634,15 @@ def test_move_closed_thread_no_permission(self): response_json['detail'][0], "This thread is closed. You can't move it." ) + @patch_categories_acl_for_move({'can_move_threads': True}, {'can_see': False}) def test_move_thread_no_category_access(self): """api move thread to category with no access fails""" - self.override_acl({'can_move_threads': True}) - self.override_other_acl({'can_see': False}) - response = self.patch( self.api_link, [ { 'op': 'replace', 'path': 'category', - 'value': self.category_b.pk, + 'value': self.dst_category.pk, }, ] ) @@ -725,22 +651,18 @@ def test_move_thread_no_category_access(self): response_json = response.json() self.assertEqual(response_json['detail'][0], 'NOT FOUND') - self.override_other_acl({}) - thread_json = self.get_thread_json() self.assertEqual(thread_json['category']['id'], self.category.pk) + @patch_categories_acl_for_move({'can_move_threads': True}, {'can_browse': False}) def test_move_thread_no_category_browse(self): """api move thread to category with no browsing access fails""" - self.override_acl({'can_move_threads': True}) - self.override_other_acl({'can_browse': False}) - response = self.patch( self.api_link, [ { 'op': 'replace', 'path': 'category', - 'value': self.category_b.pk, + 'value': self.dst_category.pk, }, ] ) @@ -749,25 +671,23 @@ def test_move_thread_no_category_browse(self): response_json = response.json() self.assertEqual( response_json['detail'][0], - 'You don\'t have permission to browse "Category B" contents.' + 'You don\'t have permission to browse "Other category" contents.' ) - self.override_other_acl({}) - thread_json = self.get_thread_json() self.assertEqual(thread_json['category']['id'], self.category.pk) + @patch_categories_acl_for_move( + {'can_move_threads': True}, {'can_start_threads': False} + ) def test_move_thread_no_category_start_threads(self): """api move thread to category with no posting access fails""" - self.override_acl({'can_move_threads': True}) - self.override_other_acl({'can_start_threads': False}) - response = self.patch( self.api_link, [ { 'op': 'replace', 'path': 'category', - 'value': self.category_b.pk, + 'value': self.dst_category.pk, }, ] ) @@ -779,16 +699,14 @@ def test_move_thread_no_category_start_threads(self): "You don't have permission to start new threads in this category." ) - self.override_other_acl({}) - thread_json = self.get_thread_json() self.assertEqual(thread_json['category']['id'], self.category.pk) + @patch_categories_acl_for_move( + {'can_move_threads': True}, {'can_start_threads': 2} + ) def test_move_thread_same_category(self): """api move thread to category it's already in fails""" - self.override_acl({'can_move_threads': True}) - self.override_other_acl({'can_start_threads': 2}) - response = self.patch( self.api_link, [ { @@ -805,8 +723,6 @@ def test_move_thread_same_category(self): response_json['detail'][0], "You can't move thread to the category it's already in." ) - self.override_other_acl({}) - thread_json = self.get_thread_json() self.assertEqual(thread_json['category']['id'], self.category.pk) @@ -828,10 +744,9 @@ def test_thread_flatten_categories(self): class ThreadCloseApiTests(ThreadPatchApiTestCase): + @patch_category_acl({'can_close_threads': True}) def test_close_thread(self): """api makes it possible to close thread""" - self.override_acl({'can_close_threads': True}) - response = self.patch( self.api_link, [ { @@ -849,6 +764,7 @@ def test_close_thread(self): thread_json = self.get_thread_json() self.assertTrue(thread_json['is_closed']) + @patch_category_acl({'can_close_threads': True}) def test_open_thread(self): """api makes it possible to open thread""" self.thread.is_closed = True @@ -857,8 +773,6 @@ def test_open_thread(self): thread_json = self.get_thread_json() self.assertTrue(thread_json['is_closed']) - self.override_acl({'can_close_threads': True}) - response = self.patch( self.api_link, [ { @@ -876,10 +790,9 @@ def test_open_thread(self): thread_json = self.get_thread_json() self.assertFalse(thread_json['is_closed']) + @patch_category_acl({'can_close_threads': False}) def test_close_thread_no_permission(self): """api close thread with no permission fails""" - self.override_acl({'can_close_threads': False}) - response = self.patch( self.api_link, [ { @@ -899,6 +812,7 @@ def test_close_thread_no_permission(self): thread_json = self.get_thread_json() self.assertFalse(thread_json['is_closed']) + @patch_category_acl({'can_close_threads': False}) def test_open_thread_no_permission(self): """api open thread with no permission fails""" self.thread.is_closed = True @@ -907,8 +821,6 @@ def test_open_thread_no_permission(self): thread_json = self.get_thread_json() self.assertTrue(thread_json['is_closed']) - self.override_acl({'can_close_threads': False}) - response = self.patch( self.api_link, [ { @@ -930,6 +842,7 @@ def test_open_thread_no_permission(self): class ThreadApproveApiTests(ThreadPatchApiTestCase): + @patch_category_acl({'can_approve_content': True}) def test_approve_thread(self): """api makes it possible to approve thread""" self.thread.first_post.is_unapproved = True @@ -941,8 +854,6 @@ def test_approve_thread(self): self.assertTrue(self.thread.is_unapproved) self.assertTrue(self.thread.has_unapproved_posts) - self.override_acl({'can_approve_content': 1}) - response = self.patch( self.api_link, [ { @@ -966,6 +877,7 @@ def test_approve_thread(self): self.assertFalse(thread.is_unapproved) self.assertFalse(thread.has_unapproved_posts) + @patch_category_acl({'can_approve_content': True, 'can_close_threads': False}) def test_approve_thread_category_closed_no_permission(self): """api checks permission for approving threads in closed categories""" self.thread.first_post.is_unapproved = True @@ -980,11 +892,6 @@ def test_approve_thread_category_closed_no_permission(self): self.category.is_closed = True self.category.save() - self.override_acl({ - 'can_approve_content': 1, - 'can_close_threads': 0, - }) - response = self.patch( self.api_link, [ { @@ -999,6 +906,7 @@ def test_approve_thread_category_closed_no_permission(self): response_json = response.json() self.assertEqual(response_json['detail'][0], "This category is closed. You can't approve threads in it.") + @patch_category_acl({'can_approve_content': True, 'can_close_threads': False}) def test_approve_thread_closed_no_permission(self): """api checks permission for approving posts in closed categories""" self.thread.first_post.is_unapproved = True @@ -1013,11 +921,6 @@ def test_approve_thread_closed_no_permission(self): self.thread.is_closed = True self.thread.save() - self.override_acl({ - 'can_approve_content': 1, - 'can_close_threads': 0, - }) - response = self.patch( self.api_link, [ { @@ -1032,10 +935,9 @@ def test_approve_thread_closed_no_permission(self): response_json = response.json() self.assertEqual(response_json['detail'][0], "This thread is closed. You can't approve it.") + @patch_category_acl({'can_approve_content': True}) def test_unapprove_thread(self): """api returns permission error on approval removal""" - self.override_acl({'can_approve_content': 1}) - response = self.patch( self.api_link, [ { @@ -1052,10 +954,9 @@ def test_unapprove_thread(self): class ThreadHideApiTests(ThreadPatchApiTestCase): + @patch_category_acl({'can_hide_threads': True}) def test_hide_thread(self): """api makes it possible to hide thread""" - self.override_acl({'can_hide_threads': 1}) - response = self.patch( self.api_link, [ { @@ -1070,15 +971,12 @@ def test_hide_thread(self): reponse_json = response.json() self.assertTrue(reponse_json['is_hidden']) - self.override_acl({'can_hide_threads': 1}) - thread_json = self.get_thread_json() self.assertTrue(thread_json['is_hidden']) + @patch_category_acl({'can_hide_threads': False}) def test_hide_thread_no_permission(self): """api hide thread with no permission fails""" - self.override_acl({'can_hide_threads': 0}) - response = self.patch( self.api_link, [ { @@ -1098,13 +996,9 @@ def test_hide_thread_no_permission(self): thread_json = self.get_thread_json() self.assertFalse(thread_json['is_hidden']) + @patch_category_acl({'can_hide_threads': False, 'can_hide_own_threads': True}) def test_hide_non_owned_thread(self): """api forbids non-moderator from hiding other users threads""" - self.override_acl({ - 'can_hide_own_threads': 1, - 'can_hide_threads': 0 - }) - response = self.patch( self.api_link, [ { @@ -1121,14 +1015,13 @@ def test_hide_non_owned_thread(self): response_json['detail'][0], "You can't hide other users theads in this category." ) + @patch_category_acl({ + 'can_hide_threads': False, + 'can_hide_own_threads': True, + 'thread_edit_time': 1, + }) def test_hide_owned_thread_no_time(self): """api forbids non-moderator from hiding other users threads""" - self.override_acl({ - 'can_hide_own_threads': 1, - 'can_hide_threads': 0, - 'thread_edit_time': 1, - }) - self.thread.started_on = timezone.now() - timedelta(minutes=5) self.thread.starter = self.user self.thread.save() @@ -1149,13 +1042,9 @@ def test_hide_owned_thread_no_time(self): response_json['detail'][0], "You can't hide threads that are older than 1 minute." ) + @patch_category_acl({'can_hide_threads': True, 'can_close_threads': False}) def test_hide_closed_category_no_permission(self): """api test permission to hide thread in closed category""" - self.override_acl({ - 'can_hide_threads': 1, - 'can_close_threads': 0 - }) - self.category.is_closed = True self.category.save() @@ -1175,13 +1064,9 @@ def test_hide_closed_category_no_permission(self): response_json['detail'][0], "This category is closed. You can't hide threads in it." ) + @patch_category_acl({'can_hide_threads': True, 'can_close_threads': False}) def test_hide_closed_thread_no_permission(self): """api test permission to hide closed thread""" - self.override_acl({ - 'can_hide_threads': 1, - 'can_close_threads': 0 - }) - self.thread.is_closed = True self.thread.save() @@ -1209,10 +1094,9 @@ def setUp(self): self.thread.is_hidden = True self.thread.save() + @patch_category_acl({'can_hide_threads': True}) def test_unhide_thread(self): """api makes it possible to unhide thread""" - self.override_acl({'can_hide_threads': 1}) - response = self.patch( self.api_link, [ { @@ -1227,15 +1111,12 @@ def test_unhide_thread(self): reponse_json = response.json() self.assertFalse(reponse_json['is_hidden']) - self.override_acl({'can_hide_threads': 1}) - thread_json = self.get_thread_json() self.assertFalse(thread_json['is_hidden']) + @patch_category_acl({'can_hide_threads': False}) def test_unhide_thread_no_permission(self): """api unhide thread with no permission fails as thread is invisible""" - self.override_acl({'can_hide_threads': 0}) - response = self.patch( self.api_link, [ { @@ -1247,13 +1128,9 @@ def test_unhide_thread_no_permission(self): ) self.assertEqual(response.status_code, 404) + @patch_category_acl({'can_hide_threads': True, 'can_close_threads': False}) def test_unhide_closed_category_no_permission(self): """api test permission to unhide thread in closed category""" - self.override_acl({ - 'can_hide_threads': 1, - 'can_close_threads': 0 - }) - self.category.is_closed = True self.category.save() @@ -1273,13 +1150,9 @@ def test_unhide_closed_category_no_permission(self): response_json['detail'][0], "This category is closed. You can't reveal threads in it." ) + @patch_category_acl({'can_hide_threads': True, 'can_close_threads': False}) def test_unhide_closed_thread_no_permission(self): """api test permission to unhide closed thread""" - self.override_acl({ - 'can_hide_threads': 1, - 'can_close_threads': 0 - }) - self.thread.is_closed = True self.thread.save() @@ -1405,10 +1278,9 @@ def test_subscribe_nonexistant_thread(self): class ThreadMarkBestAnswerApiTests(ThreadPatchApiTestCase): + @patch_category_acl({'can_mark_best_answers': 2}) def test_mark_best_answer(self): """api makes it possible to mark best answer""" - self.override_acl({'can_mark_best_answers': 2}) - best_answer = testutils.reply_thread(self.thread) response = self.patch( @@ -1442,12 +1314,11 @@ def test_mark_best_answer(self): self.assertEqual(thread_json['best_answer_marked_by_name'], self.user.username) self.assertEqual(thread_json['best_answer_marked_by_slug'], self.user.slug) + @patch_category_acl({'can_mark_best_answers': 2}) def test_mark_best_answer_anonymous(self): """api validates that user is authenticated before marking best answer""" self.logout_user() - self.override_acl({'can_mark_best_answers': 2}) - best_answer = testutils.reply_thread(self.thread) response = self.patch( @@ -1467,10 +1338,9 @@ def test_mark_best_answer_anonymous(self): thread_json = self.get_thread_json() self.assertIsNone(thread_json['best_answer']) + @patch_category_acl({'can_mark_best_answers': 0}) def test_mark_best_answer_no_permission(self): """api validates permission to mark best answers""" - self.override_acl({'can_mark_best_answers': 0}) - best_answer = testutils.reply_thread(self.thread) response = self.patch( @@ -1493,10 +1363,9 @@ def test_mark_best_answer_no_permission(self): thread_json = self.get_thread_json() self.assertIsNone(thread_json['best_answer']) + @patch_category_acl({'can_mark_best_answers': 1}) def test_mark_best_answer_not_thread_starter(self): """api validates permission to mark best answers in owned thread""" - self.override_acl({'can_mark_best_answers': 1}) - best_answer = testutils.reply_thread(self.thread) response = self.patch( @@ -1524,8 +1393,6 @@ def test_mark_best_answer_not_thread_starter(self): self.thread.starter = self.user self.thread.save() - self.override_acl({'can_mark_best_answers': 1}) - response = self.patch( self.api_link, [ { @@ -1537,10 +1404,9 @@ def test_mark_best_answer_not_thread_starter(self): ) self.assertEqual(response.status_code, 200) - def test_mark_best_answer_category_closed(self): + @patch_category_acl({'can_mark_best_answers': 2, 'can_close_threads': False}) + def test_mark_best_answer_category_closed_no_permission(self): """api validates permission to mark best answers in closed category""" - self.override_acl({'can_mark_best_answers': 2, 'can_close_threads': 0}) - best_answer = testutils.reply_thread(self.thread) self.category.is_closed = True @@ -1567,8 +1433,13 @@ def test_mark_best_answer_category_closed(self): thread_json = self.get_thread_json() self.assertIsNone(thread_json['best_answer']) - # passing scenario is possible - self.override_acl({'can_mark_best_answers': 2, 'can_close_threads': 1}) + @patch_category_acl({'can_mark_best_answers': 2, 'can_close_threads': True}) + def test_mark_best_answer_category_closed(self): + """api validates permission to mark best answers in closed category""" + best_answer = testutils.reply_thread(self.thread) + + self.category.is_closed = True + self.category.save() response = self.patch( self.api_link, [ @@ -1581,10 +1452,9 @@ def test_mark_best_answer_category_closed(self): ) self.assertEqual(response.status_code, 200) - def test_mark_best_answer_thread_closed(self): + @patch_category_acl({'can_mark_best_answers': 2, 'can_close_threads': False}) + def test_mark_best_answer_thread_closed_no_permission(self): """api validates permission to mark best answers in closed thread""" - self.override_acl({'can_mark_best_answers': 2, 'can_close_threads': 0}) - best_answer = testutils.reply_thread(self.thread) self.thread.is_closed = True @@ -1611,8 +1481,13 @@ def test_mark_best_answer_thread_closed(self): thread_json = self.get_thread_json() self.assertIsNone(thread_json['best_answer']) - # passing scenario is possible - self.override_acl({'can_mark_best_answers': 2, 'can_close_threads': 1}) + @patch_category_acl({'can_mark_best_answers': 2, 'can_close_threads': True}) + def test_mark_best_answer_thread_closed(self): + """api validates permission to mark best answers in closed thread""" + best_answer = testutils.reply_thread(self.thread) + + self.thread.is_closed = True + self.thread.save() response = self.patch( self.api_link, [ @@ -1624,11 +1499,10 @@ def test_mark_best_answer_thread_closed(self): ] ) self.assertEqual(response.status_code, 200) - + + @patch_category_acl({'can_mark_best_answers': 2}) def test_mark_best_answer_invalid_post_id(self): """api validates that post id is int""" - self.override_acl({'can_mark_best_answers': 2}) - response = self.patch( self.api_link, [ { @@ -1647,10 +1521,9 @@ def test_mark_best_answer_invalid_post_id(self): thread_json = self.get_thread_json() self.assertIsNone(thread_json['best_answer']) + @patch_category_acl({'can_mark_best_answers': 2}) def test_mark_best_answer_post_not_found(self): """api validates that post exists""" - self.override_acl({'can_mark_best_answers': 2}) - response = self.patch( self.api_link, [ { @@ -1668,11 +1541,10 @@ def test_mark_best_answer_post_not_found(self): thread_json = self.get_thread_json() self.assertIsNone(thread_json['best_answer']) - + + @patch_category_acl({'can_mark_best_answers': 2}) def test_mark_best_answer_post_invisible(self): """api validates post visibility to action author""" - self.override_acl({'can_mark_best_answers': 2}) - unapproved_post = testutils.reply_thread(self.thread, is_unapproved=True) response = self.patch( @@ -1693,10 +1565,9 @@ def test_mark_best_answer_post_invisible(self): thread_json = self.get_thread_json() self.assertIsNone(thread_json['best_answer']) + @patch_category_acl({'can_mark_best_answers': 2}) def test_mark_best_answer_post_other_thread(self): """api validates post belongs to same thread""" - self.override_acl({'can_mark_best_answers': 2}) - other_thread = testutils.post_thread(self.category) response = self.patch( @@ -1717,10 +1588,9 @@ def test_mark_best_answer_post_other_thread(self): thread_json = self.get_thread_json() self.assertIsNone(thread_json['best_answer']) + @patch_category_acl({'can_mark_best_answers': 2}) def test_mark_best_answer_event_id(self): """api validates that post is not an event""" - self.override_acl({'can_mark_best_answers': 2}) - best_answer = testutils.reply_thread(self.thread) best_answer.is_event = True best_answer.save() @@ -1743,10 +1613,9 @@ def test_mark_best_answer_event_id(self): thread_json = self.get_thread_json() self.assertIsNone(thread_json['best_answer']) + @patch_category_acl({'can_mark_best_answers': 2}) def test_mark_best_answer_first_post(self): """api validates that post is not a first post in thread""" - self.override_acl({'can_mark_best_answers': 2}) - response = self.patch( self.api_link, [ { @@ -1765,10 +1634,9 @@ def test_mark_best_answer_first_post(self): thread_json = self.get_thread_json() self.assertIsNone(thread_json['best_answer']) + @patch_category_acl({'can_mark_best_answers': 2}) def test_mark_best_answer_hidden_post(self): """api validates that post is not hidden""" - self.override_acl({'can_mark_best_answers': 2}) - best_answer = testutils.reply_thread(self.thread, is_hidden=True) response = self.patch( @@ -1789,10 +1657,9 @@ def test_mark_best_answer_hidden_post(self): thread_json = self.get_thread_json() self.assertIsNone(thread_json['best_answer']) + @patch_category_acl({'can_mark_best_answers': 2}) def test_mark_best_answer_unapproved_post(self): """api validates that post is not unapproved""" - self.override_acl({'can_mark_best_answers': 2}) - best_answer = testutils.reply_thread(self.thread, poster=self.user, is_unapproved=True) response = self.patch( @@ -1813,10 +1680,9 @@ def test_mark_best_answer_unapproved_post(self): thread_json = self.get_thread_json() self.assertIsNone(thread_json['best_answer']) - def test_mark_best_answer_protected_post(self): + @patch_category_acl({'can_mark_best_answers': 2, 'can_protect_posts': False}) + def test_mark_best_answer_protected_post_no_permission(self): """api respects post protection""" - self.override_acl({'can_mark_best_answers': 2, 'can_protect_posts': 0}) - best_answer = testutils.reply_thread(self.thread, is_protected=True) response = self.patch( @@ -1840,8 +1706,10 @@ def test_mark_best_answer_protected_post(self): thread_json = self.get_thread_json() self.assertIsNone(thread_json['best_answer']) - # passing scenario is possible - self.override_acl({'can_mark_best_answers': 2, 'can_protect_posts': 1}) + @patch_category_acl({'can_mark_best_answers': 2, 'can_protect_posts': True}) + def test_mark_best_answer_protected_post(self): + """api respects post protection""" + best_answer = testutils.reply_thread(self.thread, is_protected=True) response = self.patch( self.api_link, [ @@ -1863,10 +1731,9 @@ def setUp(self): self.thread.set_best_answer(self.user, self.best_answer) self.thread.save() + @patch_category_acl({'can_mark_best_answers': 2, 'can_change_marked_answers': 2}) def test_change_best_answer(self): """api makes it possible to change best answer""" - self.override_acl({'can_mark_best_answers': 2, 'can_change_marked_answers': 2}) - best_answer = testutils.reply_thread(self.thread) response = self.patch( @@ -1900,10 +1767,9 @@ def test_change_best_answer(self): self.assertEqual(thread_json['best_answer_marked_by_name'], self.user.username) self.assertEqual(thread_json['best_answer_marked_by_slug'], self.user.slug) + @patch_category_acl({'can_mark_best_answers': 2, 'can_change_marked_answers': 2}) def test_change_best_answer_same_post(self): """api validates if new best answer is same as current one""" - self.override_acl({'can_mark_best_answers': 2, 'can_change_marked_answers': 2}) - response = self.patch( self.api_link, [ { @@ -1922,10 +1788,9 @@ def test_change_best_answer_same_post(self): thread_json = self.get_thread_json() self.assertEqual(thread_json['best_answer'], self.best_answer.id) + @patch_category_acl({'can_mark_best_answers': 0, 'can_change_marked_answers': 2}) def test_change_best_answer_no_permission_to_mark(self): """api validates permission to mark best answers before allowing answer change""" - self.override_acl({'can_mark_best_answers': 0, 'can_change_marked_answers': 2}) - best_answer = testutils.reply_thread(self.thread) response = self.patch( @@ -1948,10 +1813,9 @@ def test_change_best_answer_no_permission_to_mark(self): thread_json = self.get_thread_json() self.assertEqual(thread_json['best_answer'], self.best_answer.id) + @patch_category_acl({'can_mark_best_answers': 2, 'can_change_marked_answers': 0}) def test_change_best_answer_no_permission(self): """api validates permission to change best answers""" - self.override_acl({'can_mark_best_answers': 2, 'can_change_marked_answers': 0}) - best_answer = testutils.reply_thread(self.thread) response = self.patch( @@ -1975,10 +1839,9 @@ def test_change_best_answer_no_permission(self): thread_json = self.get_thread_json() self.assertEqual(thread_json['best_answer'], self.best_answer.id) + @patch_category_acl({'can_mark_best_answers': 2, 'can_change_marked_answers': 1}) def test_change_best_answer_not_starter(self): """api validates permission to change best answers""" - self.override_acl({'can_mark_best_answers': 2, 'can_change_marked_answers': 1}) - best_answer = testutils.reply_thread(self.thread) response = self.patch( @@ -2003,8 +1866,6 @@ def test_change_best_answer_not_starter(self): self.assertEqual(thread_json['best_answer'], self.best_answer.id) # passing scenario is possible - self.override_acl({'can_mark_best_answers': 2, 'can_change_marked_answers': 1}) - self.thread.starter = self.user self.thread.save() @@ -2019,14 +1880,13 @@ def test_change_best_answer_not_starter(self): ) self.assertEqual(response.status_code, 200) - def test_change_best_answer_timelimit(self): + @patch_category_acl({ + 'can_mark_best_answers': 2, + 'can_change_marked_answers': 1, + 'best_answer_change_time': 5, + }) + def test_change_best_answer_timelimit_out_of_time(self): """api validates permission for starter to change best answers within timelimit""" - self.override_acl({ - 'can_mark_best_answers': 2, - 'can_change_marked_answers': 1, - 'best_answer_change_time': 5, - }) - best_answer = testutils.reply_thread(self.thread) self.thread.best_answer_marked_on = timezone.now() - timedelta(minutes=6) @@ -2054,13 +1914,19 @@ def test_change_best_answer_timelimit(self): thread_json = self.get_thread_json() self.assertEqual(thread_json['best_answer'], self.best_answer.id) - # passing scenario is possible - self.override_acl({ - 'can_mark_best_answers': 2, - 'can_change_marked_answers': 1, - 'best_answer_change_time': 10, - }) - + @patch_category_acl({ + 'can_mark_best_answers': 2, + 'can_change_marked_answers': 1, + 'best_answer_change_time': 5, + }) + def test_change_best_answer_timelimit(self): + """api validates permission for starter to change best answers within timelimit""" + best_answer = testutils.reply_thread(self.thread) + + self.thread.best_answer_marked_on = timezone.now() - timedelta(minutes=1) + self.thread.starter = self.user + self.thread.save() + response = self.patch( self.api_link, [ { @@ -2072,14 +1938,13 @@ def test_change_best_answer_timelimit(self): ) self.assertEqual(response.status_code, 200) - def test_change_best_answer_protected(self): + @patch_category_acl({ + 'can_mark_best_answers': 2, + 'can_change_marked_answers': 2, + 'can_protect_posts': False, + }) + def test_change_best_answer_protected_no_permission(self): """api validates permission to change protected best answers""" - self.override_acl({ - 'can_mark_best_answers': 2, - 'can_change_marked_answers': 2, - 'can_protect_posts': 0, - }) - best_answer = testutils.reply_thread(self.thread) self.thread.best_answer_is_protected = True @@ -2106,13 +1971,18 @@ def test_change_best_answer_protected(self): thread_json = self.get_thread_json() self.assertEqual(thread_json['best_answer'], self.best_answer.id) - # passing scenario is possible - self.override_acl({ - 'can_mark_best_answers': 2, - 'can_change_marked_answers': 2, - 'can_protect_posts': 1, - }) - + @patch_category_acl({ + 'can_mark_best_answers': 2, + 'can_change_marked_answers': 2, + 'can_protect_posts': True, + }) + def test_change_best_answer_protected(self): + """api validates permission to change protected best answers""" + best_answer = testutils.reply_thread(self.thread) + + self.thread.best_answer_is_protected = True + self.thread.save() + response = self.patch( self.api_link, [ { @@ -2124,13 +1994,9 @@ def test_change_best_answer_protected(self): ) self.assertEqual(response.status_code, 200) + @patch_category_acl({'can_mark_best_answers': 2, 'can_change_marked_answers': 2}) def test_change_best_answer_post_validation(self): """api validates new post'""" - self.override_acl({ - 'can_mark_best_answers': 2, - 'can_change_marked_answers': 2, - }) - best_answer = testutils.reply_thread(self.thread, is_hidden=True) response = self.patch( @@ -2156,10 +2022,9 @@ def setUp(self): self.thread.set_best_answer(self.user, self.best_answer) self.thread.save() + @patch_category_acl({'can_mark_best_answers': 0, 'can_change_marked_answers': 2}) def test_unmark_best_answer(self): """api makes it possible to unmark best answer""" - self.override_acl({'can_mark_best_answers': 0, 'can_change_marked_answers': 2}) - response = self.patch( self.api_link, [ { @@ -2190,10 +2055,9 @@ def test_unmark_best_answer(self): self.assertIsNone(thread_json['best_answer_marked_by_name']) self.assertIsNone(thread_json['best_answer_marked_by_slug']) + @patch_category_acl({'can_mark_best_answers': 0, 'can_change_marked_answers': 2}) def test_unmark_best_answer_invalid_post_id(self): """api validates that post id is int""" - self.override_acl({'can_mark_best_answers': 0, 'can_change_marked_answers': 2}) - response = self.patch( self.api_link, [ { @@ -2212,10 +2076,9 @@ def test_unmark_best_answer_invalid_post_id(self): thread_json = self.get_thread_json() self.assertEqual(thread_json['best_answer'], self.best_answer.id) + @patch_category_acl({'can_mark_best_answers': 0, 'can_change_marked_answers': 2}) def test_unmark_best_answer_post_not_found(self): """api validates that post to unmark exists""" - self.override_acl({'can_mark_best_answers': 0, 'can_change_marked_answers': 2}) - response = self.patch( self.api_link, [ { @@ -2233,11 +2096,10 @@ def test_unmark_best_answer_post_not_found(self): thread_json = self.get_thread_json() self.assertEqual(thread_json['best_answer'], self.best_answer.id) - + + @patch_category_acl({'can_mark_best_answers': 0, 'can_change_marked_answers': 2}) def test_unmark_best_answer_wrong_post(self): """api validates if post given to unmark is best answer""" - self.override_acl({'can_mark_best_answers': 0, 'can_change_marked_answers': 2}) - best_answer = testutils.reply_thread(self.thread) response = self.patch( @@ -2260,10 +2122,9 @@ def test_unmark_best_answer_wrong_post(self): thread_json = self.get_thread_json() self.assertEqual(thread_json['best_answer'], self.best_answer.id) + @patch_category_acl({'can_mark_best_answers': 0, 'can_change_marked_answers': 0}) def test_unmark_best_answer_no_permission(self): """api validates if user has permission to unmark best answers""" - self.override_acl({'can_mark_best_answers': 0, 'can_change_marked_answers': 0}) - response = self.patch( self.api_link, [ { @@ -2285,10 +2146,9 @@ def test_unmark_best_answer_no_permission(self): thread_json = self.get_thread_json() self.assertEqual(thread_json['best_answer'], self.best_answer.id) + @patch_category_acl({'can_mark_best_answers': 0, 'can_change_marked_answers': 1}) def test_unmark_best_answer_not_starter(self): """api validates if starter has permission to unmark best answers""" - self.override_acl({'can_mark_best_answers': 0, 'can_change_marked_answers': 1}) - response = self.patch( self.api_link, [ { @@ -2311,8 +2171,6 @@ def test_unmark_best_answer_not_starter(self): self.assertEqual(thread_json['best_answer'], self.best_answer.id) # passing scenario is possible - self.override_acl({'can_mark_best_answers': 0, 'can_change_marked_answers': 1}) - self.thread.starter = self.user self.thread.save() @@ -2327,14 +2185,13 @@ def test_unmark_best_answer_not_starter(self): ) self.assertEqual(response.status_code, 200) + @patch_category_acl({ + 'can_mark_best_answers': 0, + 'can_change_marked_answers': 1, + 'best_answer_change_time': 5, + }) def test_unmark_best_answer_timelimit(self): """api validates if starter has permission to unmark best answer within time limit""" - self.override_acl({ - 'can_mark_best_answers': 0, - 'can_change_marked_answers': 1, - 'best_answer_change_time': 5, - }) - self.thread.best_answer_marked_on = timezone.now() - timedelta(minutes=6) self.thread.starter = self.user self.thread.save() @@ -2361,11 +2218,8 @@ def test_unmark_best_answer_timelimit(self): self.assertEqual(thread_json['best_answer'], self.best_answer.id) # passing scenario is possible - self.override_acl({ - 'can_mark_best_answers': 0, - 'can_change_marked_answers': 1, - 'best_answer_change_time': 10, - }) + self.thread.best_answer_marked_on = timezone.now() - timedelta(minutes=2) + self.thread.save() response = self.patch( self.api_link, [ @@ -2378,14 +2232,13 @@ def test_unmark_best_answer_timelimit(self): ) self.assertEqual(response.status_code, 200) - def test_unmark_best_answer_closed_category(self): + @patch_category_acl({ + 'can_mark_best_answers': 0, + 'can_change_marked_answers': 2, + 'can_close_threads': False, + }) + def test_unmark_best_answer_closed_category_no_permission(self): """api validates if user has permission to unmark best answer in closed category""" - self.override_acl({ - 'can_mark_best_answers': 0, - 'can_change_marked_answers': 2, - 'can_close_threads': 0, - }) - self.category.is_closed = True self.category.save() @@ -2410,13 +2263,16 @@ def test_unmark_best_answer_closed_category(self): thread_json = self.get_thread_json() self.assertEqual(thread_json['best_answer'], self.best_answer.id) - # passing scenario is possible - self.override_acl({ - 'can_mark_best_answers': 0, - 'can_change_marked_answers': 2, - 'can_close_threads': 1, - }) - + @patch_category_acl({ + 'can_mark_best_answers': 0, + 'can_change_marked_answers': 2, + 'can_close_threads': True, + }) + def test_unmark_best_answer_closed_category(self): + """api validates if user has permission to unmark best answer in closed category""" + self.category.is_closed = True + self.category.save() + response = self.patch( self.api_link, [ { @@ -2428,14 +2284,13 @@ def test_unmark_best_answer_closed_category(self): ) self.assertEqual(response.status_code, 200) - def test_unmark_best_answer_closed_thread(self): + @patch_category_acl({ + 'can_mark_best_answers': 0, + 'can_change_marked_answers': 2, + 'can_close_threads': False, + }) + def test_unmark_best_answer_closed_thread_no_permission(self): """api validates if user has permission to unmark best answer in closed thread""" - self.override_acl({ - 'can_mark_best_answers': 0, - 'can_change_marked_answers': 2, - 'can_close_threads': 0, - }) - self.thread.is_closed = True self.thread.save() @@ -2460,13 +2315,16 @@ def test_unmark_best_answer_closed_thread(self): thread_json = self.get_thread_json() self.assertEqual(thread_json['best_answer'], self.best_answer.id) - # passing scenario is possible - self.override_acl({ - 'can_mark_best_answers': 0, - 'can_change_marked_answers': 2, - 'can_close_threads': 1, - }) - + @patch_category_acl({ + 'can_mark_best_answers': 0, + 'can_change_marked_answers': 2, + 'can_close_threads': True, + }) + def test_unmark_best_answer_closed_thread(self): + """api validates if user has permission to unmark best answer in closed thread""" + self.thread.is_closed = True + self.thread.save() + response = self.patch( self.api_link, [ { @@ -2478,14 +2336,13 @@ def test_unmark_best_answer_closed_thread(self): ) self.assertEqual(response.status_code, 200) - def test_unmark_best_answer_protected(self): + @patch_category_acl({ + 'can_mark_best_answers': 0, + 'can_change_marked_answers': 2, + 'can_protect_posts': 0, + }) + def test_unmark_best_answer_protected_no_permission(self): """api validates permission to unmark protected best answers""" - self.override_acl({ - 'can_mark_best_answers': 0, - 'can_change_marked_answers': 2, - 'can_protect_posts': 0, - }) - self.thread.best_answer_is_protected = True self.thread.save() @@ -2510,13 +2367,16 @@ def test_unmark_best_answer_protected(self): thread_json = self.get_thread_json() self.assertEqual(thread_json['best_answer'], self.best_answer.id) - # passing scenario is possible - self.override_acl({ - 'can_mark_best_answers': 0, - 'can_change_marked_answers': 2, - 'can_protect_posts': 1, - }) - + @patch_category_acl({ + 'can_mark_best_answers': 0, + 'can_change_marked_answers': 2, + 'can_protect_posts': 1, + }) + def test_unmark_best_answer_protected(self): + """api validates permission to unmark protected best answers""" + self.thread.best_answer_is_protected = True + self.thread.save() + response = self.patch( self.api_link, [ { From 37bf66be88ee5b054ece683997d3c4b51a6490db Mon Sep 17 00:00:00 2001 From: rafalp Date: Wed, 12 Dec 2018 21:28:13 +0100 Subject: [PATCH 29/86] Enable stacking patch_user_acl helper --- misago/acl/test.py | 44 +++++++++++----------- misago/acl/tests/test_patching_user_acl.py | 20 ++++++---- 2 files changed, 34 insertions(+), 30 deletions(-) diff --git a/misago/acl/test.py b/misago/acl/test.py index 316da78100..967ec4f533 100644 --- a/misago/acl/test.py +++ b/misago/acl/test.py @@ -1,4 +1,4 @@ -from contextlib import ExitStack +from contextlib import ContextDecorator, ExitStack, contextmanager from functools import wraps from unittest.mock import patch @@ -7,7 +7,7 @@ __all__ = ["patch_user_acl"] -class patch_user_acl(ExitStack): +class patch_user_acl(ContextDecorator, ExitStack): """Testing utility that patches get_user_acl results Can be used as decorator or context manager. @@ -19,9 +19,11 @@ class patch_user_acl(ExitStack): Patch should be a dict or callable. """ - def __init__(self, *patches): + _acl_patches = [] + + def __init__(self, acl_patch): super().__init__() - self._patches = patches + self.acl_patch = acl_patch def patched_get_user_acl(self, user, cache_versions): user_acl = get_user_acl(user, cache_versions) @@ -29,7 +31,7 @@ def patched_get_user_acl(self, user, cache_versions): return user_acl def apply_acl_patches(self, user, user_acl): - for acl_patch in self._patches: + for acl_patch in self._acl_patches: self.apply_acl_patch(user, user_acl, acl_patch) def apply_acl_patch(self, user, user_acl, acl_patch): @@ -40,21 +42,19 @@ def apply_acl_patch(self, user, user_acl, acl_patch): def __enter__(self): super().__enter__() - self.enter_context( - patch( - "misago.acl.useracl.get_user_acl", - side_effect=self.patched_get_user_acl, - ) + self.enter_context(self.enable_acl_patch()) + self.enter_context(self.patch_user_acl()) + + @contextmanager + def enable_acl_patch(self): + try: + self._acl_patches.append(self.acl_patch) + yield + finally: + self._acl_patches.pop(-1) + + def patch_user_acl(self): + return patch( + "misago.acl.useracl.get_user_acl", + side_effect=self.patched_get_user_acl, ) - - def __call__(self, f): - @wraps(f) - def inner(*args, **kwargs): - with self: - with patch( - "misago.acl.useracl.get_user_acl", - side_effect=self.patched_get_user_acl, - ): - return f(*args, **kwargs) - - return inner diff --git a/misago/acl/tests/test_patching_user_acl.py b/misago/acl/tests/test_patching_user_acl.py index fef1621ddd..80fd564759 100644 --- a/misago/acl/tests/test_patching_user_acl.py +++ b/misago/acl/tests/test_patching_user_acl.py @@ -59,16 +59,20 @@ def test_callable_patch_is_called_with_user_and_acl_by_context_manager(self): user_acl = useracl.get_user_acl(user, cache_versions) assert user_acl["patched_for_user_id"] == user.id - @patch_user_acl(callable_acl_patch, {"other_acl_path": True}) - def test_multiple_acl_patches_are_applied_by_decorator(self): + @patch_user_acl({"acl_patch": 1}) + @patch_user_acl({"acl_patch": 2}) + def test_multiple_acl_patches_applied_by_decorator_stack(self): user = User.objects.create_user("User", "user@example.com") user_acl = useracl.get_user_acl(user, cache_versions) - assert user_acl["patched_for_user_id"] == user.id - assert user_acl["other_acl_path"] + assert user_acl["acl_patch"] == 2 - def test_multiple_acl_patches_are_applied_by_context_manager(self): + def test_multiple_acl_patches_applied_by_context_manager_stack(self): user = User.objects.create_user("User", "user@example.com") - with patch_user_acl(callable_acl_patch, {"other_acl_path": True}): + with patch_user_acl({"acl_patch": 1}): + with patch_user_acl({"acl_patch": 2}): + user_acl = useracl.get_user_acl(user, cache_versions) + assert user_acl["acl_patch"] == 2 user_acl = useracl.get_user_acl(user, cache_versions) - assert user_acl["patched_for_user_id"] == user.id - assert user_acl["other_acl_path"] + assert user_acl["acl_patch"] == 1 + user_acl = useracl.get_user_acl(user, cache_versions) + assert "acl_patch" not in user_acl \ No newline at end of file From 4ba48a713061a1bb2a98c3e0d90b2f3775055dff Mon Sep 17 00:00:00 2001 From: rafalp Date: Wed, 12 Dec 2018 22:14:05 +0100 Subject: [PATCH 30/86] Make tests pass for thread and post patch api, redo thread_move test util --- .../threads/api/postendpoints/patch_event.py | 4 +- .../threads/api/postendpoints/patch_post.py | 10 +- misago/threads/test.py | 24 +-- misago/threads/tests/test_thread_patch_api.py | 46 +++-- .../tests/test_thread_postpatch_api.py | 167 +++++------------- 5 files changed, 83 insertions(+), 168 deletions(-) diff --git a/misago/threads/api/postendpoints/patch_event.py b/misago/threads/api/postendpoints/patch_event.py index 9c7c938ebc..6f97575d25 100644 --- a/misago/threads/api/postendpoints/patch_event.py +++ b/misago/threads/api/postendpoints/patch_event.py @@ -24,10 +24,10 @@ def patch_acl(request, event, value): def patch_is_hidden(request, event, value): if value: - allow_hide_event(request.user, event) + allow_hide_event(request.user_acl, event) moderation.hide_post(request.user, event) else: - allow_unhide_event(request.user, event) + allow_unhide_event(request.user_acl, event) moderation.unhide_post(request.user, event) return {'is_hidden': event.is_hidden} diff --git a/misago/threads/api/postendpoints/patch_post.py b/misago/threads/api/postendpoints/patch_post.py index 5383b37a1e..0e2bdb930f 100644 --- a/misago/threads/api/postendpoints/patch_post.py +++ b/misago/threads/api/postendpoints/patch_post.py @@ -89,7 +89,7 @@ def patch_is_liked(request, post, value): def patch_is_protected(request, post, value): - allow_protect_post(request.user, post) + allow_protect_post(request.user_acl, post) if value: moderation.protect_post(request.user, post) else: @@ -101,7 +101,7 @@ def patch_is_protected(request, post, value): def patch_is_unapproved(request, post, value): - allow_approve_post(request.user, post) + allow_approve_post(request.user_acl, post) if value: raise PermissionDenied(_("Content approval can't be reversed.")) @@ -116,11 +116,11 @@ def patch_is_unapproved(request, post, value): def patch_is_hidden(request, post, value): if value is True: - allow_hide_post(request.user, post) - allow_hide_best_answer(request.user, post) + allow_hide_post(request.user_acl, post) + allow_hide_best_answer(request.user_acl, post) moderation.hide_post(request.user, post) elif value is False: - allow_unhide_post(request.user, post) + allow_unhide_post(request.user_acl, post) moderation.unhide_post(request.user, post) return {'is_hidden': post.is_hidden} diff --git a/misago/threads/test.py b/misago/threads/test.py index 59feba8db2..a90da8bf4e 100644 --- a/misago/threads/test.py +++ b/misago/threads/test.py @@ -28,25 +28,19 @@ def patch_acl(_, user_acl): return patch_user_acl(patch_acl) -def patch_categories_acl_for_move(src_acl_patch=None, dst_acl_patch=None): +def patch_other_category_acl(acl_patch): def patch_acl(_, user_acl): - src = Category.objects.get(slug="first-category") - dst = Category.objects.get(slug="other-category") + src_category = Category.objects.get(slug="first-category") + category_acl = user_acl['categories'][src_category.id].copy() - src_acl = user_acl['categories'][src.id] - dst_acl = src_acl.copy() - user_acl['categories'][dst.id] = dst_acl + dst_category = Category.objects.get(slug="other-category") + user_acl['categories'][dst_category.id] = category_acl - src_acl.update(default_category_acl) - dst_acl.update(default_category_acl) - - if src_acl_patch: - src_acl.update(src_acl_patch) - if dst_acl_patch: - dst_acl.update(dst_acl_patch) + category_acl.update(default_category_acl) + if acl_patch: + category_acl.update(acl_patch) - cleanup_patched_acl(user_acl, src_acl, src) - cleanup_patched_acl(user_acl, dst_acl, dst) + cleanup_patched_acl(user_acl, category_acl, dst_category) return patch_user_acl(patch_acl) diff --git a/misago/threads/tests/test_thread_patch_api.py b/misago/threads/tests/test_thread_patch_api.py index bb0b1f7ef7..c1aba52c38 100644 --- a/misago/threads/tests/test_thread_patch_api.py +++ b/misago/threads/tests/test_thread_patch_api.py @@ -6,7 +6,7 @@ from misago.categories.models import Category from misago.readtracker import poststracker from misago.threads import testutils -from misago.threads.test import patch_category_acl, patch_categories_acl_for_move +from misago.threads.test import patch_category_acl, patch_other_category_acl from misago.threads.models import Thread from .test_threads_api import ThreadsApiTestCase @@ -427,9 +427,8 @@ def setUp(self): ) self.dst_category = Category.objects.get(slug='other-category') - @patch_categories_acl_for_move( - {'can_move_threads': True}, {'can_start_threads': 2} - ) + @patch_other_category_acl({'can_start_threads': 2}) + @patch_category_acl({'can_move_threads': True}) def test_move_thread_no_top(self): """api moves thread to other category, sets no top category""" response = self.patch( @@ -459,9 +458,8 @@ def test_move_thread_no_top(self): thread_json = self.get_thread_json() self.assertEqual(thread_json['category']['id'], self.dst_category.pk) - @patch_categories_acl_for_move( - {'can_move_threads': True}, {'can_start_threads': 2} - ) + @patch_other_category_acl({'can_start_threads': 2}) + @patch_category_acl({'can_move_threads': True}) def test_move_thread_with_top(self): """api moves thread to other category, sets top""" response = self.patch( @@ -491,9 +489,8 @@ def test_move_thread_with_top(self): thread_json = self.get_thread_json() self.assertEqual(thread_json['category']['id'], self.dst_category.pk) - @patch_categories_acl_for_move( - {'can_move_threads': True}, {'can_start_threads': 2} - ) + @patch_other_category_acl({'can_start_threads': 2}) + @patch_category_acl({'can_move_threads': True}) def test_move_thread_reads(self): """api moves thread reads together with thread""" poststracker.save_read(self.user, self.thread.first_post) @@ -528,9 +525,8 @@ def test_move_thread_reads(self): self.assertEqual(postreads.count(), 1) postreads.get(category=self.dst_category) - @patch_categories_acl_for_move( - {'can_move_threads': True}, {'can_start_threads': 2} - ) + @patch_other_category_acl({'can_start_threads': 2}) + @patch_category_acl({'can_move_threads': True}) def test_move_thread_subscriptions(self): """api moves thread subscriptions together with thread""" self.user.subscription_set.create( @@ -568,7 +564,7 @@ def test_move_thread_subscriptions(self): self.assertEqual(self.user.subscription_set.count(), 1) self.user.subscription_set.get(category=self.dst_category) - @patch_categories_acl_for_move({'can_move_threads': False}) + @patch_category_acl({'can_move_threads': False}) def test_move_thread_no_permission(self): """api move thread to other category with no permission fails""" response = self.patch( @@ -590,7 +586,8 @@ def test_move_thread_no_permission(self): thread_json = self.get_thread_json() self.assertEqual(thread_json['category']['id'], self.category.pk) - @patch_category_acl({'can_move_threads': True, 'can_close_threads': False}) + @patch_other_category_acl({'can_close_threads': False}) + @patch_category_acl({'can_move_threads': True}) def test_move_thread_closed_category_no_permission(self): """api move thread from closed category with no permission fails""" self.category.is_closed = True @@ -612,7 +609,8 @@ def test_move_thread_closed_category_no_permission(self): response_json['detail'][0], "This category is closed. You can't move it's threads." ) - @patch_category_acl({'can_move_threads': True, 'can_close_threads': False}) + @patch_other_category_acl({'can_close_threads': False}) + @patch_category_acl({'can_move_threads': True}) def test_move_closed_thread_no_permission(self): """api move closed thread with no permission fails""" self.thread.is_closed = True @@ -634,7 +632,8 @@ def test_move_closed_thread_no_permission(self): response_json['detail'][0], "This thread is closed. You can't move it." ) - @patch_categories_acl_for_move({'can_move_threads': True}, {'can_see': False}) + @patch_other_category_acl({'can_see': False}) + @patch_category_acl({'can_move_threads': True}) def test_move_thread_no_category_access(self): """api move thread to category with no access fails""" response = self.patch( @@ -654,7 +653,8 @@ def test_move_thread_no_category_access(self): thread_json = self.get_thread_json() self.assertEqual(thread_json['category']['id'], self.category.pk) - @patch_categories_acl_for_move({'can_move_threads': True}, {'can_browse': False}) + @patch_other_category_acl({'can_browse': False}) + @patch_category_acl({'can_move_threads': True}) def test_move_thread_no_category_browse(self): """api move thread to category with no browsing access fails""" response = self.patch( @@ -677,9 +677,8 @@ def test_move_thread_no_category_browse(self): thread_json = self.get_thread_json() self.assertEqual(thread_json['category']['id'], self.category.pk) - @patch_categories_acl_for_move( - {'can_move_threads': True}, {'can_start_threads': False} - ) + @patch_other_category_acl({'can_start_threads': False}) + @patch_category_acl({'can_move_threads': True}) def test_move_thread_no_category_start_threads(self): """api move thread to category with no posting access fails""" response = self.patch( @@ -702,9 +701,8 @@ def test_move_thread_no_category_start_threads(self): thread_json = self.get_thread_json() self.assertEqual(thread_json['category']['id'], self.category.pk) - @patch_categories_acl_for_move( - {'can_move_threads': True}, {'can_start_threads': 2} - ) + @patch_other_category_acl({'can_start_threads': 2}) + @patch_category_acl({'can_move_threads': True}) def test_move_thread_same_category(self): """api move thread to category it's already in fails""" response = self.patch( diff --git a/misago/threads/tests/test_thread_postpatch_api.py b/misago/threads/tests/test_thread_postpatch_api.py index 9f335f8460..1acce99b4a 100644 --- a/misago/threads/tests/test_thread_postpatch_api.py +++ b/misago/threads/tests/test_thread_postpatch_api.py @@ -4,10 +4,10 @@ from django.urls import reverse from django.utils import timezone -from misago.acl.testutils import override_acl from misago.categories.models import Category from misago.threads import testutils from misago.threads.models import Thread, Post +from misago.threads.test import patch_category_acl from misago.users.testutils import AuthenticatedUserTestCase @@ -36,21 +36,6 @@ def refresh_post(self): def refresh_thread(self): self.thread = Thread.objects.get(pk=self.thread.pk) - def override_acl(self, extra_acl=None): - new_acl = self.user.acl_cache - new_acl['categories'][self.category.pk].update({ - 'can_see': 1, - 'can_browse': 1, - 'can_start_threads': 0, - 'can_reply_threads': 0, - 'can_edit_posts': 1, - }) - - if extra_acl: - new_acl['categories'][self.category.pk].update(extra_acl) - - override_acl(self.user, new_acl) - class PostAddAclApiTests(ThreadPostPatchApiTestCase): def test_add_acl_true(self): @@ -83,10 +68,9 @@ def test_add_acl_false(self): class PostProtectApiTests(ThreadPostPatchApiTestCase): + @patch_category_acl({'can_edit_posts': 2, 'can_protect_posts': True}) def test_protect_post(self): """api makes it possible to protect post""" - self.override_acl({'can_protect_posts': 1}) - response = self.patch( self.api_link, [ { @@ -104,13 +88,12 @@ def test_protect_post(self): self.refresh_post() self.assertTrue(self.post.is_protected) + @patch_category_acl({'can_edit_posts': 2, 'can_protect_posts': True}) def test_unprotect_post(self): """api makes it possible to unprotect protected post""" self.post.is_protected = True self.post.save() - self.override_acl({'can_protect_posts': 1}) - response = self.patch( self.api_link, [ { @@ -128,14 +111,13 @@ def test_unprotect_post(self): self.refresh_post() self.assertFalse(self.post.is_protected) + @patch_category_acl({'can_edit_posts': 2, 'can_protect_posts': True}) def test_protect_best_answer(self): """api makes it possible to protect post""" self.thread.set_best_answer(self.user, self.post) self.thread.save() self.assertFalse(self.thread.best_answer_is_protected) - - self.override_acl({'can_protect_posts': 1}) response = self.patch( self.api_link, [ @@ -157,6 +139,7 @@ def test_protect_best_answer(self): self.refresh_thread() self.assertTrue(self.thread.best_answer_is_protected) + @patch_category_acl({'can_edit_posts': 2, 'can_protect_posts': True}) def test_unprotect_best_answer(self): """api makes it possible to unprotect protected post""" self.post.is_protected = True @@ -167,8 +150,6 @@ def test_unprotect_best_answer(self): self.assertTrue(self.thread.best_answer_is_protected) - self.override_acl({'can_protect_posts': 1}) - response = self.patch( self.api_link, [ { @@ -189,10 +170,9 @@ def test_unprotect_best_answer(self): self.refresh_thread() self.assertFalse(self.thread.best_answer_is_protected) + @patch_category_acl({'can_edit_posts': 2, 'can_protect_posts': False}) def test_protect_post_no_permission(self): """api validates permission to protect post""" - self.override_acl({'can_protect_posts': 0}) - response = self.patch( self.api_link, [ { @@ -210,13 +190,12 @@ def test_protect_post_no_permission(self): self.refresh_post() self.assertFalse(self.post.is_protected) + @patch_category_acl({'can_edit_posts': 2, 'can_protect_posts': False}) def test_unprotect_post_no_permission(self): """api validates permission to unprotect post""" self.post.is_protected = True self.post.save() - self.override_acl({'can_protect_posts': 0}) - response = self.patch( self.api_link, [ { @@ -234,10 +213,9 @@ def test_unprotect_post_no_permission(self): self.refresh_post() self.assertTrue(self.post.is_protected) + @patch_category_acl({'can_edit_posts': 0, 'can_protect_posts': True}) def test_protect_post_not_editable(self): """api validates if we can edit post we want to protect""" - self.override_acl({'can_edit_posts': 0, 'can_protect_posts': 1}) - response = self.patch( self.api_link, [ { @@ -255,13 +233,12 @@ def test_protect_post_not_editable(self): self.refresh_post() self.assertFalse(self.post.is_protected) + @patch_category_acl({'can_edit_posts': 0, 'can_protect_posts': True}) def test_unprotect_post_not_editable(self): """api validates if we can edit post we want to protect""" self.post.is_protected = True self.post.save() - self.override_acl({'can_edit_posts': 0, 'can_protect_posts': 1}) - response = self.patch( self.api_link, [ { @@ -281,13 +258,12 @@ def test_unprotect_post_not_editable(self): class PostApproveApiTests(ThreadPostPatchApiTestCase): + @patch_category_acl({'can_approve_content': True}) def test_approve_post(self): """api makes it possible to approve post""" self.post.is_unapproved = True self.post.save() - self.override_acl({'can_approve_content': 1}) - response = self.patch( self.api_link, [ { @@ -305,10 +281,9 @@ def test_approve_post(self): self.refresh_post() self.assertFalse(self.post.is_unapproved) + @patch_category_acl({'can_approve_content': True}) def test_unapprove_post(self): """unapproving posts is not supported by api""" - self.override_acl({'can_approve_content': 1}) - response = self.patch( self.api_link, [ { @@ -326,13 +301,12 @@ def test_unapprove_post(self): self.refresh_post() self.assertFalse(self.post.is_unapproved) + @patch_category_acl({'can_approve_content': False}) def test_approve_post_no_permission(self): """api validates approval permission""" self.post.is_unapproved = True self.post.save() - self.override_acl({'can_approve_content': 0}) - response = self.patch( self.api_link, [ { @@ -350,6 +324,7 @@ def test_approve_post_no_permission(self): self.refresh_post() self.assertTrue(self.post.is_unapproved) + @patch_category_acl({'can_approve_content': True, 'can_close_threads': False}) def test_approve_post_closed_thread_no_permission(self): """api validates approval permission in closed threads""" self.post.is_unapproved = True @@ -358,11 +333,6 @@ def test_approve_post_closed_thread_no_permission(self): self.thread.is_closed = True self.thread.save() - self.override_acl({ - 'can_approve_content': 1, - 'can_close_threads': 0, - }) - response = self.patch( self.api_link, [ { @@ -383,6 +353,7 @@ def test_approve_post_closed_thread_no_permission(self): self.refresh_post() self.assertTrue(self.post.is_unapproved) + @patch_category_acl({'can_approve_content': True, 'can_close_threads': False}) def test_approve_post_closed_category_no_permission(self): """api validates approval permission in closed categories""" self.post.is_unapproved = True @@ -391,11 +362,6 @@ def test_approve_post_closed_category_no_permission(self): self.category.is_closed = True self.category.save() - self.override_acl({ - 'can_approve_content': 1, - 'can_close_threads': 0, - }) - response = self.patch( self.api_link, [ { @@ -416,6 +382,7 @@ def test_approve_post_closed_category_no_permission(self): self.refresh_post() self.assertTrue(self.post.is_unapproved) + @patch_category_acl({'can_approve_content': True}) def test_approve_first_post(self): """api approve first post fails""" self.post.is_unapproved = True @@ -424,8 +391,6 @@ def test_approve_first_post(self): self.thread.set_first_post(self.post) self.thread.save() - self.override_acl({'can_approve_content': 1}) - response = self.patch( self.api_link, [ { @@ -443,14 +408,13 @@ def test_approve_first_post(self): self.refresh_post() self.assertTrue(self.post.is_unapproved) + @patch_category_acl({'can_approve_content': True}) def test_approve_hidden_post(self): """api approve hidden post fails""" self.post.is_unapproved = True self.post.is_hidden = True self.post.save() - self.override_acl({'can_approve_content': 1}) - response = self.patch( self.api_link, [ { @@ -472,10 +436,9 @@ def test_approve_hidden_post(self): class PostHideApiTests(ThreadPostPatchApiTestCase): + @patch_category_acl({'can_hide_posts': 1}) def test_hide_post(self): """api makes it possible to hide post""" - self.override_acl({'can_hide_posts': 1}) - response = self.patch( self.api_link, [ { @@ -493,10 +456,9 @@ def test_hide_post(self): self.refresh_post() self.assertTrue(self.post.is_hidden) + @patch_category_acl({'can_hide_posts': 1}) def test_hide_own_post(self): """api makes it possible to hide owned post""" - self.override_acl({'can_hide_own_posts': 1}) - response = self.patch( self.api_link, [ { @@ -514,10 +476,9 @@ def test_hide_own_post(self): self.refresh_post() self.assertTrue(self.post.is_hidden) + @patch_category_acl({'can_hide_posts': 0}) def test_hide_post_no_permission(self): """api hide post with no permission fails""" - self.override_acl({'can_hide_posts': 0}) - response = self.patch( self.api_link, [ { @@ -535,13 +496,12 @@ def test_hide_post_no_permission(self): self.refresh_post() self.assertFalse(self.post.is_hidden) + @patch_category_acl({'can_hide_own_posts': 1, 'can_protect_posts': False}) def test_hide_own_protected_post(self): """api validates if we are trying to hide protected post""" self.post.is_protected = True self.post.save() - self.override_acl({'can_protect_posts': 0, 'can_hide_own_posts': 1}) - response = self.patch( self.api_link, [ { @@ -559,13 +519,12 @@ def test_hide_own_protected_post(self): self.refresh_post() self.assertFalse(self.post.is_hidden) + @patch_category_acl({'can_hide_own_posts': True}) def test_hide_other_user_post(self): """api validates post ownership when hiding""" self.post.poster = None self.post.save() - self.override_acl({'can_hide_own_posts': 1}) - response = self.patch( self.api_link, [ { @@ -585,13 +544,12 @@ def test_hide_other_user_post(self): self.refresh_post() self.assertFalse(self.post.is_hidden) + @patch_category_acl({'post_edit_time': 1, 'can_hide_own_posts': True}) def test_hide_own_post_after_edit_time(self): """api validates if we are trying to hide post after edit time""" self.post.posted_on = timezone.now() - timedelta(minutes=10) self.post.save() - self.override_acl({'post_edit_time': 1, 'can_hide_own_posts': 1}) - response = self.patch( self.api_link, [ { @@ -611,13 +569,12 @@ def test_hide_own_post_after_edit_time(self): self.refresh_post() self.assertFalse(self.post.is_hidden) + @patch_category_acl({'can_close_threads': False, 'can_hide_own_posts': True}) def test_hide_post_in_closed_thread(self): """api validates if we are trying to hide post in closed thread""" self.thread.is_closed = True self.thread.save() - self.override_acl({'can_hide_own_posts': 1}) - response = self.patch( self.api_link, [ { @@ -637,13 +594,12 @@ def test_hide_post_in_closed_thread(self): self.refresh_post() self.assertFalse(self.post.is_hidden) + @patch_category_acl({'can_close_threads': False, 'can_hide_own_posts': True}) def test_hide_post_in_closed_category(self): """api validates if we are trying to hide post in closed category""" self.category.is_closed = True self.category.save() - self.override_acl({'can_hide_own_posts': 1}) - response = self.patch( self.api_link, [ { @@ -663,13 +619,12 @@ def test_hide_post_in_closed_category(self): self.refresh_post() self.assertFalse(self.post.is_hidden) + @patch_category_acl({'can_hide_posts': 1}) def test_hide_first_post(self): """api hide first post fails""" self.thread.set_first_post(self.post) self.thread.save() - self.override_acl({'can_hide_posts': 1}) - response = self.patch( self.api_link, [ { @@ -684,13 +639,12 @@ def test_hide_first_post(self): response_json = response.json() self.assertEqual(response_json['detail'][0], "You can't hide thread's first post.") + @patch_category_acl({'can_hide_posts': 1}) def test_hide_best_answer(self): """api hide first post fails""" self.thread.set_best_answer(self.user, self.post) self.thread.save() - self.override_acl({'can_hide_posts': 2}) - response = self.patch( self.api_link, [ { @@ -708,6 +662,7 @@ def test_hide_best_answer(self): class PostUnhideApiTests(ThreadPostPatchApiTestCase): + @patch_category_acl({'can_hide_posts': 1}) def test_show_post(self): """api makes it possible to unhide post""" self.post.is_hidden = True @@ -716,8 +671,6 @@ def test_show_post(self): self.refresh_post() self.assertTrue(self.post.is_hidden) - self.override_acl({'can_hide_posts': 1}) - response = self.patch( self.api_link, [ { @@ -735,6 +688,7 @@ def test_show_post(self): self.refresh_post() self.assertFalse(self.post.is_hidden) + @patch_category_acl({'can_hide_own_posts': 1}) def test_show_own_post(self): """api makes it possible to unhide owned post""" self.post.is_hidden = True @@ -743,8 +697,6 @@ def test_show_own_post(self): self.refresh_post() self.assertTrue(self.post.is_hidden) - self.override_acl({'can_hide_own_posts': 1}) - response = self.patch( self.api_link, [ { @@ -762,6 +714,7 @@ def test_show_own_post(self): self.refresh_post() self.assertFalse(self.post.is_hidden) + @patch_category_acl({'can_hide_posts': 0}) def test_show_post_no_permission(self): """api unhide post with no permission fails""" self.post.is_hidden = True @@ -770,8 +723,6 @@ def test_show_post_no_permission(self): self.refresh_post() self.assertTrue(self.post.is_hidden) - self.override_acl({'can_hide_posts': 0}) - response = self.patch( self.api_link, [ { @@ -789,13 +740,12 @@ def test_show_post_no_permission(self): self.refresh_post() self.assertTrue(self.post.is_hidden) + @patch_category_acl({'can_protect_posts': 0, 'can_hide_own_posts': 1}) def test_show_own_protected_post(self): """api validates if we are trying to reveal protected post""" self.post.is_hidden = True self.post.save() - self.override_acl({'can_protect_posts': 0, 'can_hide_own_posts': 1}) - self.post.is_protected = True self.post.save() @@ -818,14 +768,13 @@ def test_show_own_protected_post(self): self.refresh_post() self.assertTrue(self.post.is_hidden) + @patch_category_acl({'can_hide_own_posts': 1}) def test_show_other_user_post(self): """api validates post ownership when revealing""" self.post.is_hidden = True self.post.poster = None self.post.save() - self.override_acl({'can_hide_own_posts': 1}) - response = self.patch( self.api_link, [ { @@ -845,14 +794,13 @@ def test_show_other_user_post(self): self.refresh_post() self.assertTrue(self.post.is_hidden) + @patch_category_acl({'post_edit_time': 1, 'can_hide_own_posts': 1}) def test_show_own_post_after_edit_time(self): """api validates if we are trying to reveal post after edit time""" self.post.is_hidden = True self.post.posted_on = timezone.now() - timedelta(minutes=10) self.post.save() - self.override_acl({'post_edit_time': 1, 'can_hide_own_posts': 1}) - response = self.patch( self.api_link, [ { @@ -872,6 +820,7 @@ def test_show_own_post_after_edit_time(self): self.refresh_post() self.assertTrue(self.post.is_hidden) + @patch_category_acl({'can_close_threads': False, 'can_hide_own_posts': 1}) def test_show_post_in_closed_thread(self): """api validates if we are trying to reveal post in closed thread""" self.thread.is_closed = True @@ -880,8 +829,6 @@ def test_show_post_in_closed_thread(self): self.post.is_hidden = True self.post.save() - self.override_acl({'can_hide_own_posts': 1}) - response = self.patch( self.api_link, [ { @@ -901,6 +848,7 @@ def test_show_post_in_closed_thread(self): self.refresh_post() self.assertTrue(self.post.is_hidden) + @patch_category_acl({'can_close_threads': False, 'can_hide_own_posts': 1}) def test_show_post_in_closed_category(self): """api validates if we are trying to reveal post in closed category""" self.category.is_closed = True @@ -909,8 +857,6 @@ def test_show_post_in_closed_category(self): self.post.is_hidden = True self.post.save() - self.override_acl({'can_hide_own_posts': 1}) - response = self.patch( self.api_link, [ { @@ -930,13 +876,12 @@ def test_show_post_in_closed_category(self): self.refresh_post() self.assertTrue(self.post.is_hidden) + @patch_category_acl({'can_hide_posts': 1}) def test_show_first_post(self): """api unhide first post fails""" self.thread.set_first_post(self.post) self.thread.save() - self.override_acl({'can_hide_posts': 1}) - response = self.patch( self.api_link, [ { @@ -953,10 +898,9 @@ def test_show_first_post(self): class PostLikeApiTests(ThreadPostPatchApiTestCase): + @patch_category_acl({'can_see_posts_likes': 0}) def test_like_no_see_permission(self): """api validates user's permission to see posts likes""" - self.override_acl({'can_see_posts_likes': 0}) - response = self.patch( self.api_link, [ { @@ -972,10 +916,9 @@ def test_like_no_see_permission(self): "detail": ["You can't like posts in this category."], }) + @patch_category_acl({'can_like_posts': False}) def test_like_no_like_permission(self): """api validates user's permission to see posts likes""" - self.override_acl({'can_like_posts': False}) - response = self.patch( self.api_link, [ { @@ -1213,10 +1156,9 @@ def test_add_acl_false(self): class EventHideApiTests(ThreadEventPatchApiTestCase): + @patch_category_acl({'can_hide_events': 1}) def test_hide_event(self): """api makes it possible to hide event""" - self.override_acl({'can_hide_events': 1}) - response = self.patch( self.api_link, [ { @@ -1231,6 +1173,7 @@ def test_hide_event(self): self.refresh_event() self.assertTrue(self.event.is_hidden) + @patch_category_acl({'can_hide_events': 1}) def test_show_event(self): """api makes it possible to unhide event""" self.event.is_hidden = True @@ -1239,8 +1182,6 @@ def test_show_event(self): self.refresh_event() self.assertTrue(self.event.is_hidden) - self.override_acl({'can_hide_events': 1}) - response = self.patch( self.api_link, [ { @@ -1255,10 +1196,9 @@ def test_show_event(self): self.refresh_event() self.assertFalse(self.event.is_hidden) + @patch_category_acl({'can_hide_events': 0}) def test_hide_event_no_permission(self): """api hide event with no permission fails""" - self.override_acl({'can_hide_events': 0}) - response = self.patch( self.api_link, [ { @@ -1278,13 +1218,9 @@ def test_hide_event_no_permission(self): self.refresh_event() self.assertFalse(self.event.is_hidden) + @patch_category_acl({'can_close_threads': False, 'can_hide_events': 1}) def test_hide_event_closed_thread_no_permission(self): """api hide event in closed thread with no permission fails""" - self.override_acl({ - 'can_hide_events': 1, - 'can_close_threads': 0, - }) - self.thread.is_closed = True self.thread.save() @@ -1307,13 +1243,9 @@ def test_hide_event_closed_thread_no_permission(self): self.refresh_event() self.assertFalse(self.event.is_hidden) + @patch_category_acl({'can_close_threads': False, 'can_hide_events': 1}) def test_hide_event_closed_category_no_permission(self): """api hide event in closed category with no permission fails""" - self.override_acl({ - 'can_hide_events': 1, - 'can_close_threads': 0, - }) - self.category.is_closed = True self.category.save() @@ -1336,6 +1268,7 @@ def test_hide_event_closed_category_no_permission(self): self.refresh_event() self.assertFalse(self.event.is_hidden) + @patch_category_acl({'can_hide_events': 0}) def test_show_event_no_permission(self): """api unhide event with no permission fails""" self.event.is_hidden = True @@ -1344,8 +1277,6 @@ def test_show_event_no_permission(self): self.refresh_event() self.assertTrue(self.event.is_hidden) - self.override_acl({'can_hide_events': 0}) - response = self.patch( self.api_link, [ { @@ -1357,16 +1288,12 @@ def test_show_event_no_permission(self): ) self.assertEqual(response.status_code, 404) + @patch_category_acl({'can_close_threads': False, 'can_hide_events': 1}) def test_show_event_closed_thread_no_permission(self): """api show event in closed thread with no permission fails""" self.event.is_hidden = True self.event.save() - self.override_acl({ - 'can_hide_events': 1, - 'can_close_threads': 0, - }) - self.thread.is_closed = True self.thread.save() @@ -1389,16 +1316,12 @@ def test_show_event_closed_thread_no_permission(self): self.refresh_event() self.assertTrue(self.event.is_hidden) + @patch_category_acl({'can_close_threads': False, 'can_hide_events': 1}) def test_show_event_closed_category_no_permission(self): """api show event in closed category with no permission fails""" self.event.is_hidden = True self.event.save() - self.override_acl({ - 'can_hide_events': 1, - 'can_close_threads': 0, - }) - self.category.is_closed = True self.category.save() From bd1879084d7af962188b4fbadf0a4b83e7389fb5 Mon Sep 17 00:00:00 2001 From: rafalp Date: Wed, 12 Dec 2018 22:16:15 +0100 Subject: [PATCH 31/86] Remove unnecessary extra model refreshing logic --- .../tests/test_thread_postpatch_api.py | 96 +++++++++---------- 1 file changed, 45 insertions(+), 51 deletions(-) diff --git a/misago/threads/tests/test_thread_postpatch_api.py b/misago/threads/tests/test_thread_postpatch_api.py index 1acce99b4a..26fcae0b6f 100644 --- a/misago/threads/tests/test_thread_postpatch_api.py +++ b/misago/threads/tests/test_thread_postpatch_api.py @@ -30,12 +30,6 @@ def setUp(self): def patch(self, api_link, ops): return self.client.patch(api_link, json.dumps(ops), content_type="application/json") - def refresh_post(self): - self.post = self.thread.post_set.get(pk=self.post.pk) - - def refresh_thread(self): - self.thread = Thread.objects.get(pk=self.thread.pk) - class PostAddAclApiTests(ThreadPostPatchApiTestCase): def test_add_acl_true(self): @@ -85,7 +79,7 @@ def test_protect_post(self): reponse_json = response.json() self.assertTrue(reponse_json['is_protected']) - self.refresh_post() + self.post.refresh_from_db() self.assertTrue(self.post.is_protected) @patch_category_acl({'can_edit_posts': 2, 'can_protect_posts': True}) @@ -108,7 +102,7 @@ def test_unprotect_post(self): reponse_json = response.json() self.assertFalse(reponse_json['is_protected']) - self.refresh_post() + self.post.refresh_from_db() self.assertFalse(self.post.is_protected) @patch_category_acl({'can_edit_posts': 2, 'can_protect_posts': True}) @@ -133,10 +127,10 @@ def test_protect_best_answer(self): reponse_json = response.json() self.assertTrue(reponse_json['is_protected']) - self.refresh_post() + self.post.refresh_from_db() self.assertTrue(self.post.is_protected) - self.refresh_thread() + self.thread.refresh_from_db() self.assertTrue(self.thread.best_answer_is_protected) @patch_category_acl({'can_edit_posts': 2, 'can_protect_posts': True}) @@ -164,10 +158,10 @@ def test_unprotect_best_answer(self): reponse_json = response.json() self.assertFalse(reponse_json['is_protected']) - self.refresh_post() + self.post.refresh_from_db() self.assertFalse(self.post.is_protected) - self.refresh_thread() + self.thread.refresh_from_db() self.assertFalse(self.thread.best_answer_is_protected) @patch_category_acl({'can_edit_posts': 2, 'can_protect_posts': False}) @@ -187,7 +181,7 @@ def test_protect_post_no_permission(self): response_json = response.json() self.assertEqual(response_json['detail'][0], "You can't protect posts in this category.") - self.refresh_post() + self.post.refresh_from_db() self.assertFalse(self.post.is_protected) @patch_category_acl({'can_edit_posts': 2, 'can_protect_posts': False}) @@ -210,7 +204,7 @@ def test_unprotect_post_no_permission(self): response_json = response.json() self.assertEqual(response_json['detail'][0], "You can't protect posts in this category.") - self.refresh_post() + self.post.refresh_from_db() self.assertTrue(self.post.is_protected) @patch_category_acl({'can_edit_posts': 0, 'can_protect_posts': True}) @@ -230,7 +224,7 @@ def test_protect_post_not_editable(self): response_json = response.json() self.assertEqual(response_json['detail'][0], "You can't protect posts you can't edit.") - self.refresh_post() + self.post.refresh_from_db() self.assertFalse(self.post.is_protected) @patch_category_acl({'can_edit_posts': 0, 'can_protect_posts': True}) @@ -253,7 +247,7 @@ def test_unprotect_post_not_editable(self): response_json = response.json() self.assertEqual(response_json['detail'][0], "You can't protect posts you can't edit.") - self.refresh_post() + self.post.refresh_from_db() self.assertTrue(self.post.is_protected) @@ -278,7 +272,7 @@ def test_approve_post(self): reponse_json = response.json() self.assertFalse(reponse_json['is_unapproved']) - self.refresh_post() + self.post.refresh_from_db() self.assertFalse(self.post.is_unapproved) @patch_category_acl({'can_approve_content': True}) @@ -298,7 +292,7 @@ def test_unapprove_post(self): response_json = response.json() self.assertEqual(response_json['detail'][0], "Content approval can't be reversed.") - self.refresh_post() + self.post.refresh_from_db() self.assertFalse(self.post.is_unapproved) @patch_category_acl({'can_approve_content': False}) @@ -321,7 +315,7 @@ def test_approve_post_no_permission(self): response_json = response.json() self.assertEqual(response_json['detail'][0], "You can't approve posts in this category.") - self.refresh_post() + self.post.refresh_from_db() self.assertTrue(self.post.is_unapproved) @patch_category_acl({'can_approve_content': True, 'can_close_threads': False}) @@ -350,7 +344,7 @@ def test_approve_post_closed_thread_no_permission(self): "This thread is closed. You can't approve posts in it.", ) - self.refresh_post() + self.post.refresh_from_db() self.assertTrue(self.post.is_unapproved) @patch_category_acl({'can_approve_content': True, 'can_close_threads': False}) @@ -379,7 +373,7 @@ def test_approve_post_closed_category_no_permission(self): "This category is closed. You can't approve posts in it.", ) - self.refresh_post() + self.post.refresh_from_db() self.assertTrue(self.post.is_unapproved) @patch_category_acl({'can_approve_content': True}) @@ -405,7 +399,7 @@ def test_approve_first_post(self): response_json = response.json() self.assertEqual(response_json['detail'][0], "You can't approve thread's first post.") - self.refresh_post() + self.post.refresh_from_db() self.assertTrue(self.post.is_unapproved) @patch_category_acl({'can_approve_content': True}) @@ -431,7 +425,7 @@ def test_approve_hidden_post(self): response_json['detail'][0], "You can't approve posts the content you can't see." ) - self.refresh_post() + self.post.refresh_from_db() self.assertTrue(self.post.is_unapproved) @@ -453,7 +447,7 @@ def test_hide_post(self): reponse_json = response.json() self.assertTrue(reponse_json['is_hidden']) - self.refresh_post() + self.post.refresh_from_db() self.assertTrue(self.post.is_hidden) @patch_category_acl({'can_hide_posts': 1}) @@ -473,7 +467,7 @@ def test_hide_own_post(self): reponse_json = response.json() self.assertTrue(reponse_json['is_hidden']) - self.refresh_post() + self.post.refresh_from_db() self.assertTrue(self.post.is_hidden) @patch_category_acl({'can_hide_posts': 0}) @@ -493,7 +487,7 @@ def test_hide_post_no_permission(self): response_json = response.json() self.assertEqual(response_json['detail'][0], "You can't hide posts in this category.") - self.refresh_post() + self.post.refresh_from_db() self.assertFalse(self.post.is_hidden) @patch_category_acl({'can_hide_own_posts': 1, 'can_protect_posts': False}) @@ -516,7 +510,7 @@ def test_hide_own_protected_post(self): response_json = response.json() self.assertEqual(response_json['detail'][0], "This post is protected. You can't hide it.") - self.refresh_post() + self.post.refresh_from_db() self.assertFalse(self.post.is_hidden) @patch_category_acl({'can_hide_own_posts': True}) @@ -541,7 +535,7 @@ def test_hide_other_user_post(self): response_json['detail'][0], "You can't hide other users posts in this category." ) - self.refresh_post() + self.post.refresh_from_db() self.assertFalse(self.post.is_hidden) @patch_category_acl({'post_edit_time': 1, 'can_hide_own_posts': True}) @@ -566,7 +560,7 @@ def test_hide_own_post_after_edit_time(self): response_json['detail'][0], "You can't hide posts that are older than 1 minute." ) - self.refresh_post() + self.post.refresh_from_db() self.assertFalse(self.post.is_hidden) @patch_category_acl({'can_close_threads': False, 'can_hide_own_posts': True}) @@ -591,7 +585,7 @@ def test_hide_post_in_closed_thread(self): response_json['detail'][0], "This thread is closed. You can't hide posts in it." ) - self.refresh_post() + self.post.refresh_from_db() self.assertFalse(self.post.is_hidden) @patch_category_acl({'can_close_threads': False, 'can_hide_own_posts': True}) @@ -616,7 +610,7 @@ def test_hide_post_in_closed_category(self): response_json['detail'][0], "This category is closed. You can't hide posts in it." ) - self.refresh_post() + self.post.refresh_from_db() self.assertFalse(self.post.is_hidden) @patch_category_acl({'can_hide_posts': 1}) @@ -668,7 +662,7 @@ def test_show_post(self): self.post.is_hidden = True self.post.save() - self.refresh_post() + self.post.refresh_from_db() self.assertTrue(self.post.is_hidden) response = self.patch( @@ -685,7 +679,7 @@ def test_show_post(self): reponse_json = response.json() self.assertFalse(reponse_json['is_hidden']) - self.refresh_post() + self.post.refresh_from_db() self.assertFalse(self.post.is_hidden) @patch_category_acl({'can_hide_own_posts': 1}) @@ -694,7 +688,7 @@ def test_show_own_post(self): self.post.is_hidden = True self.post.save() - self.refresh_post() + self.post.refresh_from_db() self.assertTrue(self.post.is_hidden) response = self.patch( @@ -711,7 +705,7 @@ def test_show_own_post(self): reponse_json = response.json() self.assertFalse(reponse_json['is_hidden']) - self.refresh_post() + self.post.refresh_from_db() self.assertFalse(self.post.is_hidden) @patch_category_acl({'can_hide_posts': 0}) @@ -720,7 +714,7 @@ def test_show_post_no_permission(self): self.post.is_hidden = True self.post.save() - self.refresh_post() + self.post.refresh_from_db() self.assertTrue(self.post.is_hidden) response = self.patch( @@ -737,7 +731,7 @@ def test_show_post_no_permission(self): response_json = response.json() self.assertEqual(response_json['detail'][0], "You can't reveal posts in this category.") - self.refresh_post() + self.post.refresh_from_db() self.assertTrue(self.post.is_hidden) @patch_category_acl({'can_protect_posts': 0, 'can_hide_own_posts': 1}) @@ -765,7 +759,7 @@ def test_show_own_protected_post(self): response_json['detail'][0], "This post is protected. You can't reveal it." ) - self.refresh_post() + self.post.refresh_from_db() self.assertTrue(self.post.is_hidden) @patch_category_acl({'can_hide_own_posts': 1}) @@ -791,7 +785,7 @@ def test_show_other_user_post(self): response_json['detail'][0], "You can't reveal other users posts in this category." ) - self.refresh_post() + self.post.refresh_from_db() self.assertTrue(self.post.is_hidden) @patch_category_acl({'post_edit_time': 1, 'can_hide_own_posts': 1}) @@ -817,7 +811,7 @@ def test_show_own_post_after_edit_time(self): response_json['detail'][0], "You can't reveal posts that are older than 1 minute." ) - self.refresh_post() + self.post.refresh_from_db() self.assertTrue(self.post.is_hidden) @patch_category_acl({'can_close_threads': False, 'can_hide_own_posts': 1}) @@ -845,7 +839,7 @@ def test_show_post_in_closed_thread(self): response_json['detail'][0], "This thread is closed. You can't reveal posts in it." ) - self.refresh_post() + self.post.refresh_from_db() self.assertTrue(self.post.is_hidden) @patch_category_acl({'can_close_threads': False, 'can_hide_own_posts': 1}) @@ -873,7 +867,7 @@ def test_show_post_in_closed_category(self): response_json['detail'][0], "This category is closed. You can't reveal posts in it." ) - self.refresh_post() + self.post.refresh_from_db() self.assertTrue(self.post.is_hidden) @patch_category_acl({'can_hide_posts': 1}) @@ -1170,7 +1164,7 @@ def test_hide_event(self): ) self.assertEqual(response.status_code, 200) - self.refresh_event() + self.event.refresh_from_db() self.assertTrue(self.event.is_hidden) @patch_category_acl({'can_hide_events': 1}) @@ -1179,7 +1173,7 @@ def test_show_event(self): self.event.is_hidden = True self.event.save() - self.refresh_event() + self.event.refresh_from_db() self.assertTrue(self.event.is_hidden) response = self.patch( @@ -1193,7 +1187,7 @@ def test_show_event(self): ) self.assertEqual(response.status_code, 200) - self.refresh_event() + self.event.refresh_from_db() self.assertFalse(self.event.is_hidden) @patch_category_acl({'can_hide_events': 0}) @@ -1215,7 +1209,7 @@ def test_hide_event_no_permission(self): response_json['detail'][0], "You can't hide events in this category." ) - self.refresh_event() + self.event.refresh_from_db() self.assertFalse(self.event.is_hidden) @patch_category_acl({'can_close_threads': False, 'can_hide_events': 1}) @@ -1240,7 +1234,7 @@ def test_hide_event_closed_thread_no_permission(self): response_json['detail'][0], "This thread is closed. You can't hide events in it." ) - self.refresh_event() + self.event.refresh_from_db() self.assertFalse(self.event.is_hidden) @patch_category_acl({'can_close_threads': False, 'can_hide_events': 1}) @@ -1265,7 +1259,7 @@ def test_hide_event_closed_category_no_permission(self): response_json['detail'][0], "This category is closed. You can't hide events in it." ) - self.refresh_event() + self.event.refresh_from_db() self.assertFalse(self.event.is_hidden) @patch_category_acl({'can_hide_events': 0}) @@ -1274,7 +1268,7 @@ def test_show_event_no_permission(self): self.event.is_hidden = True self.event.save() - self.refresh_event() + self.event.refresh_from_db() self.assertTrue(self.event.is_hidden) response = self.patch( @@ -1313,7 +1307,7 @@ def test_show_event_closed_thread_no_permission(self): response_json['detail'][0], "This thread is closed. You can't reveal events in it." ) - self.refresh_event() + self.event.refresh_from_db() self.assertTrue(self.event.is_hidden) @patch_category_acl({'can_close_threads': False, 'can_hide_events': 1}) @@ -1341,5 +1335,5 @@ def test_show_event_closed_category_no_permission(self): response_json['detail'][0], "This category is closed. You can't reveal events in it." ) - self.refresh_event() + self.event.refresh_from_db() self.assertTrue(self.event.is_hidden) From 4307c0acb9cebe5ab901bbcf895c891fa0d6d4c1 Mon Sep 17 00:00:00 2001 From: rafalp Date: Wed, 12 Dec 2018 22:48:07 +0100 Subject: [PATCH 32/86] Fix tests for threads editor api --- misago/threads/api/threadendpoints/editor.py | 2 +- misago/threads/api/threadposts.py | 10 +- .../threads/tests/test_threads_editor_api.py | 358 +++++++----------- 3 files changed, 143 insertions(+), 227 deletions(-) diff --git a/misago/threads/api/threadendpoints/editor.py b/misago/threads/api/threadendpoints/editor.py index f10ee1ea20..18bdc56ff9 100644 --- a/misago/threads/api/threadendpoints/editor.py +++ b/misago/threads/api/threadendpoints/editor.py @@ -27,7 +27,7 @@ def thread_start_editor(request): add_acl(request.user_acl, category) post = False - if can_start_thread(request.user, category): + if can_start_thread(request.user_acl, category): post = { 'close': bool(category.acl['can_close_threads']), 'hide': bool(category.acl['can_hide_threads']), diff --git a/misago/threads/api/threadposts.py b/misago/threads/api/threadposts.py index ae6ce5d9a4..e86f8f2201 100644 --- a/misago/threads/api/threadposts.py +++ b/misago/threads/api/threadposts.py @@ -86,7 +86,7 @@ def split(self, request, thread_pk): @transaction.atomic def create(self, request, thread_pk): thread = self.get_thread(request, thread_pk).unwrap() - allow_reply_thread(request.user, thread) + allow_reply_thread(request.user_acl, thread) post = Post( thread=thread, @@ -122,7 +122,7 @@ def update(self, request, thread_pk, pk=None): thread = self.get_thread(request, thread_pk).unwrap() post = self.get_post(request, thread, pk).unwrap() - allow_edit_post(request.user, post) + allow_edit_post(request.user_acl, post) posting = PostingEndpoint( request, @@ -188,7 +188,7 @@ def post_editor(self, request, thread_pk, pk=None): thread = self.get_thread(request, thread_pk) post = self.get_post(request, thread, pk).unwrap() - allow_edit_post(request.user, post) + allow_edit_post(request.user_acl, post) attachments = [] for attachment in post.attachment_set.order_by('-id'): @@ -211,7 +211,7 @@ def post_editor(self, request, thread_pk, pk=None): @list_route(methods=['get'], url_path='editor') def reply_editor(self, request, thread_pk): thread = self.get_thread(request, thread_pk).unwrap() - allow_reply_thread(request.user, thread) + allow_reply_thread(request.user_acl, thread) if 'reply' in request.query_params: reply_to = self.get_post(request, thread, request.query_params['reply']).unwrap() @@ -242,7 +242,7 @@ def edits(self, request, thread_pk, pk=None): thread = self.get_thread(request, thread_pk) post = self.get_post(request, thread, pk).unwrap() - allow_edit_post(request.user, post) + allow_edit_post(request.user_acl, post) return revert_post_endpoint(request, post) diff --git a/misago/threads/tests/test_threads_editor_api.py b/misago/threads/tests/test_threads_editor_api.py index cb7f35c835..635803ddb8 100644 --- a/misago/threads/tests/test_threads_editor_api.py +++ b/misago/threads/tests/test_threads_editor_api.py @@ -2,18 +2,19 @@ from django.urls import reverse -from misago.acl import add_acl -from misago.acl.testutils import override_acl +from misago.acl import add_acl, useracl from misago.categories.models import Category from misago.threads import testutils from misago.threads.models import Attachment from misago.threads.serializers import AttachmentSerializer +from misago.threads.test import patch_category_acl from misago.users.testutils import AuthenticatedUserTestCase - TESTFILES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'testfiles') TEST_DOCUMENT_PATH = os.path.join(TESTFILES_DIR, 'document.pdf') +cache_versions = {"acl": "abcdefgh"} + class EditorApiTestCase(AuthenticatedUserTestCase): def setUp(self): @@ -21,53 +22,6 @@ def setUp(self): self.category = Category.objects.get(slug='first-category') - def override_acl(self, acl=None): - final_acl = self.user.acl_cache['categories'][self.category.pk] - final_acl.update({ - 'can_see': 1, - 'can_browse': 1, - 'can_see_all_threads': 1, - 'can_start_threads': 0, - 'can_reply_threads': 0, - 'can_edit_threads': 0, - 'can_edit_posts': 0, - 'can_hide_own_threads': 0, - 'can_hide_own_posts': 0, - 'thread_edit_time': 0, - 'post_edit_time': 0, - 'can_hide_threads': 0, - 'can_hide_posts': 0, - 'can_protect_posts': 0, - 'can_move_posts': 0, - 'can_merge_posts': 0, - 'can_pin_threads': 0, - 'can_close_threads': 0, - 'can_move_threads': 0, - 'can_merge_threads': 0, - 'can_approve_content': 0, - 'can_report_content': 0, - 'can_see_reports': 0, - 'can_see_posts_likes': 0, - 'can_like_posts': 0, - 'can_hide_events': 0, - }) - - if acl: - final_acl.update(acl) - - browseable_categories = [] - if final_acl['can_browse']: - browseable_categories.append(self.category.pk) - - override_acl( - self.user, { - 'browseable_categories': browseable_categories, - 'categories': { - self.category.pk: final_acl, - }, - } - ) - class ThreadPostEditorApiTests(EditorApiTestCase): def setUp(self): @@ -85,30 +39,27 @@ def test_anonymous_user_request(self): "detail": "You need to be signed in to start threads.", }) + @patch_category_acl({'can_browse': False}) def test_category_visibility_validation(self): """endpoint omits non-browseable categories""" - self.override_acl({'can_browse': 0}) - response = self.client.get(self.api_link) self.assertEqual(response.status_code, 403) self.assertEqual(response.json(), { "detail": "No categories that allow new threads are available to you at the moment.", }) + @patch_category_acl({'can_start_threads': False}) def test_category_disallowing_new_threads(self): """endpoint omits category disallowing starting threads""" - self.override_acl({'can_start_threads': 0}) - response = self.client.get(self.api_link) self.assertEqual(response.status_code, 403) self.assertEqual(response.json(), { "detail": "No categories that allow new threads are available to you at the moment.", }) + @patch_category_acl({'can_close_threads': False, 'can_start_threads': True}) def test_category_closed_disallowing_new_threads(self): """endpoint omits closed category""" - self.override_acl({'can_start_threads': 2, 'can_close_threads': 0}) - self.category.is_closed = True self.category.save() @@ -118,10 +69,9 @@ def test_category_closed_disallowing_new_threads(self): "detail": "No categories that allow new threads are available to you at the moment.", }) + @patch_category_acl({'can_close_threads': True, 'can_start_threads': True}) def test_category_closed_allowing_new_threads(self): """endpoint adds closed category that allows new threads""" - self.override_acl({'can_start_threads': 2, 'can_close_threads': 1}) - self.category.is_closed = True self.category.save() @@ -142,10 +92,9 @@ def test_category_closed_allowing_new_threads(self): } ) + @patch_category_acl({'can_start_threads': True}) def test_category_allowing_new_threads(self): """endpoint adds category that allows new threads""" - self.override_acl({'can_start_threads': 2}) - response = self.client.get(self.api_link) self.assertEqual(response.status_code, 200) @@ -163,10 +112,9 @@ def test_category_allowing_new_threads(self): } ) + @patch_category_acl({'can_close_threads': True, 'can_start_threads': True}) def test_category_allowing_closing_threads(self): """endpoint adds category that allows new closed threads""" - self.override_acl({'can_start_threads': 2, 'can_close_threads': 1}) - response = self.client.get(self.api_link) self.assertEqual(response.status_code, 200) @@ -184,10 +132,9 @@ def test_category_allowing_closing_threads(self): } ) + @patch_category_acl({'can_start_threads': True, 'can_pin_threads': 1}) def test_category_allowing_locally_pinned_threads(self): """endpoint adds category that allows locally pinned threads""" - self.override_acl({'can_start_threads': 2, 'can_pin_threads': 1}) - response = self.client.get(self.api_link) self.assertEqual(response.status_code, 200) @@ -205,10 +152,9 @@ def test_category_allowing_locally_pinned_threads(self): } ) + @patch_category_acl({'can_start_threads': True, 'can_pin_threads': 2}) def test_category_allowing_globally_pinned_threads(self): """endpoint adds category that allows globally pinned threads""" - self.override_acl({'can_start_threads': 2, 'can_pin_threads': 2}) - response = self.client.get(self.api_link) self.assertEqual(response.status_code, 200) @@ -226,10 +172,9 @@ def test_category_allowing_globally_pinned_threads(self): } ) - def test_category_allowing_hidden_threads(self): - """endpoint adds category that allows globally pinned threads""" - self.override_acl({'can_start_threads': 2, 'can_hide_threads': 1}) - + @patch_category_acl({'can_start_threads': True, 'can_hide_threads': 1}) + def test_category_allowing_hidding_threads(self): + """endpoint adds category that allows hiding threads""" response = self.client.get(self.api_link) self.assertEqual(response.status_code, 200) @@ -247,8 +192,9 @@ def test_category_allowing_hidden_threads(self): } ) - self.override_acl({'can_start_threads': 2, 'can_hide_threads': 2}) - + @patch_category_acl({'can_start_threads': True, 'can_hide_threads': 2}) + def test_category_allowing_hidding_and_deleting_threads(self): + """endpoint adds category that allows hiding and deleting threads""" response = self.client.get(self.api_link) self.assertEqual(response.status_code, 200) @@ -260,7 +206,7 @@ def test_category_allowing_hidden_threads(self): 'level': 0, 'post': { 'close': False, - 'hide': True, + 'hide': 1, 'pin': 0, }, } @@ -290,22 +236,21 @@ def test_anonymous_user_request(self): def test_thread_visibility(self): """thread's visibility is validated""" - self.override_acl({'can_see': 0}) - response = self.client.get(self.api_link) - self.assertEqual(response.status_code, 404) + with patch_category_acl({'can_see': False}): + response = self.client.get(self.api_link) + self.assertEqual(response.status_code, 404) - self.override_acl({'can_browse': 0}) - response = self.client.get(self.api_link) - self.assertEqual(response.status_code, 404) + with patch_category_acl({'can_browse': False}): + response = self.client.get(self.api_link) + self.assertEqual(response.status_code, 404) - self.override_acl({'can_see_all_threads': 0}) - response = self.client.get(self.api_link) - self.assertEqual(response.status_code, 404) + with patch_category_acl({'can_see_all_threads': False}): + response = self.client.get(self.api_link) + self.assertEqual(response.status_code, 404) + @patch_category_acl({'can_reply_threads': False}) def test_no_reply_permission(self): """permssion to reply is validated""" - self.override_acl({'can_reply_threads': 0}) - response = self.client.get(self.api_link) self.assertEqual(response.status_code, 403) self.assertEqual(response.json(), { @@ -314,72 +259,63 @@ def test_no_reply_permission(self): def test_closed_category(self): """permssion to reply in closed category is validated""" - self.override_acl({'can_reply_threads': 1, 'can_close_threads': 0}) - self.category.is_closed = True self.category.save() - response = self.client.get(self.api_link) - self.assertEqual(response.status_code, 403) - self.assertEqual(response.json(), { - "detail": "This category is closed. You can't reply to threads in it.", - }) + with patch_category_acl({'can_reply_threads': True, 'can_close_threads': False}): + response = self.client.get(self.api_link) + self.assertEqual(response.status_code, 403) + self.assertEqual(response.json(), { + "detail": "This category is closed. You can't reply to threads in it.", + }) # allow to post in closed category - self.override_acl({'can_reply_threads': 1, 'can_close_threads': 1}) - - response = self.client.get(self.api_link) - self.assertEqual(response.status_code, 200) + with patch_category_acl({'can_reply_threads': True, 'can_close_threads': True}): + response = self.client.get(self.api_link) + self.assertEqual(response.status_code, 200) def test_closed_thread(self): """permssion to reply in closed thread is validated""" - self.override_acl({'can_reply_threads': 1, 'can_close_threads': 0}) - self.thread.is_closed = True self.thread.save() - response = self.client.get(self.api_link) - self.assertEqual(response.status_code, 403) - self.assertEqual(response.json(), { - "detail": "You can't reply to closed threads in this category.", - }) + with patch_category_acl({'can_reply_threads': True, 'can_close_threads': False}): + response = self.client.get(self.api_link) + self.assertEqual(response.status_code, 403) + self.assertEqual(response.json(), { + "detail": "You can't reply to closed threads in this category.", + }) # allow to post in closed thread - self.override_acl({'can_reply_threads': 1, 'can_close_threads': 1}) - - response = self.client.get(self.api_link) - self.assertEqual(response.status_code, 200) + with patch_category_acl({'can_reply_threads': True, 'can_close_threads': True}): + response = self.client.get(self.api_link) + self.assertEqual(response.status_code, 200) + @patch_category_acl({'can_reply_threads': True}) def test_allow_reply_thread(self): """api returns 200 code if thread reply is allowed""" - self.override_acl({'can_reply_threads': 1}) - response = self.client.get(self.api_link) self.assertEqual(response.status_code, 200) def test_reply_to_visibility(self): """api validates replied post visibility""" - self.override_acl({'can_reply_threads': 1}) # unapproved reply can't be replied to - unapproved_reply = testutils.reply_thread( - self.thread, - is_unapproved=True, - ) + unapproved_reply = testutils.reply_thread(self.thread, is_unapproved=True) - response = self.client.get('%s?reply=%s' % (self.api_link, unapproved_reply.pk)) - self.assertEqual(response.status_code, 404) + with patch_category_acl({'can_reply_threads': True}): + response = self.client.get('%s?reply=%s' % (self.api_link, unapproved_reply.pk)) + self.assertEqual(response.status_code, 404) # hidden reply can't be replied to - self.override_acl({'can_reply_threads': 1}) - hidden_reply = testutils.reply_thread(self.thread, is_hidden=True) - response = self.client.get('%s?reply=%s' % (self.api_link, hidden_reply.pk)) - self.assertEqual(response.status_code, 403) - self.assertEqual(response.json(), { - "detail": "You can't reply to hidden posts.", - }) + with patch_category_acl({'can_reply_threads': True}): + response = self.client.get('%s?reply=%s' % (self.api_link, hidden_reply.pk)) + self.assertEqual(response.status_code, 403) + self.assertEqual(response.json(), { + "detail": "You can't reply to hidden posts.", + }) def test_reply_to_other_thread_post(self): """api validates is replied post belongs to same thread""" @@ -389,10 +325,9 @@ def test_reply_to_other_thread_post(self): response = self.client.get('%s?reply=%s' % (self.api_link, reply_to.pk)) self.assertEqual(response.status_code, 404) + @patch_category_acl({'can_reply_threads': True}) def test_reply_to_event(self): - """events can't be edited""" - self.override_acl({'can_reply_threads': 1}) - + """events can't be replied to""" reply_to = testutils.reply_thread(self.thread, is_event=True) response = self.client.get('%s?reply=%s' % (self.api_link, reply_to.pk)) @@ -401,10 +336,9 @@ def test_reply_to_event(self): "detail": "You can't reply to events.", }) + @patch_category_acl({'can_reply_threads': True}) def test_reply_to(self): """api includes replied to post details in response""" - self.override_acl({'can_reply_threads': 1}) - reply_to = testutils.reply_thread(self.thread) response = self.client.get('%s?reply=%s' % (self.api_link, reply_to.pk)) @@ -446,22 +380,21 @@ def test_anonymous_user_request(self): def test_thread_visibility(self): """thread's visibility is validated""" - self.override_acl({'can_see': 0}) - response = self.client.get(self.api_link) - self.assertEqual(response.status_code, 404) + with patch_category_acl({'can_see': False}): + response = self.client.get(self.api_link) + self.assertEqual(response.status_code, 404) - self.override_acl({'can_browse': 0}) - response = self.client.get(self.api_link) - self.assertEqual(response.status_code, 404) + with patch_category_acl({'can_browse': False}): + response = self.client.get(self.api_link) + self.assertEqual(response.status_code, 404) - self.override_acl({'can_see_all_threads': 0}) - response = self.client.get(self.api_link) - self.assertEqual(response.status_code, 404) + with patch_category_acl({'can_see_all_threads': False}): + response = self.client.get(self.api_link) + self.assertEqual(response.status_code, 404) + @patch_category_acl({'can_edit_posts': 0}) def test_no_edit_permission(self): """permssion to edit is validated""" - self.override_acl({'can_edit_posts': 0}) - response = self.client.get(self.api_link) self.assertEqual(response.status_code, 403) self.assertEqual(response.json(), { @@ -470,103 +403,90 @@ def test_no_edit_permission(self): def test_closed_category(self): """permssion to edit in closed category is validated""" - self.override_acl({'can_edit_posts': 1, 'can_close_threads': 0}) - self.category.is_closed = True self.category.save() - response = self.client.get(self.api_link) - self.assertEqual(response.status_code, 403) - self.assertEqual(response.json(), { - "detail": "This category is closed. You can't edit posts in it.", - }) + with patch_category_acl({'can_edit_posts': 1, 'can_close_threads': False}): + response = self.client.get(self.api_link) + self.assertEqual(response.status_code, 403) + self.assertEqual(response.json(), { + "detail": "This category is closed. You can't edit posts in it.", + }) # allow to edit in closed category - self.override_acl({'can_edit_posts': 1, 'can_close_threads': 1}) - - response = self.client.get(self.api_link) - self.assertEqual(response.status_code, 200) + with patch_category_acl({'can_edit_posts': 1, 'can_close_threads': True}): + response = self.client.get(self.api_link) + self.assertEqual(response.status_code, 200) def test_closed_thread(self): """permssion to edit in closed thread is validated""" - self.override_acl({'can_edit_posts': 1, 'can_close_threads': 0}) - self.thread.is_closed = True self.thread.save() - response = self.client.get(self.api_link) - self.assertEqual(response.status_code, 403) - self.assertEqual(response.json(), { - "detail": "This thread is closed. You can't edit posts in it.", - }) + with patch_category_acl({'can_edit_posts': 1, 'can_close_threads': False}): + response = self.client.get(self.api_link) + self.assertEqual(response.status_code, 403) + self.assertEqual(response.json(), { + "detail": "This thread is closed. You can't edit posts in it.", + }) # allow to edit in closed thread - self.override_acl({'can_edit_posts': 1, 'can_close_threads': 1}) - - response = self.client.get(self.api_link) - self.assertEqual(response.status_code, 200) + with patch_category_acl({'can_edit_posts': 1, 'can_close_threads': True}): + response = self.client.get(self.api_link) + self.assertEqual(response.status_code, 200) def test_protected_post(self): """permssion to edit protected post is validated""" - self.override_acl({'can_edit_posts': 1, 'can_protect_posts': 0}) - self.post.is_protected = True self.post.save() - response = self.client.get(self.api_link) - self.assertEqual(response.status_code, 403) - self.assertEqual(response.json(), { - "detail": "This post is protected. You can't edit it.", - }) + with patch_category_acl({'can_edit_posts': 1, 'can_protect_posts': False}): + response = self.client.get(self.api_link) + self.assertEqual(response.status_code, 403) + self.assertEqual(response.json(), { + "detail": "This post is protected. You can't edit it.", + }) # allow to post in closed thread - self.override_acl({'can_edit_posts': 1, 'can_protect_posts': 1}) - - response = self.client.get(self.api_link) - self.assertEqual(response.status_code, 200) + with patch_category_acl({'can_edit_posts': 1, 'can_protect_posts': True}): + response = self.client.get(self.api_link) + self.assertEqual(response.status_code, 200) def test_post_visibility(self): """edited posts visibility is validated""" - self.override_acl({'can_edit_posts': 1}) - self.post.is_hidden = True self.post.save() - response = self.client.get(self.api_link) - self.assertEqual(response.status_code, 403) - self.assertEqual(response.json(), { - "detail": "This post is hidden, you can't edit it.", - }) + with patch_category_acl({'can_edit_posts': 1}): + response = self.client.get(self.api_link) + self.assertEqual(response.status_code, 403) + self.assertEqual(response.json(), { + "detail": "This post is hidden, you can't edit it.", + }) # allow hidden edition - self.override_acl({'can_edit_posts': 1, 'can_hide_posts': 1}) - - response = self.client.get(self.api_link) - self.assertEqual(response.status_code, 200) + with patch_category_acl({'can_edit_posts': 1, 'can_hide_posts': 1}): + response = self.client.get(self.api_link) + self.assertEqual(response.status_code, 200) # test unapproved post + self.post.is_unapproved = True self.post.is_hidden = False self.post.poster = None self.post.save() - self.override_acl({'can_edit_posts': 2, 'can_approve_content': 0}) - - self.post.is_unapproved = True - self.post.save() - - response = self.client.get(self.api_link) - self.assertEqual(response.status_code, 404) + with patch_category_acl({'can_edit_posts': 2, 'can_approve_content': 0}): + response = self.client.get(self.api_link) + self.assertEqual(response.status_code, 404) # allow unapproved edition - self.override_acl({'can_edit_posts': 2, 'can_approve_content': 1}) - - response = self.client.get(self.api_link) - self.assertEqual(response.status_code, 200) + with patch_category_acl({'can_edit_posts': 2, 'can_approve_content': 1}): + response = self.client.get(self.api_link) + self.assertEqual(response.status_code, 200) + @patch_category_acl({'can_edit_posts': 2}) def test_post_is_event(self): """events can't be edited""" - self.override_acl() - self.post.is_event = True self.post.save() @@ -578,27 +498,24 @@ def test_post_is_event(self): def test_other_user_post(self): """api validates if other user's post can be edited""" - self.override_acl({'can_edit_posts': 1}) - self.post.poster = None self.post.save() - response = self.client.get(self.api_link) - self.assertEqual(response.status_code, 403) - self.assertEqual(response.json(), { - "detail": "You can't edit other users posts in this category.", - }) + with patch_category_acl({'can_edit_posts': 1}): + response = self.client.get(self.api_link) + self.assertEqual(response.status_code, 403) + self.assertEqual(response.json(), { + "detail": "You can't edit other users posts in this category.", + }) # allow other users post edition - self.override_acl({'can_edit_posts': 2}) - - response = self.client.get(self.api_link) - self.assertEqual(response.status_code, 200) + with patch_category_acl({'can_edit_posts': 2}): + response = self.client.get(self.api_link) + self.assertEqual(response.status_code, 200) + @patch_category_acl({'can_hide_threads': 1, 'can_edit_posts': 2}) def test_edit_first_post_hidden(self): """endpoint returns valid configuration for editor of hidden thread's first post""" - self.override_acl({'can_hide_threads': 1, 'can_edit_posts': 2}) - self.thread.is_hidden = True self.thread.save() self.thread.first_post.is_hidden = True @@ -615,18 +532,18 @@ def test_edit_first_post_hidden(self): response = self.client.get(api_link) self.assertEqual(response.status_code, 200) + @patch_category_acl({'can_edit_posts': 1}) def test_edit(self): """endpoint returns valid configuration for editor""" - for _ in range(3): - self.override_acl({'max_attachment_size': 1000}) - - with open(TEST_DOCUMENT_PATH, 'rb') as upload: - response = self.client.post( - reverse('misago:api:attachment-list'), data={ - 'upload': upload, - } - ) - self.assertEqual(response.status_code, 200) + with patch_category_acl({'max_attachment_size': 1000}): + for _ in range(3): + with open(TEST_DOCUMENT_PATH, 'rb') as upload: + response = self.client.post( + reverse('misago:api:attachment-list'), data={ + 'upload': upload, + } + ) + self.assertEqual(response.status_code, 200) attachments = list(Attachment.objects.order_by('id')) @@ -637,11 +554,10 @@ def test_edit(self): attachment.post = self.post attachment.save() - self.override_acl({'can_edit_posts': 1}) response = self.client.get(self.api_link) - + user_acl = useracl.get_user_acl(self.user, cache_versions) for attachment in attachments: - add_acl(self.user, attachment) + add_acl(user_acl, attachment) self.assertEqual(response.status_code, 200) self.assertEqual( From a8184326989154a2ee9965ca732f44a4d7c459ae Mon Sep 17 00:00:00 2001 From: rafalp Date: Thu, 13 Dec 2018 01:37:28 +0100 Subject: [PATCH 33/86] Make override_acl function raise error on use --- misago/acl/testutils.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/misago/acl/testutils.py b/misago/acl/testutils.py index cce5157108..6c0cc5e18a 100644 --- a/misago/acl/testutils.py +++ b/misago/acl/testutils.py @@ -1,8 +1,3 @@ -from copy import deepcopy -from hashlib import md5 - -from misago.core import threadstore - from .forms import get_permissions_forms @@ -25,13 +20,4 @@ def fake_post_data(target, data_dict): def override_acl(user, new_acl): """overrides user permissions with specified ones""" - final_cache = deepcopy(user.acl_cache) - final_cache.update(new_acl) - - if user.is_authenticated: - user._acl_cache = final_cache - user.acl_key = md5(str(user.pk).encode()).hexdigest()[:8] - user.save(update_fields=['acl_key']) - threadstore.set('acl_%s' % user.acl_key, final_cache) - else: - threadstore.set('acl_%s' % user.acl_key, final_cache) + raise Exception("override_acl has been removed") From bf157d864598dc82f74c14b36c3c1cbdcaf7bd12 Mon Sep 17 00:00:00 2001 From: rafalp Date: Thu, 13 Dec 2018 01:45:51 +0100 Subject: [PATCH 34/86] Tweak test util and fix regressions in misago.users.tests --- misago/acl/test.py | 4 ---- misago/users/tests/test_user_details_api.py | 4 ++-- misago/users/tests/test_user_editdetails_api.py | 4 ++-- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/misago/acl/test.py b/misago/acl/test.py index 967ec4f533..c5d32790f2 100644 --- a/misago/acl/test.py +++ b/misago/acl/test.py @@ -11,10 +11,6 @@ class patch_user_acl(ContextDecorator, ExitStack): """Testing utility that patches get_user_acl results Can be used as decorator or context manager. - - Accepts one or two arguments: - - patch_user_acl(acl_patch) - - patch_user_acl(user, acl_patch) Patch should be a dict or callable. """ diff --git a/misago/users/tests/test_user_details_api.py b/misago/users/tests/test_user_details_api.py index c74fd84995..7800faab84 100644 --- a/misago/users/tests/test_user_details_api.py +++ b/misago/users/tests/test_user_details_api.py @@ -55,13 +55,13 @@ def test_other_user(self): ) # moderator has permission to edit details - with patch_user_acl(self.user, {'can_moderate_profile_details': True}): + with patch_user_acl({'can_moderate_profile_details': True}): response = self.client.get(api_link) self.assertEqual(response.status_code, 200) self.assertTrue(response.json()['edit']) # non-moderator has no permission to edit details - with patch_user_acl(self.user, {'can_moderate_profile_details': False}): + with patch_user_acl({'can_moderate_profile_details': False}): response = self.client.get(api_link) self.assertEqual(response.status_code, 200) self.assertFalse(response.json()['edit']) diff --git a/misago/users/tests/test_user_editdetails_api.py b/misago/users/tests/test_user_editdetails_api.py index d42fb47c86..7106f396d3 100644 --- a/misago/users/tests/test_user_editdetails_api.py +++ b/misago/users/tests/test_user_editdetails_api.py @@ -44,12 +44,12 @@ def test_other_user(self): ) # moderator has permission to edit details - with patch_user_acl(self.user, {'can_moderate_profile_details': True}): + with patch_user_acl({'can_moderate_profile_details': True}): response = self.client.get(api_link) self.assertEqual(response.status_code, 200) # non-moderator has no permission to edit details - with patch_user_acl(self.user, {'can_moderate_profile_details': False}): + with patch_user_acl({'can_moderate_profile_details': False}): response = self.client.get(api_link) self.assertEqual(response.status_code, 403) From 56cc7c69d0ea79909ebd7247ece8f09effffed00 Mon Sep 17 00:00:00 2001 From: rafalp Date: Sat, 15 Dec 2018 16:27:24 +0100 Subject: [PATCH 35/86] Fix misago.search tests --- misago/search/tests/test_api.py | 5 ++--- misago/search/tests/test_views.py | 16 ++++++---------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/misago/search/tests/test_api.py b/misago/search/tests/test_api.py index 4c5cd94c0a..b538b51734 100644 --- a/misago/search/tests/test_api.py +++ b/misago/search/tests/test_api.py @@ -1,6 +1,6 @@ from django.urls import reverse -from misago.acl.testutils import override_acl +from misago.acl.test import patch_user_acl from misago.search.searchproviders import searchproviders from misago.users.testutils import AuthenticatedUserTestCase @@ -11,10 +11,9 @@ def setUp(self): self.test_link = reverse('misago:api:search') + @patch_user_acl({"can_search": False}) def test_no_permission(self): """api validates permission to search""" - override_acl(self.user, {'can_search': 0}) - response = self.client.get(self.test_link) self.assertEqual(response.status_code, 403) self.assertEqual(response.json(), { diff --git a/misago/search/tests/test_views.py b/misago/search/tests/test_views.py index f7f65c1625..c0c52ea9bc 100644 --- a/misago/search/tests/test_views.py +++ b/misago/search/tests/test_views.py @@ -1,6 +1,6 @@ from django.urls import reverse -from misago.acl.testutils import override_acl +from misago.acl.test import patch_user_acl from misago.threads.search import SearchThreads from misago.users.testutils import AuthenticatedUserTestCase @@ -11,27 +11,24 @@ def setUp(self): self.test_link = reverse('misago:search') + @patch_user_acl({'can_search': False}) def test_no_permission(self): """view validates permission to search forum""" - override_acl(self.user, {'can_search': 0}) - response = self.client.get(self.test_link) - self.assertContains(response, "have permission to search site", status_code=403) + @patch_user_acl({'can_search': True}) def test_redirect_to_provider(self): """view validates permission to search forum""" response = self.client.get(self.test_link) - self.assertEqual(response.status_code, 302) self.assertIn(SearchThreads.url, response['location']) class SearchTests(AuthenticatedUserTestCase): + @patch_user_acl({'can_search': False}) def test_no_permission(self): """view validates permission to search forum""" - override_acl(self.user, {'can_search': 0}) - response = self.client.get( reverse('misago:search', kwargs={ 'search_provider': 'users', @@ -48,10 +45,9 @@ def test_not_found(self): self.assertEqual(response.status_code, 404) + @patch_user_acl({'can_search': True, 'can_search_users': False}) def test_provider_no_permission(self): """provider raises 403 without permission""" - override_acl(self.user, {'can_search_users': 0}) - response = self.client.get( reverse('misago:search', kwargs={ 'search_provider': 'users', @@ -64,7 +60,7 @@ def test_provider(self): """provider displays no script page""" response = self.client.get( reverse('misago:search', kwargs={ - 'search_provider': 'threads', + 'search_provider': 'users', }) ) From 19d77eaeea5144fbb3ce61b9952c7f620afbad6d Mon Sep 17 00:00:00 2001 From: rafalp Date: Sat, 15 Dec 2018 17:49:17 +0100 Subject: [PATCH 36/86] Fix merging threads --- .../threads/api/postingendpoint/__init__.py | 7 +- .../threads/api/postingendpoint/category.py | 18 +- misago/threads/api/threadendpoints/merge.py | 8 +- misago/threads/serializers/moderation.py | 47 +-- misago/threads/test.py | 4 +- misago/threads/tests/test_thread_merge_api.py | 284 ++++++------------ .../threads/tests/test_threads_merge_api.py | 255 ++++------------ misago/threads/validators.py | 6 +- 8 files changed, 202 insertions(+), 427 deletions(-) diff --git a/misago/threads/api/postingendpoint/__init__.py b/misago/threads/api/postingendpoint/__init__.py index 46f78174cd..bdd18a1320 100755 --- a/misago/threads/api/postingendpoint/__init__.py +++ b/misago/threads/api/postingendpoint/__init__.py @@ -26,7 +26,12 @@ def __init__(self, request, mode, **kwargs): # build kwargs dict for passing to middlewares self.kwargs = kwargs - self.kwargs.update({'mode': mode, 'request': request, 'user': request.user}) + self.kwargs.update({ + 'mode': mode, + 'request': request, + 'user': request.user, + 'user_acl': request.user_acl, + }) self.__dict__.update(kwargs) diff --git a/misago/threads/api/postingendpoint/category.py b/misago/threads/api/postingendpoint/category.py index e918c01ddb..94f7922961 100755 --- a/misago/threads/api/postingendpoint/category.py +++ b/misago/threads/api/postingendpoint/category.py @@ -23,12 +23,12 @@ def use_this_middleware(self): return False def get_serializer(self): - return CategorySerializer(self.user, data=self.request.data) + return CategorySerializer(self.user_acl, data=self.request.data) def pre_save(self, serializer): category = serializer.category_cache - add_acl(self.user, category) + add_acl(self.user_acl, category) # set flags for savechanges middleware category.update_all = False @@ -47,8 +47,8 @@ class CategorySerializer(serializers.Serializer): } ) - def __init__(self, user, *args, **kwargs): - self.user = user + def __init__(self, user_acl, *args, **kwargs): + self.user_acl = user_acl self.category_cache = None super().__init__(*args, **kwargs) @@ -59,15 +59,15 @@ def validate_category(self, value): pk=value, tree_id=trees_map.get_tree_id_for_root(THREADS_ROOT_NAME) ) - can_see = can_see_category(self.user, self.category_cache) - can_browse = can_browse_category(self.user, self.category_cache) + can_see = can_see_category(self.user_acl, self.category_cache) + can_browse = can_browse_category(self.user_acl, self.category_cache) if not (self.category_cache.level and can_see and can_browse): raise PermissionDenied(_("Selected category is invalid.")) - allow_start_thread(self.user, self.category_cache) - except PermissionDenied as e: - raise serializers.ValidationError(e.args[0]) + allow_start_thread(self.user_acl, self.category_cache) except Category.DoesNotExist: raise serializers.ValidationError( _("Selected category doesn't exist or you don't have permission to browse it.") ) + except PermissionDenied as e: + raise serializers.ValidationError(e.args[0]) diff --git a/misago/threads/api/threadendpoints/merge.py b/misago/threads/api/threadendpoints/merge.py index cab462bd5d..fa414a2353 100644 --- a/misago/threads/api/threadendpoints/merge.py +++ b/misago/threads/api/threadendpoints/merge.py @@ -15,7 +15,7 @@ def thread_merge_endpoint(request, thread, viewmodel): - allow_merge_thread(request.user, thread) + allow_merge_thread(request.user_acl, thread) serializer = MergeThreadSerializer( data=request.data, @@ -88,9 +88,7 @@ def thread_merge_endpoint(request, thread, viewmodel): def threads_merge_endpoint(request): serializer = MergeThreadsSerializer( data=request.data, - context={ - 'user': request.user - }, + context={'user_acl': request.user_acl}, ) if not serializer.is_valid(): @@ -108,7 +106,7 @@ def threads_merge_endpoint(request): for thread in threads: try: - allow_merge_thread(request.user, thread) + allow_merge_thread(request.user_acl, thread) except PermissionDenied as e: invalid_threads.append({ 'id': thread.pk, diff --git a/misago/threads/serializers/moderation.py b/misago/threads/serializers/moderation.py index 41f3cc63bb..58fc05ca37 100644 --- a/misago/threads/serializers/moderation.py +++ b/misago/threads/serializers/moderation.py @@ -62,10 +62,10 @@ def validate_posts(self, data): ) raise ValidationError(message % {'limit': POSTS_LIMIT}) - user = self.context['user'] + user_acl = self.context['user_acl'] thread = self.context['thread'] - posts_queryset = exclude_invisible_posts(user, thread.category, thread.post_set) + posts_queryset = exclude_invisible_posts(user_acl, thread.category, thread.post_set) posts_queryset = posts_queryset.filter(id__in=data).order_by('id') posts = [] @@ -74,10 +74,10 @@ def validate_posts(self, data): post.thread = thread if post.is_event: - allow_delete_event(user, post) + allow_delete_event(user_acl, post) else: - allow_delete_best_answer(user, post) - allow_delete_post(user, post) + allow_delete_best_answer(user_acl, post) + allow_delete_post(user_acl, post) posts.append(post) @@ -115,10 +115,10 @@ def validate_posts(self, data): ) raise serializers.ValidationError(message % {'limit': POSTS_LIMIT}) - user = self.context['user'] + user_acl = self.context['user_acl'] thread = self.context['thread'] - posts_queryset = exclude_invisible_posts(user, thread.category, thread.post_set) + posts_queryset = exclude_invisible_posts(user_acl, thread.category, thread.post_set) posts_queryset = posts_queryset.filter(id__in=data).order_by('id') posts = [] @@ -127,7 +127,7 @@ def validate_posts(self, data): post.thread = thread try: - allow_merge_post(user, post) + allow_merge_post(user_acl, post) except PermissionDenied as e: raise serializers.ValidationError(e) @@ -232,7 +232,7 @@ def validate_posts(self, data): post.thread = thread try: - allow_move_post(request.user, post) + allow_move_post(request.user_acl, post) posts.append(post) except PermissionDenied as e: raise serializers.ValidationError(e) @@ -259,14 +259,15 @@ def validate_title(self, title): return validate_title(title) def validate_category(self, category_id): - self.category = validate_category(self.context['user'], category_id) - if not can_start_thread(self.context['user'], self.category): + user_acl = self.context['user_acl'] + self.category = validate_category(user_acl, category_id) + if not can_start_thread(user_acl, self.category): raise ValidationError(_("You can't create new threads in selected category.")) return self.category def validate_weight(self, weight): try: - add_acl(self.context['user'], self.category) + add_acl(self.context['user_acl'], self.category) except AttributeError: return weight # don't validate weight further if category failed @@ -283,7 +284,7 @@ def validate_weight(self, weight): def validate_is_hidden(self, is_hidden): try: - add_acl(self.context['user'], self.category) + add_acl(self.context['user_acl'], self.category) except AttributeError: return is_hidden # don't validate hidden further if category failed @@ -293,7 +294,7 @@ def validate_is_hidden(self, is_hidden): def validate_is_closed(self, is_closed): try: - add_acl(self.context['user'], self.category) + add_acl(self.context['user_acl'], self.category) except AttributeError: return is_closed # don't validate closed further if category failed @@ -331,9 +332,9 @@ def validate_posts(self, data): raise ValidationError(message % {'limit': POSTS_LIMIT}) thread = self.context['thread'] - user = self.context['user'] + user_acl = self.context['user_acl'] - posts_queryset = exclude_invisible_posts(user, thread.category, thread.post_set) + posts_queryset = exclude_invisible_posts(user_acl, thread.category, thread.post_set) posts_queryset = posts_queryset.filter(id__in=data).order_by('id') posts = [] @@ -342,7 +343,7 @@ def validate_posts(self, data): post.thread = thread try: - allow_split_post(user, post) + allow_split_post(user_acl, post) except PermissionDenied as e: raise ValidationError(e) @@ -389,7 +390,7 @@ def validate_threads(self, data): for thread_id in data: try: thread = viewmodel(request, thread_id).unwrap() - allow_delete_thread(request.user, thread) + allow_delete_thread(request.user_acl, thread) threads.append(thread) except PermissionDenied as e: errors.append({ @@ -443,7 +444,7 @@ def validate_other_thread(self, data): try: other_thread = viewmodel(request, other_thread_id).unwrap() - allow_merge_thread(request.user, other_thread, otherthread=True) + allow_merge_thread(request.user_acl, other_thread, otherthread=True) except PermissionDenied as e: raise serializers.ValidationError(e) except Http404: @@ -454,7 +455,7 @@ def validate_other_thread(self, data): ) ) - if not can_reply_thread(request.user, other_thread): + if not can_reply_thread(request.user_acl, other_thread): raise ValidationError(_("You can't merge this thread into thread you can't reply.")) return other_thread @@ -518,12 +519,12 @@ def validate_threads(self, data): category__tree_id=threads_tree_id, ).select_related('category').order_by('-id') - user = self.context['user'] + user_acl = self.context['user_acl'] threads = [] for thread in threads_queryset: - add_acl(user, thread) - if can_see_thread(user, thread): + add_acl(user_acl, thread) + if can_see_thread(user_acl, thread): threads.append(thread) if len(threads) != len(data): diff --git a/misago/threads/test.py b/misago/threads/test.py index a90da8bf4e..b3ce502b3b 100644 --- a/misago/threads/test.py +++ b/misago/threads/test.py @@ -16,7 +16,7 @@ } -def patch_category_acl(acl_patch): +def patch_category_acl(acl_patch=None): def patch_acl(_, user_acl): category = Category.objects.get(slug="first-category") category_acl = user_acl['categories'][category.id] @@ -28,7 +28,7 @@ def patch_acl(_, user_acl): return patch_user_acl(patch_acl) -def patch_other_category_acl(acl_patch): +def patch_other_category_acl(acl_patch=None): def patch_acl(_, user_acl): src_category = Category.objects.get(slug="first-category") category_acl = user_acl['categories'][src_category.id].copy() diff --git a/misago/threads/tests/test_thread_merge_api.py b/misago/threads/tests/test_thread_merge_api.py index 8b07893836..839949c7e2 100644 --- a/misago/threads/tests/test_thread_merge_api.py +++ b/misago/threads/tests/test_thread_merge_api.py @@ -1,10 +1,10 @@ from django.urls import reverse -from misago.acl.testutils import override_acl from misago.categories.models import Category from misago.readtracker import poststracker from misago.threads import testutils from misago.threads.models import Poll, PollVote, Thread +from misago.threads.test import patch_category_acl, patch_other_category_acl from .test_threads_api import ThreadsApiTestCase @@ -12,16 +12,16 @@ class ThreadMergeApiTests(ThreadsApiTestCase): def setUp(self): super().setUp() - + Category( - name='Category B', - slug='category-b', + name='Other Category', + slug='other-category', ).insert_at( self.category, position='last-child', save=True, ) - self.category_b = Category.objects.get(slug='category-b') + self.other_category = Category.objects.get(slug='other-category') self.api_link = reverse( 'misago:api:thread-merge', kwargs={ @@ -29,63 +29,27 @@ def setUp(self): } ) - def override_other_acl(self, acl=None): - other_category_acl = self.user.acl_cache['categories'][self.category.pk].copy() - other_category_acl.update({ - 'can_see': 1, - 'can_browse': 1, - 'can_see_all_threads': 1, - 'can_see_own_threads': 0, - 'can_hide_threads': 0, - 'can_approve_content': 0, - 'can_edit_posts': 0, - 'can_hide_posts': 0, - 'can_hide_own_posts': 0, - 'can_merge_threads': 0, - 'can_close_threads': 0, - }) - - if acl: - other_category_acl.update(acl) - - categories_acl = self.user.acl_cache['categories'] - categories_acl[self.category_b.pk] = other_category_acl - - visible_categories = [self.category.pk] - if other_category_acl['can_see']: - visible_categories.append(self.category_b.pk) - - override_acl( - self.user, { - 'visible_categories': visible_categories, - 'categories': categories_acl, - } - ) - + @patch_category_acl({"can_merge_threads": False}) def test_merge_no_permission(self): """api validates if thread can be merged with other one""" - self.override_acl({'can_merge_threads': 0}) - response = self.client.post(self.api_link) self.assertEqual(response.status_code, 403) self.assertEqual(response.json(), { "detail": "You can't merge threads in this category." }) + @patch_category_acl({"can_merge_threads": True}) def test_merge_no_url(self): """api validates if thread url was given""" - self.override_acl({'can_merge_threads': 1}) - response = self.client.post(self.api_link) self.assertEqual(response.status_code, 400) self.assertEqual(response.json(), { "detail": "Enter link to new thread." }) + @patch_category_acl({"can_merge_threads": True}) def test_invalid_url(self): """api validates thread url""" - self.override_acl({'can_merge_threads': 1}) - response = self.client.post(self.api_link, { 'other_thread': self.user.get_absolute_url(), }) @@ -94,10 +58,9 @@ def test_invalid_url(self): "detail": "This is not a valid thread link." }) + @patch_category_acl({"can_merge_threads": True}) def test_current_other_thread(self): """api validates if thread url given is to current thread""" - self.override_acl({'can_merge_threads': 1}) - response = self.client.post( self.api_link, { 'other_thread': self.thread.get_absolute_url(), @@ -108,12 +71,11 @@ def test_current_other_thread(self): "detail": "You can't merge thread with itself." }) + @patch_other_category_acl() + @patch_category_acl({"can_merge_threads": True}) def test_other_thread_exists(self): """api validates if other thread exists""" - self.override_acl({'can_merge_threads': 1}) - self.override_other_acl() - - other_thread = testutils.post_thread(self.category_b) + other_thread = testutils.post_thread(self.other_category) other_other_thread = other_thread.get_absolute_url() other_thread.delete() @@ -128,12 +90,11 @@ def test_other_thread_exists(self): ) }) + @patch_other_category_acl({"can_see": False}) + @patch_category_acl({"can_merge_threads": True}) def test_other_thread_is_invisible(self): """api validates if other thread is visible""" - self.override_acl({'can_merge_threads': 1}) - self.override_other_acl({'can_see': 0}) - - other_thread = testutils.post_thread(self.category_b) + other_thread = testutils.post_thread(self.other_category) response = self.client.post( self.api_link, { @@ -148,12 +109,11 @@ def test_other_thread_is_invisible(self): ) }) + @patch_other_category_acl({"can_merge_threads": False}) + @patch_category_acl({"can_merge_threads": True}) def test_other_thread_isnt_mergeable(self): """api validates if other thread can be merged""" - self.override_acl({'can_merge_threads': 1}) - self.override_other_acl({'can_merge_threads': 0}) - - other_thread = testutils.post_thread(self.category_b) + other_thread = testutils.post_thread(self.other_category) response = self.client.post( self.api_link, { @@ -165,17 +125,11 @@ def test_other_thread_isnt_mergeable(self): "detail": "Other thread can't be merged with." }) + @patch_other_category_acl({"can_merge_threads": True, "can_close_threads": False}) + @patch_category_acl({"can_merge_threads": True}) def test_thread_category_is_closed(self): """api validates if thread's category is open""" - self.override_acl({'can_merge_threads': 1}) - - self.override_other_acl({ - 'can_merge_threads': 1, - 'can_reply_threads': 0, - 'can_close_threads': 0, - }) - - other_thread = testutils.post_thread(self.category_b) + other_thread = testutils.post_thread(self.other_category) self.category.is_closed = True self.category.save() @@ -190,17 +144,11 @@ def test_thread_category_is_closed(self): "detail": "This category is closed. You can't merge it's threads." }) + @patch_other_category_acl({"can_merge_threads": True, "can_close_threads": False}) + @patch_category_acl({"can_merge_threads": True}) def test_thread_is_closed(self): """api validates if thread is open""" - self.override_acl({'can_merge_threads': 1}) - - self.override_other_acl({ - 'can_merge_threads': 1, - 'can_reply_threads': 0, - 'can_close_threads': 0, - }) - - other_thread = testutils.post_thread(self.category_b) + other_thread = testutils.post_thread(self.other_category) self.thread.is_closed = True self.thread.save() @@ -215,20 +163,14 @@ def test_thread_is_closed(self): "detail": "This thread is closed. You can't merge it with other threads." }) + @patch_other_category_acl({"can_merge_threads": True, "can_close_threads": False}) + @patch_category_acl({"can_merge_threads": True}) def test_other_thread_category_is_closed(self): """api validates if other thread's category is open""" - self.override_acl({'can_merge_threads': 1}) - - self.override_other_acl({ - 'can_merge_threads': 1, - 'can_reply_threads': 0, - 'can_close_threads': 0, - }) - - other_thread = testutils.post_thread(self.category_b) + other_thread = testutils.post_thread(self.other_category) - self.category_b.is_closed = True - self.category_b.save() + self.other_category.is_closed = True + self.other_category.save() response = self.client.post( self.api_link, { @@ -240,17 +182,11 @@ def test_other_thread_category_is_closed(self): "detail": "Other thread's category is closed. You can't merge with it." }) + @patch_other_category_acl({"can_merge_threads": True, "can_close_threads": False}) + @patch_category_acl({"can_merge_threads": True}) def test_other_thread_is_closed(self): """api validates if other thread is open""" - self.override_acl({'can_merge_threads': 1}) - - self.override_other_acl({ - 'can_merge_threads': 1, - 'can_reply_threads': 0, - 'can_close_threads': 0, - }) - - other_thread = testutils.post_thread(self.category_b) + other_thread = testutils.post_thread(self.other_category) other_thread.is_closed = True other_thread.save() @@ -265,16 +201,11 @@ def test_other_thread_is_closed(self): "detail": "Other thread is closed and can't be merged with." }) + @patch_other_category_acl({"can_merge_threads": True, "can_reply_threads": False}) + @patch_category_acl({"can_merge_threads": True}) def test_other_thread_isnt_replyable(self): """api validates if other thread can be replied, which is condition for merge""" - self.override_acl({'can_merge_threads': 1}) - - self.override_other_acl({ - 'can_merge_threads': 1, - 'can_reply_threads': 0, - }) - - other_thread = testutils.post_thread(self.category_b) + other_thread = testutils.post_thread(self.other_category) response = self.client.post( self.api_link, { @@ -286,12 +217,11 @@ def test_other_thread_isnt_replyable(self): "detail": "You can't merge this thread into thread you can't reply." }) + @patch_other_category_acl({"can_merge_threads": True}) + @patch_category_acl({"can_merge_threads": True}) def test_merge_threads(self): """api merges two threads successfully""" - self.override_acl({'can_merge_threads': 1}) - self.override_other_acl({'can_merge_threads': 1}) - - other_thread = testutils.post_thread(self.category_b) + other_thread = testutils.post_thread(self.other_category) response = self.client.post( self.api_link, { @@ -312,12 +242,11 @@ def test_merge_threads(self): with self.assertRaises(Thread.DoesNotExist): Thread.objects.get(pk=self.thread.pk) + @patch_other_category_acl({"can_merge_threads": True}) + @patch_category_acl({"can_merge_threads": True}) def test_merge_threads_kept_reads(self): """api keeps both threads readtrackers after merge""" - self.override_acl({'can_merge_threads': 1}) - self.override_other_acl({'can_merge_threads': 1}) - - other_thread = testutils.post_thread(self.category_b) + other_thread = testutils.post_thread(self.other_category) poststracker.save_read(self.user, self.thread.first_post) poststracker.save_read(self.user, other_thread.first_post) @@ -342,14 +271,13 @@ def test_merge_threads_kept_reads(self): [self.thread.first_post_id, other_thread.first_post_id] ) self.assertEqual(postreads.filter(thread=other_thread).count(), 2) - self.assertEqual(postreads.filter(category=self.category_b).count(), 2) + self.assertEqual(postreads.filter(category=self.other_category).count(), 2) + @patch_other_category_acl({"can_merge_threads": True}) + @patch_category_acl({"can_merge_threads": True}) def test_merge_threads_kept_subs(self): """api keeps other thread's subscription after merge""" - self.override_acl({'can_merge_threads': 1}) - self.override_other_acl({'can_merge_threads': 1}) - - other_thread = testutils.post_thread(self.category_b) + other_thread = testutils.post_thread(self.other_category) self.user.subscription_set.create( thread=self.thread, @@ -377,14 +305,13 @@ def test_merge_threads_kept_subs(self): # subscriptions are kept self.assertEqual(self.user.subscription_set.count(), 1) self.user.subscription_set.get(thread=other_thread) - self.user.subscription_set.get(category=self.category_b) + self.user.subscription_set.get(category=self.other_category) + @patch_other_category_acl({"can_merge_threads": True}) + @patch_category_acl({"can_merge_threads": True}) def test_merge_threads_moved_subs(self): """api keeps other thread's subscription after merge""" - self.override_acl({'can_merge_threads': 1}) - self.override_other_acl({'can_merge_threads': 1}) - - other_thread = testutils.post_thread(self.category_b) + other_thread = testutils.post_thread(self.other_category) self.user.subscription_set.create( thread=other_thread, @@ -395,7 +322,7 @@ def test_merge_threads_moved_subs(self): self.assertEqual(self.user.subscription_set.count(), 1) self.user.subscription_set.get(thread=other_thread) - self.user.subscription_set.get(category=self.category_b) + self.user.subscription_set.get(category=self.other_category) response = self.client.post( self.api_link, { @@ -412,13 +339,12 @@ def test_merge_threads_moved_subs(self): # subscriptions are kept self.assertEqual(self.user.subscription_set.count(), 1) self.user.subscription_set.get(thread=other_thread) - self.user.subscription_set.get(category=self.category_b) + self.user.subscription_set.get(category=self.other_category) + @patch_other_category_acl({"can_merge_threads": True}) + @patch_category_acl({"can_merge_threads": True}) def test_merge_threads_handle_subs_colision(self): """api resolves conflicting thread subscriptions after merge""" - self.override_acl({'can_merge_threads': 1}) - self.override_other_acl({'can_merge_threads': 1}) - self.user.subscription_set.create( thread=self.thread, category=self.thread.category, @@ -426,7 +352,7 @@ def test_merge_threads_handle_subs_colision(self): send_email=False, ) - other_thread = testutils.post_thread(self.category_b) + other_thread = testutils.post_thread(self.other_category) self.user.subscription_set.create( thread=other_thread, @@ -439,7 +365,7 @@ def test_merge_threads_handle_subs_colision(self): self.user.subscription_set.get(thread=self.thread) self.user.subscription_set.get(category=self.category) self.user.subscription_set.get(thread=other_thread) - self.user.subscription_set.get(category=self.category_b) + self.user.subscription_set.get(category=self.other_category) response = self.client.post( self.api_link, { @@ -456,14 +382,13 @@ def test_merge_threads_handle_subs_colision(self): # subscriptions are kept self.assertEqual(self.user.subscription_set.count(), 1) self.user.subscription_set.get(thread=other_thread) - self.user.subscription_set.get(category=self.category_b) + self.user.subscription_set.get(category=self.other_category) + @patch_other_category_acl({"can_merge_threads": True}) + @patch_category_acl({"can_merge_threads": True}) def test_merge_threads_kept_best_answer(self): """api merges two threads successfully, keeping best answer from old thread""" - self.override_acl({'can_merge_threads': 1}) - self.override_other_acl({'can_merge_threads': 1}) - - other_thread = testutils.post_thread(self.category_b) + other_thread = testutils.post_thread(self.other_category) best_answer = testutils.reply_thread(other_thread) other_thread.set_best_answer(self.user, best_answer) other_thread.save() @@ -491,12 +416,11 @@ def test_merge_threads_kept_best_answer(self): other_thread = Thread.objects.get(pk=other_thread.pk) self.assertEqual(other_thread.best_answer, best_answer) + @patch_other_category_acl({"can_merge_threads": True}) + @patch_category_acl({"can_merge_threads": True}) def test_merge_threads_moved_best_answer(self): """api merges two threads successfully, moving best answer to old thread""" - self.override_acl({'can_merge_threads': 1}) - self.override_other_acl({'can_merge_threads': 1}) - - other_thread = testutils.post_thread(self.category_b) + other_thread = testutils.post_thread(self.other_category) best_answer = testutils.reply_thread(self.thread) self.thread.set_best_answer(self.user, best_answer) @@ -525,16 +449,15 @@ def test_merge_threads_moved_best_answer(self): other_thread = Thread.objects.get(pk=other_thread.pk) self.assertEqual(other_thread.best_answer, best_answer) + @patch_other_category_acl({"can_merge_threads": True}) + @patch_category_acl({"can_merge_threads": True}) def test_merge_threads_merge_conflict_best_answer(self): """api errors on merge conflict, returning list of available best answers""" - self.override_acl({'can_merge_threads': 1}) - self.override_other_acl({'can_merge_threads': 1}) - best_answer = testutils.reply_thread(self.thread) self.thread.set_best_answer(self.user, best_answer) self.thread.save() - other_thread = testutils.post_thread(self.category_b) + other_thread = testutils.post_thread(self.other_category) other_best_answer = testutils.reply_thread(other_thread) other_thread.set_best_answer(self.user, other_best_answer) other_thread.save() @@ -560,16 +483,15 @@ def test_merge_threads_merge_conflict_best_answer(self): self.assertEqual( Thread.objects.get(pk=other_thread.pk).best_answer_id, other_best_answer.id) + @patch_other_category_acl({"can_merge_threads": True}) + @patch_category_acl({"can_merge_threads": True}) def test_threads_merge_conflict_best_answer_invalid_resolution(self): """api errors on invalid merge conflict resolution""" - self.override_acl({'can_merge_threads': 1}) - self.override_other_acl({'can_merge_threads': 1}) - best_answer = testutils.reply_thread(self.thread) self.thread.set_best_answer(self.user, best_answer) self.thread.save() - other_thread = testutils.post_thread(self.category_b) + other_thread = testutils.post_thread(self.other_category) other_best_answer = testutils.reply_thread(other_thread) other_thread.set_best_answer(self.user, other_best_answer) other_thread.save() @@ -590,16 +512,15 @@ def test_threads_merge_conflict_best_answer_invalid_resolution(self): self.assertEqual( Thread.objects.get(pk=other_thread.pk).best_answer_id, other_best_answer.id) + @patch_other_category_acl({"can_merge_threads": True}) + @patch_category_acl({"can_merge_threads": True}) def test_threads_merge_conflict_unmark_all_best_answers(self): """api unmarks all best answers when unmark all choice is selected""" - self.override_acl({'can_merge_threads': 1}) - self.override_other_acl({'can_merge_threads': 1}) - best_answer = testutils.reply_thread(self.thread) self.thread.set_best_answer(self.user, best_answer) self.thread.save() - other_thread = testutils.post_thread(self.category_b) + other_thread = testutils.post_thread(self.other_category) other_best_answer = testutils.reply_thread(other_thread) other_thread.set_best_answer(self.user, other_best_answer) other_thread.save() @@ -627,16 +548,15 @@ def test_threads_merge_conflict_unmark_all_best_answers(self): # final thread has no marked best answer self.assertIsNone(Thread.objects.get(pk=other_thread.pk).best_answer_id) + @patch_other_category_acl({"can_merge_threads": True}) + @patch_category_acl({"can_merge_threads": True}) def test_threads_merge_conflict_keep_first_best_answer(self): """api unmarks other best answer on merge""" - self.override_acl({'can_merge_threads': 1}) - self.override_other_acl({'can_merge_threads': 1}) - best_answer = testutils.reply_thread(self.thread) self.thread.set_best_answer(self.user, best_answer) self.thread.save() - other_thread = testutils.post_thread(self.category_b) + other_thread = testutils.post_thread(self.other_category) other_best_answer = testutils.reply_thread(other_thread) other_thread.set_best_answer(self.user, other_best_answer) other_thread.save() @@ -664,16 +584,15 @@ def test_threads_merge_conflict_keep_first_best_answer(self): # other thread's best answer was unchanged self.assertEqual(Thread.objects.get(pk=other_thread.pk).best_answer_id, best_answer.id) + @patch_other_category_acl({"can_merge_threads": True}) + @patch_category_acl({"can_merge_threads": True}) def test_threads_merge_conflict_keep_other_best_answer(self): """api unmarks first best answer on merge""" - self.override_acl({'can_merge_threads': 1}) - self.override_other_acl({'can_merge_threads': 1}) - best_answer = testutils.reply_thread(self.thread) self.thread.set_best_answer(self.user, best_answer) self.thread.save() - other_thread = testutils.post_thread(self.category_b) + other_thread = testutils.post_thread(self.other_category) other_best_answer = testutils.reply_thread(other_thread) other_thread.set_best_answer(self.user, other_best_answer) other_thread.save() @@ -702,12 +621,11 @@ def test_threads_merge_conflict_keep_other_best_answer(self): self.assertEqual( Thread.objects.get(pk=other_thread.pk).best_answer_id, other_best_answer.id) + @patch_other_category_acl({"can_merge_threads": True}) + @patch_category_acl({"can_merge_threads": True}) def test_merge_threads_kept_poll(self): """api merges two threads successfully, keeping poll from other thread""" - self.override_acl({'can_merge_threads': 1}) - self.override_other_acl({'can_merge_threads': 1}) - - other_thread = testutils.post_thread(self.category_b) + other_thread = testutils.post_thread(self.other_category) poll = testutils.post_poll(other_thread, self.user) response = self.client.post( @@ -733,12 +651,11 @@ def test_merge_threads_kept_poll(self): self.assertEqual(Poll.objects.filter(pk=poll.pk, thread=other_thread).count(), 1) self.assertEqual(PollVote.objects.filter(poll=poll, thread=other_thread).count(), 4) + @patch_other_category_acl({"can_merge_threads": True}) + @patch_category_acl({"can_merge_threads": True}) def test_merge_threads_moved_poll(self): """api merges two threads successfully, moving poll from old thread""" - self.override_acl({'can_merge_threads': 1}) - self.override_other_acl({'can_merge_threads': 1}) - - other_thread = testutils.post_thread(self.category_b) + other_thread = testutils.post_thread(self.other_category) poll = testutils.post_poll(self.thread, self.user) response = self.client.post( @@ -764,12 +681,11 @@ def test_merge_threads_moved_poll(self): self.assertEqual(Poll.objects.filter(pk=poll.pk, thread=other_thread).count(), 1) self.assertEqual(PollVote.objects.filter(poll=poll, thread=other_thread).count(), 4) + @patch_other_category_acl({"can_merge_threads": True}) + @patch_category_acl({"can_merge_threads": True}) def test_threads_merge_conflict_polls(self): """api errors on merge conflict, returning list of available polls""" - self.override_acl({'can_merge_threads': 1}) - self.override_other_acl({'can_merge_threads': 1}) - - other_thread = testutils.post_thread(self.category_b) + other_thread = testutils.post_thread(self.other_category) poll = testutils.post_poll(self.thread, self.user) other_poll = testutils.post_poll(other_thread, self.user) @@ -799,12 +715,11 @@ def test_threads_merge_conflict_polls(self): self.assertEqual(Poll.objects.count(), 2) self.assertEqual(PollVote.objects.count(), 8) + @patch_other_category_acl({"can_merge_threads": True}) + @patch_category_acl({"can_merge_threads": True}) def test_threads_merge_conflict_poll_invalid_resolution(self): """api errors on invalid merge conflict resolution""" - self.override_acl({'can_merge_threads': 1}) - self.override_other_acl({'can_merge_threads': 1}) - - other_thread = testutils.post_thread(self.category_b) + other_thread = testutils.post_thread(self.other_category) testutils.post_poll(self.thread, self.user) testutils.post_poll(other_thread, self.user) @@ -822,12 +737,11 @@ def test_threads_merge_conflict_poll_invalid_resolution(self): self.assertEqual(Poll.objects.count(), 2) self.assertEqual(PollVote.objects.count(), 8) + @patch_other_category_acl({"can_merge_threads": True}) + @patch_category_acl({"can_merge_threads": True}) def test_threads_merge_conflict_delete_all_polls(self): """api deletes all polls when delete all choice is selected""" - self.override_acl({'can_merge_threads': 1}) - self.override_other_acl({'can_merge_threads': 1}) - - other_thread = testutils.post_thread(self.category_b) + other_thread = testutils.post_thread(self.other_category) testutils.post_poll(self.thread, self.user) testutils.post_poll(other_thread, self.user) @@ -855,12 +769,11 @@ def test_threads_merge_conflict_delete_all_polls(self): self.assertEqual(Poll.objects.count(), 0) self.assertEqual(PollVote.objects.count(), 0) + @patch_other_category_acl({"can_merge_threads": True}) + @patch_category_acl({"can_merge_threads": True}) def test_threads_merge_conflict_keep_first_poll(self): """api deletes other poll on merge""" - self.override_acl({'can_merge_threads': 1}) - self.override_other_acl({'can_merge_threads': 1}) - - other_thread = testutils.post_thread(self.category_b) + other_thread = testutils.post_thread(self.other_category) poll = testutils.post_poll(self.thread, self.user) other_poll = testutils.post_poll(other_thread, self.user) @@ -895,12 +808,11 @@ def test_threads_merge_conflict_keep_first_poll(self): with self.assertRaises(Poll.DoesNotExist): Poll.objects.get(pk=other_poll.pk) + @patch_other_category_acl({"can_merge_threads": True}) + @patch_category_acl({"can_merge_threads": True}) def test_threads_merge_conflict_keep_other_poll(self): """api deletes first poll on merge""" - self.override_acl({'can_merge_threads': 1}) - self.override_other_acl({'can_merge_threads': 1}) - - other_thread = testutils.post_thread(self.category_b) + other_thread = testutils.post_thread(self.other_category) poll = testutils.post_poll(self.thread, self.user) other_poll = testutils.post_poll(other_thread, self.user) diff --git a/misago/threads/tests/test_threads_merge_api.py b/misago/threads/tests/test_threads_merge_api.py index b529916486..0052cd3bdf 100644 --- a/misago/threads/tests/test_threads_merge_api.py +++ b/misago/threads/tests/test_threads_merge_api.py @@ -2,17 +2,19 @@ from django.urls import reverse -from misago.acl import add_acl -from misago.acl.testutils import override_acl +from misago.acl import add_acl, useracl from misago.categories.models import Category from misago.readtracker import poststracker from misago.threads import testutils -from misago.threads.serializers.moderation import THREADS_LIMIT from misago.threads.models import Poll, PollVote, Post, Thread from misago.threads.serializers import ThreadsListSerializer +from misago.threads.serializers.moderation import THREADS_LIMIT +from misago.threads.test import patch_category_acl, patch_other_category_acl from .test_threads_api import ThreadsApiTestCase +cache_versions = {"acl": "abcdefgh"} + class ThreadsMergeApiTests(ThreadsApiTestCase): def setUp(self): @@ -20,40 +22,14 @@ def setUp(self): self.api_link = reverse('misago:api:thread-merge') Category( - name='Category B', - slug='category-b', + name='Other Category', + slug='other-category', ).insert_at( self.category, position='last-child', save=True, ) - self.category_b = Category.objects.get(slug='category-b') - - def override_other_category(self): - categories = self.user.acl_cache['categories'] - - visible_categories = self.user.acl_cache['visible_categories'] - browseable_categories = self.user.acl_cache['browseable_categories'] - - visible_categories.append(self.category_b.pk) - browseable_categories.append(self.category_b.pk) - - override_acl( - self.user, { - 'visible_categories': visible_categories, - 'browseable_categories': browseable_categories, - 'categories': { - self.category.pk: categories[self.category.pk], - self.category_b.pk: { - 'can_see': 1, - 'can_browse': 1, - 'can_see_all_threads': 1, - 'can_see_own_threads': 0, - 'can_start_threads': 2, - }, - }, - } - ) + self.other_category = Category.objects.get(slug='other-category') def test_merge_no_threads(self): """api validates if we are trying to merge no threads""" @@ -143,7 +119,7 @@ def test_merge_with_nonexisting_thread(self): def test_merge_with_invisible_thread(self): """api validates if we are trying to merge with inaccesible thread""" - unaccesible_thread = testutils.post_thread(category=self.category_b) + unaccesible_thread = testutils.post_thread(category=self.other_category) response = self.client.post( self.api_link, @@ -166,7 +142,7 @@ def test_merge_no_permission(self): response = self.client.post( self.api_link, json.dumps({ - 'category': self.category.pk, + 'category': self.category.id, 'title': 'Lorem ipsum dolor', 'threads': [self.thread.id, thread.id], }), @@ -188,14 +164,10 @@ def test_merge_no_permission(self): ] ) + @patch_other_category_acl() + @patch_category_acl({"can_merge_threads": True, "can_close_threads": False}) def test_thread_category_is_closed(self): """api validates if thread's category is open""" - self.override_acl({ - 'can_merge_threads': 1, - 'can_close_threads': 0, - }) - self.override_other_category() - other_thread = testutils.post_thread(self.category) self.category.is_closed = True @@ -204,7 +176,7 @@ def test_thread_category_is_closed(self): response = self.client.post( self.api_link, json.dumps({ - 'category': self.category_b.pk, + 'category': self.other_category.id, 'title': 'Lorem ipsum dolor', 'threads': [self.thread.id, other_thread.id], }), @@ -224,14 +196,10 @@ def test_thread_category_is_closed(self): }, ]) + @patch_other_category_acl() + @patch_category_acl({"can_merge_threads": True, "can_close_threads": False}) def test_thread_is_closed(self): """api validates if thread is open""" - self.override_acl({ - 'can_merge_threads': 1, - 'can_close_threads': 0, - }) - self.override_other_category() - other_thread = testutils.post_thread(self.category) other_thread.is_closed = True @@ -240,7 +208,7 @@ def test_thread_is_closed(self): response = self.client.post( self.api_link, json.dumps({ - 'category': self.category_b.pk, + 'category': self.other_category.id, 'title': 'Lorem ipsum dolor', 'threads': [self.thread.id, other_thread.id], }), @@ -255,19 +223,13 @@ def test_thread_is_closed(self): }, ]) + @patch_category_acl({"can_merge_threads": True}) def test_merge_too_many_threads(self): """api rejects too many threads to merge""" threads = [] for _ in range(THREADS_LIMIT + 1): threads.append(testutils.post_thread(category=self.category).pk) - self.override_acl({ - 'can_merge_threads': True, - 'can_close_threads': False, - 'can_edit_threads': False, - 'can_reply_threads': False, - }) - response = self.client.post( self.api_link, json.dumps({ @@ -282,15 +244,9 @@ def test_merge_too_many_threads(self): } ) + @patch_category_acl({"can_merge_threads": True}) def test_merge_no_final_thread(self): """api rejects merge because no data to merge threads was specified""" - self.override_acl({ - 'can_merge_threads': True, - 'can_close_threads': False, - 'can_edit_threads': False, - 'can_reply_threads': False, - }) - thread = testutils.post_thread(category=self.category) response = self.client.post( @@ -308,15 +264,9 @@ def test_merge_no_final_thread(self): } ) + @patch_category_acl({"can_merge_threads": True}) def test_merge_invalid_final_title(self): """api rejects merge because final thread title was invalid""" - self.override_acl({ - 'can_merge_threads': True, - 'can_close_threads': False, - 'can_edit_threads': False, - 'can_reply_threads': False, - }) - thread = testutils.post_thread(category=self.category) response = self.client.post( @@ -335,15 +285,9 @@ def test_merge_invalid_final_title(self): } ) + @patch_category_acl({"can_merge_threads": True}) def test_merge_invalid_category(self): """api rejects merge because final category was invalid""" - self.override_acl({ - 'can_merge_threads': True, - 'can_close_threads': False, - 'can_edit_threads': False, - 'can_reply_threads': False, - }) - thread = testutils.post_thread(category=self.category) response = self.client.post( @@ -351,7 +295,7 @@ def test_merge_invalid_category(self): json.dumps({ 'threads': [self.thread.id, thread.id], 'title': 'Valid thread title', - 'category': self.category_b.id, + 'category': self.other_category.id, }), content_type="application/json", ) @@ -362,16 +306,9 @@ def test_merge_invalid_category(self): } ) + @patch_category_acl({"can_merge_threads": True, "can_start_threads": False}) def test_merge_unallowed_start_thread(self): """api rejects merge because category isn't allowing starting threads""" - self.override_acl({ - 'can_merge_threads': True, - 'can_close_threads': False, - 'can_edit_threads': False, - 'can_reply_threads': False, - 'can_start_threads': 0, - }) - thread = testutils.post_thread(category=self.category) response = self.client.post( @@ -390,15 +327,9 @@ def test_merge_unallowed_start_thread(self): } ) + @patch_category_acl({"can_merge_threads": True}) def test_merge_invalid_weight(self): """api rejects merge because final weight was invalid""" - self.override_acl({ - 'can_merge_threads': True, - 'can_close_threads': False, - 'can_edit_threads': False, - 'can_reply_threads': False, - }) - thread = testutils.post_thread(category=self.category) response = self.client.post( @@ -418,15 +349,9 @@ def test_merge_invalid_weight(self): } ) + @patch_category_acl({"can_merge_threads": True}) def test_merge_unallowed_global_weight(self): """api rejects merge because global weight was unallowed""" - self.override_acl({ - 'can_merge_threads': True, - 'can_close_threads': False, - 'can_edit_threads': False, - 'can_reply_threads': False, - }) - thread = testutils.post_thread(category=self.category) response = self.client.post( @@ -446,15 +371,9 @@ def test_merge_unallowed_global_weight(self): } ) + @patch_category_acl({"can_merge_threads": True}) def test_merge_unallowed_local_weight(self): """api rejects merge because local weight was unallowed""" - self.override_acl({ - 'can_merge_threads': True, - 'can_close_threads': False, - 'can_edit_threads': False, - 'can_reply_threads': False, - }) - thread = testutils.post_thread(category=self.category) response = self.client.post( @@ -474,16 +393,9 @@ def test_merge_unallowed_local_weight(self): } ) + @patch_category_acl({"can_merge_threads": True, "can_pin_threads": 1}) def test_merge_allowed_local_weight(self): """api allows local weight""" - self.override_acl({ - 'can_merge_threads': True, - 'can_close_threads': False, - 'can_edit_threads': False, - 'can_reply_threads': False, - 'can_pin_threads': 1, - }) - thread = testutils.post_thread(category=self.category) response = self.client.post( @@ -503,16 +415,9 @@ def test_merge_allowed_local_weight(self): } ) + @patch_category_acl({"can_merge_threads": True, "can_pin_threads": 2}) def test_merge_allowed_global_weight(self): """api allows global weight""" - self.override_acl({ - 'can_merge_threads': True, - 'can_close_threads': False, - 'can_edit_threads': False, - 'can_reply_threads': False, - 'can_pin_threads': 2, - }) - thread = testutils.post_thread(category=self.category) response = self.client.post( @@ -532,15 +437,9 @@ def test_merge_allowed_global_weight(self): } ) + @patch_category_acl({"can_merge_threads": True, "can_close_threads": False}) def test_merge_unallowed_close(self): """api rejects merge because closing thread was unallowed""" - self.override_acl({ - 'can_merge_threads': True, - 'can_close_threads': False, - 'can_edit_threads': False, - 'can_reply_threads': False, - }) - thread = testutils.post_thread(category=self.category) response = self.client.post( @@ -560,15 +459,9 @@ def test_merge_unallowed_close(self): } ) + @patch_category_acl({"can_merge_threads": True, "can_close_threads": True}) def test_merge_with_close(self): """api allows for closing thread""" - self.override_acl({ - 'can_merge_threads': True, - 'can_edit_threads': False, - 'can_reply_threads': False, - 'can_close_threads': True, - }) - thread = testutils.post_thread(category=self.category) response = self.client.post( @@ -589,16 +482,9 @@ def test_merge_with_close(self): } ) + @patch_category_acl({"can_merge_threads": True, "can_hide_threads": 0}) def test_merge_unallowed_hidden(self): """api rejects merge because hidden thread was unallowed""" - self.override_acl({ - 'can_merge_threads': True, - 'can_close_threads': False, - 'can_edit_threads': False, - 'can_reply_threads': False, - 'can_hide_threads': 0, - }) - thread = testutils.post_thread(category=self.category) response = self.client.post( @@ -618,16 +504,9 @@ def test_merge_unallowed_hidden(self): } ) + @patch_category_acl({"can_merge_threads": True, "can_hide_threads": 1}) def test_merge_with_hide(self): """api allows for hiding thread""" - self.override_acl({ - 'can_merge_threads': True, - 'can_close_threads': False, - 'can_edit_threads': False, - 'can_reply_threads': False, - 'can_hide_threads': 1, - }) - thread = testutils.post_thread(category=self.category) response = self.client.post( @@ -648,17 +527,10 @@ def test_merge_with_hide(self): } ) + @patch_category_acl({"can_merge_threads": True}) def test_merge(self): """api performs basic merge""" posts_ids = [p.id for p in Post.objects.all()] - - self.override_acl({ - 'can_merge_threads': True, - 'can_close_threads': False, - 'can_edit_threads': False, - 'can_reply_threads': False, - }) - thread = testutils.post_thread(category=self.category) response = self.client.post( @@ -679,8 +551,9 @@ def test_merge(self): new_thread.is_read = False new_thread.subscription = None - add_acl(self.user, new_thread.category) - add_acl(self.user, new_thread) + user_acl = useracl.get_user_acl(self.user, cache_versions) + add_acl(user_acl, new_thread.category) + add_acl(user_acl, new_thread) self.assertEqual(response_json, ThreadsListSerializer(new_thread).data) @@ -691,17 +564,15 @@ def test_merge(self): # are old threads gone? self.assertEqual([t.pk for t in Thread.objects.all()], [new_thread.pk]) + @patch_category_acl({ + "can_merge_threads": True, + "can_close_threads": True, + "can_hide_threads": 1, + "can_pin_threads": 2, + }) def test_merge_kitchensink(self): """api performs merge""" posts_ids = [p.id for p in Post.objects.all()] - - self.override_acl({ - 'can_merge_threads': True, - 'can_close_threads': True, - 'can_hide_threads': 1, - 'can_pin_threads': 2, - }) - thread = testutils.post_thread(category=self.category) poststracker.save_read(self.user, self.thread.first_post) @@ -745,8 +616,9 @@ def test_merge_kitchensink(self): self.assertTrue(new_thread.is_closed) self.assertTrue(new_thread.is_hidden) - add_acl(self.user, new_thread.category) - add_acl(self.user, new_thread) + user_acl = useracl.get_user_acl(self.user, cache_versions) + add_acl(user_acl, new_thread.category) + add_acl(user_acl, new_thread) self.assertEqual(response_json, ThreadsListSerializer(new_thread).data) @@ -772,10 +644,9 @@ def test_merge_kitchensink(self): self.user.subscription_set.get(thread=new_thread) self.user.subscription_set.get(category=self.category) + @patch_category_acl({"can_merge_threads": True}) def test_merge_threads_merged_best_answer(self): """api merges two threads successfully, moving best answer to old thread""" - self.override_acl({'can_merge_threads': 1}) - other_thread = testutils.post_thread(self.category) best_answer = testutils.reply_thread(self.thread) @@ -797,10 +668,9 @@ def test_merge_threads_merged_best_answer(self): new_thread = Thread.objects.get(pk=response.json()['id']) self.assertEqual(new_thread.best_answer_id, best_answer.id) + @patch_category_acl({"can_merge_threads": True}) def test_merge_threads_merge_conflict_best_answer(self): """api errors on merge conflict, returning list of available best answers""" - self.override_acl({'can_merge_threads': 1}) - best_answer = testutils.reply_thread(self.thread) self.thread.set_best_answer(self.user, best_answer) self.thread.save() @@ -835,10 +705,9 @@ def test_merge_threads_merge_conflict_best_answer(self): self.assertEqual( Thread.objects.get(pk=other_thread.pk).best_answer_id, other_best_answer.id) + @patch_category_acl({"can_merge_threads": True}) def test_threads_merge_conflict_best_answer_invalid_resolution(self): """api errors on invalid merge conflict resolution""" - self.override_acl({'can_merge_threads': 1}) - best_answer = testutils.reply_thread(self.thread) self.thread.set_best_answer(self.user, best_answer) self.thread.save() @@ -868,10 +737,9 @@ def test_threads_merge_conflict_best_answer_invalid_resolution(self): self.assertEqual( Thread.objects.get(pk=other_thread.pk).best_answer_id, other_best_answer.id) + @patch_category_acl({"can_merge_threads": True}) def test_threads_merge_conflict_unmark_all_best_answers(self): """api unmarks all best answers when unmark all choice is selected""" - self.override_acl({'can_merge_threads': 1}) - best_answer = testutils.reply_thread(self.thread) self.thread.set_best_answer(self.user, best_answer) self.thread.save() @@ -898,10 +766,9 @@ def test_threads_merge_conflict_unmark_all_best_answers(self): self.assertFalse(new_thread.has_best_answer) self.assertIsNone(new_thread.best_answer_id) + @patch_category_acl({"can_merge_threads": True}) def test_threads_merge_conflict_keep_first_best_answer(self): """api unmarks other best answer on merge""" - self.override_acl({'can_merge_threads': 1}) - best_answer = testutils.reply_thread(self.thread) self.thread.set_best_answer(self.user, best_answer) self.thread.save() @@ -927,10 +794,9 @@ def test_threads_merge_conflict_keep_first_best_answer(self): new_thread = Thread.objects.get(pk=response.json()['id']) self.assertEqual(new_thread.best_answer_id, best_answer.id) + @patch_category_acl({"can_merge_threads": True}) def test_threads_merge_conflict_keep_other_best_answer(self): """api unmarks first best answer on merge""" - self.override_acl({'can_merge_threads': 1}) - best_answer = testutils.reply_thread(self.thread) self.thread.set_best_answer(self.user, best_answer) self.thread.save() @@ -956,10 +822,9 @@ def test_threads_merge_conflict_keep_other_best_answer(self): new_thread = Thread.objects.get(pk=response.json()['id']) self.assertEqual(new_thread.best_answer_id, other_best_answer.id) + @patch_category_acl({"can_merge_threads": True}) def test_merge_threads_kept_poll(self): """api merges two threads successfully, keeping poll from other thread""" - self.override_acl({'can_merge_threads': True}) - other_thread = testutils.post_thread(self.category) poll = testutils.post_poll(other_thread, self.user) @@ -983,10 +848,9 @@ def test_merge_threads_kept_poll(self): self.assertEqual(Poll.objects.count(), 1) self.assertEqual(PollVote.objects.count(), 4) + @patch_category_acl({"can_merge_threads": True}) def test_merge_threads_moved_poll(self): """api merges two threads successfully, moving poll from old thread""" - self.override_acl({'can_merge_threads': True}) - other_thread = testutils.post_thread(self.category) poll = testutils.post_poll(self.thread, self.user) @@ -1010,10 +874,9 @@ def test_merge_threads_moved_poll(self): self.assertEqual(Poll.objects.count(), 1) self.assertEqual(PollVote.objects.count(), 4) + @patch_category_acl({"can_merge_threads": True}) def test_threads_merge_conflict_poll(self): """api errors on merge conflict, returning list of available polls""" - self.override_acl({'can_merge_threads': True}) - other_thread = testutils.post_thread(self.category) poll = testutils.post_poll(self.thread, self.user) other_poll = testutils.post_poll(other_thread, self.user) @@ -1049,10 +912,9 @@ def test_threads_merge_conflict_poll(self): self.assertEqual(Poll.objects.count(), 2) self.assertEqual(PollVote.objects.count(), 8) + @patch_category_acl({"can_merge_threads": True}) def test_threads_merge_conflict_poll_invalid_resolution(self): """api errors on invalid merge conflict resolution""" - self.override_acl({'can_merge_threads': True}) - other_thread = testutils.post_thread(self.category) testutils.post_poll(self.thread, self.user) @@ -1078,10 +940,9 @@ def test_threads_merge_conflict_poll_invalid_resolution(self): self.assertEqual(Poll.objects.count(), 2) self.assertEqual(PollVote.objects.count(), 8) + @patch_category_acl({"can_merge_threads": True}) def test_threads_merge_conflict_delete_all_polls(self): """api deletes all polls when delete all choice is selected""" - self.override_acl({'can_merge_threads': True}) - other_thread = testutils.post_thread(self.category) testutils.post_poll(self.thread, self.user) @@ -1103,10 +964,9 @@ def test_threads_merge_conflict_delete_all_polls(self): self.assertEqual(Poll.objects.count(), 0) self.assertEqual(PollVote.objects.count(), 0) + @patch_category_acl({"can_merge_threads": True}) def test_threads_merge_conflict_keep_first_poll(self): """api deletes other poll on merge""" - self.override_acl({'can_merge_threads': True}) - other_thread = testutils.post_thread(self.category) poll = testutils.post_poll(self.thread, self.user) other_poll = testutils.post_poll(other_thread, self.user) @@ -1131,10 +991,9 @@ def test_threads_merge_conflict_keep_first_poll(self): with self.assertRaises(Poll.DoesNotExist): Poll.objects.get(pk=other_poll.pk) + @patch_category_acl({"can_merge_threads": True}) def test_threads_merge_conflict_keep_other_poll(self): """api deletes first poll on merge""" - self.override_acl({'can_merge_threads': True}) - other_thread = testutils.post_thread(self.category) poll = testutils.post_poll(self.thread, self.user) other_poll = testutils.post_poll(other_thread, self.user) diff --git a/misago/threads/validators.py b/misago/threads/validators.py index b523cfd95d..b4a76f83e0 100644 --- a/misago/threads/validators.py +++ b/misago/threads/validators.py @@ -12,7 +12,7 @@ from .threadtypes import trees_map -def validate_category(user, category_id, allow_root=False): +def validate_category(user_acl, category_id, allow_root=False): try: threads_tree_id = trees_map.get_tree_id_for_root(THREADS_ROOT_NAME) category = Category.objects.get( @@ -26,10 +26,10 @@ def validate_category(user, category_id, allow_root=False): if allow_root and category and not category.level: return category - if not category or not can_see_category(user, category): + if not category or not can_see_category(user_acl, category): raise ValidationError(_("Requested category could not be found.")) - if not can_browse_category(user, category): + if not can_browse_category(user_acl, category): raise ValidationError(_("You don't have permission to access this category.")) return category From 5b48ed6182b8f8a393bab26f8aa55a6e253d3ac3 Mon Sep 17 00:00:00 2001 From: rafalp Date: Sat, 15 Dec 2018 18:34:51 +0100 Subject: [PATCH 37/86] Make tests for thread start/reply/edit reply pass --- .../tests/test_thread_editreply_api.py | 165 ++++++++---------- misago/threads/tests/test_thread_reply_api.py | 101 +++++------ misago/threads/tests/test_thread_start_api.py | 113 ++++-------- 3 files changed, 144 insertions(+), 235 deletions(-) diff --git a/misago/threads/tests/test_thread_editreply_api.py b/misago/threads/tests/test_thread_editreply_api.py index 9a63eb14d6..22a82be912 100644 --- a/misago/threads/tests/test_thread_editreply_api.py +++ b/misago/threads/tests/test_thread_editreply_api.py @@ -4,10 +4,11 @@ from django.urls import reverse from django.utils import timezone -from misago.acl.testutils import override_acl +from misago.acl.test import patch_user_acl from misago.categories.models import Category from misago.threads import testutils from misago.threads.models import Post, Thread +from misago.threads.test import patch_category_acl from misago.users.testutils import AuthenticatedUserTestCase @@ -27,21 +28,6 @@ def setUp(self): } ) - def override_acl(self, extra_acl=None): - new_acl = self.user.acl_cache - new_acl['categories'][self.category.pk].update({ - 'can_see': 1, - 'can_browse': 1, - 'can_start_threads': 0, - 'can_reply_threads': 0, - 'can_edit_posts': 1, - }) - - if extra_acl: - new_acl['categories'][self.category.pk].update(extra_acl) - - override_acl(self.user, new_acl) - def put(self, url, data=None): content = encode_multipart(BOUNDARY, data or {}) return self.client.put(url, content, content_type=MULTIPART_CONTENT) @@ -55,32 +41,30 @@ def test_cant_edit_reply_as_guest(self): def test_thread_visibility(self): """thread's visibility is validated""" - self.override_acl({'can_see': 0}) - response = self.put(self.api_link) - self.assertEqual(response.status_code, 404) + with patch_category_acl({'can_see': False}): + response = self.put(self.api_link) + self.assertEqual(response.status_code, 404) - self.override_acl({'can_browse': 0}) - response = self.put(self.api_link) - self.assertEqual(response.status_code, 404) + with patch_category_acl({'can_browse': False}): + response = self.put(self.api_link) + self.assertEqual(response.status_code, 404) - self.override_acl({'can_see_all_threads': 0}) - response = self.put(self.api_link) - self.assertEqual(response.status_code, 404) + with patch_category_acl({'can_see_all_threads': False}): + response = self.put(self.api_link) + self.assertEqual(response.status_code, 404) + @patch_category_acl({"can_edit_posts": 0}) def test_cant_edit_reply(self): """permission to edit reply is validated""" - self.override_acl({'can_edit_posts': 0}) - response = self.put(self.api_link) self.assertEqual(response.status_code, 403) self.assertEqual(response.json(), { "detail": "You can't edit posts in this category.", }) + @patch_category_acl({"can_edit_posts": 1}) def test_cant_edit_other_user_reply(self): """permission to edit reply by other users is validated""" - self.override_acl({'can_edit_posts': 1}) - self.post.poster = None self.post.save() @@ -90,13 +74,9 @@ def test_cant_edit_other_user_reply(self): "detail": "You can't edit other users posts in this category.", }) + @patch_category_acl({"can_edit_posts": 1, "post_edit_time": 1}) def test_edit_too_old(self): """permission to edit reply within timelimit is validated""" - self.override_acl({ - 'can_edit_posts': 1, - 'post_edit_time': 1, - }) - self.post.posted_on = timezone.now() - timedelta(minutes=5) self.post.save() @@ -106,10 +86,9 @@ def test_edit_too_old(self): "detail": "You can't edit posts that are older than 1 minute.", }) - def test_closed_category(self): + @patch_category_acl({"can_edit_posts": 1, "can_close_threads": False}) + def test_closed_category_no_permission(self): """permssion to edit reply in closed category is validated""" - self.override_acl({'can_close_threads': 0}) - self.category.is_closed = True self.category.save() @@ -119,16 +98,18 @@ def test_closed_category(self): "detail": "This category is closed. You can't edit posts in it.", }) - # allow to post in closed category - self.override_acl({'can_close_threads': 1}) + @patch_category_acl({"can_edit_posts": 1, "can_close_threads": True}) + def test_closed_category(self): + """permssion to edit reply in closed category is validated""" + self.category.is_closed = True + self.category.save() response = self.put(self.api_link) self.assertEqual(response.status_code, 400) - def test_closed_thread(self): + @patch_category_acl({"can_edit_posts": 1, "can_close_threads": False}) + def test_closed_thread_no_permission(self): """permssion to edit reply in closed thread is validated""" - self.override_acl({'can_close_threads': 0}) - self.thread.is_closed = True self.thread.save() @@ -138,16 +119,18 @@ def test_closed_thread(self): "detail": "This thread is closed. You can't edit posts in it.", }) - # allow to post in closed thread - self.override_acl({'can_close_threads': 1}) + @patch_category_acl({"can_edit_posts": 1, "can_close_threads": True}) + def test_closed_thread(self): + """permssion to edit reply in closed thread is validated""" + self.thread.is_closed = True + self.thread.save() response = self.put(self.api_link) self.assertEqual(response.status_code, 400) - def test_protected_post(self): + @patch_category_acl({"can_edit_posts": 1, "can_protect_posts": False}) + def test_protected_post_no_permission(self): """permssion to edit protected post is validated""" - self.override_acl({'can_protect_posts': 0}) - self.post.is_protected = True self.post.save() @@ -157,26 +140,27 @@ def test_protected_post(self): "detail": "This post is protected. You can't edit it.", }) - # allow to post in closed thread - self.override_acl({'can_protect_posts': 1}) + @patch_category_acl({"can_edit_posts": 1, "can_protect_posts": True}) + def test_protected_post_no(self): + """permssion to edit protected post is validated""" + self.post.is_protected = True + self.post.save() response = self.put(self.api_link) self.assertEqual(response.status_code, 400) + @patch_category_acl({"can_edit_posts": 1}) def test_empty_data(self): """no data sent handling has no showstoppers""" - self.override_acl() - response = self.put(self.api_link, data={}) self.assertEqual(response.status_code, 400) self.assertEqual(response.json(), { "post": ["You have to enter a message."], }) + @patch_category_acl({"can_edit_posts": 1}) def test_invalid_data(self): """api errors for invalid request data""" - self.override_acl() - response = self.client.put( self.api_link, 'false', @@ -187,10 +171,9 @@ def test_invalid_data(self): "non_field_errors": ["Invalid data. Expected a dictionary, but got bool."] }) + @patch_category_acl({"can_edit_posts": 1}) def test_edit_event(self): """events can't be edited""" - self.override_acl() - self.post.is_event = True self.post.save() @@ -200,10 +183,9 @@ def test_edit_event(self): "detail": "Events can't be edited.", }) + @patch_category_acl({"can_edit_posts": 1}) def test_post_is_validated(self): """post is validated""" - self.override_acl() - response = self.put( self.api_link, data={ 'post': "a", @@ -216,15 +198,14 @@ def test_post_is_validated(self): } ) + @patch_category_acl({"can_edit_posts": 1}) def test_edit_reply_no_change(self): """endpoint isn't bumping edits count if no change was made to post's body""" - self.override_acl() self.assertEqual(self.post.edits_record.count(), 0) response = self.put(self.api_link, data={'post': self.post.original}) self.assertEqual(response.status_code, 200) - self.override_acl() response = self.client.get(self.thread.get_absolute_url()) self.assertContains(response, self.post.parsed) @@ -237,15 +218,14 @@ def test_edit_reply_no_change(self): self.assertEqual(self.post.edits_record.count(), 0) + @patch_category_acl({"can_edit_posts": 1}) def test_edit_reply(self): """endpoint updates reply""" - self.override_acl() self.assertEqual(self.post.edits_record.count(), 0) response = self.put(self.api_link, data={'post': "This is test edit!"}) self.assertEqual(response.status_code, 200) - self.override_acl() response = self.client.get(self.thread.get_absolute_url()) self.assertContains(response, "

This is test edit!

") @@ -268,10 +248,9 @@ def test_edit_reply(self): self.assertEqual(post_edit.editor_name, self.user.username) self.assertEqual(post_edit.editor_slug, self.user.slug) + @patch_category_acl({"can_edit_posts": 2, "can_hide_threads": 1}) def test_edit_first_post_hidden(self): """endpoint updates hidden thread's first post""" - self.override_acl({'can_hide_threads': 1, 'can_edit_posts': 2}) - self.thread.is_hidden = True self.thread.save() self.thread.first_post.is_hidden = True @@ -288,10 +267,9 @@ def test_edit_first_post_hidden(self): response = self.put(api_link, data={'post': "This is test edit!"}) self.assertEqual(response.status_code, 200) + @patch_category_acl({"can_edit_posts": 1, "can_protect_posts": True}) def test_protect_post(self): """can protect post""" - self.override_acl({'can_protect_posts': 1}) - response = self.put( self.api_link, data={ 'post': "Lorem ipsum dolor met!", @@ -303,10 +281,9 @@ def test_protect_post(self): post = self.user.post_set.order_by('id').last() self.assertTrue(post.is_protected) + @patch_category_acl({"can_edit_posts": 1, "can_protect_posts": False}) def test_protect_post_no_permission(self): """cant protect post without permission""" - self.override_acl({'can_protect_posts': 0}) - response = self.put( self.api_link, data={ 'post': "Lorem ipsum dolor met!", @@ -318,10 +295,9 @@ def test_protect_post_no_permission(self): post = self.user.post_set.order_by('id').last() self.assertFalse(post.is_protected) + @patch_category_acl({"can_edit_posts": 1}) def test_post_unicode(self): """unicode characters can be posted""" - self.override_acl() - response = self.put( self.api_link, data={ 'post': "Chrzążczyżewoszyce, powiat Łękółody.", @@ -329,6 +305,7 @@ def test_post_unicode(self): ) self.assertEqual(response.status_code, 200) + @patch_category_acl({"can_edit_posts": 1}) def test_reply_category_moderation_queue(self): """edit sends reply to queue due to category setup""" self.category.require_edits_approval = True @@ -344,10 +321,10 @@ def test_reply_category_moderation_queue(self): post = self.user.post_set.all()[:1][0] self.assertTrue(post.is_unapproved) + @patch_category_acl({"can_edit_posts": 1}) + @patch_user_acl({"can_approve_content": True}) def test_reply_category_moderation_queue_bypass(self): """bypass moderation queue due to user's acl""" - override_acl(self.user, {'can_approve_content': 1}) - self.category.require_edits_approval = True self.category.save() @@ -361,10 +338,9 @@ def test_reply_category_moderation_queue_bypass(self): post = self.user.post_set.all()[:1][0] self.assertFalse(post.is_unapproved) + @patch_category_acl({"can_edit_posts": 1, "require_edits_approval": True}) def test_reply_user_moderation_queue(self): """edit sends reply to queue due to user acl""" - self.override_acl({'require_edits_approval': 1}) - response = self.put( self.api_link, data={ 'post': "Lorem ipsum dolor met!", @@ -375,12 +351,13 @@ def test_reply_user_moderation_queue(self): post = self.user.post_set.all()[:1][0] self.assertTrue(post.is_unapproved) + @patch_category_acl({ + "can_edit_posts": 1, + "require_edits_approval": True, + }) + @patch_user_acl({"can_approve_content": True}) def test_reply_user_moderation_queue_bypass(self): """bypass moderation queue due to user's acl""" - override_acl(self.user, {'can_approve_content': 1}) - - self.override_acl({'require_edits_approval': 1}) - response = self.put( self.api_link, data={ 'post': "Lorem ipsum dolor met!", @@ -391,17 +368,17 @@ def test_reply_user_moderation_queue_bypass(self): post = self.user.post_set.all()[:1][0] self.assertFalse(post.is_unapproved) + @patch_category_acl({ + "can_edit_posts": 1, + "require_threads_approval": True, + "require_replies_approval": True, + }) def test_reply_omit_other_moderation_queues(self): """other queues are omitted""" self.category.require_threads_approval = True self.category.require_replies_approval = True self.category.save() - self.override_acl({ - 'require_threads_approval': 1, - 'require_replies_approval': 1, - }) - response = self.put( self.api_link, data={ 'post': "Lorem ipsum dolor met!", @@ -426,6 +403,7 @@ def setUpFirstReplyTest(self): } ) + @patch_category_acl({"can_edit_posts": 1}) def test_first_reply_category_moderation_queue(self): """edit sends thread to queue due to category setup""" self.setUpFirstReplyTest() @@ -447,12 +425,12 @@ def test_first_reply_category_moderation_queue(self): post = Post.objects.get(pk=self.post.pk) self.assertTrue(post.is_unapproved) + @patch_category_acl({"can_edit_posts": 1}) + @patch_user_acl({'can_approve_content': True}) def test_first_reply_category_moderation_queue_bypass(self): """bypass moderation queue due to user's acl""" self.setUpFirstReplyTest() - override_acl(self.user, {'can_approve_content': 1}) - self.category.require_edits_approval = True self.category.save() @@ -470,12 +448,11 @@ def test_first_reply_category_moderation_queue_bypass(self): post = Post.objects.get(pk=self.post.pk) self.assertFalse(post.is_unapproved) + @patch_category_acl({"can_edit_posts": 1, "require_edits_approval": True}) def test_first_reply_user_moderation_queue(self): """edit sends thread to queue due to user acl""" self.setUpFirstReplyTest() - self.override_acl({'require_edits_approval': 1}) - response = self.put( self.api_link, data={ 'post': "Lorem ipsum dolor met!", @@ -490,14 +467,12 @@ def test_first_reply_user_moderation_queue(self): post = Post.objects.get(pk=self.post.pk) self.assertTrue(post.is_unapproved) + @patch_category_acl({"can_edit_posts": 1, "require_edits_approval": True}) + @patch_user_acl({'can_approve_content': True}) def test_first_reply_user_moderation_queue_bypass(self): """bypass moderation queue due to user's acl""" self.setUpFirstReplyTest() - override_acl(self.user, {'can_approve_content': 1}) - - self.override_acl({'require_edits_approval': 1}) - response = self.put( self.api_link, data={ 'post': "Lorem ipsum dolor met!", @@ -512,6 +487,11 @@ def test_first_reply_user_moderation_queue_bypass(self): post = Post.objects.get(pk=self.post.pk) self.assertFalse(post.is_unapproved) + @patch_category_acl({ + "can_edit_posts": 1, + "require_threads_approval": True, + "require_replies_approval": True, + }) def test_first_reply_omit_other_moderation_queues(self): """other queues are omitted""" self.setUpFirstReplyTest() @@ -520,11 +500,6 @@ def test_first_reply_omit_other_moderation_queues(self): self.category.require_replies_approval = True self.category.save() - self.override_acl({ - 'require_threads_approval': 1, - 'require_replies_approval': 1, - }) - response = self.put( self.api_link, data={ 'post': "Lorem ipsum dolor met!", diff --git a/misago/threads/tests/test_thread_reply_api.py b/misago/threads/tests/test_thread_reply_api.py index 8b18bff95f..c8911668ec 100644 --- a/misago/threads/tests/test_thread_reply_api.py +++ b/misago/threads/tests/test_thread_reply_api.py @@ -1,9 +1,10 @@ from django.urls import reverse -from misago.acl.testutils import override_acl +from misago.acl.test import patch_user_acl from misago.categories.models import Category from misago.threads import testutils from misago.threads.models import Thread +from misago.threads.test import patch_category_acl from misago.users.testutils import AuthenticatedUserTestCase @@ -20,20 +21,6 @@ def setUp(self): } ) - def override_acl(self, extra_acl=None): - new_acl = self.user.acl_cache - new_acl['categories'][self.category.pk].update({ - 'can_see': 1, - 'can_browse': 1, - 'can_start_threads': 0, - 'can_reply_threads': 1, - }) - - if extra_acl: - new_acl['categories'][self.category.pk].update(extra_acl) - - override_acl(self.user, new_acl) - def test_cant_reply_thread_as_guest(self): """user has to be authenticated to be able to post reply""" self.logout_user() @@ -43,32 +30,30 @@ def test_cant_reply_thread_as_guest(self): def test_thread_visibility(self): """thread's visibility is validated""" - self.override_acl({'can_see': 0}) - response = self.client.post(self.api_link) - self.assertEqual(response.status_code, 404) + with patch_category_acl({'can_see': 0}): + response = self.client.post(self.api_link) + self.assertEqual(response.status_code, 404) - self.override_acl({'can_browse': 0}) - response = self.client.post(self.api_link) - self.assertEqual(response.status_code, 404) + with patch_category_acl({'can_browse': 0}): + response = self.client.post(self.api_link) + self.assertEqual(response.status_code, 404) - self.override_acl({'can_see_all_threads': 0}) - response = self.client.post(self.api_link) - self.assertEqual(response.status_code, 404) + with patch_category_acl({'can_see_all_threads': 0}): + response = self.client.post(self.api_link) + self.assertEqual(response.status_code, 404) + @patch_category_acl({"can_reply_threads": False}) def test_cant_reply_thread(self): """permission to reply thread is validated""" - self.override_acl({'can_reply_threads': 0}) - response = self.client.post(self.api_link) self.assertEqual(response.status_code, 403) self.assertEqual(response.json(), { "detail": "You can't reply to threads in this category.", }) - def test_closed_category(self): + @patch_category_acl({"can_reply_threads": True, "can_close_threads": False}) + def test_closed_category_no_permission(self): """permssion to reply in closed category is validated""" - self.override_acl({'can_close_threads': 0}) - self.category.is_closed = True self.category.save() @@ -78,16 +63,18 @@ def test_closed_category(self): "detail": "This category is closed. You can't reply to threads in it.", }) - # allow to post in closed category - self.override_acl({'can_close_threads': 1}) + @patch_category_acl({"can_reply_threads": True, "can_close_threads": True}) + def test_closed_category(self): + """permssion to reply in closed category is validated""" + self.category.is_closed = True + self.category.save() response = self.client.post(self.api_link) self.assertEqual(response.status_code, 400) - def test_closed_thread(self): + @patch_category_acl({"can_reply_threads": True, "can_close_threads": False}) + def test_closed_thread_no_permission(self): """permssion to reply in closed thread is validated""" - self.override_acl({'can_close_threads': 0}) - self.thread.is_closed = True self.thread.save() @@ -97,26 +84,27 @@ def test_closed_thread(self): "detail": "You can't reply to closed threads in this category.", }) - # allow to post in closed thread - self.override_acl({'can_close_threads': 1}) + @patch_category_acl({"can_reply_threads": True, "can_close_threads": True}) + def test_closed_thread(self): + """permssion to reply in closed thread is validated""" + self.thread.is_closed = True + self.thread.save() response = self.client.post(self.api_link) self.assertEqual(response.status_code, 400) + @patch_category_acl({"can_reply_threads": True}) def test_empty_data(self): """no data sent handling has no showstoppers""" - self.override_acl() - response = self.client.post(self.api_link, data={}) self.assertEqual(response.status_code, 400) self.assertEqual(response.json(), { "post": ["You have to enter a message."], }) + @patch_category_acl({"can_reply_threads": True}) def test_invalid_data(self): """api errors for invalid request data""" - self.override_acl() - response = self.client.post( self.api_link, 'false', @@ -127,10 +115,9 @@ def test_invalid_data(self): 'non_field_errors': ['Invalid data. Expected a dictionary, but got bool.'] }) + @patch_category_acl({"can_reply_threads": True}) def test_post_is_validated(self): """post is validated""" - self.override_acl() - response = self.client.post( self.api_link, data={ 'post': "a", @@ -144,9 +131,9 @@ def test_post_is_validated(self): } ) + @patch_category_acl({"can_reply_threads": True}) def test_can_reply_thread(self): """endpoint creates new reply""" - self.override_acl() response = self.client.post( self.api_link, data={ 'post': "This is test response!", @@ -156,7 +143,6 @@ def test_can_reply_thread(self): thread = Thread.objects.get(pk=self.thread.pk) - self.override_acl() response = self.client.get(self.thread.get_absolute_url()) self.assertContains(response, "

This is test response!

") @@ -187,10 +173,9 @@ def test_can_reply_thread(self): self.assertEqual(category.last_poster_name, self.user.username) self.assertEqual(category.last_poster_slug, self.user.slug) + @patch_category_acl({"can_reply_threads": True}) def test_post_unicode(self): """unicode characters can be posted""" - self.override_acl() - response = self.client.post( self.api_link, data={ 'post': "Chrzążczyżewoszyce, powiat Łękółody.", @@ -198,6 +183,7 @@ def test_post_unicode(self): ) self.assertEqual(response.status_code, 200) + @patch_category_acl({"can_reply_threads": True}) def test_category_moderation_queue(self): """reply thread in category that requires approval""" self.category.require_replies_approval = True @@ -222,10 +208,10 @@ def test_category_moderation_queue(self): self.assertEqual(category.threads, self.category.threads) self.assertEqual(category.posts, self.category.posts) + @patch_category_acl({"can_reply_threads": True}) + @patch_user_acl({"can_approve_content": True}) def test_category_moderation_queue_bypass(self): """bypass moderation queue due to user's acl""" - override_acl(self.user, {'can_approve_content': 1}) - self.category.require_replies_approval = True self.category.save() @@ -248,10 +234,9 @@ def test_category_moderation_queue_bypass(self): self.assertEqual(category.threads, self.category.threads) self.assertEqual(category.posts, self.category.posts + 1) + @patch_category_acl({"can_reply_threads": True, "require_replies_approval": True}) def test_user_moderation_queue(self): """reply thread by user that requires approval""" - self.override_acl({'require_replies_approval': 1}) - response = self.client.post( self.api_link, data={ 'post': "Lorem ipsum dolor met!", @@ -271,12 +256,10 @@ def test_user_moderation_queue(self): self.assertEqual(category.threads, self.category.threads) self.assertEqual(category.posts, self.category.posts) + @patch_category_acl({"can_reply_threads": True, "require_replies_approval": True}) + @patch_user_acl({"can_approve_content": True}) def test_user_moderation_queue_bypass(self): """bypass moderation queue due to user's acl""" - override_acl(self.user, {'can_approve_content': 1}) - - self.override_acl({'require_replies_approval': 1}) - response = self.client.post( self.api_link, data={ 'post': "Lorem ipsum dolor met!", @@ -296,17 +279,17 @@ def test_user_moderation_queue_bypass(self): self.assertEqual(category.threads, self.category.threads) self.assertEqual(category.posts, self.category.posts + 1) + @patch_category_acl({ + "can_reply_threads": True, + "require_threads_approval": True, + "require_edits_approval": True, + }) def test_omit_other_moderation_queues(self): """other queues are omitted""" self.category.require_threads_approval = True self.category.require_edits_approval = True self.category.save() - self.override_acl({ - 'require_threads_approval': 1, - 'require_edits_approval': 1, - }) - response = self.client.post( self.api_link, data={ 'post': "Lorem ipsum dolor met!", diff --git a/misago/threads/tests/test_thread_start_api.py b/misago/threads/tests/test_thread_start_api.py index 6811f4cf96..dea401b48b 100644 --- a/misago/threads/tests/test_thread_start_api.py +++ b/misago/threads/tests/test_thread_start_api.py @@ -1,7 +1,8 @@ from django.urls import reverse -from misago.acl.testutils import override_acl +from misago.acl.test import patch_user_acl from misago.categories.models import Category +from misago.threads.test import patch_category_acl from misago.users.testutils import AuthenticatedUserTestCase @@ -12,30 +13,6 @@ def setUp(self): self.category = Category.objects.get(slug='first-category') self.api_link = reverse('misago:api:thread-list') - def override_acl(self, extra_acl=None): - new_acl = self.user.acl_cache - new_acl['categories'][self.category.pk].update({ - 'can_see': 1, - 'can_browse': 1, - 'can_start_threads': 1, - 'can_pin_threads': 0, - 'can_close_threads': 0, - 'can_hide_threads': 0, - 'can_hide_own_threads': 0, - }) - - if extra_acl: - new_acl['categories'][self.category.pk].update(extra_acl) - - if 'can_see' in extra_acl and not extra_acl['can_see']: - new_acl['visible_categories'].remove(self.category.pk) - new_acl['browseable_categories'].remove(self.category.pk) - - if 'can_browse' in extra_acl and not extra_acl['can_browse']: - new_acl['browseable_categories'].remove(self.category.pk) - - override_acl(self.user, new_acl) - def test_cant_start_thread_as_guest(self): """user has to be authenticated to be able to post thread""" self.logout_user() @@ -43,10 +20,9 @@ def test_cant_start_thread_as_guest(self): response = self.client.post(self.api_link) self.assertEqual(response.status_code, 403) + @patch_category_acl({"can_see": False}) def test_cant_see(self): """has no permission to see selected category""" - self.override_acl({'can_see': 0}) - response = self.client.post(self.api_link, { 'category': self.category.pk, }) @@ -57,10 +33,9 @@ def test_cant_see(self): 'title': ['You have to enter thread title.'], }) + @patch_category_acl({"can_browse": False}) def test_cant_browse(self): """has no permission to browse selected category""" - self.override_acl({'can_browse': 0}) - response = self.client.post(self.api_link, { 'category': self.category.pk, }) @@ -71,10 +46,9 @@ def test_cant_browse(self): 'title': ['You have to enter thread title.'], }) + @patch_category_acl({"can_start_threads": False}) def test_cant_start_thread(self): """permission to start thread in category is validated""" - self.override_acl({'can_start_threads': 0}) - response = self.client.post(self.api_link, { 'category': self.category.pk, }) @@ -85,13 +59,12 @@ def test_cant_start_thread(self): 'title': ['You have to enter thread title.'], }) + @patch_category_acl({"can_start_threads": True, "can_close_threads": False}) def test_cant_start_thread_in_locked_category(self): """can't post in closed category""" self.category.is_closed = True self.category.save() - self.override_acl({'can_close_threads': 0}) - response = self.client.post(self.api_link, { 'category': self.category.pk, }) @@ -104,11 +77,6 @@ def test_cant_start_thread_in_locked_category(self): def test_cant_start_thread_in_invalid_category(self): """can't post in invalid category""" - self.category.is_closed = True - self.category.save() - - self.override_acl({'can_close_threads': 0}) - response = self.client.post(self.api_link, {'category': self.category.pk * 100000}) self.assertEqual(response.status_code, 400) self.assertEqual(response.json(), { @@ -120,10 +88,9 @@ def test_cant_start_thread_in_invalid_category(self): 'title': ['You have to enter thread title.'], }) + @patch_category_acl({"can_start_threads": True}) def test_empty_data(self): """no data sent handling has no showstoppers""" - self.override_acl() - response = self.client.post(self.api_link, data={}) self.assertEqual(response.status_code, 400) self.assertEqual( @@ -134,10 +101,9 @@ def test_empty_data(self): } ) + @patch_category_acl({"can_start_threads": True}) def test_invalid_data(self): """api errors for invalid request data""" - self.override_acl() - response = self.client.post( self.api_link, 'false', @@ -148,10 +114,9 @@ def test_invalid_data(self): 'non_field_errors': ['Invalid data. Expected a dictionary, but got bool.'] }) + @patch_category_acl({"can_start_threads": True}) def test_title_is_validated(self): """title is validated""" - self.override_acl() - response = self.client.post( self.api_link, data={ @@ -168,10 +133,9 @@ def test_title_is_validated(self): } ) + @patch_category_acl({"can_start_threads": True}) def test_post_is_validated(self): """post is validated""" - self.override_acl() - response = self.client.post( self.api_link, data={ @@ -188,9 +152,9 @@ def test_post_is_validated(self): } ) + @patch_category_acl({"can_start_threads": True}) def test_can_start_thread(self): """endpoint creates new thread""" - self.override_acl() response = self.client.post( self.api_link, data={ @@ -206,7 +170,6 @@ def test_can_start_thread(self): response_json = response.json() self.assertEqual(response_json['url'], thread.get_absolute_url()) - self.override_acl() response = self.client.get(thread.get_absolute_url()) self.assertContains(response, self.category.name) self.assertContains(response, thread.title) @@ -245,10 +208,9 @@ def test_can_start_thread(self): self.assertEqual(category.last_poster_name, self.user.username) self.assertEqual(category.last_poster_slug, self.user.slug) + @patch_category_acl({"can_start_threads": True, "can_close_threads": False}) def test_start_closed_thread_no_permission(self): """permission is checked before thread is closed""" - self.override_acl({'can_close_threads': 0}) - response = self.client.post( self.api_link, data={ @@ -263,10 +225,9 @@ def test_start_closed_thread_no_permission(self): thread = self.user.thread_set.all()[:1][0] self.assertFalse(thread.is_closed) + @patch_category_acl({"can_start_threads": True, "can_close_threads": True}) def test_start_closed_thread(self): """can post closed thread""" - self.override_acl({'can_close_threads': 1}) - response = self.client.post( self.api_link, data={ @@ -281,10 +242,9 @@ def test_start_closed_thread(self): thread = self.user.thread_set.all()[:1][0] self.assertTrue(thread.is_closed) + @patch_category_acl({"can_start_threads": True, "can_pin_threads": 1}) def test_start_unpinned_thread(self): """can post unpinned thread""" - self.override_acl({'can_pin_threads': 1}) - response = self.client.post( self.api_link, data={ @@ -299,10 +259,9 @@ def test_start_unpinned_thread(self): thread = self.user.thread_set.all()[:1][0] self.assertEqual(thread.weight, 0) + @patch_category_acl({"can_start_threads": True, "can_pin_threads": 1}) def test_start_locally_pinned_thread(self): """can post locally pinned thread""" - self.override_acl({'can_pin_threads': 1}) - response = self.client.post( self.api_link, data={ @@ -317,10 +276,9 @@ def test_start_locally_pinned_thread(self): thread = self.user.thread_set.all()[:1][0] self.assertEqual(thread.weight, 1) + @patch_category_acl({"can_start_threads": True, "can_pin_threads": 2}) def test_start_globally_pinned_thread(self): """can post globally pinned thread""" - self.override_acl({'can_pin_threads': 2}) - response = self.client.post( self.api_link, data={ @@ -335,10 +293,9 @@ def test_start_globally_pinned_thread(self): thread = self.user.thread_set.all()[:1][0] self.assertEqual(thread.weight, 2) + @patch_category_acl({"can_start_threads": True, "can_pin_threads": 1}) def test_start_globally_pinned_thread_no_permission(self): """cant post globally pinned thread without permission""" - self.override_acl({'can_pin_threads': 1}) - response = self.client.post( self.api_link, data={ @@ -353,10 +310,9 @@ def test_start_globally_pinned_thread_no_permission(self): thread = self.user.thread_set.all()[:1][0] self.assertEqual(thread.weight, 0) + @patch_category_acl({"can_start_threads": True, "can_pin_threads": 0}) def test_start_locally_pinned_thread_no_permission(self): """cant post locally pinned thread without permission""" - self.override_acl({'can_pin_threads': 0}) - response = self.client.post( self.api_link, data={ @@ -371,10 +327,9 @@ def test_start_locally_pinned_thread_no_permission(self): thread = self.user.thread_set.all()[:1][0] self.assertEqual(thread.weight, 0) + @patch_category_acl({"can_start_threads": True, "can_hide_threads": 1}) def test_start_hidden_thread(self): """can post hidden thread""" - self.override_acl({'can_hide_threads': 1}) - response = self.client.post( self.api_link, data={ @@ -392,10 +347,9 @@ def test_start_hidden_thread(self): category = Category.objects.get(pk=self.category.pk) self.assertNotEqual(category.last_thread_id, thread.id) + @patch_category_acl({"can_start_threads": True, "can_hide_threads": 0}) def test_start_hidden_thread_no_permission(self): """cant post hidden thread without permission""" - self.override_acl({'can_hide_threads': 0}) - response = self.client.post( self.api_link, data={ @@ -410,10 +364,9 @@ def test_start_hidden_thread_no_permission(self): thread = self.user.thread_set.all()[:1][0] self.assertFalse(thread.is_hidden) + @patch_category_acl({"can_start_threads": True}) def test_post_unicode(self): """unicode characters can be posted""" - self.override_acl() - response = self.client.post( self.api_link, data={ @@ -424,6 +377,7 @@ def test_post_unicode(self): ) self.assertEqual(response.status_code, 200) + @patch_category_acl({"can_start_threads": True}) def test_category_moderation_queue(self): """start unapproved thread in category that requires approval""" self.category.require_threads_approval = True @@ -451,10 +405,10 @@ def test_category_moderation_queue(self): self.assertEqual(category.posts, self.category.posts) self.assertFalse(category.last_thread_id == thread.id) + @patch_category_acl({"can_start_threads": True}) + @patch_user_acl({"can_approve_content": True}) def test_category_moderation_queue_bypass(self): """bypass moderation queue due to user's acl""" - override_acl(self.user, {'can_approve_content': 1}) - self.category.require_threads_approval = True self.category.save() @@ -480,10 +434,9 @@ def test_category_moderation_queue_bypass(self): self.assertEqual(category.posts, self.category.posts + 1) self.assertEqual(category.last_thread_id, thread.id) + @patch_category_acl({"can_start_threads": True, "require_threads_approval": True}) def test_user_moderation_queue(self): """start unapproved thread in category that requires approval""" - self.override_acl({'require_threads_approval': 1}) - response = self.client.post( self.api_link, data={ @@ -506,12 +459,10 @@ def test_user_moderation_queue(self): self.assertEqual(category.posts, self.category.posts) self.assertFalse(category.last_thread_id == thread.id) + @patch_category_acl({"can_start_threads": True, "require_threads_approval": True}) + @patch_user_acl({"can_approve_content": True}) def test_user_moderation_queue_bypass(self): """bypass moderation queue due to user's acl""" - override_acl(self.user, {'can_approve_content': 1}) - - self.override_acl({'require_threads_approval': 1}) - response = self.client.post( self.api_link, data={ @@ -534,17 +485,17 @@ def test_user_moderation_queue_bypass(self): self.assertEqual(category.posts, self.category.posts + 1) self.assertEqual(category.last_thread_id, thread.id) + @patch_category_acl({ + "can_start_threads": True, + "require_replies_approval": True, + "require_edits_approval": True, + }) def test_omit_other_moderation_queues(self): """other queues are omitted""" self.category.require_replies_approval = True self.category.require_edits_approval = True self.category.save() - self.override_acl({ - 'require_replies_approval': 1, - 'require_edits_approval': 1, - }) - response = self.client.post( self.api_link, data={ From e7cd7e113bb9221589d11a68d93a1195a67fc3cc Mon Sep 17 00:00:00 2001 From: rafalp Date: Sat, 15 Dec 2018 18:47:39 +0100 Subject: [PATCH 38/86] Fix threads api tests --- misago/threads/api/threadendpoints/delete.py | 2 +- misago/threads/tests/test_threads_api.py | 211 +++++++------------ 2 files changed, 72 insertions(+), 141 deletions(-) diff --git a/misago/threads/api/threadendpoints/delete.py b/misago/threads/api/threadendpoints/delete.py index c41a0e3fd1..c42e4a01ac 100644 --- a/misago/threads/api/threadendpoints/delete.py +++ b/misago/threads/api/threadendpoints/delete.py @@ -10,7 +10,7 @@ @transaction.atomic def delete_thread(request, thread): - allow_delete_thread(request.user, thread) + allow_delete_thread(request.user_acl, thread) moderation.delete_thread(request.user, thread) return Response({}) diff --git a/misago/threads/tests/test_threads_api.py b/misago/threads/tests/test_threads_api.py index e02235de1a..5b97250d78 100644 --- a/misago/threads/tests/test_threads_api.py +++ b/misago/threads/tests/test_threads_api.py @@ -3,11 +3,11 @@ from django.utils import timezone from django.urls import reverse -from misago.acl.testutils import override_acl from misago.categories import THREADS_ROOT_NAME from misago.categories.models import Category from misago.threads import testutils from misago.threads.models import Thread +from misago.threads.test import patch_category_acl from misago.threads.threadtypes import trees_map from misago.users.testutils import AuthenticatedUserTestCase @@ -24,45 +24,6 @@ def setUp(self): self.thread = testutils.post_thread(category=self.category) self.api_link = self.thread.get_api_url() - def override_acl(self, acl=None): - final_acl = self.user.acl_cache['categories'][self.category.pk] - final_acl.update({ - 'can_see': 1, - 'can_browse': 1, - 'can_see_all_threads': 1, - 'can_see_own_threads': 0, - 'can_hide_threads': 0, - 'can_approve_content': 0, - 'can_edit_posts': 0, - 'can_hide_posts': 0, - 'can_hide_own_posts': 0, - 'can_merge_threads': 0, - 'can_close_threads': 0, - }) - - if acl: - final_acl.update(acl) - - visible_categories = self.user.acl_cache['visible_categories'] - browseable_categories = self.user.acl_cache['browseable_categories'] - - if not final_acl['can_see'] and self.category.pk in visible_categories: - visible_categories.remove(self.category.pk) - browseable_categories.remove(self.category.pk) - - if not final_acl['can_browse'] and self.category.pk in browseable_categories: - browseable_categories.remove(self.category.pk) - - override_acl( - self.user, { - 'visible_categories': visible_categories, - 'browseable_categories': browseable_categories, - 'categories': { - self.category.pk: final_acl, - }, - } - ) - def get_thread_json(self): response = self.client.get(self.thread.get_api_url()) self.assertEqual(response.status_code, 200) @@ -80,11 +41,10 @@ def setUp(self): '%sposts/?page=1' % self.api_link, ] + @patch_category_acl() def test_api_returns_thread(self): """api has no showstoppers""" for link in self.tested_links: - self.override_acl() - response = self.client.get(link) self.assertEqual(response.status_code, 200) @@ -95,11 +55,10 @@ def test_api_returns_thread(self): if 'posts' in link: self.assertIn('post_set', response_json) + @patch_category_acl({"can_see_all_threads": False}) def test_api_shows_owned_thread(self): """api handles "owned threads only""" for link in self.tested_links: - self.override_acl({'can_see_all_threads': 0}) - response = self.client.get(link) self.assertEqual(response.status_code, 404) @@ -107,49 +66,41 @@ def test_api_shows_owned_thread(self): self.thread.save() for link in self.tested_links: - self.override_acl({'can_see_all_threads': 0}) - response = self.client.get(link) self.assertEqual(response.status_code, 200) + @patch_category_acl({"can_see": False}) def test_api_validates_category_see_permission(self): """api validates category visiblity""" for link in self.tested_links: - self.override_acl({'can_see': 0}) - response = self.client.get(link) self.assertEqual(response.status_code, 404) + @patch_category_acl({"can_browse": False}) def test_api_validates_category_browse_permission(self): """api validates category browsability""" for link in self.tested_links: - self.override_acl({'can_browse': 0}) - response = self.client.get(link) self.assertEqual(response.status_code, 404) def test_api_validates_posts_visibility(self): """api validates posts visiblity""" - self.override_acl({'can_hide_posts': 0}) - hidden_post = testutils.reply_thread( self.thread, is_hidden=True, message="I'am hidden test message!", ) - response = self.client.get(self.tested_links[1]) - self.assertNotContains(response, hidden_post.parsed) # post's body is hidden + with patch_category_acl({"can_hide_posts": 0}): + response = self.client.get(self.tested_links[1]) + self.assertNotContains(response, hidden_post.parsed) # post's body is hidden # add permission to see hidden posts - self.override_acl({'can_hide_posts': 1}) - - response = self.client.get(self.tested_links[1]) - self.assertContains( - response, hidden_post.parsed - ) # hidden post's body is visible with permission - - self.override_acl({'can_approve_content': 0}) + with patch_category_acl({"can_hide_posts": 1}): + response = self.client.get(self.tested_links[1]) + self.assertContains( + response, hidden_post.parsed + ) # hidden post's body is visible with permission # unapproved posts shouldn't show at all unapproved_post = testutils.reply_thread( @@ -157,41 +108,39 @@ def test_api_validates_posts_visibility(self): is_unapproved=True, ) - response = self.client.get(self.tested_links[1]) - self.assertNotContains(response, unapproved_post.get_absolute_url()) + with patch_category_acl({"can_approve_content": False}): + response = self.client.get(self.tested_links[1]) + self.assertNotContains(response, unapproved_post.get_absolute_url()) # add permission to see unapproved posts - self.override_acl({'can_approve_content': 1}) - - response = self.client.get(self.tested_links[1]) - self.assertContains(response, unapproved_post.get_absolute_url()) + with patch_category_acl({"can_approve_content": True}): + response = self.client.get(self.tested_links[1]) + self.assertContains(response, unapproved_post.get_absolute_url()) def test_api_validates_has_unapproved_posts_visibility(self): """api checks acl before exposing unapproved flag""" self.thread.has_unapproved_posts = True self.thread.save() - for link in self.tested_links: - self.override_acl() + with patch_category_acl({"can_approve_content": False}): + for link in self.tested_links: + response = self.client.get(link) + self.assertEqual(response.status_code, 200) - response = self.client.get(link) - self.assertEqual(response.status_code, 200) - - response_json = response.json() - self.assertEqual(response_json['id'], self.thread.pk) - self.assertEqual(response_json['title'], self.thread.title) - self.assertFalse(response_json['has_unapproved_posts']) - - for link in self.tested_links: - self.override_acl({'can_approve_content': 1}) + response_json = response.json() + self.assertEqual(response_json['id'], self.thread.pk) + self.assertEqual(response_json['title'], self.thread.title) + self.assertFalse(response_json['has_unapproved_posts']) - response = self.client.get(link) - self.assertEqual(response.status_code, 200) + with patch_category_acl({"can_approve_content": True}): + for link in self.tested_links: + response = self.client.get(link) + self.assertEqual(response.status_code, 200) - response_json = response.json() - self.assertEqual(response_json['id'], self.thread.pk) - self.assertEqual(response_json['title'], self.thread.title) - self.assertTrue(response_json['has_unapproved_posts']) + response_json = response.json() + self.assertEqual(response_json['id'], self.thread.pk) + self.assertEqual(response_json['title'], self.thread.title) + self.assertTrue(response_json['has_unapproved_posts']) class ThreadDeleteApiTests(ThreadsApiTestCase): @@ -203,82 +152,68 @@ def setUp(self): def test_delete_thread_no_permission(self): """api tests permission to delete threads""" - self.override_acl({'can_hide_threads': 0}) - - response = self.client.delete(self.api_link) - self.assertEqual(response.status_code, 403) - - self.assertEqual( - response.json()['detail'], "You can't delete threads in this category." - ) - - self.override_acl({'can_hide_threads': 1}) - - response = self.client.delete(self.api_link) - self.assertEqual(response.status_code, 403) - - self.assertEqual( - response.json()['detail'], "You can't delete threads in this category." - ) - + with patch_category_acl({"can_hide_threads": 0}): + response = self.client.delete(self.api_link) + self.assertEqual(response.status_code, 403) + self.assertEqual( + response.json()['detail'], "You can't delete threads in this category." + ) + + with patch_category_acl({"can_hide_threads": 1}): + response = self.client.delete(self.api_link) + self.assertEqual(response.status_code, 403) + self.assertEqual( + response.json()['detail'], "You can't delete threads in this category." + ) + + @patch_category_acl({'can_hide_threads': 1, 'can_hide_own_threads': 2}) def test_delete_other_user_thread_no_permission(self): """api tests thread owner when deleting own thread""" - self.override_acl({ - 'can_hide_threads': 1, - 'can_hide_own_threads': 2, - }) - response = self.client.delete(self.api_link) self.assertEqual(response.status_code, 403) - self.assertEqual( response.json()['detail'], "You can't delete other users theads in this category." ) + @patch_category_acl({ + 'can_hide_threads': 2, + 'can_hide_own_threads': 2, + 'can_close_threads': False, + }) def test_delete_thread_closed_category_no_permission(self): """api tests category's closed state""" self.category.is_closed = True self.category.save() - self.override_acl({ - 'can_hide_threads': 2, - 'can_hide_own_threads': 2, - 'can_close_threads': False, - }) - response = self.client.delete(self.api_link) self.assertEqual(response.status_code, 403) - self.assertEqual( response.json()['detail'], "This category is closed. You can't delete threads in it." ) + @patch_category_acl({ + 'can_hide_threads': 2, + 'can_hide_own_threads': 2, + 'can_close_threads': False, + }) def test_delete_thread_closed_no_permission(self): """api tests thread's closed state""" self.last_thread.is_closed = True self.last_thread.save() - self.override_acl({ - 'can_hide_threads': 2, - 'can_hide_own_threads': 2, - 'can_close_threads': False, - }) - response = self.client.delete(self.api_link) self.assertEqual(response.status_code, 403) - self.assertEqual( response.json()['detail'], "This thread is closed. You can't delete it." ) + @patch_category_acl({ + 'can_hide_threads': 1, + 'can_hide_own_threads': 2, + 'thread_edit_time': 1 + }) def test_delete_owned_thread_no_time(self): """api tests permission to delete owned thread within time limit""" - self.override_acl({ - 'can_hide_threads': 1, - 'can_hide_own_threads': 2, - 'thread_edit_time': 1 - }) - self.last_thread.starter = self.user self.last_thread.started_on = timezone.now() - timedelta(minutes=10) self.last_thread.save() @@ -289,10 +224,9 @@ def test_delete_owned_thread_no_time(self): response.json()['detail'], "You can't delete threads that are older than 1 minute." ) + @patch_category_acl({'can_hide_threads': 2}) def test_delete_thread(self): """DELETE to API link with permission deletes thread""" - self.override_acl({'can_hide_threads': 2}) - category = Category.objects.get(slug='first-category') self.assertEqual(category.last_thread_id, self.last_thread.pk) @@ -307,8 +241,6 @@ def test_delete_thread(self): self.assertEqual(category.last_thread_id, self.thread.pk) # test that last thread's deletion triggers category sync - self.override_acl({'can_hide_threads': 2}) - response = self.client.delete(self.thread.get_api_url()) self.assertEqual(response.status_code, 200) @@ -318,14 +250,13 @@ def test_delete_thread(self): category = Category.objects.get(slug='first-category') self.assertIsNone(category.last_thread_id) + @patch_category_acl({ + 'can_hide_threads': 1, + 'can_hide_own_threads': 2, + 'thread_edit_time': 30 + }) def test_delete_owned_thread(self): """api lets owner to delete owned thread within time limit""" - self.override_acl({ - 'can_hide_threads': 1, - 'can_hide_own_threads': 2, - 'thread_edit_time': 30 - }) - self.last_thread.starter = self.user self.last_thread.started_on = timezone.now() - timedelta(minutes=10) self.last_thread.save() From 637ec70846609dbf72219caddb0017945a2b9ab4 Mon Sep 17 00:00:00 2001 From: rafalp Date: Sat, 15 Dec 2018 19:26:09 +0100 Subject: [PATCH 39/86] Fix threads splitting --- misago/threads/api/postendpoints/split.py | 2 +- .../tests/test_thread_postsplit_api.py | 146 +++++++----------- 2 files changed, 53 insertions(+), 95 deletions(-) diff --git a/misago/threads/api/postendpoints/split.py b/misago/threads/api/postendpoints/split.py index 8a5c329cf0..777fb4f0ed 100644 --- a/misago/threads/api/postendpoints/split.py +++ b/misago/threads/api/postendpoints/split.py @@ -16,7 +16,7 @@ def posts_split_endpoint(request, thread): data=request.data, context={ 'thread': thread, - 'user': request.user, + 'user_acl': request.user_acl, }, ) diff --git a/misago/threads/tests/test_thread_postsplit_api.py b/misago/threads/tests/test_thread_postsplit_api.py index 94ea331a1e..74a1259a15 100644 --- a/misago/threads/tests/test_thread_postsplit_api.py +++ b/misago/threads/tests/test_thread_postsplit_api.py @@ -2,12 +2,12 @@ from django.urls import reverse -from misago.acl.testutils import override_acl from misago.categories.models import Category from misago.readtracker import poststracker from misago.threads import testutils from misago.threads.models import Post, Thread from misago.threads.serializers.moderation import POSTS_LIMIT +from misago.threads.test import patch_category_acl, patch_other_category_acl from misago.users.testutils import AuthenticatedUserTestCase @@ -29,66 +29,14 @@ def setUp(self): ) Category( - name='Category B', - slug='category-b', + name='Other category', + slug='other-category', ).insert_at( self.category, position='last-child', save=True, ) - self.category_b = Category.objects.get(slug='category-b') - - self.override_acl() - self.override_other_acl() - - def refresh_thread(self): - self.thread = Thread.objects.get(pk=self.thread.pk) - - def override_acl(self, extra_acl=None): - new_acl = self.user.acl_cache - new_acl['categories'][self.category.pk].update({ - 'can_see': 1, - 'can_browse': 1, - 'can_start_threads': 1, - 'can_reply_threads': 1, - 'can_edit_posts': 1, - 'can_approve_content': 0, - 'can_move_posts': 1, - }) - - if extra_acl: - new_acl['categories'][self.category.pk].update(extra_acl) - - override_acl(self.user, new_acl) - - def override_other_acl(self, acl=None): - other_category_acl = self.user.acl_cache['categories'][self.category.pk].copy() - other_category_acl.update({ - 'can_see': 1, - 'can_browse': 1, - 'can_start_threads': 0, - 'can_reply_threads': 0, - 'can_edit_posts': 1, - 'can_approve_content': 0, - 'can_move_posts': 1, - }) - - if acl: - other_category_acl.update(acl) - - categories_acl = self.user.acl_cache['categories'] - categories_acl[self.category_b.pk] = other_category_acl - - visible_categories = [self.category.pk] - if other_category_acl['can_see']: - visible_categories.append(self.category_b.pk) - - override_acl( - self.user, { - 'visible_categories': visible_categories, - 'categories': categories_acl, - } - ) + self.other_category = Category.objects.get(slug='other-category') def test_anonymous_user(self): """you need to authenticate to split posts""" @@ -100,16 +48,16 @@ def test_anonymous_user(self): "detail": "This action is not available to guests.", }) + @patch_category_acl({"can_move_posts": False}) def test_no_permission(self): """api validates permission to split""" - self.override_acl({'can_move_posts': 0}) - response = self.client.post(self.api_link, json.dumps({}), content_type="application/json") self.assertEqual(response.status_code, 403) self.assertEqual(response.json(), { "detail": "You can't split posts from this thread.", }) + @patch_category_acl({"can_move_posts": True}) def test_empty_data(self): """api handles empty data""" response = self.client.post(self.api_link) @@ -118,36 +66,34 @@ def test_empty_data(self): "detail": "You have to specify at least one post to split.", }) + @patch_category_acl({"can_move_posts": True}) def test_invalid_data(self): """api handles post that is invalid type""" - self.override_acl() response = self.client.post(self.api_link, '[]', content_type="application/json") self.assertEqual(response.status_code, 400) self.assertEqual(response.json(), { "non_field_errors": ["Invalid data. Expected a dictionary, but got list."], }) - self.override_acl() response = self.client.post(self.api_link, '123', content_type="application/json") self.assertEqual(response.status_code, 400) self.assertEqual(response.json(), { "non_field_errors": ["Invalid data. Expected a dictionary, but got int."], }) - self.override_acl() response = self.client.post(self.api_link, '"string"', content_type="application/json") self.assertEqual(response.status_code, 400) self.assertEqual(response.json(), { "non_field_errors": ["Invalid data. Expected a dictionary, but got str."], }) - self.override_acl() response = self.client.post(self.api_link, 'malformed', content_type="application/json") self.assertEqual(response.status_code, 400) self.assertEqual(response.json(), { "detail": "JSON parse error - Expecting value: line 1 column 1 (char 0)", }) + @patch_category_acl({"can_move_posts": True}) def test_no_posts_ids(self): """api rejects no posts ids""" response = self.client.post( @@ -159,6 +105,8 @@ def test_no_posts_ids(self): self.assertEqual(response.json(), { "detail": "You have to specify at least one post to split.", }) + + @patch_category_acl({"can_move_posts": True}) def test_empty_posts_ids(self): """api rejects empty posts ids list""" response = self.client.post( @@ -173,6 +121,7 @@ def test_empty_posts_ids(self): "detail": "You have to specify at least one post to split.", }) + @patch_category_acl({"can_move_posts": True}) def test_invalid_posts_data(self): """api handles invalid data""" response = self.client.post( @@ -187,6 +136,7 @@ def test_invalid_posts_data(self): "detail": 'Expected a list of items but got type "str".', }) + @patch_category_acl({"can_move_posts": True}) def test_invalid_posts_ids(self): """api handles invalid post id""" response = self.client.post( @@ -201,6 +151,7 @@ def test_invalid_posts_ids(self): "detail": "One or more post ids received were invalid.", }) + @patch_category_acl({"can_move_posts": True}) def test_split_limit(self): """api rejects more posts than split limit""" response = self.client.post( @@ -215,6 +166,7 @@ def test_split_limit(self): "detail": "No more than %s posts can be split at single time." % POSTS_LIMIT, }) + @patch_category_acl({"can_move_posts": True}) def test_split_invisible(self): """api validates posts visibility""" response = self.client.post( @@ -229,6 +181,7 @@ def test_split_invisible(self): "detail": "One or more posts to split could not be found.", }) + @patch_category_acl({"can_move_posts": True}) def test_split_event(self): """api rejects events split""" response = self.client.post( @@ -243,6 +196,7 @@ def test_split_event(self): "detail": "Events can't be split.", }) + @patch_category_acl({"can_move_posts": True}) def test_split_first_post(self): """api rejects first post split""" response = self.client.post( @@ -257,6 +211,7 @@ def test_split_first_post(self): "detail": "You can't split thread's first post.", }) + @patch_category_acl({"can_move_posts": True}) def test_split_hidden_posts(self): """api recjects attempt to split urneadable hidden post""" response = self.client.post( @@ -271,13 +226,12 @@ def test_split_hidden_posts(self): "detail": "You can't split posts the content you can't see.", }) + @patch_category_acl({"can_move_posts": True, "can_close_threads": False}) def test_split_posts_closed_thread_no_permission(self): """api recjects attempt to split posts from closed thread""" self.thread.is_closed = True self.thread.save() - self.override_acl({'can_close_threads': 0}) - response = self.client.post( self.api_link, json.dumps({ @@ -290,13 +244,12 @@ def test_split_posts_closed_thread_no_permission(self): "detail": "This thread is closed. You can't split posts in it.", }) + @patch_category_acl({"can_move_posts": True, "can_close_threads": False}) def test_split_posts_closed_category_no_permission(self): """api recjects attempt to split posts from closed thread""" self.category.is_closed = True self.category.save() - self.override_acl({'can_close_threads': 0}) - response = self.client.post( self.api_link, json.dumps({ @@ -309,6 +262,7 @@ def test_split_posts_closed_category_no_permission(self): "detail": "This category is closed. You can't split posts in it.", }) + @patch_category_acl({"can_move_posts": True}) def test_split_other_thread_posts(self): """api recjects attempt to split other thread's post""" other_thread = testutils.post_thread(self.category) @@ -325,6 +279,7 @@ def test_split_other_thread_posts(self): "detail": "One or more posts to split could not be found.", }) + @patch_category_acl({"can_move_posts": True}) def test_split_empty_new_thread_data(self): """api handles empty form data""" response = self.client.post( @@ -344,6 +299,7 @@ def test_split_empty_new_thread_data(self): } ) + @patch_category_acl({"can_move_posts": True}) def test_split_invalid_final_title(self): """api rejects split because final thread title was invalid""" response = self.client.post( @@ -364,16 +320,16 @@ def test_split_invalid_final_title(self): } ) + @patch_other_category_acl({"can_see": False}) + @patch_category_acl({"can_move_posts": True}) def test_split_invalid_category(self): """api rejects split because final category was invalid""" - self.override_other_acl({'can_see': 0}) - response = self.client.post( self.api_link, json.dumps({ 'posts': self.posts, 'title': 'Valid thread title', - 'category': self.category_b.id, + 'category': self.other_category.id, }), content_type="application/json", ) @@ -386,10 +342,9 @@ def test_split_invalid_category(self): } ) + @patch_category_acl({"can_move_posts": True, "can_start_threads": False}) def test_split_unallowed_start_thread(self): """api rejects split because category isn't allowing starting threads""" - self.override_acl({'can_start_threads': 0}) - response = self.client.post( self.api_link, json.dumps({ @@ -408,6 +363,7 @@ def test_split_unallowed_start_thread(self): } ) + @patch_category_acl({"can_move_posts": True}) def test_split_invalid_weight(self): """api rejects split because final weight was invalid""" response = self.client.post( @@ -429,6 +385,7 @@ def test_split_invalid_weight(self): } ) + @patch_category_acl({"can_move_posts": True}) def test_split_unallowed_global_weight(self): """api rejects split because global weight was unallowed""" response = self.client.post( @@ -450,6 +407,7 @@ def test_split_unallowed_global_weight(self): } ) + @patch_category_acl({"can_move_posts": True, "can_pin_threads": 0}) def test_split_unallowed_local_weight(self): """api rejects split because local weight was unallowed""" response = self.client.post( @@ -471,10 +429,9 @@ def test_split_unallowed_local_weight(self): } ) + @patch_category_acl({"can_move_posts": True, "can_pin_threads": 1}) def test_split_allowed_local_weight(self): """api allows local weight""" - self.override_acl({'can_pin_threads': 1}) - response = self.client.post( self.api_link, json.dumps({ @@ -494,10 +451,9 @@ def test_split_allowed_local_weight(self): } ) + @patch_category_acl({"can_move_posts": True, "can_pin_threads": 2}) def test_split_allowed_global_weight(self): """api allows global weight""" - self.override_acl({'can_pin_threads': 2}) - response = self.client.post( self.api_link, json.dumps({ @@ -517,6 +473,7 @@ def test_split_allowed_global_weight(self): } ) + @patch_category_acl({"can_move_posts": True, "can_close_threads": False}) def test_split_unallowed_close(self): """api rejects split because closing thread was unallowed""" response = self.client.post( @@ -538,10 +495,9 @@ def test_split_unallowed_close(self): } ) + @patch_category_acl({"can_move_posts": True, "can_close_threads": True}) def test_split_with_close(self): """api allows for closing thread""" - self.override_acl({'can_close_threads': True}) - response = self.client.post( self.api_link, json.dumps({ @@ -562,6 +518,7 @@ def test_split_with_close(self): } ) + @patch_category_acl({"can_move_posts": True, "can_hide_threads": 0}) def test_split_unallowed_hidden(self): """api rejects split because hidden thread was unallowed""" response = self.client.post( @@ -583,10 +540,9 @@ def test_split_unallowed_hidden(self): } ) + @patch_category_acl({"can_move_posts": True, "can_hide_threads": 1}) def test_split_with_hide(self): """api allows for hiding thread""" - self.override_acl({'can_hide_threads': True}) - response = self.client.post( self.api_link, json.dumps({ @@ -607,9 +563,10 @@ def test_split_with_hide(self): } ) + @patch_category_acl({"can_move_posts": True}) def test_split(self): """api splits posts to new thread""" - self.refresh_thread() + self.thread.refresh_from_db() self.assertEqual(self.thread.replies, 2) response = self.client.post( @@ -628,12 +585,13 @@ def test_split(self): self.assertEqual(split_thread.replies, 1) # posts were removed from old thread - self.refresh_thread() + self.thread.refresh_from_db() self.assertEqual(self.thread.replies, 0) # posts were moved to new thread self.assertEqual(split_thread.post_set.filter(pk__in=self.posts).count(), 2) + @patch_category_acl({"can_move_posts": True}) def test_split_best_answer(self): """api splits best answer to new thread""" best_answer = testutils.reply_thread(self.thread) @@ -642,7 +600,7 @@ def test_split_best_answer(self): self.thread.synchronize() self.thread.save() - self.refresh_thread() + self.thread.refresh_from_db() self.assertEqual(self.thread.best_answer, best_answer) self.assertEqual(self.thread.replies, 3) @@ -658,7 +616,7 @@ def test_split_best_answer(self): self.assertEqual(response.status_code, 200) # best_answer was moved and unmarked - self.refresh_thread() + self.thread.refresh_from_db() self.assertEqual(self.thread.replies, 2) self.assertIsNone(self.thread.best_answer) @@ -666,18 +624,18 @@ def test_split_best_answer(self): self.assertEqual(split_thread.replies, 0) self.assertIsNone(split_thread.best_answer) + @patch_other_category_acl({ + 'can_start_threads': True, + 'can_close_threads': True, + 'can_hide_threads': True, + 'can_pin_threads': 2, + }) + @patch_category_acl({"can_move_posts": True}) def test_split_kitchensink(self): """api splits posts with kitchensink""" - self.refresh_thread() + self.thread.refresh_from_db() self.assertEqual(self.thread.replies, 2) - self.override_other_acl({ - 'can_start_threads': 2, - 'can_close_threads': True, - 'can_hide_threads': True, - 'can_pin_threads': 2, - }) - poststracker.save_read(self.user, self.thread.first_post) for post in self.posts: poststracker.save_read(self.user, Post.objects.select_related().get(pk=post)) @@ -687,7 +645,7 @@ def test_split_kitchensink(self): json.dumps({ 'posts': self.posts, 'title': 'Split thread', - 'category': self.category_b.id, + 'category': self.other_category.id, 'weight': 2, 'is_closed': 1, 'is_hidden': 1, @@ -697,14 +655,14 @@ def test_split_kitchensink(self): self.assertEqual(response.status_code, 200) # thread was created - split_thread = self.category_b.thread_set.get(slug='split-thread') + split_thread = self.other_category.thread_set.get(slug='split-thread') self.assertEqual(split_thread.replies, 1) self.assertEqual(split_thread.weight, 2) self.assertTrue(split_thread.is_closed) self.assertTrue(split_thread.is_hidden) # posts were removed from old thread - self.refresh_thread() + self.thread.refresh_from_db() self.assertEqual(self.thread.replies, 0) # posts were moved to new thread From 35a80d08f792634c8246d662d3c902564cdcf446 Mon Sep 17 00:00:00 2001 From: rafalp Date: Sat, 15 Dec 2018 19:47:43 +0100 Subject: [PATCH 40/86] Fix bulk post delete --- misago/threads/api/postendpoints/delete.py | 8 +- .../tests/test_thread_postbulkdelete_api.py | 127 +++++++----------- 2 files changed, 49 insertions(+), 86 deletions(-) diff --git a/misago/threads/api/postendpoints/delete.py b/misago/threads/api/postendpoints/delete.py index 70ae73e73d..d3e2cf1517 100644 --- a/misago/threads/api/postendpoints/delete.py +++ b/misago/threads/api/postendpoints/delete.py @@ -18,10 +18,10 @@ def delete_post(request, thread, post): if post.is_event: - allow_delete_event(request.user, post) + allow_delete_event(request.user_acl, post) else: - allow_delete_best_answer(request.user, post) - allow_delete_post(request.user, post) + allow_delete_best_answer(request.user_acl, post) + allow_delete_post(request.user_acl, post) moderation.delete_post(request.user, post) @@ -34,7 +34,7 @@ def delete_bulk(request, thread): data={'posts': request.data}, context={ 'thread': thread, - 'user': request.user, + 'user_acl': request.user_acl, }, ) diff --git a/misago/threads/tests/test_thread_postbulkdelete_api.py b/misago/threads/tests/test_thread_postbulkdelete_api.py index 8218d16452..bc5961e7b5 100644 --- a/misago/threads/tests/test_thread_postbulkdelete_api.py +++ b/misago/threads/tests/test_thread_postbulkdelete_api.py @@ -6,6 +6,7 @@ from misago.threads import testutils from misago.threads.models import Post, Thread +from misago.threads.test import patch_category_acl from .test_threads_api import ThreadsApiTestCase @@ -64,13 +65,9 @@ def test_delete_empty_ids(self): "detail": "You have to specify at least one post to delete.", }) + @patch_category_acl({'can_hide_posts': 2}) def test_validate_ids(self): """api validates that ids are list of ints""" - self.override_acl({ - 'can_hide_own_posts': 2, - 'can_hide_posts': 2, - }) - response = self.delete(self.api_link, True) self.assertEqual(response.status_code, 400) self.assertEqual(response.json(), { @@ -89,39 +86,27 @@ def test_validate_ids(self): "detail": "One or more post ids received were invalid.", }) + @patch_category_acl({'can_hide_posts': 2}) def test_validate_ids_length(self): """api validates that ids are list of ints""" - self.override_acl({ - 'can_hide_own_posts': 2, - 'can_hide_posts': 2, - }) - response = self.delete(self.api_link, list(range(100))) self.assertEqual(response.status_code, 400) self.assertEqual(response.json(), { "detail": "No more than 24 posts can be deleted at single time.", }) + @patch_category_acl({'can_hide_posts': 2}) def test_validate_posts_exist(self): """api validates that ids are visible posts""" - self.override_acl({ - 'can_hide_own_posts': 2, - 'can_hide_posts': 0, - }) - response = self.delete(self.api_link, [p.id * 10 for p in self.posts]) self.assertEqual(response.status_code, 403) self.assertEqual(response.json(), { "detail": "One or more posts to delete could not be found.", }) + @patch_category_acl({'can_hide_posts': 2}) def test_validate_posts_visibility(self): """api validates that ids are visible posts""" - self.override_acl({ - 'can_hide_own_posts': 2, - 'can_hide_posts': 0, - }) - self.posts[1].is_unapproved = True self.posts[1].save() @@ -131,13 +116,9 @@ def test_validate_posts_visibility(self): "detail": "One or more posts to delete could not be found.", }) + @patch_category_acl({'can_hide_posts': 2}) def test_validate_posts_same_thread(self): """api validates that ids are same thread posts""" - self.override_acl({ - 'can_hide_own_posts': 2, - 'can_hide_posts': 2, - }) - other_thread = testutils.post_thread(category=self.category) self.posts.append(testutils.reply_thread(other_thread, poster=self.user)) @@ -147,41 +128,35 @@ def test_validate_posts_same_thread(self): "detail": "One or more posts to delete could not be found.", }) + @patch_category_acl({'can_hide_posts': 1, 'can_hide_own_posts': 1}) def test_no_permission(self): """api validates permission to delete""" - self.override_acl({ - 'can_hide_own_posts': 1, - 'can_hide_posts': 1, - }) - response = self.delete(self.api_link, [p.id for p in self.posts]) self.assertEqual(response.status_code, 403) self.assertEqual(response.json(), { "detail": "You can't delete posts in this category.", }) + @patch_category_acl({ + 'can_hide_posts': 0, + 'can_hide_own_posts': 2, + 'post_edit_time': 10, + }) def test_delete_other_user_post_no_permission(self): """api valdiates if user can delete other users posts""" - self.override_acl({ - 'post_edit_time': 0, - 'can_hide_own_posts': 2, - 'can_hide_posts': 0, - }) - response = self.delete(self.api_link, [p.id for p in self.posts]) self.assertEqual(response.status_code, 403) self.assertEqual(response.json(), { "detail": "You can't delete other users posts in this category.", }) + @patch_category_acl({ + 'can_hide_posts': 0, + 'can_hide_own_posts': 2, + 'can_protect_posts': False, + }) def test_delete_protected_post_no_permission(self): """api validates if user can delete protected post""" - self.override_acl({ - 'can_protect_posts': 0, - 'can_hide_own_posts': 2, - 'can_hide_posts': 0, - }) - self.posts[0].is_protected = True self.posts[0].save() @@ -191,14 +166,13 @@ def test_delete_protected_post_no_permission(self): "detail": "This post is protected. You can't delete it.", }) + @patch_category_acl({ + 'can_hide_posts': 0, + 'can_hide_own_posts': 2, + 'post_edit_time': 1, + }) def test_delete_protected_post_after_edit_time(self): """api validates if user can delete delete post after edit time""" - self.override_acl({ - 'post_edit_time': 1, - 'can_hide_own_posts': 2, - 'can_hide_posts': 0, - }) - self.posts[0].posted_on = timezone.now() - timedelta(minutes=10) self.posts[0].save() @@ -208,13 +182,13 @@ def test_delete_protected_post_after_edit_time(self): "detail": "You can't delete posts that are older than 1 minute.", }) + @patch_category_acl({ + 'can_hide_posts': 2, + 'can_hide_own_posts': 2, + 'can_close_threads': False, + }) def test_delete_post_closed_thread_no_permission(self): """api valdiates if user can delete posts in closed threads""" - self.override_acl({ - 'can_hide_own_posts': 2, - 'can_hide_posts': 0, - }) - self.thread.is_closed = True self.thread.save() @@ -224,13 +198,13 @@ def test_delete_post_closed_thread_no_permission(self): "detail": "This thread is closed. You can't delete posts in it.", }) + @patch_category_acl({ + 'can_hide_posts': 2, + 'can_hide_own_posts': 2, + 'can_close_threads': False, + }) def test_delete_post_closed_category_no_permission(self): """api valdiates if user can delete posts in closed categories""" - self.override_acl({ - 'can_hide_own_posts': 2, - 'can_hide_posts': 0, - }) - self.category.is_closed = True self.category.save() @@ -240,13 +214,9 @@ def test_delete_post_closed_category_no_permission(self): "detail": "This category is closed. You can't delete posts in it.", }) + @patch_category_acl({'can_hide_posts': 2, 'can_hide_own_posts': 2}) def test_delete_first_post(self): """api disallows first post's deletion""" - self.override_acl({ - 'can_hide_own_posts': 2, - 'can_hide_posts': 2, - }) - ids = [p.id for p in self.posts] ids.append(self.thread.first_post_id) @@ -256,10 +226,9 @@ def test_delete_first_post(self): "detail": "You can't delete thread's first post.", }) + @patch_category_acl({'can_hide_posts': 2, 'can_hide_own_posts': 2}) def test_delete_best_answer(self): """api disallows best answer deletion""" - self.override_acl({'can_hide_own_posts': 2, 'can_hide_posts': 2}) - self.thread.set_best_answer(self.user, self.posts[0]) self.thread.save() @@ -269,14 +238,13 @@ def test_delete_best_answer(self): "detail": "You can't delete this post because its marked as best answer.", }) + @patch_category_acl({ + 'can_hide_posts': 2, + 'can_hide_own_posts': 2, + 'can_hide_events': 0, + }) def test_delete_event(self): """api differs posts from events""" - self.override_acl({ - 'can_hide_own_posts': 2, - 'can_hide_posts': 2, - 'can_hide_events': 0, - }) - self.posts[1].is_event = True self.posts[1].save() @@ -286,14 +254,13 @@ def test_delete_event(self): "detail": "You can't delete events in this category.", }) + @patch_category_acl({ + 'can_hide_posts': 0, + 'can_hide_own_posts': 2, + 'post_edit_time': 10, + }) def test_delete_owned_posts(self): """api deletes owned thread posts""" - self.override_acl({ - 'post_edit_time': 0, - 'can_hide_own_posts': 2, - 'can_hide_posts': 0, - }) - ids = [self.posts[0].id, self.posts[-1].id] response = self.delete(self.api_link, ids) @@ -304,13 +271,9 @@ def test_delete_owned_posts(self): with self.assertRaises(Post.DoesNotExist): self.thread.post_set.get(pk=post) + @patch_category_acl({'can_hide_posts': 2, 'can_hide_own_posts': 0}) def test_delete_posts(self): """api deletes thread posts""" - self.override_acl({ - 'can_hide_own_posts': 0, - 'can_hide_posts': 2, - }) - response = self.delete(self.api_link, [p.id for p in self.posts]) self.assertEqual(response.status_code, 200) From b3dd25a959d5de36b8da4300f251d35b1527f0f2 Mon Sep 17 00:00:00 2001 From: rafalp Date: Sat, 15 Dec 2018 19:55:46 +0100 Subject: [PATCH 41/86] Fix post merge api --- misago/threads/api/postendpoints/merge.py | 2 +- .../tests/test_thread_postmerge_api.py | 108 ++++++++++-------- 2 files changed, 60 insertions(+), 50 deletions(-) diff --git a/misago/threads/api/postendpoints/merge.py b/misago/threads/api/postendpoints/merge.py index adb3f4e49d..59afef0580 100644 --- a/misago/threads/api/postendpoints/merge.py +++ b/misago/threads/api/postendpoints/merge.py @@ -15,7 +15,7 @@ def posts_merge_endpoint(request, thread): data=request.data, context={ 'thread': thread, - 'user': request.user, + 'user_acl': request.user_acl, }, ) diff --git a/misago/threads/tests/test_thread_postmerge_api.py b/misago/threads/tests/test_thread_postmerge_api.py index 55374f12b6..8c8cb4f2e7 100644 --- a/misago/threads/tests/test_thread_postmerge_api.py +++ b/misago/threads/tests/test_thread_postmerge_api.py @@ -2,12 +2,12 @@ from django.urls import reverse -from misago.acl.testutils import override_acl from misago.categories.models import Category from misago.readtracker import poststracker from misago.threads import testutils from misago.threads.models import Post, Thread from misago.threads.serializers.moderation import POSTS_LIMIT +from misago.threads.test import patch_category_acl from misago.users.testutils import AuthenticatedUserTestCase @@ -25,28 +25,6 @@ def setUp(self): } ) - self.override_acl() - - def refresh_thread(self): - self.thread = Thread.objects.get(pk=self.thread.pk) - - def override_acl(self, extra_acl=None): - new_acl = self.user.acl_cache - new_acl['categories'][self.category.pk].update({ - 'can_see': 1, - 'can_browse': 1, - 'can_start_threads': 0, - 'can_reply_threads': 0, - 'can_edit_posts': 1, - 'can_approve_content': 0, - 'can_merge_posts': 1, - }) - - if extra_acl: - new_acl['categories'][self.category.pk].update(extra_acl) - - override_acl(self.user, new_acl) - def test_anonymous_user(self): """you need to authenticate to merge posts""" self.logout_user() @@ -61,10 +39,9 @@ def test_anonymous_user(self): "detail": "This action is not available to guests.", }) + @patch_category_acl({"can_merge_posts": False}) def test_no_permission(self): """api validates permission to merge""" - self.override_acl({'can_merge_posts': 0}) - response = self.client.post( self.api_link, json.dumps({}), @@ -75,6 +52,7 @@ def test_no_permission(self): "detail": "You can't merge posts in this thread.", }) + @patch_category_acl({"can_merge_posts": True}) def test_empty_data_json(self): """api handles empty json data""" response = self.client.post( @@ -85,6 +63,7 @@ def test_empty_data_json(self): "detail": "You have to select at least two posts to merge.", }) + @patch_category_acl({"can_merge_posts": True}) def test_empty_data_form(self): """api handles empty form data""" response = self.client.post(self.api_link, {}) @@ -93,36 +72,34 @@ def test_empty_data_form(self): "detail": "You have to select at least two posts to merge.", }) + @patch_category_acl({"can_merge_posts": True}) def test_invalid_data(self): """api handles post that is invalid type""" - self.override_acl() response = self.client.post(self.api_link, '[]', content_type="application/json") self.assertEqual(response.status_code, 400) self.assertEqual(response.json(), { "detail": "Invalid data. Expected a dictionary, but got list.", }) - self.override_acl() response = self.client.post(self.api_link, '123', content_type="application/json") self.assertEqual(response.status_code, 400) self.assertEqual(response.json(), { "detail": "Invalid data. Expected a dictionary, but got int.", }) - self.override_acl() response = self.client.post(self.api_link, '"string"', content_type="application/json") self.assertEqual(response.status_code, 400) self.assertEqual(response.json(), { "detail": "Invalid data. Expected a dictionary, but got str.", }) - self.override_acl() response = self.client.post(self.api_link, 'malformed', content_type="application/json") self.assertEqual(response.status_code, 400) self.assertEqual(response.json(), { "detail": "JSON parse error - Expecting value: line 1 column 1 (char 0)", }) + @patch_category_acl({"can_merge_posts": True}) def test_no_posts_ids(self): """api rejects no posts ids""" response = self.client.post( @@ -137,6 +114,7 @@ def test_no_posts_ids(self): "detail": "You have to select at least two posts to merge.", }) + @patch_category_acl({"can_merge_posts": True}) def test_invalid_posts_data(self): """api handles invalid data""" response = self.client.post( @@ -151,6 +129,7 @@ def test_invalid_posts_data(self): "detail": 'Expected a list of items but got type "str".', }) + @patch_category_acl({"can_merge_posts": True}) def test_invalid_posts_ids(self): """api handles invalid post id""" response = self.client.post( @@ -165,6 +144,7 @@ def test_invalid_posts_ids(self): "detail": "One or more post ids received were invalid.", }) + @patch_category_acl({"can_merge_posts": True}) def test_one_post_id(self): """api rejects one post id""" response = self.client.post( @@ -179,6 +159,7 @@ def test_one_post_id(self): "detail": "You have to select at least two posts to merge.", }) + @patch_category_acl({"can_merge_posts": True}) def test_merge_limit(self): """api rejects more posts than merge limit""" response = self.client.post( @@ -193,6 +174,7 @@ def test_merge_limit(self): "detail": "No more than %s posts can be merged at single time." % POSTS_LIMIT, }) + @patch_category_acl({"can_merge_posts": True}) def test_merge_event(self): """api recjects events""" event = testutils.reply_thread(self.thread, is_event=True, poster=self.user) @@ -209,6 +191,7 @@ def test_merge_event(self): "detail": "Events can't be merged.", }) + @patch_category_acl({"can_merge_posts": True}) def test_merge_notfound_pk(self): """api recjects nonexistant pk's""" response = self.client.post( @@ -223,6 +206,7 @@ def test_merge_notfound_pk(self): "detail": "One or more posts to merge could not be found.", }) + @patch_category_acl({"can_merge_posts": True}) def test_merge_cross_threads(self): """api recjects attempt to merge with post made in other thread""" other_thread = testutils.post_thread(category=self.category) @@ -240,6 +224,7 @@ def test_merge_cross_threads(self): "detail": "One or more posts to merge could not be found.", }) + @patch_category_acl({"can_merge_posts": True}) def test_merge_authenticated_with_guest_post(self): """api recjects attempt to merge with post made by deleted user""" other_post = testutils.reply_thread(self.thread) @@ -256,6 +241,7 @@ def test_merge_authenticated_with_guest_post(self): "detail": "Posts made by different users can't be merged.", }) + @patch_category_acl({"can_merge_posts": True}) def test_merge_guest_with_authenticated_post(self): """api recjects attempt to merge with post made by deleted user""" other_post = testutils.reply_thread(self.thread) @@ -272,6 +258,7 @@ def test_merge_guest_with_authenticated_post(self): "detail": "Posts made by different users can't be merged.", }) + @patch_category_acl({"can_merge_posts": True}) def test_merge_guest_posts_different_usernames(self): """api recjects attempt to merge posts made by different guests""" response = self.client.post( @@ -289,10 +276,9 @@ def test_merge_guest_posts_different_usernames(self): "detail": "Posts made by different users can't be merged.", }) + @patch_category_acl({"can_merge_posts": True, "can_hide_posts": 1}) def test_merge_different_visibility(self): """api recjects attempt to merge posts with different visibility""" - self.override_acl({'can_hide_posts': 1}) - response = self.client.post( self.api_link, json.dumps({ @@ -308,10 +294,9 @@ def test_merge_different_visibility(self): "detail": "Posts with different visibility can't be merged.", }) + @patch_category_acl({"can_merge_posts": True, "can_approve_content": True}) def test_merge_different_approval(self): """api recjects attempt to merge posts with different approval""" - self.override_acl({'can_approve_content': 1}) - response = self.client.post( self.api_link, json.dumps({ @@ -327,7 +312,8 @@ def test_merge_different_approval(self): "detail": "Posts with different visibility can't be merged.", }) - def test_closed_thread(self): + @patch_category_acl({"can_merge_posts": True, "can_close_threads": False}) + def test_closed_thread_no_permission(self): """api validates permission to merge in closed thread""" self.thread.is_closed = True self.thread.save() @@ -347,8 +333,16 @@ def test_closed_thread(self): "detail": "This thread is closed. You can't merge posts in it.", }) - # allow closing threads - self.override_acl({'can_close_threads': 1}) + @patch_category_acl({"can_merge_posts": True, "can_close_threads": True}) + def test_closed_thread(self): + """api validates permission to merge in closed thread""" + self.thread.is_closed = True + self.thread.save() + + posts = [ + testutils.reply_thread(self.thread, poster=self.user).pk, + testutils.reply_thread(self.thread, poster=self.user).pk, + ] response = self.client.post( self.api_link, @@ -357,7 +351,8 @@ def test_closed_thread(self): ) self.assertEqual(response.status_code, 200) - def test_closed_category(self): + @patch_category_acl({"can_merge_posts": True, "can_close_threads": False}) + def test_closed_category_no_permission(self): """api validates permission to merge in closed category""" self.category.is_closed = True self.category.save() @@ -377,8 +372,16 @@ def test_closed_category(self): "detail": "This category is closed. You can't merge posts in it.", }) - # allow closing threads - self.override_acl({'can_close_threads': 1}) + @patch_category_acl({"can_merge_posts": True, "can_close_threads": True}) + def test_closed_category(self): + """api validates permission to merge in closed category""" + self.category.is_closed = True + self.category.save() + + posts = [ + testutils.reply_thread(self.thread, poster=self.user).pk, + testutils.reply_thread(self.thread, poster=self.user).pk, + ] response = self.client.post( self.api_link, @@ -387,6 +390,7 @@ def test_closed_category(self): ) self.assertEqual(response.status_code, 200) + @patch_category_acl({"can_merge_posts": True}) def test_merge_best_answer_first_post(self): """api recjects attempt to merge best_answer with first post""" self.thread.first_post.poster = self.user @@ -413,6 +417,7 @@ def test_merge_best_answer_first_post(self): "detail": "Post marked as best answer can't be merged with thread's first post.", }) + @patch_category_acl({"can_merge_posts": True}) def test_merge_posts(self): """api merges two posts""" post_a = testutils.reply_thread(self.thread, poster=self.user, message="Battęry") @@ -429,7 +434,7 @@ def test_merge_posts(self): ) self.assertEqual(response.status_code, 200) - self.refresh_thread() + self.thread.refresh_from_db() self.assertEqual(self.thread.replies, thread_replies - 1) with self.assertRaises(Post.DoesNotExist): @@ -438,6 +443,7 @@ def test_merge_posts(self): merged_post = Post.objects.get(pk=post_a.pk) self.assertEqual(merged_post.parsed, '%s\n%s' % (post_a.parsed, post_b.parsed)) + @patch_category_acl({"can_merge_posts": True}) def test_merge_guest_posts(self): """api recjects attempt to merge posts made by same guest""" response = self.client.post( @@ -452,10 +458,9 @@ def test_merge_guest_posts(self): ) self.assertEqual(response.status_code, 200) + @patch_category_acl({"can_merge_posts": True, 'can_hide_posts': 1}) def test_merge_hidden_posts(self): """api merges two hidden posts""" - self.override_acl({'can_hide_posts': 1}) - response = self.client.post( self.api_link, json.dumps({ @@ -468,10 +473,9 @@ def test_merge_hidden_posts(self): ) self.assertEqual(response.status_code, 200) + @patch_category_acl({"can_merge_posts": True, 'can_approve_content': True}) def test_merge_unapproved_posts(self): """api merges two unapproved posts""" - self.override_acl({'can_approve_content': 1}) - response = self.client.post( self.api_link, json.dumps({ @@ -484,6 +488,7 @@ def test_merge_unapproved_posts(self): ) self.assertEqual(response.status_code, 200) + @patch_category_acl({"can_merge_posts": True, 'can_hide_threads': True}) def test_merge_with_hidden_thread(self): """api excludes thread's first post from visibility checks""" self.thread.first_post.is_hidden = True @@ -492,8 +497,6 @@ def test_merge_with_hidden_thread(self): post_visible = testutils.reply_thread(self.thread, poster=self.user, is_hidden=False) - self.override_acl({'can_hide_threads': 1}) - response = self.client.post( self.api_link, json.dumps({ @@ -503,6 +506,7 @@ def test_merge_with_hidden_thread(self): ) self.assertEqual(response.status_code, 200) + @patch_category_acl({"can_merge_posts": True}) def test_merge_protected(self): """api preserves protected status after merge""" response = self.client.post( @@ -520,6 +524,7 @@ def test_merge_protected(self): merged_post = self.thread.post_set.order_by('-id')[0] self.assertTrue(merged_post.is_protected) + @patch_category_acl({"can_merge_posts": True}) def test_merge_best_answer(self): """api merges best answer with other post""" best_answer = testutils.reply_thread(self.thread, poster="Bob") @@ -539,9 +544,10 @@ def test_merge_best_answer(self): ) self.assertEqual(response.status_code, 200) - self.refresh_thread() + self.thread.refresh_from_db() self.assertEqual(self.thread.best_answer, best_answer) + @patch_category_acl({"can_merge_posts": True}) def test_merge_best_answer_in(self): """api merges best answer into other post""" other_post = testutils.reply_thread(self.thread, poster="Bob") @@ -562,9 +568,10 @@ def test_merge_best_answer_in(self): ) self.assertEqual(response.status_code, 200) - self.refresh_thread() + self.thread.refresh_from_db() self.assertEqual(self.thread.best_answer, other_post) + @patch_category_acl({"can_merge_posts": True}) def test_merge_best_answer_in_protected(self): """api merges best answer into protected post""" best_answer = testutils.reply_thread(self.thread, poster="Bob") @@ -584,11 +591,14 @@ def test_merge_best_answer_in_protected(self): ) self.assertEqual(response.status_code, 200) - self.refresh_thread() + self.thread.refresh_from_db() self.assertEqual(self.thread.best_answer, best_answer) + + self.thread.best_answer.refresh_from_db() self.assertTrue(self.thread.best_answer.is_protected) self.assertTrue(self.thread.best_answer_is_protected) + @patch_category_acl({"can_merge_posts": True}) def test_merge_remove_reads(self): """two posts merge removes read tracker from post""" post_a = testutils.reply_thread(self.thread, poster=self.user, message="Battęry") From 489741ba2cb23c8f56ca8b0ae1e352636b30881e Mon Sep 17 00:00:00 2001 From: rafalp Date: Sat, 15 Dec 2018 20:26:59 +0100 Subject: [PATCH 42/86] Fix post deletion --- .../tests/test_thread_postdelete_api.py | 118 ++++++++---------- 1 file changed, 54 insertions(+), 64 deletions(-) diff --git a/misago/threads/tests/test_thread_postdelete_api.py b/misago/threads/tests/test_thread_postdelete_api.py index 39d029608e..8f0feec655 100644 --- a/misago/threads/tests/test_thread_postdelete_api.py +++ b/misago/threads/tests/test_thread_postdelete_api.py @@ -5,6 +5,7 @@ from misago.threads import testutils from misago.threads.models import Post, Thread +from misago.threads.test import patch_category_acl from .test_threads_api import ThreadsApiTestCase @@ -33,24 +34,22 @@ def test_delete_anonymous(self): "detail": "This action is not available to guests.", }) + @patch_category_acl({'can_hide_posts': 1, 'can_hide_own_posts': 1}) def test_no_permission(self): """api validates permission to delete post""" - self.override_acl({'can_hide_own_posts': 1, 'can_hide_posts': 1}) - response = self.client.delete(self.api_link) self.assertEqual(response.status_code, 403) self.assertEqual(response.json(), { "detail": "You can't delete posts in this category.", }) + @patch_category_acl({ + 'can_hide_posts': 1, + 'can_hide_own_posts': 2, + 'post_edit_time': 0, + }) def test_delete_other_user_post_no_permission(self): """api valdiates if user can delete other users posts""" - self.override_acl({ - 'post_edit_time': 0, - 'can_hide_own_posts': 2, - 'can_hide_posts': 0, - }) - self.post.poster = None self.post.save() @@ -60,14 +59,13 @@ def test_delete_other_user_post_no_permission(self): "detail": "You can't delete other users posts in this category.", }) + @patch_category_acl({ + 'can_hide_posts': 1, + 'can_hide_own_posts': 2, + 'post_edit_time': 0, + }) def test_delete_protected_post_no_permission(self): """api validates if user can delete protected post""" - self.override_acl({ - 'can_protect_posts': 0, - 'can_hide_own_posts': 2, - 'can_hide_posts': 0, - }) - self.post.is_protected = True self.post.save() @@ -77,14 +75,13 @@ def test_delete_protected_post_no_permission(self): "detail": "This post is protected. You can't delete it.", }) + @patch_category_acl({ + 'can_hide_posts': 1, + 'can_hide_own_posts': 2, + 'post_edit_time': 1, + }) def test_delete_protected_post_after_edit_time(self): """api validates if user can delete delete post after edit time""" - self.override_acl({ - 'post_edit_time': 1, - 'can_hide_own_posts': 2, - 'can_hide_posts': 0, - }) - self.post.posted_on = timezone.now() - timedelta(minutes=10) self.post.save() @@ -94,13 +91,14 @@ def test_delete_protected_post_after_edit_time(self): "detail": "You can't delete posts that are older than 1 minute.", }) + @patch_category_acl({ + 'can_hide_posts': 0, + 'can_hide_own_posts': 2, + 'post_edit_time': 0, + 'can_close_threads': False, + }) def test_delete_post_closed_thread_no_permission(self): """api valdiates if user can delete posts in closed threads""" - self.override_acl({ - 'can_hide_own_posts': 2, - 'can_hide_posts': 0, - }) - self.thread.is_closed = True self.thread.save() @@ -110,13 +108,14 @@ def test_delete_post_closed_thread_no_permission(self): "detail": "This thread is closed. You can't delete posts in it.", }) + @patch_category_acl({ + 'can_hide_posts': 0, + 'can_hide_own_posts': 2, + 'post_edit_time': 0, + 'can_close_threads': False, + }) def test_delete_post_closed_category_no_permission(self): """api valdiates if user can delete posts in closed categories""" - self.override_acl({ - 'can_hide_own_posts': 2, - 'can_hide_posts': 0, - }) - self.category.is_closed = True self.category.save() @@ -126,10 +125,9 @@ def test_delete_post_closed_category_no_permission(self): "detail": "This category is closed. You can't delete posts in it.", }) + @patch_category_acl({'can_hide_posts': 2, 'can_hide_own_posts': 2}) def test_delete_first_post(self): """api disallows first post deletion""" - self.override_acl({'can_hide_own_posts': 2, 'can_hide_posts': 2}) - api_link = reverse( 'misago:api:thread-post-detail', kwargs={ @@ -144,10 +142,9 @@ def test_delete_first_post(self): "detail": "You can't delete thread's first post.", }) + @patch_category_acl({'can_hide_posts': 2, 'can_hide_own_posts': 2}) def test_delete_best_answer(self): """api disallows best answer deletion""" - self.override_acl({'can_hide_own_posts': 2, 'can_hide_posts': 2}) - self.thread.set_best_answer(self.user, self.post) self.thread.save() @@ -157,14 +154,13 @@ def test_delete_best_answer(self): 'detail': "You can't delete this post because its marked as best answer.", }) + @patch_category_acl({ + 'can_hide_posts': 0, + 'can_hide_own_posts': 2, + 'post_edit_time': 0 + }) def test_delete_owned_post(self): """api deletes owned thread post""" - self.override_acl({ - 'post_edit_time': 0, - 'can_hide_own_posts': 2, - 'can_hide_posts': 0, - }) - response = self.client.delete(self.api_link) self.assertEqual(response.status_code, 200) @@ -174,10 +170,9 @@ def test_delete_owned_post(self): with self.assertRaises(Post.DoesNotExist): self.thread.post_set.get(pk=self.post.pk) + @patch_category_acl({'can_hide_posts': 2, 'can_hide_own_posts': 0}) def test_delete_post(self): """api deletes thread post""" - self.override_acl({'can_hide_own_posts': 0, 'can_hide_posts': 2}) - response = self.client.delete(self.api_link) self.assertEqual(response.status_code, 200) @@ -212,27 +207,27 @@ def test_delete_anonymous(self): "detail": "This action is not available to guests.", }) + @patch_category_acl({ + 'can_hide_posts': 2, + 'can_hide_own_posts': 0, + 'can_hide_events': 0, + }) def test_no_permission(self): """api validates permission to delete event""" - self.override_acl({ - 'can_hide_own_posts': 2, - 'can_hide_posts': 2, - 'can_hide_events': 0, - }) - response = self.client.delete(self.api_link) self.assertEqual(response.status_code, 403) self.assertEqual(response.json(), { "detail": "You can't delete events in this category.", }) + @patch_category_acl({ + 'can_hide_posts': 2, + 'can_hide_own_posts': 0, + 'can_hide_events': 2, + 'can_close_threads': False, + }) def test_delete_event_closed_thread_no_permission(self): """api valdiates if user can delete events in closed threads""" - self.override_acl({ - 'can_hide_events': 2, - 'can_close_threads': 0, - }) - self.thread.is_closed = True self.thread.save() @@ -242,13 +237,13 @@ def test_delete_event_closed_thread_no_permission(self): "detail": "This thread is closed. You can't delete events in it.", }) + @patch_category_acl({ + 'can_hide_posts': 2, + 'can_hide_events': 2, + 'can_close_threads': False, + }) def test_delete_event_closed_category_no_permission(self): """api valdiates if user can delete events in closed categories""" - self.override_acl({ - 'can_hide_events': 2, - 'can_close_threads': 0, - }) - self.category.is_closed = True self.category.save() @@ -258,14 +253,9 @@ def test_delete_event_closed_category_no_permission(self): "detail": "This category is closed. You can't delete events in it.", }) + @patch_category_acl({'can_hide_posts': 0, 'can_hide_events': 2}) def test_delete_event(self): """api differs posts from events""" - self.override_acl({ - 'can_hide_own_posts': 0, - 'can_hide_posts': 0, - 'can_hide_events': 2, - }) - response = self.client.delete(self.api_link) self.assertEqual(response.status_code, 200) From 2f0befd230a092b14171697fbc02b540466cc748 Mon Sep 17 00:00:00 2001 From: rafalp Date: Sat, 15 Dec 2018 20:41:34 +0100 Subject: [PATCH 43/86] Fix posts moving --- misago/threads/serializers/moderation.py | 2 +- .../threads/tests/test_thread_postmove_api.py | 144 ++++++------------ 2 files changed, 51 insertions(+), 95 deletions(-) diff --git a/misago/threads/serializers/moderation.py b/misago/threads/serializers/moderation.py index 58fc05ca37..f2ddeb1c46 100644 --- a/misago/threads/serializers/moderation.py +++ b/misago/threads/serializers/moderation.py @@ -223,7 +223,7 @@ def validate_posts(self, data): request = self.context['request'] thread = self.context['thread'] - posts_queryset = exclude_invisible_posts(request.user, thread.category, thread.post_set) + posts_queryset = exclude_invisible_posts(request.user_acl, thread.category, thread.post_set) posts_queryset = posts_queryset.filter(id__in=data).order_by('id') posts = [] diff --git a/misago/threads/tests/test_thread_postmove_api.py b/misago/threads/tests/test_thread_postmove_api.py index 9d919b9520..c4b5193e01 100644 --- a/misago/threads/tests/test_thread_postmove_api.py +++ b/misago/threads/tests/test_thread_postmove_api.py @@ -2,12 +2,12 @@ from django.urls import reverse -from misago.acl.testutils import override_acl from misago.categories.models import Category from misago.readtracker import poststracker from misago.threads import testutils from misago.threads.models import Thread from misago.threads.serializers.moderation import POSTS_LIMIT +from misago.threads.test import patch_category_acl, patch_other_category_acl from misago.users.testutils import AuthenticatedUserTestCase @@ -25,66 +25,14 @@ def setUp(self): ) Category( - name='Category B', - slug='category-b', + name='Other category', + slug='other-category', ).insert_at( self.category, position='last-child', save=True, ) - self.category_b = Category.objects.get(slug='category-b') - - self.override_acl() - self.override_other_acl() - - def refresh_thread(self): - self.thread = Thread.objects.get(pk=self.thread.pk) - - def override_acl(self, extra_acl=None): - new_acl = self.user.acl_cache - new_acl['categories'][self.category.pk].update({ - 'can_see': 1, - 'can_browse': 1, - 'can_start_threads': 1, - 'can_reply_threads': 1, - 'can_edit_posts': 1, - 'can_approve_content': 0, - 'can_move_posts': 1, - }) - - if extra_acl: - new_acl['categories'][self.category.pk].update(extra_acl) - - override_acl(self.user, new_acl) - - def override_other_acl(self, extra_acl=None): - other_category_acl = self.user.acl_cache['categories'][self.category.pk].copy() - other_category_acl.update({ - 'can_see': 1, - 'can_browse': 1, - 'can_start_threads': 0, - 'can_reply_threads': 0, - 'can_edit_posts': 1, - 'can_approve_content': 0, - 'can_move_posts': 1, - }) - - if extra_acl: - other_category_acl.update(extra_acl) - - categories_acl = self.user.acl_cache['categories'] - categories_acl[self.category_b.pk] = other_category_acl - - visible_categories = [self.category.pk] - if other_category_acl['can_see']: - visible_categories.append(self.category_b.pk) - - override_acl( - self.user, { - 'visible_categories': visible_categories, - 'categories': categories_acl, - } - ) + self.other_category = Category.objects.get(slug='other-category') def test_anonymous_user(self): """you need to authenticate to move posts""" @@ -96,46 +44,43 @@ def test_anonymous_user(self): "detail": "This action is not available to guests.", }) + @patch_category_acl({"can_move_posts": True}) def test_invalid_data(self): """api handles post that is invalid type""" - self.override_acl() response = self.client.post(self.api_link, '[]', content_type="application/json") self.assertEqual(response.status_code, 400) self.assertEqual(response.json(), { "detail": "Invalid data. Expected a dictionary, but got list.", }) - self.override_acl() response = self.client.post(self.api_link, '123', content_type="application/json") self.assertEqual(response.status_code, 400) self.assertEqual(response.json(), { "detail": "Invalid data. Expected a dictionary, but got int.", }) - self.override_acl() response = self.client.post(self.api_link, '"string"', content_type="application/json") self.assertEqual(response.status_code, 400) self.assertEqual(response.json(), { "detail": "Invalid data. Expected a dictionary, but got str.", }) - self.override_acl() response = self.client.post(self.api_link, 'malformed', content_type="application/json") self.assertEqual(response.status_code, 400) self.assertEqual(response.json(), { "detail": "JSON parse error - Expecting value: line 1 column 1 (char 0)", }) + @patch_category_acl({"can_move_posts": False}) def test_no_permission(self): """api validates permission to move""" - self.override_acl({'can_move_posts': 0}) - response = self.client.post(self.api_link, json.dumps({}), content_type="application/json") self.assertEqual(response.status_code, 403) self.assertEqual(response.json(), { "detail": "You can't move posts in this thread.", }) + @patch_category_acl({"can_move_posts": True}) def test_move_no_new_thread_url(self): """api validates if new thread url was given""" response = self.client.post(self.api_link) @@ -144,6 +89,7 @@ def test_move_no_new_thread_url(self): "detail": "Enter link to new thread.", }) + @patch_category_acl({"can_move_posts": True}) def test_invalid_new_thread_url(self): """api validates new thread url""" response = self.client.post(self.api_link, { @@ -154,6 +100,7 @@ def test_invalid_new_thread_url(self): "detail": "This is not a valid thread link.", }) + @patch_category_acl({"can_move_posts": True}) def test_current_new_thread_url(self): """api validates if new thread url points to current thread""" response = self.client.post( @@ -166,16 +113,14 @@ def test_current_new_thread_url(self): "detail": "Thread to move posts to is same as current one.", }) + @patch_other_category_acl({"can_see": False}) + @patch_category_acl({"can_move_posts": True}) def test_other_thread_exists(self): """api validates if other thread exists""" - self.override_other_acl() - - other_thread = testutils.post_thread(self.category_b) - other_new_thread = other_thread.get_absolute_url() - other_thread.delete() + other_thread = testutils.post_thread(self.other_category) response = self.client.post(self.api_link, { - 'new_thread': other_new_thread, + 'new_thread': other_thread.get_absolute_url(), }) self.assertEqual(response.status_code, 400) self.assertEqual(response.json(), { @@ -185,11 +130,11 @@ def test_other_thread_exists(self): ), }) + @patch_other_category_acl({"can_browse": False}) + @patch_category_acl({"can_move_posts": True}) def test_other_thread_is_invisible(self): """api validates if other thread is visible""" - self.override_other_acl({'can_see': 0}) - - other_thread = testutils.post_thread(self.category_b) + other_thread = testutils.post_thread(self.other_category) response = self.client.post( self.api_link, { @@ -204,11 +149,11 @@ def test_other_thread_is_invisible(self): ), }) + @patch_other_category_acl({"can_reply_threads": False}) + @patch_category_acl({"can_move_posts": True}) def test_other_thread_isnt_replyable(self): """api validates if other thread can be replied""" - self.override_other_acl({'can_reply_threads': 0}) - - other_thread = testutils.post_thread(self.category_b) + other_thread = testutils.post_thread(self.other_category) response = self.client.post( self.api_link, { @@ -220,6 +165,7 @@ def test_other_thread_isnt_replyable(self): "detail": "You can't move posts to threads you can't reply.", }) + @patch_category_acl({"can_move_posts": True}) def test_empty_data(self): """api handles empty data""" other_thread = testutils.post_thread(self.category) @@ -230,6 +176,7 @@ def test_empty_data(self): "detail": "Enter link to new thread.", }) + @patch_category_acl({"can_move_posts": True}) def test_empty_posts_data_json(self): """api handles empty json data""" other_thread = testutils.post_thread(self.category) @@ -246,6 +193,7 @@ def test_empty_posts_data_json(self): "detail": "You have to specify at least one post to move.", }) + @patch_category_acl({"can_move_posts": True}) def test_empty_posts_data_form(self): """api handles empty form data""" other_thread = testutils.post_thread(self.category) @@ -261,6 +209,7 @@ def test_empty_posts_data_form(self): "detail": "You have to specify at least one post to move.", }) + @patch_category_acl({"can_move_posts": True}) def test_no_posts_ids(self): """api rejects no posts ids""" other_thread = testutils.post_thread(self.category) @@ -278,6 +227,7 @@ def test_no_posts_ids(self): "detail": "You have to specify at least one post to move.", }) + @patch_category_acl({"can_move_posts": True}) def test_invalid_posts_data(self): """api handles invalid data""" other_thread = testutils.post_thread(self.category) @@ -295,6 +245,7 @@ def test_invalid_posts_data(self): "detail": 'Expected a list of items but got type "str".', }) + @patch_category_acl({"can_move_posts": True}) def test_invalid_posts_ids(self): """api handles invalid post id""" other_thread = testutils.post_thread(self.category) @@ -312,6 +263,7 @@ def test_invalid_posts_ids(self): "detail": "One or more post ids received were invalid.", }) + @patch_category_acl({"can_move_posts": True}) def test_move_limit(self): """api rejects more posts than move limit""" other_thread = testutils.post_thread(self.category) @@ -329,6 +281,7 @@ def test_move_limit(self): "detail": "No more than %s posts can be moved at single time." % POSTS_LIMIT, }) + @patch_category_acl({"can_move_posts": True}) def test_move_invisible(self): """api validates posts visibility""" other_thread = testutils.post_thread(self.category) @@ -346,6 +299,7 @@ def test_move_invisible(self): "detail": "One or more posts to move could not be found.", }) + @patch_category_acl({"can_move_posts": True}) def test_move_other_thread_posts(self): """api recjects attempt to move other thread's post""" other_thread = testutils.post_thread(self.category) @@ -363,6 +317,7 @@ def test_move_other_thread_posts(self): "detail": "One or more posts to move could not be found.", }) + @patch_category_acl({"can_move_posts": True}) def test_move_event(self): """api rejects events move""" other_thread = testutils.post_thread(self.category) @@ -380,6 +335,7 @@ def test_move_event(self): "detail": "Events can't be moved.", }) + @patch_category_acl({"can_move_posts": True}) def test_move_first_post(self): """api rejects first post move""" other_thread = testutils.post_thread(self.category) @@ -397,6 +353,7 @@ def test_move_first_post(self): "detail": "You can't move thread's first post.", }) + @patch_category_acl({"can_move_posts": True}) def test_move_hidden_posts(self): """api recjects attempt to move urneadable hidden post""" other_thread = testutils.post_thread(self.category) @@ -414,6 +371,7 @@ def test_move_hidden_posts(self): "detail": "You can't move posts the content you can't see.", }) + @patch_category_acl({"can_move_posts": True, "can_close_threads": False}) def test_move_posts_closed_thread_no_permission(self): """api recjects attempt to move posts from closed thread""" other_thread = testutils.post_thread(self.category) @@ -421,8 +379,6 @@ def test_move_posts_closed_thread_no_permission(self): self.thread.is_closed = True self.thread.save() - self.override_acl({'can_close_threads': 0}) - response = self.client.post( self.api_link, json.dumps({ @@ -436,16 +392,15 @@ def test_move_posts_closed_thread_no_permission(self): "detail": "This thread is closed. You can't move posts in it.", }) + @patch_other_category_acl({"can_reply_threads": True, "can_close_threads": False}) + @patch_category_acl({"can_move_posts": True}) def test_move_posts_closed_category_no_permission(self): """api recjects attempt to move posts from closed thread""" - other_thread = testutils.post_thread(self.category_b) + other_thread = testutils.post_thread(self.other_category) self.category.is_closed = True self.category.save() - self.override_acl({'can_close_threads': 0}) - self.override_other_acl({'can_reply_threads': 1}) - response = self.client.post( self.api_link, json.dumps({ @@ -459,11 +414,11 @@ def test_move_posts_closed_category_no_permission(self): "detail": "This category is closed. You can't move posts in it.", }) + @patch_other_category_acl({"can_reply_threads": True}) + @patch_category_acl({"can_move_posts": True}) def test_move_posts(self): """api moves posts to other thread""" - self.override_other_acl({'can_reply_threads': 1}) - - other_thread = testutils.post_thread(self.category_b) + other_thread = testutils.post_thread(self.other_category) posts = ( testutils.reply_thread(self.thread).pk, @@ -472,7 +427,7 @@ def test_move_posts(self): testutils.reply_thread(self.thread).pk, ) - self.refresh_thread() + self.thread.refresh_from_db() self.assertEqual(self.thread.replies, 4) response = self.client.post( @@ -486,25 +441,25 @@ def test_move_posts(self): self.assertEqual(response.status_code, 200) # replies were moved - self.refresh_thread() + self.thread.refresh_from_db() self.assertEqual(self.thread.replies, 0) other_thread = Thread.objects.get(pk=other_thread.pk) self.assertEqual(other_thread.post_set.filter(pk__in=posts).count(), 4) self.assertEqual(other_thread.replies, 4) + @patch_other_category_acl({"can_reply_threads": True}) + @patch_category_acl({"can_move_posts": True}) def test_move_best_answer(self): """api moves best answer to other thread""" - self.override_other_acl({'can_reply_threads': 1}) - - other_thread = testutils.post_thread(self.category_b) + other_thread = testutils.post_thread(self.other_category) best_answer = testutils.reply_thread(self.thread) self.thread.set_best_answer(self.user, best_answer) self.thread.synchronize() self.thread.save() - self.refresh_thread() + self.thread.refresh_from_db() self.assertEqual(self.thread.best_answer, best_answer) self.assertEqual(self.thread.replies, 1) @@ -519,7 +474,7 @@ def test_move_best_answer(self): self.assertEqual(response.status_code, 200) # best_answer was moved and unmarked - self.refresh_thread() + self.thread.refresh_from_db() self.assertEqual(self.thread.replies, 0) self.assertIsNone(self.thread.best_answer) @@ -527,18 +482,19 @@ def test_move_best_answer(self): self.assertEqual(other_thread.replies, 1) self.assertIsNone(other_thread.best_answer) + + @patch_other_category_acl({"can_reply_threads": True}) + @patch_category_acl({"can_move_posts": True}) def test_move_posts_reads(self): """api moves posts reads together with posts""" - self.override_other_acl({'can_reply_threads': 1}) - - other_thread = testutils.post_thread(self.category_b) + other_thread = testutils.post_thread(self.other_category) posts = ( testutils.reply_thread(self.thread), testutils.reply_thread(self.thread), ) - self.refresh_thread() + self.thread.refresh_from_db() self.assertEqual(self.thread.replies, 2) poststracker.save_read(self.user, self.thread.first_post) From e35302775a8306f83f922208c95ed4d3e9c65e33 Mon Sep 17 00:00:00 2001 From: rafalp Date: Sat, 15 Dec 2018 20:50:39 +0100 Subject: [PATCH 44/86] Fix attachments api tests --- misago/threads/tests/test_attachments_api.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/misago/threads/tests/test_attachments_api.py b/misago/threads/tests/test_attachments_api.py index b0baeafb8f..ac9baafec4 100644 --- a/misago/threads/tests/test_attachments_api.py +++ b/misago/threads/tests/test_attachments_api.py @@ -5,12 +5,11 @@ from django.urls import reverse from misago.acl.models import Role -from misago.acl.testutils import override_acl +from misago.acl.test import patch_user_acl from misago.conf import settings from misago.threads.models import Attachment, AttachmentType from misago.users.testutils import AuthenticatedUserTestCase - TESTFILES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'testfiles') TEST_DOCUMENT_PATH = os.path.join(TESTFILES_DIR, 'document.pdf') TEST_LARGEPNG_PATH = os.path.join(TESTFILES_DIR, 'large.png') @@ -27,12 +26,6 @@ def setUp(self): self.api_link = reverse('misago:api:attachment-list') - def override_acl(self, new_acl=None): - if new_acl: - acl = self.user.acl_cache.copy() - acl.update(new_acl) - override_acl(self.user, acl) - def test_anonymous(self): """user has to be authenticated to be able to upload files""" self.logout_user() @@ -40,10 +33,9 @@ def test_anonymous(self): response = self.client.post(self.api_link) self.assertEqual(response.status_code, 403) + @patch_user_acl({"max_attachment_size": 0}) def test_no_permission(self): """user needs permission to upload files""" - self.override_acl({'max_attachment_size': 0}) - response = self.client.post(self.api_link) self.assertEqual(response.status_code, 403) self.assertEqual(response.json(), { @@ -181,10 +173,9 @@ def test_upload_too_big_for_type(self): ), }) + @patch_user_acl({"max_attachment_size": 100}) def test_upload_too_big_for_user(self): """too big uploads are rejected""" - self.override_acl({'max_attachment_size': 100}) - AttachmentType.objects.create( name="Test extension", extensions='png', @@ -302,10 +293,9 @@ def test_small_image_upload(self): self.assertEqual(self.user.audittrail_set.count(), 1) + @patch_user_acl({"max_attachment_size": 10 * 1024}) def test_large_image_upload(self): """successful large image upload creates orphan attachment with thumbnail""" - self.override_acl({'max_attachment_size': 10 * 1024}) - AttachmentType.objects.create( name="Test extension", extensions='png', From a0bd67c0cbcb5eaf75cc3d964b6df10beb1cd0cc Mon Sep 17 00:00:00 2001 From: rafalp Date: Sat, 15 Dec 2018 21:13:26 +0100 Subject: [PATCH 45/86] Fix attachments --- .../api/postingendpoint/attachments.py | 9 +-- .../tests/test_attachments_middleware.py | 68 +++++++++++++------ misago/threads/tests/test_attachmentview.py | 48 +++++++------ 3 files changed, 81 insertions(+), 44 deletions(-) diff --git a/misago/threads/api/postingendpoint/attachments.py b/misago/threads/api/postingendpoint/attachments.py index 666359d1c3..6ae74bb303 100644 --- a/misago/threads/api/postingendpoint/attachments.py +++ b/misago/threads/api/postingendpoint/attachments.py @@ -12,7 +12,7 @@ class AttachmentsMiddleware(PostingMiddleware): def use_this_middleware(self): - return bool(self.user.acl_cache['max_attachment_size']) + return bool(self.user_acl['max_attachment_size']) def get_serializer(self): return AttachmentsSerializer( @@ -20,6 +20,7 @@ def get_serializer(self): context={ 'mode': self.mode, 'user': self.user, + 'user_acl': self.user_acl, 'post': self.post, } ) @@ -41,7 +42,7 @@ def validate_attachments(self, ids): validate_attachments_count(ids) attachments = self.get_initial_attachments( - self.context['mode'], self.context['user'], self.context['post'] + self.context['mode'], self.context['user_acl'], self.context['post'] ) new_attachments = self.get_new_attachments(self.context['user'], ids) @@ -69,12 +70,12 @@ def validate_attachments(self, ids): self.final_attachments += new_attachments self.final_attachments.sort(key=lambda a: a.pk, reverse=True) - def get_initial_attachments(self, mode, user, post): + def get_initial_attachments(self, mode, user_acl, post): attachments = [] if mode == PostingEndpoint.EDIT: queryset = post.attachment_set.select_related('filetype') attachments = list(queryset) - add_acl(user, attachments) + add_acl(user_acl, attachments) return attachments def get_new_attachments(self, user, ids): diff --git a/misago/threads/tests/test_attachments_middleware.py b/misago/threads/tests/test_attachments_middleware.py index 63e36db7cd..f160151f98 100644 --- a/misago/threads/tests/test_attachments_middleware.py +++ b/misago/threads/tests/test_attachments_middleware.py @@ -1,6 +1,7 @@ from rest_framework import serializers -from misago.acl.testutils import override_acl +from misago.acl import useracl +from misago.acl.test import patch_user_acl from misago.categories.models import Category from misago.conf import settings from misago.threads import testutils @@ -15,6 +16,14 @@ class RequestMock(object): def __init__(self, data=None): self.data = data or {} +cache_versions = {"acl": "abcdefgh"} + + +def patch_attachments_acl(acl_patch=None): + acl_patch = acl_patch or {} + acl_patch.setdefault("max_attachment_size", 1024) + return patch_user_acl(acl_patch) + class AttachmentsMiddlewareTests(AuthenticatedUserTestCase): def setUp(self): @@ -26,12 +35,8 @@ def setUp(self): self.post.update_fields = [] - self.override_acl() self.filetype = AttachmentType.objects.order_by('id').last() - def override_acl(self, new_acl=None): - override_acl(self.user, new_acl or {'max_attachment_size': 1024}) - def mock_attachment(self, user=True, post=None): return Attachment.objects.create( secret=Attachment.generate_new_secret(), @@ -46,35 +51,42 @@ def mock_attachment(self, user=True, post=None): def test_use_this_middleware(self): """use_this_middleware returns False if we can't upload attachments""" - middleware = AttachmentsMiddleware(user=self.user) - - self.override_acl({'max_attachment_size': 0}) - - self.assertFalse(middleware.use_this_middleware()) + with patch_user_acl({'max_attachment_size': 0}): + user_acl = useracl.get_user_acl(self.user, cache_versions) + middleware = AttachmentsMiddleware(user=self.user, user_acl=user_acl) + self.assertFalse(middleware.use_this_middleware()) - self.override_acl({'max_attachment_size': 1024}) - - self.assertTrue(middleware.use_this_middleware()) + with patch_user_acl({'max_attachment_size': 1024}): + user_acl = useracl.get_user_acl(self.user, cache_versions) + middleware = AttachmentsMiddleware(user=self.user, user_acl=user_acl) + self.assertTrue(middleware.use_this_middleware()) + @patch_attachments_acl() def test_middleware_is_optional(self): """middleware is optional""" INPUTS = [{}, {'attachments': []}] + user_acl = useracl.get_user_acl(self.user, cache_versions) + for test_input in INPUTS: middleware = AttachmentsMiddleware( request=RequestMock(test_input), mode=PostingEndpoint.START, user=self.user, + user_acl=user_acl, post=self.post, ) serializer = middleware.get_serializer() self.assertTrue(serializer.is_valid()) + @patch_attachments_acl() def test_middleware_validates_ids(self): """middleware validates attachments ids""" INPUTS = ['none', ['a', 'b', 123], range(settings.MISAGO_POST_ATTACHMENTS_LIMIT + 1)] + user_acl = useracl.get_user_acl(self.user, cache_versions) + for test_input in INPUTS: middleware = AttachmentsMiddleware( request=RequestMock({ @@ -82,18 +94,22 @@ def test_middleware_validates_ids(self): }), mode=PostingEndpoint.START, user=self.user, + user_acl=user_acl, post=self.post, ) serializer = middleware.get_serializer() self.assertFalse(serializer.is_valid(), "%r shouldn't validate" % test_input) + @patch_attachments_acl() def test_get_initial_attachments(self): """get_initial_attachments returns list of attachments already existing on post""" + user_acl = useracl.get_user_acl(self.user, cache_versions) middleware = AttachmentsMiddleware( request=RequestMock(), mode=PostingEndpoint.EDIT, user=self.user, + user_acl=user_acl, post=self.post, ) @@ -106,16 +122,19 @@ def test_get_initial_attachments(self): attachment = self.mock_attachment(post=self.post) attachments = serializer.get_initial_attachments( - middleware.mode, middleware.user, middleware.post + middleware.mode, middleware.user_acl, middleware.post ) self.assertEqual(attachments, [attachment]) + @patch_attachments_acl() def test_get_new_attachments(self): """get_initial_attachments returns list of attachments already existing on post""" + user_acl = useracl.get_user_acl(self.user, cache_versions) middleware = AttachmentsMiddleware( request=RequestMock(), mode=PostingEndpoint.EDIT, user=self.user, + user_acl=user_acl, post=self.post, ) @@ -133,27 +152,27 @@ def test_get_new_attachments(self): attachments = serializer.get_new_attachments(middleware.user, [other_user_attachment.pk]) self.assertEqual(attachments, []) + + @patch_attachments_acl({'can_delete_other_users_attachments': False}) def test_cant_delete_attachment(self): """middleware validates if we have permission to delete other users attachments""" - self.override_acl({ - 'max_attachment_size': 1024, - 'can_delete_other_users_attachments': False, - }) - attachment = self.mock_attachment(user=False, post=self.post) self.assertIsNone(attachment.uploader) + user_acl = useracl.get_user_acl(self.user, cache_versions) serializer = AttachmentsMiddleware( request=RequestMock({ 'attachments': [] }), mode=PostingEndpoint.EDIT, user=self.user, + user_acl=user_acl, post=self.post, ).get_serializer() self.assertFalse(serializer.is_valid()) + @patch_attachments_acl() def test_add_attachments(self): """middleware adds attachments to post""" attachments = [ @@ -161,12 +180,14 @@ def test_add_attachments(self): self.mock_attachment(), ] + user_acl = useracl.get_user_acl(self.user, cache_versions) middleware = AttachmentsMiddleware( request=RequestMock({ 'attachments': [a.pk for a in attachments] }), mode=PostingEndpoint.EDIT, user=self.user, + user_acl=user_acl, post=self.post, ) @@ -182,6 +203,7 @@ def test_add_attachments(self): self.assertEqual([a['filename'] for a in self.post.attachments_cache], attachments_filenames) + @patch_attachments_acl() def test_remove_attachments(self): """middleware removes attachment from post and db""" attachments = [ @@ -189,12 +211,14 @@ def test_remove_attachments(self): self.mock_attachment(post=self.post), ] + user_acl = useracl.get_user_acl(self.user, cache_versions) middleware = AttachmentsMiddleware( request=RequestMock({ 'attachments': [attachments[0].pk] }), mode=PostingEndpoint.EDIT, user=self.user, + user_acl=user_acl, post=self.post, ) @@ -212,6 +236,7 @@ def test_remove_attachments(self): self.assertEqual([a['filename'] for a in self.post.attachments_cache], attachments_filenames) + @patch_attachments_acl() def test_steal_attachments(self): """middleware validates if attachments are already assigned to other posts""" other_post = testutils.reply_thread(self.thread) @@ -221,12 +246,14 @@ def test_steal_attachments(self): self.mock_attachment(), ] + user_acl = useracl.get_user_acl(self.user, cache_versions) middleware = AttachmentsMiddleware( request=RequestMock({ 'attachments': [attachments[0].pk, attachments[1].pk] }), mode=PostingEndpoint.EDIT, user=self.user, + user_acl=user_acl, post=self.post, ) @@ -241,6 +268,7 @@ def test_steal_attachments(self): self.assertEqual(Attachment.objects.get(pk=attachments[0].pk).post, other_post) self.assertEqual(Attachment.objects.get(pk=attachments[1].pk).post, self.post) + @patch_attachments_acl() def test_edit_attachments(self): """middleware removes and adds attachments to post""" attachments = [ @@ -249,12 +277,14 @@ def test_edit_attachments(self): self.mock_attachment(), ] + user_acl = useracl.get_user_acl(self.user, cache_versions) middleware = AttachmentsMiddleware( request=RequestMock({ 'attachments': [attachments[0].pk, attachments[2].pk] }), mode=PostingEndpoint.EDIT, user=self.user, + user_acl=user_acl, post=self.post, ) diff --git a/misago/threads/tests/test_attachmentview.py b/misago/threads/tests/test_attachmentview.py index 80910d6ad7..f07af68220 100644 --- a/misago/threads/tests/test_attachmentview.py +++ b/misago/threads/tests/test_attachmentview.py @@ -3,19 +3,25 @@ from django.urls import reverse from misago.acl.models import Role -from misago.acl.testutils import override_acl +from misago.acl.test import patch_user_acl from misago.categories.models import Category from misago.conf import settings from misago.threads import testutils from misago.threads.models import Attachment, AttachmentType from misago.users.testutils import AuthenticatedUserTestCase - TESTFILES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'testfiles') TEST_DOCUMENT_PATH = os.path.join(TESTFILES_DIR, 'document.pdf') TEST_SMALLJPG_PATH = os.path.join(TESTFILES_DIR, 'small.jpg') +def patch_attachments_acl(acl_patch=None): + acl_patch = acl_patch or {} + acl_patch.setdefault("max_attachment_size", 1024) + acl_patch.setdefault("can_download_other_users_attachments", True) + return patch_user_acl(acl_patch) + + class AttachmentViewTestCase(AuthenticatedUserTestCase): def setUp(self): super().setUp() @@ -36,16 +42,6 @@ def setUp(self): extensions='pdf', ) - self.override_acl() - - def override_acl(self, allow_download=True): - acl = self.user.acl_cache.copy() - acl.update({ - 'max_attachment_size': 1000, - 'can_download_other_users_attachments': allow_download, - }) - override_acl(self.user, acl) - def upload_document(self, is_orphaned=False, by_other_user=False): with open(TEST_DOCUMENT_PATH, 'rb') as upload: response = self.client.post( @@ -64,8 +60,6 @@ def upload_document(self, is_orphaned=False, by_other_user=False): attachment.uploader = None attachment.save() - self.override_acl() - return attachment def upload_image(self): @@ -77,25 +71,25 @@ def upload_image(self): ) self.assertEqual(response.status_code, 200) - attachment = Attachment.objects.order_by('id').last() - - self.override_acl() - - return attachment + return Attachment.objects.order_by('id').last() + @patch_attachments_acl() def assertIs404(self, response): self.assertEqual(response.status_code, 302) self.assertTrue(response['location'].endswith(settings.MISAGO_404_IMAGE)) + @patch_attachments_acl() def assertIs403(self, response): self.assertEqual(response.status_code, 302) self.assertTrue(response['location'].endswith(settings.MISAGO_403_IMAGE)) + @patch_attachments_acl() def assertSuccess(self, response): self.assertEqual(response.status_code, 302) self.assertFalse(response['location'].endswith(settings.MISAGO_404_IMAGE)) self.assertFalse(response['location'].endswith(settings.MISAGO_403_IMAGE)) + @patch_attachments_acl() def test_nonexistant_file(self): """user tries to retrieve nonexistant file""" response = self.client.get( @@ -107,6 +101,7 @@ def test_nonexistant_file(self): self.assertIs404(response) + @patch_attachments_acl() def test_invalid_secret(self): """user tries to retrieve existing file using invalid secret""" attachment = self.upload_document() @@ -120,15 +115,15 @@ def test_invalid_secret(self): self.assertIs404(response) + @patch_attachments_acl({"can_download_other_users_attachments": False}) def test_other_user_file_no_permission(self): """user tries to retrieve other user's file without perm""" attachment = self.upload_document(by_other_user=True) - self.override_acl(False) - response = self.client.get(attachment.get_absolute_url()) self.assertIs403(response) + @patch_attachments_acl({"can_download_other_users_attachments": False}) def test_other_user_orphaned_file(self): """user tries to retrieve other user's orphaned file""" attachment = self.upload_document(is_orphaned=True, by_other_user=True) @@ -139,6 +134,7 @@ def test_other_user_orphaned_file(self): response = self.client.get(attachment.get_absolute_url() + '?shva=1') self.assertIs404(response) + @patch_attachments_acl() def test_document_thumbnail(self): """user tries to retrieve thumbnail from non-image attachment""" attachment = self.upload_document() @@ -154,6 +150,7 @@ def test_document_thumbnail(self): ) self.assertIs404(response) + @patch_attachments_acl() def test_no_role(self): """user tries to retrieve attachment without perm to its type""" attachment = self.upload_document() @@ -164,6 +161,7 @@ def test_no_role(self): response = self.client.get(attachment.get_absolute_url()) self.assertIs403(response) + @patch_attachments_acl() def test_type_disabled(self): """user tries to retrieve attachment the type disabled downloads""" attachment = self.upload_document() @@ -174,6 +172,7 @@ def test_type_disabled(self): response = self.client.get(attachment.get_absolute_url()) self.assertIs403(response) + @patch_attachments_acl() def test_locked_type(self): """user retrieves own locked file""" attachment = self.upload_document() @@ -184,6 +183,7 @@ def test_locked_type(self): response = self.client.get(attachment.get_absolute_url()) self.assertSuccess(response) + @patch_attachments_acl() def test_own_file(self): """user retrieves own file""" attachment = self.upload_document() @@ -191,6 +191,7 @@ def test_own_file(self): response = self.client.get(attachment.get_absolute_url()) self.assertSuccess(response) + @patch_attachments_acl() def test_other_user_file(self): """user retrieves other user's file with perm""" attachment = self.upload_document(by_other_user=True) @@ -198,6 +199,7 @@ def test_other_user_file(self): response = self.client.get(attachment.get_absolute_url()) self.assertSuccess(response) + @patch_attachments_acl() def test_other_user_orphaned_file_is_staff(self): """user retrieves other user's orphaned file because he is staff""" attachment = self.upload_document(is_orphaned=True, by_other_user=True) @@ -211,6 +213,7 @@ def test_other_user_orphaned_file_is_staff(self): response = self.client.get(attachment.get_absolute_url() + '?shva=1') self.assertSuccess(response) + @patch_attachments_acl() def test_orphaned_file_is_uploader(self): """user retrieves orphaned file because he is its uploader""" attachment = self.upload_document(is_orphaned=True) @@ -221,6 +224,7 @@ def test_orphaned_file_is_uploader(self): response = self.client.get(attachment.get_absolute_url() + '?shva=1') self.assertSuccess(response) + @patch_attachments_acl() def test_has_role(self): """user retrieves file he has roles to download""" attachment = self.upload_document() @@ -231,6 +235,7 @@ def test_has_role(self): response = self.client.get(attachment.get_absolute_url() + '?shva=1') self.assertSuccess(response) + @patch_attachments_acl() def test_image(self): """user retrieves """ attachment = self.upload_image() @@ -238,6 +243,7 @@ def test_image(self): response = self.client.get(attachment.get_absolute_url() + '?shva=1') self.assertSuccess(response) + @patch_attachments_acl() def test_image_thumb(self): """user retrieves image's thumbnail""" attachment = self.upload_image() From 26cc404b0653c5a6904451dd64be7edf56a6a43d Mon Sep 17 00:00:00 2001 From: rafalp Date: Sat, 15 Dec 2018 21:23:21 +0100 Subject: [PATCH 46/86] Replace RequestMock with unittest.mocks.Mock --- .../tests/test_attachments_middleware.py | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/misago/threads/tests/test_attachments_middleware.py b/misago/threads/tests/test_attachments_middleware.py index f160151f98..17a32430a0 100644 --- a/misago/threads/tests/test_attachments_middleware.py +++ b/misago/threads/tests/test_attachments_middleware.py @@ -1,3 +1,5 @@ +from unittest.mock import Mock + from rest_framework import serializers from misago.acl import useracl @@ -11,11 +13,6 @@ from misago.threads.models import Attachment, AttachmentType from misago.users.testutils import AuthenticatedUserTestCase - -class RequestMock(object): - def __init__(self, data=None): - self.data = data or {} - cache_versions = {"acl": "abcdefgh"} @@ -70,7 +67,7 @@ def test_middleware_is_optional(self): for test_input in INPUTS: middleware = AttachmentsMiddleware( - request=RequestMock(test_input), + request=Mock(data=test_input), mode=PostingEndpoint.START, user=self.user, user_acl=user_acl, @@ -89,7 +86,7 @@ def test_middleware_validates_ids(self): for test_input in INPUTS: middleware = AttachmentsMiddleware( - request=RequestMock({ + request=Mock(data={ 'attachments': test_input }), mode=PostingEndpoint.START, @@ -106,7 +103,7 @@ def test_get_initial_attachments(self): """get_initial_attachments returns list of attachments already existing on post""" user_acl = useracl.get_user_acl(self.user, cache_versions) middleware = AttachmentsMiddleware( - request=RequestMock(), + request=Mock(data={}), mode=PostingEndpoint.EDIT, user=self.user, user_acl=user_acl, @@ -131,7 +128,7 @@ def test_get_new_attachments(self): """get_initial_attachments returns list of attachments already existing on post""" user_acl = useracl.get_user_acl(self.user, cache_versions) middleware = AttachmentsMiddleware( - request=RequestMock(), + request=Mock(data={}), mode=PostingEndpoint.EDIT, user=self.user, user_acl=user_acl, @@ -161,7 +158,7 @@ def test_cant_delete_attachment(self): user_acl = useracl.get_user_acl(self.user, cache_versions) serializer = AttachmentsMiddleware( - request=RequestMock({ + request=Mock(data={ 'attachments': [] }), mode=PostingEndpoint.EDIT, @@ -182,7 +179,7 @@ def test_add_attachments(self): user_acl = useracl.get_user_acl(self.user, cache_versions) middleware = AttachmentsMiddleware( - request=RequestMock({ + request=Mock(data={ 'attachments': [a.pk for a in attachments] }), mode=PostingEndpoint.EDIT, @@ -213,7 +210,7 @@ def test_remove_attachments(self): user_acl = useracl.get_user_acl(self.user, cache_versions) middleware = AttachmentsMiddleware( - request=RequestMock({ + request=Mock(data={ 'attachments': [attachments[0].pk] }), mode=PostingEndpoint.EDIT, @@ -248,7 +245,7 @@ def test_steal_attachments(self): user_acl = useracl.get_user_acl(self.user, cache_versions) middleware = AttachmentsMiddleware( - request=RequestMock({ + request=Mock(data={ 'attachments': [attachments[0].pk, attachments[1].pk] }), mode=PostingEndpoint.EDIT, @@ -279,7 +276,7 @@ def test_edit_attachments(self): user_acl = useracl.get_user_acl(self.user, cache_versions) middleware = AttachmentsMiddleware( - request=RequestMock({ + request=Mock(data={ 'attachments': [attachments[0].pk, attachments[2].pk] }), mode=PostingEndpoint.EDIT, From 5750abf487376cbd650c86b966a0241a0c8866ef Mon Sep 17 00:00:00 2001 From: rafalp Date: Sat, 15 Dec 2018 21:34:22 +0100 Subject: [PATCH 47/86] Fix bulk threads delete tests --- .../tests/test_threads_bulkdelete_api.py | 62 +++++-------------- 1 file changed, 17 insertions(+), 45 deletions(-) diff --git a/misago/threads/tests/test_threads_bulkdelete_api.py b/misago/threads/tests/test_threads_bulkdelete_api.py index ba7f318761..c09e01c8b6 100644 --- a/misago/threads/tests/test_threads_bulkdelete_api.py +++ b/misago/threads/tests/test_threads_bulkdelete_api.py @@ -2,12 +2,12 @@ from django.urls import reverse -from misago.acl.testutils import override_acl from misago.categories import PRIVATE_THREADS_ROOT_NAME from misago.categories.models import Category from misago.threads import testutils from misago.threads.models import Thread from misago.threads.serializers.moderation import THREADS_LIMIT +from misago.threads.test import patch_category_acl from misago.threads.threadtypes import trees_map from .test_threads_api import ThreadsApiTestCase @@ -44,26 +44,17 @@ def test_delete_anonymous(self): "detail": "This action is not available to guests.", }) + @patch_category_acl({"can_hide_threads": 2, "can_hide_own_threads": 2}) def test_delete_no_ids(self): """api requires ids to delete""" - self.override_acl({ - 'can_hide_own_threads': 0, - 'can_hide_threads': 0, - }) - response = self.delete(self.api_link) self.assertEqual(response.status_code, 403) self.assertEqual(response.json(), { "detail": "You have to specify at least one thread to delete.", }) + @patch_category_acl({"can_hide_threads": 2, "can_hide_own_threads": 2}) def test_validate_ids(self): - """api validates that ids are list of ints""" - self.override_acl({ - 'can_hide_own_threads': 2, - 'can_hide_threads': 2, - }) - response = self.delete(self.api_link, True) self.assertEqual(response.status_code, 403) self.assertEqual(response.json(), { @@ -82,26 +73,18 @@ def test_validate_ids(self): "detail": "One or more thread ids received were invalid.", }) + @patch_category_acl({"can_hide_threads": 2, "can_hide_own_threads": 2}) def test_validate_ids_length(self): """api validates that ids are list of ints""" - self.override_acl({ - 'can_hide_own_threads': 2, - 'can_hide_threads': 2, - }) - response = self.delete(self.api_link, list(range(THREADS_LIMIT + 1))) self.assertEqual(response.status_code, 403) self.assertEqual(response.json(), { "detail": "No more than %s threads can be deleted at single time." % THREADS_LIMIT, }) + @patch_category_acl({"can_hide_threads": 2, "can_hide_own_threads": 2}) def test_validate_thread_visibility(self): """api valdiates if user can see deleted thread""" - self.override_acl({ - 'can_hide_own_threads': 2, - 'can_hide_threads': 2, - }) - unapproved_thread = self.threads[1] unapproved_thread.is_unapproved = True @@ -119,17 +102,12 @@ def test_validate_thread_visibility(self): for thread in self.threads: Thread.objects.get(pk=thread.pk) + @patch_category_acl({"can_hide_threads": 0, "can_hide_own_threads": 2}) def test_delete_other_user_thread_no_permission(self): """api valdiates if user can delete other users threads""" - self.override_acl({ - 'can_hide_own_threads': 2, - 'can_hide_threads': 0, - }) - other_thread = self.threads[1] response = self.delete(self.api_link, [p.id for p in self.threads]) - self.assertEqual(response.status_code, 400) self.assertEqual(response.json(), [ { @@ -145,17 +123,16 @@ def test_delete_other_user_thread_no_permission(self): for thread in self.threads: Thread.objects.get(pk=thread.pk) + @patch_category_acl({ + "can_hide_threads": 2, + "can_hide_own_threads": 2, + "can_close_threads": False, + }) def test_delete_thread_closed_category_no_permission(self): """api tests category's closed state""" self.category.is_closed = True self.category.save() - self.override_acl({ - 'can_hide_threads': 2, - 'can_hide_own_threads': 2, - 'can_close_threads': False, - }) - response = self.delete(self.api_link, [p.id for p in self.threads]) self.assertEqual(response.status_code, 400) @@ -169,18 +146,17 @@ def test_delete_thread_closed_category_no_permission(self): } for thread in sorted(self.threads, key=lambda i: i.pk) ]) + @patch_category_acl({ + "can_hide_threads": 2, + "can_hide_own_threads": 2, + "can_close_threads": False, + }) def test_delete_thread_closed_no_permission(self): """api tests thread's closed state""" closed_thread = self.threads[1] closed_thread.is_closed = True closed_thread.save() - self.override_acl({ - 'can_hide_threads': 2, - 'can_hide_own_threads': 2, - 'can_close_threads': False, - }) - response = self.delete(self.api_link, [p.id for p in self.threads]) self.assertEqual(response.status_code, 400) @@ -194,6 +170,7 @@ def test_delete_thread_closed_no_permission(self): } ]) + @patch_category_acl({"can_hide_threads": 2, "can_hide_own_threads": 2}) def test_delete_private_thread(self): """attempt to delete private thread fails""" private_thread = self.threads[0] @@ -208,11 +185,6 @@ def test_delete_private_thread(self): is_owner=True, ) - self.override_acl({ - 'can_hide_own_threads': 2, - 'can_hide_threads': 2, - }) - threads_ids = [p.id for p in self.threads] response = self.delete(self.api_link, threads_ids) From 0d79134d7b44c9e7f5c022c4c20c63de06c7bc48 Mon Sep 17 00:00:00 2001 From: rafalp Date: Sat, 15 Dec 2018 22:23:58 +0100 Subject: [PATCH 48/86] Fix private threads to work with new acl --- .../api/postingendpoint/participants.py | 15 +++- .../api/postingendpoint/privatethread.py | 2 +- misago/threads/api/threadendpoints/patch.py | 5 +- misago/threads/api/threads.py | 2 +- misago/threads/permissions/privatethreads.py | 10 +-- misago/threads/test.py | 17 ++++ .../tests/test_privatethread_patch_api.py | 29 +++---- .../tests/test_privatethread_start_api.py | 86 +++++++++---------- .../threads/tests/test_privatethread_view.py | 11 +-- misago/threads/tests/test_privatethreads.py | 32 ------- .../threads/tests/test_privatethreads_api.py | 60 +++++++------ .../tests/test_privatethreads_lists.py | 16 ++-- 12 files changed, 136 insertions(+), 149 deletions(-) diff --git a/misago/threads/api/postingendpoint/participants.py b/misago/threads/api/postingendpoint/participants.py index c79c8a0c3f..090641d6fb 100755 --- a/misago/threads/api/postingendpoint/participants.py +++ b/misago/threads/api/postingendpoint/participants.py @@ -4,6 +4,7 @@ from django.core.exceptions import PermissionDenied from django.utils.translation import gettext as _, ngettext +from misago.acl import useracl from misago.categories import PRIVATE_THREADS_ROOT_NAME from misago.threads.participants import add_participants, set_owner from misago.threads.permissions import allow_message_user @@ -21,7 +22,14 @@ def use_this_middleware(self): return False def get_serializer(self): - return ParticipantsSerializer(data=self.request.data, context={'user': self.user}) + return ParticipantsSerializer( + data=self.request.data, + context={ + 'request': self.request, + 'user': self.user, + 'user_acl': self.user_acl, + }, + ) def save(self, serializer): set_owner(self.thread, self.user) @@ -51,7 +59,7 @@ def clean_usernames(self, usernames): if not clean_usernames: raise serializers.ValidationError(_("You have to enter user names.")) - max_participants = self.context['user'].acl_cache['max_private_thread_participants'] + max_participants = self.context['user_acl']['max_private_thread_participants'] if max_participants and len(clean_usernames) > max_participants: message = ngettext( "You can't add more than %(users)s user to private thread (you've added %(added)s).", @@ -71,7 +79,8 @@ def get_users(self, usernames): users = [] for user in UserModel.objects.filter(slug__in=usernames): try: - allow_message_user(self.context['user'], user) + user_acl = useracl.get_user_acl(user, self.context["request"].cache_versions) + allow_message_user(self.context['user_acl'], user, user_acl) except PermissionDenied as e: raise serializers.ValidationError(str(e)) users.append(user) diff --git a/misago/threads/api/postingendpoint/privatethread.py b/misago/threads/api/postingendpoint/privatethread.py index 7e8fd1a390..38212a5ce7 100644 --- a/misago/threads/api/postingendpoint/privatethread.py +++ b/misago/threads/api/postingendpoint/privatethread.py @@ -16,7 +16,7 @@ def use_this_middleware(self): def pre_save(self, serializer): category = Category.objects.private_threads() - add_acl(self.user, category) + add_acl(self.user_acl, category) # set flags for savechanges middleware category.update_all = False diff --git a/misago/threads/api/threadendpoints/patch.py b/misago/threads/api/threadendpoints/patch.py index fce18a11e4..513f3afa1c 100644 --- a/misago/threads/api/threadendpoints/patch.py +++ b/misago/threads/api/threadendpoints/patch.py @@ -7,7 +7,7 @@ from django.shortcuts import get_object_or_404 from django.utils.translation import gettext as _ -from misago.acl import add_acl +from misago.acl import add_acl, useracl from misago.categories.models import Category from misago.categories.permissions import allow_browse_category, allow_see_category from misago.categories.serializers import CategorySerializer @@ -281,7 +281,8 @@ def patch_add_participant(request, thread, value): if participant in [p.user for p in thread.participants_list]: raise PermissionDenied(_("This user is already thread participant.")) - allow_add_participant(request.user_acl, participant) + participant_acl = useracl.get_user_acl(participant, request.cache_versions) + allow_add_participant(request.user_acl, participant, participant_acl) add_participant(request, thread, participant) make_participants_aware(request.user, thread) diff --git a/misago/threads/api/threads.py b/misago/threads/api/threads.py index 272610e329..1aa98cf6d7 100644 --- a/misago/threads/api/threads.py +++ b/misago/threads/api/threads.py @@ -118,7 +118,7 @@ def list(self, request): @transaction.atomic def create(self, request): - allow_use_private_threads(request.user) + allow_use_private_threads(request.user_acl) if not request.user_acl['can_start_private_threads']: raise PermissionDenied(_("You can't start private threads.")) diff --git a/misago/threads/permissions/privatethreads.py b/misago/threads/permissions/privatethreads.py index 81b8793c61..bf40ab3bec 100644 --- a/misago/threads/permissions/privatethreads.py +++ b/misago/threads/permissions/privatethreads.py @@ -247,10 +247,10 @@ def allow_remove_participant(user_acl, thread, target): can_remove_participant = return_boolean(allow_remove_participant) -def allow_add_participant(user_acl, target): +def allow_add_participant(user_acl, target, target_acl): message_format = {'user': target.username} - if not can_use_private_threads(target): + if not can_use_private_threads(target_acl): raise PermissionDenied( _("%(user)s can't participate in private threads.") % message_format ) @@ -258,7 +258,6 @@ def allow_add_participant(user_acl, target): if user_acl['can_add_everyone_to_private_threads']: return - # FIXME: User.is_blocking() needs to work with ids if user_acl['can_be_blocked'] and target.is_blocking(user_acl["user_id"]): raise PermissionDenied(_("%(user)s is blocking you.") % message_format) @@ -267,7 +266,6 @@ def allow_add_participant(user_acl, target): _("%(user)s is not allowing invitations to private threads.") % message_format ) - # FIXME: User.is_following() needs to work with ids if target.can_be_messaged_by_followed and not target.is_following(user_acl["user_id"]): message = _("%(user)s limits invitations to private threads to followed users.") raise PermissionDenied(message % message_format) @@ -276,9 +274,9 @@ def allow_add_participant(user_acl, target): can_add_participant = return_boolean(allow_add_participant) -def allow_message_user(user_acl, target): +def allow_message_user(user_acl, target, target_acl): allow_use_private_threads(user_acl) - allow_add_participant(user_acl, target) + allow_add_participant(user_acl, target, target_acl) can_message_user = return_boolean(allow_message_user) diff --git a/misago/threads/test.py b/misago/threads/test.py index b3ce502b3b..98471db287 100644 --- a/misago/threads/test.py +++ b/misago/threads/test.py @@ -45,6 +45,23 @@ def patch_acl(_, user_acl): return patch_user_acl(patch_acl) +def patch_private_threads_acl(acl_patch=None): + def patch_acl(_, user_acl): + category = Category.objects.private_threads() + category_acl = user_acl['categories'][category.id] + category_acl.update(default_category_acl) + if acl_patch: + category_acl.update(acl_patch) + cleanup_patched_acl(user_acl, category_acl, category) + + return patch_user_acl(patch_acl) + + +def other_user_cant_use_private_threads(user, user_acl): + if user.slug == "bobboberson": + user_acl.update({"can_use_private_threads": False}) + + def create_category_acl_patch(category_slug, acl_patch): def created_category_acl_patch(_, user_acl): category = Category.objects.get(slug=category_slug) diff --git a/misago/threads/tests/test_privatethread_patch_api.py b/misago/threads/tests/test_privatethread_patch_api.py index 5e60345c92..ae6a168c8f 100644 --- a/misago/threads/tests/test_privatethread_patch_api.py +++ b/misago/threads/tests/test_privatethread_patch_api.py @@ -3,13 +3,13 @@ from django.contrib.auth import get_user_model from django.core import mail -from misago.acl.testutils import override_acl +from misago.acl.test import patch_user_acl from misago.threads import testutils +from misago.threads.test import other_user_cant_use_private_threads from misago.threads.models import Thread, ThreadParticipant from .test_privatethreads import PrivateThreadsTestCase - UserModel = get_user_model() @@ -125,12 +125,11 @@ def test_add_blocking_user(self): 'detail': ["BobBoberson is blocking you."], }) + @patch_user_acl(other_user_cant_use_private_threads) def test_add_no_perm_user(self): """can't add user that has no permission to use private threads""" ThreadParticipant.objects.set_owner(self.thread, self.user) - override_acl(self.other_user, {'can_use_private_threads': 0}) - response = self.patch( self.api_link, [ { @@ -219,6 +218,7 @@ def test_add_user(self): self.assertIn(self.user.username, email.subject) self.assertIn(self.thread.title, email.subject) + @patch_user_acl({'can_moderate_private_threads': True}) def test_add_user_to_other_user_thread_moderator(self): """moderators can add users to other users threads""" ThreadParticipant.objects.set_owner(self.thread, self.other_user) @@ -226,8 +226,6 @@ def test_add_user_to_other_user_thread_moderator(self): self.thread.has_reported_posts = True self.thread.save() - override_acl(self.user, {'can_moderate_private_threads': 1}) - self.patch( self.api_link, [ { @@ -246,6 +244,7 @@ def test_add_user_to_other_user_thread_moderator(self): # notification about new private thread wasn't send because we invited ourselves self.assertEqual(len(mail.outbox), 0) + @patch_user_acl({'can_moderate_private_threads': True}) def test_add_user_to_closed_moderator(self): """moderators can add users to closed threads""" ThreadParticipant.objects.set_owner(self.thread, self.user) @@ -253,8 +252,6 @@ def test_add_user_to_closed_moderator(self): self.thread.is_closed = True self.thread.save() - override_acl(self.user, {'can_moderate_private_threads': 1}) - self.patch( self.api_link, [ { @@ -458,6 +455,7 @@ def test_user_leave_closed_thread(self): self.assertEqual(self.thread.participants.count(), 1) self.assertEqual(self.thread.participants.filter(pk=self.user.pk).count(), 0) + @patch_user_acl({'can_moderate_private_threads': True}) def test_moderator_remove_user(self): """api allows moderator to remove other user""" removed_user = UserModel.objects.create_user('Vigilante', 'test@test.com', 'pass123') @@ -465,8 +463,6 @@ def test_moderator_remove_user(self): ThreadParticipant.objects.set_owner(self.thread, self.other_user) ThreadParticipant.objects.add_participants(self.thread, [self.user, removed_user]) - override_acl(self.user, {'can_moderate_private_threads': True}) - response = self.patch( self.api_link, [ { @@ -742,6 +738,7 @@ def test_owner_change_thread_owner(self): self.assertTrue(event.is_event) self.assertTrue(event.event_type, 'changed_owner') + @patch_user_acl({'can_moderate_private_threads': True}) def test_moderator_change_owner(self): """moderator can change thread owner to other user""" new_owner = UserModel.objects.create_user('NewOwner', 'new@owner.com', 'pass123') @@ -749,8 +746,6 @@ def test_moderator_change_owner(self): ThreadParticipant.objects.set_owner(self.thread, self.other_user) ThreadParticipant.objects.add_participants(self.thread, [self.user, new_owner]) - override_acl(self.user, {'can_moderate_private_threads': 1}) - response = self.patch( self.api_link, [ { @@ -768,7 +763,7 @@ def test_moderator_change_owner(self): self.assertFalse(UserModel.objects.get(pk=self.user.pk).sync_unread_private_threads) self.assertTrue(UserModel.objects.get(pk=self.other_user.pk).sync_unread_private_threads) - # ownership was transfered + # ownership was transferred self.assertEqual(self.thread.participants.count(), 3) self.assertTrue(ThreadParticipant.objects.get(user=new_owner).is_owner) self.assertFalse(ThreadParticipant.objects.get(user=self.user).is_owner) @@ -779,13 +774,12 @@ def test_moderator_change_owner(self): self.assertTrue(event.is_event) self.assertTrue(event.event_type, 'changed_owner') + @patch_user_acl({'can_moderate_private_threads': True}) def test_moderator_takeover(self): """moderator can takeover the thread""" ThreadParticipant.objects.set_owner(self.thread, self.other_user) ThreadParticipant.objects.add_participants(self.thread, [self.user]) - override_acl(self.user, {'can_moderate_private_threads': 1}) - response = self.patch( self.api_link, [ { @@ -812,6 +806,7 @@ def test_moderator_takeover(self): self.assertTrue(event.is_event) self.assertTrue(event.event_type, 'tookover') + @patch_user_acl({'can_moderate_private_threads': True}) def test_moderator_closed_thread_takeover(self): """moderator can takeover closed thread thread""" ThreadParticipant.objects.set_owner(self.thread, self.other_user) @@ -820,8 +815,6 @@ def test_moderator_closed_thread_takeover(self): self.thread.is_closed = True self.thread.save() - override_acl(self.user, {'can_moderate_private_threads': 1}) - response = self.patch( self.api_link, [ { @@ -838,7 +831,7 @@ def test_moderator_closed_thread_takeover(self): self.assertFalse(UserModel.objects.get(pk=self.user.pk).sync_unread_private_threads) self.assertTrue(UserModel.objects.get(pk=self.other_user.pk).sync_unread_private_threads) - # ownership was transfered + # ownership was transferred self.assertEqual(self.thread.participants.count(), 2) self.assertTrue(ThreadParticipant.objects.get(user=self.user).is_owner) self.assertFalse(ThreadParticipant.objects.get(user=self.other_user).is_owner) diff --git a/misago/threads/tests/test_privatethread_start_api.py b/misago/threads/tests/test_privatethread_start_api.py index 38a2de2427..45dadd5c46 100644 --- a/misago/threads/tests/test_privatethread_start_api.py +++ b/misago/threads/tests/test_privatethread_start_api.py @@ -3,12 +3,12 @@ from django.urls import reverse from django.utils.encoding import smart_str -from misago.acl.testutils import override_acl +from misago.acl.test import patch_user_acl from misago.categories.models import Category from misago.threads.models import ThreadParticipant +from misago.threads.test import other_user_cant_use_private_threads from misago.users.testutils import AuthenticatedUserTestCase - UserModel = get_user_model() @@ -30,20 +30,18 @@ def test_cant_start_thread_as_guest(self): response = self.client.post(self.api_link) self.assertEqual(response.status_code, 403) + @patch_user_acl({'can_use_private_threads': False}) def test_cant_use_private_threads(self): """has no permission to use private threads""" - override_acl(self.user, {'can_use_private_threads': 0}) - response = self.client.post(self.api_link) self.assertEqual(response.status_code, 403) self.assertEqual(response.json(), { "detail": "You can't use private threads.", }) + @patch_user_acl({'can_start_private_threads': False}) def test_cant_start_private_thread(self): """permission to start private thread is validated""" - override_acl(self.user, {'can_start_private_threads': 0}) - response = self.client.post(self.api_link) self.assertEqual(response.status_code, 403) self.assertEqual(response.json(), { @@ -153,10 +151,9 @@ def test_cant_invite_too_many(self): } ) + @patch_user_acl(other_user_cant_use_private_threads) def test_cant_invite_no_permission(self): """api validates invited user permission to private thread""" - override_acl(self.other_user, {'can_use_private_threads': 0}) - response = self.client.post( self.api_link, data={ @@ -191,8 +188,10 @@ def test_cant_invite_blocking(self): 'to': ["BobBoberson is blocking you."], }) - # allow us to bypass blocked check - override_acl(self.user, {'can_add_everyone_to_private_threads': 1}) + @patch_user_acl({'can_add_everyone_to_private_threads': 1}) + def test_cant_invite_blocking_override(self): + """api validates that you cant invite blocking user to thread""" + self.other_user.blocks.add(self.user) response = self.client.post( self.api_link, @@ -233,26 +232,24 @@ def test_cant_invite_followers_only(self): ) # allow us to bypass following check - override_acl(self.user, {'can_add_everyone_to_private_threads': 1}) - - response = self.client.post( - self.api_link, - data={ - 'to': [self.other_user.username], - 'title': "-----", - 'post': "Lorem ipsum dolor.", - } - ) - - self.assertEqual(response.status_code, 400) - self.assertEqual( - response.json(), { - 'title': ["Thread title should contain alpha-numeric characters."], - } - ) + with patch_user_acl({'can_add_everyone_to_private_threads': 1}): + response = self.client.post( + self.api_link, + data={ + 'to': [self.other_user.username], + 'title': "-----", + 'post': "Lorem ipsum dolor.", + } + ) + + self.assertEqual(response.status_code, 400) + self.assertEqual( + response.json(), { + 'title': ["Thread title should contain alpha-numeric characters."], + } + ) # make user follow us - override_acl(self.user, {'can_add_everyone_to_private_threads': 0}) self.other_user.follows.add(self.user) response = self.client.post( @@ -294,23 +291,22 @@ def test_cant_invite_anyone(self): ) # allow us to bypass user preference check - override_acl(self.user, {'can_add_everyone_to_private_threads': 1}) - - response = self.client.post( - self.api_link, - data={ - 'to': [self.other_user.username], - 'title': "-----", - 'post': "Lorem ipsum dolor.", - } - ) - - self.assertEqual(response.status_code, 400) - self.assertEqual( - response.json(), { - 'title': ["Thread title should contain alpha-numeric characters."], - } - ) + with patch_user_acl({'can_add_everyone_to_private_threads': 1}): + response = self.client.post( + self.api_link, + data={ + 'to': [self.other_user.username], + 'title': "-----", + 'post': "Lorem ipsum dolor.", + } + ) + + self.assertEqual(response.status_code, 400) + self.assertEqual( + response.json(), { + 'title': ["Thread title should contain alpha-numeric characters."], + } + ) def test_can_start_thread(self): """endpoint creates new thread""" diff --git a/misago/threads/tests/test_privatethread_view.py b/misago/threads/tests/test_privatethread_view.py index f8c8593d43..5c706746e5 100644 --- a/misago/threads/tests/test_privatethread_view.py +++ b/misago/threads/tests/test_privatethread_view.py @@ -1,4 +1,4 @@ -from misago.acl.testutils import override_acl +from misago.acl.test import patch_user_acl from misago.threads import testutils from misago.threads.models import ThreadParticipant @@ -19,10 +19,9 @@ def test_anonymous(self): response = self.client.get(self.test_link) self.assertContains(response, "sign in to use private threads", status_code=403) + @patch_user_acl({"can_use_private_threads": False}) def test_no_permission(self): """user needs to have permission to see private thread""" - override_acl(self.user, {'can_use_private_threads': 0}) - response = self.client.get(self.test_link) self.assertContains(response, "t use private threads", status_code=403) @@ -31,10 +30,9 @@ def test_no_participant(self): response = self.client.get(self.test_link) self.assertEqual(response.status_code, 404) + @patch_user_acl({"can_moderate_private_threads": True}) def test_mod_not_reported(self): """moderator can't see private thread that has no reports""" - override_acl(self.user, {'can_moderate_private_threads': 1}) - response = self.client.get(self.test_link) self.assertEqual(response.status_code, 404) @@ -60,10 +58,9 @@ def test_can_see_participant(self): response = self.client.get(self.test_link) self.assertContains(response, self.thread.title) + @patch_user_acl({"can_moderate_private_threads": True}) def test_mod_can_see_reported(self): """moderator can see private thread that has reports""" - override_acl(self.user, {'can_moderate_private_threads': 1}) - self.thread.has_reported_posts = True self.thread.save() diff --git a/misago/threads/tests/test_privatethreads.py b/misago/threads/tests/test_privatethreads.py index 4eca7563ef..5da5193c9c 100644 --- a/misago/threads/tests/test_privatethreads.py +++ b/misago/threads/tests/test_privatethreads.py @@ -1,4 +1,3 @@ -from misago.acl.testutils import override_acl from misago.categories.models import Category from misago.users.testutils import AuthenticatedUserTestCase @@ -7,34 +6,3 @@ class PrivateThreadsTestCase(AuthenticatedUserTestCase): def setUp(self): super().setUp() self.category = Category.objects.private_threads() - - override_acl(self.user, { - 'can_use_private_threads': 1, - 'can_start_private_threads': 1, - }) - - self.override_acl() - - def override_acl(self, acl=None): - final_acl = self.user.acl_cache['categories'][self.category.pk] - final_acl.update({ - 'can_see': 1, - 'can_browse': 1, - 'can_see_all_threads': 1, - 'can_see_own_threads': 0, - 'can_hide_threads': 0, - 'can_approve_content': 0, - 'can_edit_posts': 0, - 'can_hide_posts': 0, - 'can_hide_own_posts': 0, - 'can_merge_threads': 0, - }) - - if acl: - final_acl.update(acl) - - override_acl(self.user, { - 'categories': { - self.category.pk: final_acl, - }, - }) diff --git a/misago/threads/tests/test_privatethreads_api.py b/misago/threads/tests/test_privatethreads_api.py index b2cad070df..282ab084b6 100644 --- a/misago/threads/tests/test_privatethreads_api.py +++ b/misago/threads/tests/test_privatethreads_api.py @@ -1,8 +1,9 @@ from django.urls import reverse -from misago.acl.testutils import override_acl +from misago.acl.test import patch_user_acl from misago.threads import testutils from misago.threads.models import Thread, ThreadParticipant +from misago.threads.test import patch_private_threads_acl from .test_privatethreads import PrivateThreadsTestCase @@ -23,16 +24,16 @@ def test_unauthenticated(self): "detail": "You have to sign in to use private threads." }) + @patch_user_acl({"can_use_private_threads": False}) def test_no_permission(self): """api requires user to have permission to be able to access it""" - override_acl(self.user, {'can_use_private_threads': 0}) - response = self.client.get(self.api_link) self.assertEqual(response.status_code, 403) self.assertEqual(response.json(), { "detail": "You can't use private threads." }) + @patch_user_acl({"can_use_private_threads": True}) def test_empty_list(self): """api has no showstoppers on returning empty list""" response = self.client.get(self.api_link) @@ -41,6 +42,7 @@ def test_empty_list(self): response_json = response.json() self.assertEqual(response_json['count'], 0) + @patch_user_acl({"can_use_private_threads": True}) def test_thread_visibility(self): """only participated threads are returned by private threads api""" visible = testutils.post_thread(category=self.category, poster=self.user) @@ -62,15 +64,14 @@ def test_thread_visibility(self): self.assertEqual(response_json['results'][0]['id'], visible.id) # threads with reported posts will also show to moderators - override_acl(self.user, {'can_moderate_private_threads': 1}) - - response = self.client.get(self.api_link) - self.assertEqual(response.status_code, 200) + with patch_user_acl({"can_moderate_private_threads": True}): + response = self.client.get(self.api_link) + self.assertEqual(response.status_code, 200) - response_json = response.json() - self.assertEqual(response_json['count'], 2) - self.assertEqual(response_json['results'][0]['id'], reported.id) - self.assertEqual(response_json['results'][1]['id'], visible.id) + response_json = response.json() + self.assertEqual(response_json['count'], 2) + self.assertEqual(response_json['results'][0]['id'], reported.id) + self.assertEqual(response_json['results'][1]['id'], visible.id) class PrivateThreadRetrieveApiTests(PrivateThreadsTestCase): @@ -90,28 +91,34 @@ def test_anonymous(self): "detail": "You have to sign in to use private threads." }) + @patch_user_acl({"can_use_private_threads": False}) def test_no_permission(self): """user needs to have permission to see private thread""" - override_acl(self.user, {'can_use_private_threads': 0}) - response = self.client.get(self.api_link) self.assertEqual(response.status_code, 403) self.assertEqual(response.json(), { "detail": "You can't use private threads." }) + @patch_user_acl({"can_use_private_threads": True}) def test_no_participant(self): """user cant see thread he isn't part of""" response = self.client.get(self.api_link) self.assertEqual(response.status_code, 404) + @patch_user_acl({ + "can_use_private_threads": True, + "can_moderate_private_threads": True, + }) def test_mod_not_reported(self): """moderator can't see private thread that has no reports""" - override_acl(self.user, {'can_moderate_private_threads': 1}) - response = self.client.get(self.api_link) self.assertEqual(response.status_code, 404) + @patch_user_acl({ + "can_use_private_threads": True, + "can_moderate_private_threads": False, + }) def test_reported_not_mod(self): """non-mod can't see private thread that has reported posts""" self.thread.has_reported_posts = True @@ -120,6 +127,7 @@ def test_reported_not_mod(self): response = self.client.get(self.api_link) self.assertEqual(response.status_code, 404) + @patch_user_acl({"can_use_private_threads": True}) def test_can_see_owner(self): """user can see thread he is owner of""" ThreadParticipant.objects.set_owner(self.thread, self.user) @@ -141,6 +149,7 @@ def test_can_see_owner(self): ] ) + @patch_user_acl({"can_use_private_threads": True}) def test_can_see_participant(self): """user can see thread he is participant of""" ThreadParticipant.objects.add_participants(self.thread, [self.user]) @@ -162,10 +171,12 @@ def test_can_see_participant(self): ] ) + @patch_user_acl({ + "can_use_private_threads": True, + "can_moderate_private_threads": True, + }) def test_mod_can_see_reported(self): """moderator can see private thread that has reports""" - override_acl(self.user, {'can_moderate_private_threads': 1}) - self.thread.has_reported_posts = True self.thread.save() @@ -186,30 +197,29 @@ def setUp(self): ThreadParticipant.objects.add_participants(self.thread, [self.user]) - def test_delete_thread_no_permission(self): + @patch_private_threads_acl({"can_hide_threads": 0}) + def test_hide_thread_no_permission(self): """api tests permission to delete threads""" - self.override_acl({'can_hide_threads': 0}) - + response = self.client.delete(self.api_link) self.assertEqual(response.status_code, 403) - self.assertEqual( response.json()['detail'], "You can't delete threads in this category." ) - self.override_acl({'can_hide_threads': 1}) + @patch_private_threads_acl({"can_hide_threads": 1}) + def test_delete_thread_no_permission(self): + """api tests permission to delete threads""" response = self.client.delete(self.api_link) self.assertEqual(response.status_code, 403) - self.assertEqual( response.json()['detail'], "You can't delete threads in this category." ) + @patch_private_threads_acl({"can_hide_threads": 2}) def test_delete_thread(self): """DELETE to API link with permission deletes thread""" - self.override_acl({'can_hide_threads': 2}) - response = self.client.delete(self.api_link) self.assertEqual(response.status_code, 200) diff --git a/misago/threads/tests/test_privatethreads_lists.py b/misago/threads/tests/test_privatethreads_lists.py index 0b878a2a3f..1bf4c5f1de 100644 --- a/misago/threads/tests/test_privatethreads_lists.py +++ b/misago/threads/tests/test_privatethreads_lists.py @@ -1,6 +1,6 @@ from django.urls import reverse -from misago.acl.testutils import override_acl +from misago.acl.test import patch_user_acl from misago.threads import testutils from misago.threads.models import ThreadParticipant @@ -20,10 +20,9 @@ def test_unauthenticated(self): response = self.client.get(self.test_link) self.assertContains(response, "sign in to use private threads", status_code=403) + @patch_user_acl({"can_use_private_threads": False}) def test_no_permission(self): """view requires user to have permission to be able to access it""" - override_acl(self.user, {'can_use_private_threads': 0}) - response = self.client.get(self.test_link) self.assertContains(response, "use private threads", status_code=403) @@ -51,9 +50,8 @@ def test_thread_visibility(self): self.assertContains(response, visible.get_absolute_url()) # threads with reported posts will also show to moderators - override_acl(self.user, {'can_moderate_private_threads': 1}) - - response = self.client.get(self.test_link) - self.assertEqual(response.status_code, 200) - self.assertContains(response, reported.get_absolute_url()) - self.assertContains(response, visible.get_absolute_url()) + with patch_user_acl({"can_moderate_private_threads": True}): + response = self.client.get(self.test_link) + self.assertEqual(response.status_code, 200) + self.assertContains(response, reported.get_absolute_url()) + self.assertContains(response, visible.get_absolute_url()) From b489fdbba696688a6debe381bdb104d841ad7618 Mon Sep 17 00:00:00 2001 From: rafalp Date: Sat, 15 Dec 2018 22:54:26 +0100 Subject: [PATCH 49/86] Fix remaining posting middlewares --- .../api/postingendpoint/emailnotification.py | 10 +-- .../api/postingendpoint/floodprotection.py | 6 +- misago/threads/test.py | 15 ++++ .../test_emailnotification_middleware.py | 70 +++++++++---------- misago/threads/tests/test_floodprotection.py | 32 +++++---- .../tests/test_floodprotection_middleware.py | 16 +++-- .../tests/test_subscription_middleware.py | 25 +++---- 7 files changed, 97 insertions(+), 77 deletions(-) diff --git a/misago/threads/api/postingendpoint/emailnotification.py b/misago/threads/api/postingendpoint/emailnotification.py index fc1402ab06..bbfae4ffd0 100644 --- a/misago/threads/api/postingendpoint/emailnotification.py +++ b/misago/threads/api/postingendpoint/emailnotification.py @@ -1,5 +1,6 @@ from django.utils.translation import gettext as _ +from misago.acl import useracl from misago.core.mail import build_mail, send_messages from misago.threads.permissions import can_see_post, can_see_thread @@ -23,15 +24,16 @@ def post_save(self, serializer): notifications = [] for subscription in queryset.iterator(): - if self.notify_user_of_post(subscription.user): + if self.subscriber_can_see_post(subscription.user): notifications.append(self.build_mail(subscription.user)) if notifications: send_messages(notifications) - def notify_user_of_post(self, subscriber): - see_thread = can_see_thread(subscriber, self.thread) - see_post = can_see_post(subscriber, self.post) + def subscriber_can_see_post(self, subscriber): + user_acl = useracl.get_user_acl(subscriber, self.request.cache_versions) + see_thread = can_see_thread(user_acl, self.thread) + see_post = can_see_post(user_acl, self.post) return see_thread and see_post def build_mail(self, subscriber): diff --git a/misago/threads/api/postingendpoint/floodprotection.py b/misago/threads/api/postingendpoint/floodprotection.py index 075428bceb..5eeeb8f85f 100755 --- a/misago/threads/api/postingendpoint/floodprotection.py +++ b/misago/threads/api/postingendpoint/floodprotection.py @@ -13,8 +13,10 @@ class FloodProtectionMiddleware(PostingMiddleware): def use_this_middleware(self): - return not self.user.acl_cache['can_omit_flood_protection' - ] and self.mode != PostingEndpoint.EDIT + return ( + not self.user_acl['can_omit_flood_protection'] and + self.mode != PostingEndpoint.EDIT + ) def interrupt_posting(self, serializer): now = timezone.now() diff --git a/misago/threads/test.py b/misago/threads/test.py index 98471db287..a6d4b558f2 100644 --- a/misago/threads/test.py +++ b/misago/threads/test.py @@ -28,6 +28,21 @@ def patch_acl(_, user_acl): return patch_user_acl(patch_acl) +def patch_other_user_category_acl(acl_patch=None): + def patch_acl(user, user_acl): + if user.slug != "bobbobertson": + return + + category = Category.objects.get(slug="first-category") + category_acl = user_acl['categories'][category.id] + category_acl.update(default_category_acl) + if acl_patch: + category_acl.update(acl_patch) + cleanup_patched_acl(user_acl, category_acl, category) + + return patch_user_acl(patch_acl) + + def patch_other_category_acl(acl_patch=None): def patch_acl(_, user_acl): src_category = Category.objects.get(slug="first-category") diff --git a/misago/threads/tests/test_emailnotification_middleware.py b/misago/threads/tests/test_emailnotification_middleware.py index 1462b2eae1..233a948e94 100644 --- a/misago/threads/tests/test_emailnotification_middleware.py +++ b/misago/threads/tests/test_emailnotification_middleware.py @@ -7,9 +7,11 @@ from django.utils import timezone from django.utils.encoding import smart_str -from misago.acl.testutils import override_acl from misago.categories.models import Category from misago.threads import testutils +from misago.threads.test import ( + patch_category_acl, patch_other_user_category_acl +) from misago.users.testutils import AuthenticatedUserTestCase @@ -25,7 +27,6 @@ def setUp(self): category=self.category, started_on=timezone.now() - timedelta(seconds=5), ) - self.override_acl() self.api_link = reverse( 'misago:api:thread-post-list', kwargs={ @@ -33,37 +34,9 @@ def setUp(self): } ) - self.other_user = UserModel.objects.create_user('Bob', 'bob@boberson.com', 'pass123') - - def override_acl(self): - new_acl = deepcopy(self.user.acl_cache) - new_acl['categories'][self.category.pk].update({ - 'can_see': 1, - 'can_browse': 1, - 'can_start_threads': 1, - 'can_reply_threads': 1, - 'can_edit_posts': 1, - }) - - override_acl(self.user, new_acl) - - def override_other_user_acl(self, hide=False): - new_acl = deepcopy(self.other_user.acl_cache) - new_acl['categories'][self.category.pk].update({ - 'can_see': 1, - 'can_browse': 1, - 'can_start_threads': 1, - 'can_reply_threads': 1, - 'can_edit_posts': 1, - }) - - if hide: - new_acl['categories'][self.category.pk].update({ - 'can_browse': False, - }) - - override_acl(self.other_user, new_acl) + self.other_user = UserModel.objects.create_user('BobBobertson', 'bob@boberson.com') + @patch_category_acl({"can_reply_threads": True}) def test_no_subscriptions(self): """no emails are sent because noone subscibes to thread""" response = self.client.post( @@ -75,6 +48,7 @@ def test_no_subscriptions(self): self.assertEqual(len(mail.outbox), 0) + @patch_category_acl({"can_reply_threads": True}) def test_poster_not_notified(self): """no emails are sent because only poster subscribes to thread""" self.user.subscription_set.create( @@ -93,6 +67,7 @@ def test_poster_not_notified(self): self.assertEqual(len(mail.outbox), 0) + @patch_category_acl({"can_reply_threads": True}) def test_other_user_no_email_subscription(self): """no emails are sent because subscriber has e-mails off""" self.other_user.subscription_set.create( @@ -111,6 +86,8 @@ def test_other_user_no_email_subscription(self): self.assertEqual(len(mail.outbox), 0) + @patch_category_acl({"can_reply_threads": True}) + @patch_other_user_category_acl({"can_see": False}) def test_other_user_no_permission(self): """no emails are sent because subscriber has no permission to read thread""" self.other_user.subscription_set.create( @@ -119,7 +96,6 @@ def test_other_user_no_permission(self): last_read_on=timezone.now(), send_email=True, ) - self.override_other_user_acl(hide=True) response = self.client.post( self.api_link, data={ @@ -130,6 +106,29 @@ def test_other_user_no_permission(self): self.assertEqual(len(mail.outbox), 0) + @patch_category_acl({"can_reply_threads": True}) + def test_moderation_queue(self): + """no emails are sent because new post is moderated""" + self.category.require_replies_approval = True + self.category.save() + + self.other_user.subscription_set.create( + thread=self.thread, + category=self.category, + last_read_on=timezone.now(), + send_email=True, + ) + + response = self.client.post( + self.api_link, data={ + 'post': 'This is test response!', + } + ) + self.assertEqual(response.status_code, 200) + + self.assertEqual(len(mail.outbox), 0) + + @patch_category_acl({"can_reply_threads": True}) def test_other_user_not_read(self): """no emails are sent because subscriber didn't read previous post""" self.other_user.subscription_set.create( @@ -138,7 +137,6 @@ def test_other_user_not_read(self): last_read_on=timezone.now(), send_email=True, ) - self.override_other_user_acl() testutils.reply_thread(self.thread, posted_on=timezone.now()) @@ -151,6 +149,7 @@ def test_other_user_not_read(self): self.assertEqual(len(mail.outbox), 0) + @patch_category_acl({"can_reply_threads": True}) def test_other_notified(self): """email is sent to subscriber""" self.other_user.subscription_set.create( @@ -159,7 +158,6 @@ def test_other_notified(self): last_read_on=timezone.now(), send_email=True, ) - self.override_other_user_acl() response = self.client.post( self.api_link, data={ @@ -183,6 +181,7 @@ def test_other_notified(self): last_post = self.thread.post_set.order_by('id').last() self.assertIn(last_post.get_absolute_url(), message) + @patch_category_acl({"can_reply_threads": True}) def test_other_notified_after_reading(self): """email is sent to subscriber that had sub updated by read api""" self.other_user.subscription_set.create( @@ -191,7 +190,6 @@ def test_other_notified_after_reading(self): last_read_on=self.thread.last_post_on, send_email=True, ) - self.override_other_user_acl() response = self.client.post( self.api_link, data={ diff --git a/misago/threads/tests/test_floodprotection.py b/misago/threads/tests/test_floodprotection.py index b8953e5eb2..bb1c6ae0fe 100644 --- a/misago/threads/tests/test_floodprotection.py +++ b/misago/threads/tests/test_floodprotection.py @@ -1,18 +1,17 @@ from django.urls import reverse -from misago.acl.testutils import override_acl +from misago.acl.test import patch_user_acl from misago.categories.models import Category from misago.threads import testutils from misago.users.testutils import AuthenticatedUserTestCase -class PostMentionsTests(AuthenticatedUserTestCase): +class FloodProtectionTests(AuthenticatedUserTestCase): def setUp(self): super().setUp() self.category = Category.objects.get(slug='first-category') self.thread = testutils.post_thread(category=self.category) - self.override_acl() self.post_link = reverse( 'misago:api:thread-post-list', kwargs={ @@ -20,17 +19,6 @@ def setUp(self): } ) - def override_acl(self): - new_acl = self.user.acl_cache - new_acl['categories'][self.category.pk].update({ - 'can_see': 1, - 'can_browse': 1, - 'can_start_threads': 1, - 'can_reply_threads': 1, - }) - - override_acl(self.user, new_acl) - def test_flood_has_no_showstoppers(self): """endpoint handles posting interruption""" response = self.client.post( @@ -49,3 +37,19 @@ def test_flood_has_no_showstoppers(self): self.assertEqual(response.json(), { "detail": "You can't post message so quickly after previous one." }) + + @patch_user_acl({"can_omit_flood_protection": True}) + def test_user_with_permission_omits_flood_protection(self): + response = self.client.post( + self.post_link, data={ + 'post': "This is test response!", + } + ) + self.assertEqual(response.status_code, 200) + + response = self.client.post( + self.post_link, data={ + 'post': "This is test response!", + } + ) + self.assertEqual(response.status_code, 200) diff --git a/misago/threads/tests/test_floodprotection_middleware.py b/misago/threads/tests/test_floodprotection_middleware.py index 752b9f3872..41bae57f8c 100644 --- a/misago/threads/tests/test_floodprotection_middleware.py +++ b/misago/threads/tests/test_floodprotection_middleware.py @@ -2,11 +2,12 @@ from django.utils import timezone -from misago.acl.testutils import override_acl from misago.threads.api.postingendpoint import PostingInterrupt from misago.threads.api.postingendpoint.floodprotection import FloodProtectionMiddleware from misago.users.testutils import AuthenticatedUserTestCase +user_acl = {'can_omit_flood_protection': False} + class FloodProtectionMiddlewareTests(AuthenticatedUserTestCase): def test_flood_protection_middleware_on_no_posts(self): @@ -14,7 +15,7 @@ def test_flood_protection_middleware_on_no_posts(self): self.user.update_fields = [] self.assertIsNone(self.user.last_posted_on) - middleware = FloodProtectionMiddleware(user=self.user) + middleware = FloodProtectionMiddleware(user=self.user, user_acl=user_acl) middleware.interrupt_posting(None) self.assertIsNotNone(self.user.last_posted_on) @@ -26,7 +27,7 @@ def test_flood_protection_middleware_old_posts(self): original_last_posted_on = timezone.now() - timedelta(days=1) self.user.last_posted_on = original_last_posted_on - middleware = FloodProtectionMiddleware(user=self.user) + middleware = FloodProtectionMiddleware(user=self.user, user_acl=user_acl) middleware.interrupt_posting(None) self.assertTrue(self.user.last_posted_on > original_last_posted_on) @@ -36,12 +37,13 @@ def test_flood_protection_middleware_on_flood(self): self.user.last_posted_on = timezone.now() with self.assertRaises(PostingInterrupt): - middleware = FloodProtectionMiddleware(user=self.user) + middleware = FloodProtectionMiddleware(user=self.user, user_acl=user_acl) middleware.interrupt_posting(None) def test_flood_permission(self): """middleware is respects permission to flood for team members""" - override_acl(self.user, {'can_omit_flood_protection': True}) - - middleware = FloodProtectionMiddleware(user=self.user) + can_omit_flood_protection_user_acl = {'can_omit_flood_protection': True} + middleware = FloodProtectionMiddleware( + user=self.user, user_acl=can_omit_flood_protection_user_acl + ) self.assertFalse(middleware.use_this_middleware()) diff --git a/misago/threads/tests/test_subscription_middleware.py b/misago/threads/tests/test_subscription_middleware.py index 8e92b24f8a..117d649c43 100644 --- a/misago/threads/tests/test_subscription_middleware.py +++ b/misago/threads/tests/test_subscription_middleware.py @@ -1,9 +1,10 @@ from django.contrib.auth import get_user_model from django.urls import reverse -from misago.acl.testutils import override_acl +from misago.acl.test import patch_user_acl from misago.categories.models import Category from misago.threads import testutils +from misago.threads.test import patch_category_acl from misago.users.testutils import AuthenticatedUserTestCase @@ -14,19 +15,6 @@ class SubscriptionMiddlewareTestCase(AuthenticatedUserTestCase): def setUp(self): super().setUp() self.category = Category.objects.get(slug='first-category') - self.override_acl() - - def override_acl(self): - new_acl = self.user.acl_cache - new_acl['can_omit_flood_protection'] = True - new_acl['categories'][self.category.pk].update({ - 'can_see': 1, - 'can_browse': 1, - 'can_start_threads': 1, - 'can_reply_threads': 1, - }) - - override_acl(self.user, new_acl) class SubscribeStartedThreadTests(SubscriptionMiddlewareTestCase): @@ -34,6 +22,7 @@ def setUp(self): super().setUp() self.api_link = reverse('misago:api:thread-list') + @patch_category_acl({"can_start_threads": True}) def test_dont_subscribe(self): """middleware makes no subscription to thread""" self.user.subscribe_to_started_threads = UserModel.SUBSCRIBE_NONE @@ -53,6 +42,7 @@ def test_dont_subscribe(self): # user has no subscriptions self.assertEqual(self.user.subscription_set.count(), 0) + @patch_category_acl({"can_start_threads": True}) def test_subscribe(self): """middleware subscribes thread""" self.user.subscribe_to_started_threads = UserModel.SUBSCRIBE_NOTIFY @@ -75,6 +65,7 @@ def test_subscribe(self): self.assertEqual(subscription.category_id, self.category.id) self.assertFalse(subscription.send_email) + @patch_category_acl({"can_start_threads": True}) def test_email_subscribe(self): """middleware subscribes thread with an email""" self.user.subscribe_to_started_threads = UserModel.SUBSCRIBE_ALL @@ -108,6 +99,7 @@ def setUp(self): } ) + @patch_category_acl({"can_reply_threads": True}) def test_dont_subscribe(self): """middleware makes no subscription to thread""" self.user.subscribe_to_started_threads = UserModel.SUBSCRIBE_NOTIFY @@ -124,6 +116,7 @@ def test_dont_subscribe(self): # user has no subscriptions self.assertEqual(self.user.subscription_set.count(), 0) + @patch_category_acl({"can_reply_threads": True}) def test_subscribe(self): """middleware subscribes thread""" self.user.subscribe_to_replied_threads = UserModel.SUBSCRIBE_NOTIFY @@ -142,6 +135,7 @@ def test_subscribe(self): self.assertEqual(subscription.category_id, self.category.id) self.assertFalse(subscription.send_email) + @patch_category_acl({"can_reply_threads": True}) def test_email_subscribe(self): """middleware subscribes thread with an email""" self.user.subscribe_to_replied_threads = UserModel.SUBSCRIBE_ALL @@ -160,6 +154,7 @@ def test_email_subscribe(self): self.assertEqual(subscription.category_id, self.category.id) self.assertTrue(subscription.send_email) + @patch_category_acl({"can_reply_threads": True}) def test_subscribe_with_events(self): """middleware omits events when testing for replied thread""" self.user.subscribe_to_replied_threads = UserModel.SUBSCRIBE_ALL @@ -182,6 +177,8 @@ def test_subscribe_with_events(self): self.assertEqual(subscription.category_id, self.category.id) self.assertTrue(subscription.send_email) + @patch_category_acl({"can_reply_threads": True}) + @patch_user_acl({"can_omit_flood_protection": True}) def test_dont_subscribe_replied(self): """middleware omits threads user already replied""" self.user.subscribe_to_replied_threads = UserModel.SUBSCRIBE_ALL From 4a75270d251169b5e2fd645aac5a104e19fb28ea Mon Sep 17 00:00:00 2001 From: rafalp Date: Sat, 15 Dec 2018 23:50:08 +0100 Subject: [PATCH 50/86] Remove override_acl from remaining tests --- misago/acl/testutils.py | 5 -- misago/threads/api/pollvotecreateendpoint.py | 2 +- .../threads/api/postendpoints/patch_post.py | 4 +- misago/threads/api/threadpoll.py | 10 +-- misago/threads/tests/test_gotoviews.py | 19 ++---- misago/threads/tests/test_post_mentions.py | 18 ----- .../tests/test_thread_bulkpatch_api.py | 67 +++++-------------- misago/threads/tests/test_thread_poll_api.py | 25 ------- .../tests/test_thread_pollcreate_api.py | 42 ++++++++---- .../tests/test_thread_polldelete_api.py | 40 ++++++----- .../threads/tests/test_thread_polledit_api.py | 39 ++++++----- .../tests/test_thread_pollvotes_api.py | 40 ++++++----- .../tests/test_thread_postbulkpatch_api.py | 29 ++------ .../tests/test_thread_postedits_api.py | 12 ++-- .../tests/test_thread_postlikes_api.py | 9 +-- misago/threads/views/goto.py | 9 ++- 16 files changed, 151 insertions(+), 219 deletions(-) diff --git a/misago/acl/testutils.py b/misago/acl/testutils.py index 6c0cc5e18a..b66e9b8bbd 100644 --- a/misago/acl/testutils.py +++ b/misago/acl/testutils.py @@ -16,8 +16,3 @@ def fake_post_data(target, data_dict): else: data_dict[field.html_name] = field.value() return data_dict - - -def override_acl(user, new_acl): - """overrides user permissions with specified ones""" - raise Exception("override_acl has been removed") diff --git a/misago/threads/api/pollvotecreateendpoint.py b/misago/threads/api/pollvotecreateendpoint.py index 40280ae8ab..2898d83b88 100644 --- a/misago/threads/api/pollvotecreateendpoint.py +++ b/misago/threads/api/pollvotecreateendpoint.py @@ -10,7 +10,7 @@ def poll_vote_create(request, thread, poll): poll.make_choices_votes_aware(request.user) - allow_vote_poll(request.user, poll) + allow_vote_poll(request.user_acl, poll) serializer = NewVoteSerializer( data={ diff --git a/misago/threads/api/postendpoints/patch_post.py b/misago/threads/api/postendpoints/patch_post.py index 0e2bdb930f..d26b205672 100644 --- a/misago/threads/api/postendpoints/patch_post.py +++ b/misago/threads/api/postendpoints/patch_post.py @@ -169,7 +169,9 @@ def bulk_patch_endpoint(request, thread): def clean_posts_for_patch(request, thread, posts_ids): - posts_queryset = exclude_invisible_posts(request.user, thread.category, thread.post_set) + posts_queryset = exclude_invisible_posts( + request.user_acl, thread.category, thread.post_set + ) posts_queryset = posts_queryset.filter( id__in=posts_ids, is_event=False, diff --git a/misago/threads/api/threadpoll.py b/misago/threads/api/threadpoll.py index dd2fa3912e..a411435021 100644 --- a/misago/threads/api/threadpoll.py +++ b/misago/threads/api/threadpoll.py @@ -47,7 +47,7 @@ def get_poll(self, thread, pk): @transaction.atomic def create(self, request, thread_pk): thread = self.get_thread(request, thread_pk) - allow_start_poll(request.user, thread) + allow_start_poll(request.user_acl, thread) try: if thread.poll and thread.poll.pk: @@ -84,7 +84,7 @@ def update(self, request, thread_pk, pk=None): thread = self.get_thread(request, thread_pk) instance = self.get_poll(thread, pk) - allow_edit_poll(request.user, instance) + allow_edit_poll(request.user_acl, instance) serializer = EditPollSerializer(instance, data=request.data) serializer.is_valid(raise_exception=True) @@ -103,7 +103,7 @@ def delete(self, request, thread_pk, pk=None): thread = self.get_thread(request, thread_pk) instance = self.get_poll(thread, pk) - allow_delete_poll(request.user, instance) + allow_delete_poll(request.user_acl, instance) thread.poll.delete() @@ -111,7 +111,7 @@ def delete(self, request, thread_pk, pk=None): thread.save() return Response({ - 'can_start_poll': can_start_poll(request.user, thread), + 'can_start_poll': can_start_poll(request.user_acl, thread), }) @detail_route(methods=['get', 'post']) @@ -138,7 +138,7 @@ def get_votes(self, request, thread_pk, pk=None): except Poll.DoesNotExist: raise Http404() - allow_see_poll_votes(request.user, thread.poll) + allow_see_poll_votes(request.user_acl, thread.poll) choices = [] voters = {} diff --git a/misago/threads/tests/test_gotoviews.py b/misago/threads/tests/test_gotoviews.py index e149ad67b2..eb9668a324 100644 --- a/misago/threads/tests/test_gotoviews.py +++ b/misago/threads/tests/test_gotoviews.py @@ -1,10 +1,10 @@ from django.utils import timezone -from misago.acl.testutils import override_acl from misago.categories.models import Category from misago.conf import settings from misago.readtracker.poststracker import save_read from misago.threads import testutils +from misago.threads.test import patch_category_acl from misago.users.testutils import AuthenticatedUserTestCase @@ -233,24 +233,18 @@ def test_view_handles_best_answer(self): class GotoUnapprovedTests(GotoViewTestCase): - def grant_permission(self): - self.user.acl_cache['categories'][self.category.pk]['can_approve_content'] = 1 - override_acl(self.user, self.user.acl_cache) - def test_view_validates_permission(self): """view validates permission to see unapproved posts""" response = self.client.get(self.thread.get_unapproved_post_url()) self.assertContains(response, "You need permission to approve content", status_code=403) - self.grant_permission() - - response = self.client.get(self.thread.get_unapproved_post_url()) - self.assertEqual(response.status_code, 302) + with patch_category_acl({"can_approve_content": True}): + response = self.client.get(self.thread.get_unapproved_post_url()) + self.assertEqual(response.status_code, 302) + @patch_category_acl({"can_approve_content": True}) def test_view_handles_no_unapproved_posts(self): """if thread has no unapproved posts, redirect to last post""" - self.grant_permission() - response = self.client.get(self.thread.get_unapproved_post_url()) self.assertEqual(response.status_code, 302) self.assertEqual( @@ -258,6 +252,7 @@ def test_view_handles_no_unapproved_posts(self): GOTO_URL % (self.thread.get_absolute_url(), self.thread.first_post_id) ) + @patch_category_acl({"can_approve_content": True}) def test_view_handles_unapproved_posts(self): """if thread has unapproved posts, redirect to first of them""" for _ in range(settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL): @@ -267,8 +262,6 @@ def test_view_handles_unapproved_posts(self): for _ in range(settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_POSTS_TAIL - 1): testutils.reply_thread(self.thread, posted_on=timezone.now()) - self.grant_permission() - response = self.client.get(self.thread.get_unapproved_post_url()) self.assertEqual(response.status_code, 302) self.assertEqual( diff --git a/misago/threads/tests/test_post_mentions.py b/misago/threads/tests/test_post_mentions.py index 951f873c6b..f14fff9060 100644 --- a/misago/threads/tests/test_post_mentions.py +++ b/misago/threads/tests/test_post_mentions.py @@ -2,13 +2,11 @@ from django.test.client import BOUNDARY, MULTIPART_CONTENT, encode_multipart from django.urls import reverse -from misago.acl.testutils import override_acl from misago.categories.models import Category from misago.markup.mentions import MENTIONS_LIMIT from misago.threads import testutils from misago.users.testutils import AuthenticatedUserTestCase - UserModel = get_user_model() @@ -18,7 +16,6 @@ def setUp(self): self.category = Category.objects.get(slug='first-category') self.thread = testutils.post_thread(category=self.category) - self.override_acl() self.post_link = reverse( 'misago:api:thread-post-list', kwargs={ @@ -26,18 +23,6 @@ def setUp(self): } ) - def override_acl(self): - new_acl = self.user.acl_cache - new_acl['categories'][self.category.pk].update({ - 'can_see': 1, - 'can_browse': 1, - 'can_start_threads': 1, - 'can_reply_threads': 1, - 'can_edit_posts': 1, - }) - - override_acl(self.user, new_acl) - def put(self, url, data=None): content = encode_multipart(BOUNDARY, data or {}) return self.client.put(url, content, content_type=MULTIPART_CONTENT) @@ -129,7 +114,6 @@ def test_mention_update(self): } ) - self.override_acl() response = self.put( edit_link, data={ @@ -142,7 +126,6 @@ def test_mention_update(self): self.assertEqual(list(post.mentions.order_by('id')), [user_a, user_b]) # remove first mention from post - should preserve mentions - self.override_acl() response = self.put( edit_link, data={ 'post': "This is test response, @%s!" % user_b, @@ -154,7 +137,6 @@ def test_mention_update(self): self.assertEqual(list(post.mentions.order_by('id')), [user_a, user_b]) # remove mentions from post - should preserve mentions - self.override_acl() response = self.put( edit_link, data={ 'post': "This is test response!", diff --git a/misago/threads/tests/test_thread_bulkpatch_api.py b/misago/threads/tests/test_thread_bulkpatch_api.py index a7fc3d3150..7ae8ac340a 100644 --- a/misago/threads/tests/test_thread_bulkpatch_api.py +++ b/misago/threads/tests/test_thread_bulkpatch_api.py @@ -2,10 +2,10 @@ from django.urls import reverse -from misago.acl.testutils import override_acl from misago.categories.models import Category from misago.threads import testutils from misago.threads.models import Thread +from misago.threads.test import patch_category_acl, patch_other_category_acl from .test_threads_api import ThreadsApiTestCase @@ -183,10 +183,9 @@ def test_add_acl_true(self): class BulkThreadChangeTitleApiTests(ThreadsBulkPatchApiTestCase): + @patch_category_acl({"can_edit_threads": 2}) def test_change_thread_title(self): """api changes thread title and resyncs the category""" - self.override_acl({'can_edit_threads': 2}) - response = self.patch( self.api_link, { @@ -210,13 +209,12 @@ def test_change_thread_title(self): for thread in Thread.objects.filter(id__in=self.ids): self.assertEqual(thread.title, 'Changed the title!') - category = Category.objects.get(pk=self.category.pk) + category = Category.objects.get(pk=self.category.id) self.assertEqual(category.last_thread_title, 'Changed the title!') + @patch_category_acl({"can_edit_threads": 0}) def test_change_thread_title_no_permission(self): """api validates permission to change title, returns errors""" - self.override_acl({'can_edit_threads': 0}) - response = self.patch( self.api_link, { @@ -246,46 +244,19 @@ def setUp(self): super().setUp() Category( - name='Category B', - slug='category-b', + name='Other Category', + slug='other-category', ).insert_at( self.category, position='last-child', save=True, ) - self.category_b = Category.objects.get(slug='category-b') - - def override_other_acl(self, acl): - other_category_acl = self.user.acl_cache['categories'][self.category.pk].copy() - other_category_acl.update({ - 'can_see': 1, - 'can_browse': 1, - 'can_see_all_threads': 1, - 'can_see_own_threads': 0, - 'can_hide_threads': 0, - 'can_approve_content': 0, - }) - other_category_acl.update(acl) - - categories_acl = self.user.acl_cache['categories'] - categories_acl[self.category_b.pk] = other_category_acl - - visible_categories = [self.category.pk] - if other_category_acl['can_see']: - visible_categories.append(self.category_b.pk) - - override_acl( - self.user, { - 'visible_categories': visible_categories, - 'categories': categories_acl, - } - ) + self.other_category = Category.objects.get(slug='other-category') + @patch_category_acl({"can_move_threads": True}) + @patch_other_category_acl({"can_start_threads": 2}) def test_move_thread(self): """api moves threads to other category and syncs both categories""" - self.override_acl({'can_move_threads': True}) - self.override_other_acl({'can_start_threads': 2}) - response = self.patch( self.api_link, { @@ -294,7 +265,7 @@ def test_move_thread(self): { 'op': 'replace', 'path': 'category', - 'value': self.category_b.pk, + 'value': self.other_category.id, }, { 'op': 'replace', @@ -309,23 +280,22 @@ def test_move_thread(self): response_json = response.json() for i, thread in enumerate(self.threads): self.assertEqual(response_json[i]['id'], thread.id) - self.assertEqual(response_json[i]['category'], self.category_b.pk) + self.assertEqual(response_json[i]['category'], self.other_category.id) for thread in Thread.objects.filter(id__in=self.ids): - self.assertEqual(thread.category_id, self.category_b.pk) + self.assertEqual(thread.category_id, self.other_category.id) - category = Category.objects.get(pk=self.category.pk) + category = Category.objects.get(pk=self.category.id) self.assertEqual(category.threads, self.category.threads - 3) - new_category = Category.objects.get(pk=self.category_b.pk) + new_category = Category.objects.get(pk=self.other_category.id) self.assertEqual(new_category.threads, 3) class BulkThreadsHideApiTests(ThreadsBulkPatchApiTestCase): + @patch_category_acl({"can_hide_threads": 1}) def test_hide_thread(self): """api makes it possible to hide thread""" - self.override_acl({'can_hide_threads': 1}) - response = self.patch( self.api_link, { @@ -349,11 +319,12 @@ def test_hide_thread(self): for thread in Thread.objects.filter(id__in=self.ids): self.assertTrue(thread.is_hidden) - category = Category.objects.get(pk=self.category.pk) + category = Category.objects.get(pk=self.category.id) self.assertNotIn(category.last_thread_id, self.ids) class BulkThreadsApproveApiTests(ThreadsBulkPatchApiTestCase): + @patch_category_acl({"can_approve_content": True}) def test_approve_thread(self): """api approvse threads and syncs category""" for thread in self.threads: @@ -369,8 +340,6 @@ def test_approve_thread(self): self.category.synchronize() self.category.save() - self.override_acl({'can_approve_content': 1}) - response = self.patch( self.api_link, { @@ -396,5 +365,5 @@ def test_approve_thread(self): self.assertFalse(thread.is_unapproved) self.assertFalse(thread.has_unapproved_posts) - category = Category.objects.get(pk=self.category.pk) + category = Category.objects.get(pk=self.category.id) self.assertIn(category.last_thread_id, self.ids) diff --git a/misago/threads/tests/test_thread_poll_api.py b/misago/threads/tests/test_thread_poll_api.py index b19eaee77b..ab6f0190aa 100644 --- a/misago/threads/tests/test_thread_poll_api.py +++ b/misago/threads/tests/test_thread_poll_api.py @@ -2,7 +2,6 @@ from django.urls import reverse -from misago.acl.testutils import override_acl from misago.categories.models import Category from misago.threads import testutils from misago.users.testutils import AuthenticatedUserTestCase @@ -14,7 +13,6 @@ def setUp(self): self.category = Category.objects.get(slug='first-category') self.thread = testutils.post_thread(self.category, poster=self.user) - self.override_acl() self.api_link = reverse( 'misago:api:thread-poll-list', kwargs={ @@ -28,29 +26,6 @@ def post(self, url, data=None): def put(self, url, data=None): return self.client.put(url, json.dumps(data or {}), content_type='application/json') - def override_acl(self, user=None, category=None): - new_acl = self.user.acl_cache - new_acl['categories'][self.category.pk].update({ - 'can_see': 1, - 'can_browse': 1, - 'can_close_threads': 0, - }) - - new_acl.update({ - 'can_start_polls': 1, - 'can_edit_polls': 1, - 'can_delete_polls': 1, - 'poll_edit_time': 0, - 'can_always_see_poll_voters': 0, - }) - - if user: - new_acl.update(user) - if category: - new_acl['categories'][self.category.pk].update(category) - - override_acl(self.user, new_acl) - def mock_poll(self): self.poll = self.thread.poll = testutils.post_poll(self.thread, self.user) diff --git a/misago/threads/tests/test_thread_pollcreate_api.py b/misago/threads/tests/test_thread_pollcreate_api.py index 3d6191ab21..f829116d3c 100644 --- a/misago/threads/tests/test_thread_pollcreate_api.py +++ b/misago/threads/tests/test_thread_pollcreate_api.py @@ -1,7 +1,9 @@ from django.urls import reverse +from misago.acl.test import patch_user_acl from misago.threads.models import Poll, Thread from misago.threads.serializers.poll import MAX_POLL_OPTIONS +from misago.threads.test import patch_category_acl from .test_thread_poll_api import ThreadPollApiTestCase @@ -36,20 +38,19 @@ def test_nonexistant_thread_id(self): response = self.post(api_link) self.assertEqual(response.status_code, 404) + @patch_user_acl({"can_start_polls": 0}) def test_no_permission(self): """api validates that user has permission to start poll in thread""" - self.override_acl({'can_start_polls': 0}) - response = self.post(self.api_link) self.assertEqual(response.status_code, 403) self.assertEqual(response.json(), { "detail": "You can't start polls." }) - def test_no_permission_closed_thread(self): + @patch_user_acl({"can_start_polls": 1}) + @patch_category_acl({"can_close_threads": False}) + def test_closed_thread_no_permission(self): """api validates that user has permission to start poll in closed thread""" - self.override_acl(category={'can_close_threads': 0}) - self.thread.is_closed = True self.thread.save() @@ -59,15 +60,20 @@ def test_no_permission_closed_thread(self): "detail": "This thread is closed. You can't start polls in it." }) - self.override_acl(category={'can_close_threads': 1}) + @patch_user_acl({"can_start_polls": 1}) + @patch_category_acl({"can_close_threads": True}) + def test_closed_thread(self): + """api validates that user has permission to start poll in closed thread""" + self.thread.is_closed = True + self.thread.save() response = self.post(self.api_link) self.assertEqual(response.status_code, 400) - def test_no_permission_closed_category(self): + @patch_user_acl({"can_start_polls": 1}) + @patch_category_acl({"can_close_threads": False}) + def test_closed_category_no_permission(self): """api validates that user has permission to start poll in closed category""" - self.override_acl(category={'can_close_threads': 0}) - self.category.is_closed = True self.category.save() @@ -77,15 +83,19 @@ def test_no_permission_closed_category(self): "detail": "This category is closed. You can't start polls in it." }) - self.override_acl(category={'can_close_threads': 1}) + @patch_user_acl({"can_start_polls": 1}) + @patch_category_acl({"can_close_threads": True}) + def test_closed_category(self): + """api validates that user has permission to start poll in closed category""" + self.category.is_closed = True + self.category.save() response = self.post(self.api_link) self.assertEqual(response.status_code, 400) - def test_no_permission_other_user_thread(self): + @patch_user_acl({"can_start_polls": 1}) + def test_other_user_thread_no_permission(self): """api validates that user has permission to start poll in other user's thread""" - self.override_acl({'can_start_polls': 1}) - self.thread.starter = None self.thread.save() @@ -95,7 +105,11 @@ def test_no_permission_other_user_thread(self): "detail": "You can't start polls in other users threads." }) - self.override_acl({'can_start_polls': 2}) + @patch_user_acl({"can_start_polls": 2}) + def test_other_user_thread(self): + """api validates that user has permission to start poll in other user's thread""" + self.thread.starter = None + self.thread.save() response = self.post(self.api_link) self.assertEqual(response.status_code, 400) diff --git a/misago/threads/tests/test_thread_polldelete_api.py b/misago/threads/tests/test_thread_polldelete_api.py index 7a96bdad15..568fff2d0e 100644 --- a/misago/threads/tests/test_thread_polldelete_api.py +++ b/misago/threads/tests/test_thread_polldelete_api.py @@ -3,7 +3,9 @@ from django.urls import reverse from django.utils import timezone +from misago.acl.test import patch_user_acl from misago.threads.models import Poll, PollVote, Thread +from misago.threads.test import patch_category_acl from .test_thread_poll_api import ThreadPollApiTestCase @@ -73,20 +75,18 @@ def test_nonexistant_poll_id(self): response = self.client.delete(api_link) self.assertEqual(response.status_code, 404) + @patch_user_acl({"can_delete_polls": 0}) def test_no_permission(self): """api validates that user has permission to delete poll in thread""" - self.override_acl({'can_delete_polls': 0}) - response = self.client.delete(self.api_link) self.assertEqual(response.status_code, 403) self.assertEqual(response.json(), { "detail": "You can't delete polls." }) + @patch_user_acl({"can_delete_polls": 1, "poll_edit_time": 5}) def test_no_permission_timeout(self): """api validates that user's window to delete poll in thread has closed""" - self.override_acl({'can_delete_polls': 1, 'poll_edit_time': 5}) - self.poll.posted_on = timezone.now() - timedelta(minutes=15) self.poll.save() @@ -96,10 +96,9 @@ def test_no_permission_timeout(self): "detail": "You can't delete polls that are older than 5 minutes." }) + @patch_user_acl({"can_delete_polls": 1}) def test_no_permission_poll_closed(self): """api validates that user's window to delete poll in thread has closed""" - self.override_acl({'can_delete_polls': 1}) - self.poll.posted_on = timezone.now() - timedelta(days=15) self.poll.length = 5 self.poll.save() @@ -110,10 +109,9 @@ def test_no_permission_poll_closed(self): "detail": "This poll is over. You can't delete it." }) + @patch_user_acl({"can_delete_polls": 1}) def test_no_permission_other_user_poll(self): """api validates that user has permission to delete other user poll in thread""" - self.override_acl({'can_delete_polls': 1}) - self.poll.poster = None self.poll.save() @@ -123,10 +121,10 @@ def test_no_permission_other_user_poll(self): "detail": "You can't delete other users polls in this category." }) + @patch_user_acl({"can_delete_polls": 1}) + @patch_category_acl({"can_close_threads": False}) def test_no_permission_closed_thread(self): """api validates that user has permission to delete poll in closed thread""" - self.override_acl(category={'can_close_threads': 0}) - self.thread.is_closed = True self.thread.save() @@ -136,15 +134,20 @@ def test_no_permission_closed_thread(self): "detail": "This thread is closed. You can't delete polls in it." }) - self.override_acl(category={'can_close_threads': 1}) + @patch_user_acl({"can_delete_polls": 1}) + @patch_category_acl({"can_close_threads": True}) + def test_closed_thread(self): + """api validates that user has permission to delete poll in closed thread""" + self.thread.is_closed = True + self.thread.save() response = self.client.delete(self.api_link) self.assertEqual(response.status_code, 200) + @patch_user_acl({"can_delete_polls": 1}) + @patch_category_acl({"can_close_threads": False}) def test_no_permission_closed_category(self): """api validates that user has permission to delete poll in closed category""" - self.override_acl(category={'can_close_threads': 0}) - self.category.is_closed = True self.category.save() @@ -154,11 +157,17 @@ def test_no_permission_closed_category(self): "detail": "This category is closed. You can't delete polls in it." }) - self.override_acl(category={'can_close_threads': 1}) + @patch_user_acl({"can_delete_polls": 1}) + @patch_category_acl({"can_close_threads": True}) + def test_closed_category(self): + """api validates that user has permission to delete poll in closed category""" + self.category.is_closed = True + self.category.save() response = self.client.delete(self.api_link) self.assertEqual(response.status_code, 200) + @patch_user_acl({"can_delete_polls": 1, "poll_edit_time": 5}) def test_poll_delete(self): """api deletes poll and associated votes""" response = self.client.delete(self.api_link) @@ -173,10 +182,9 @@ def test_poll_delete(self): thread = Thread.objects.get(pk=self.thread.pk) self.assertFalse(thread.has_poll) + @patch_user_acl({"can_delete_polls": 2, "poll_edit_time": 5}) def test_other_user_poll_delete(self): """api deletes other user's poll and associated votes, even if its over""" - self.override_acl({'can_delete_polls': 2, 'poll_edit_time': 5}) - self.poll.poster = None self.poll.posted_on = timezone.now() - timedelta(days=15) self.poll.length = 5 diff --git a/misago/threads/tests/test_thread_polledit_api.py b/misago/threads/tests/test_thread_polledit_api.py index 1e437526c4..53bea851ac 100644 --- a/misago/threads/tests/test_thread_polledit_api.py +++ b/misago/threads/tests/test_thread_polledit_api.py @@ -3,7 +3,9 @@ from django.urls import reverse from django.utils import timezone +from misago.acl.test import patch_user_acl from misago.threads.serializers.poll import MAX_POLL_OPTIONS +from misago.threads.test import patch_category_acl from .test_thread_poll_api import ThreadPollApiTestCase @@ -73,20 +75,18 @@ def test_nonexistant_poll_id(self): response = self.put(api_link) self.assertEqual(response.status_code, 404) + @patch_user_acl({"can_edit_polls": 0}) def test_no_permission(self): """api validates that user has permission to edit poll in thread""" - self.override_acl({'can_edit_polls': 0}) - response = self.put(self.api_link) self.assertEqual(response.status_code, 403) self.assertEqual(response.json(), { "detail": "You can't edit polls.", }) + @patch_user_acl({"can_edit_polls": 1, "poll_edit_time": 5}) def test_no_permission_timeout(self): """api validates that user's window to edit poll in thread has closed""" - self.override_acl({'can_edit_polls': 1, 'poll_edit_time': 5}) - self.poll.posted_on = timezone.now() - timedelta(minutes=15) self.poll.save() @@ -96,10 +96,9 @@ def test_no_permission_timeout(self): "detail": "You can't edit polls that are older than 5 minutes.", }) + @patch_user_acl({"can_edit_polls": 1}) def test_no_permission_poll_closed(self): """api validates that user's window to edit poll in thread has closed""" - self.override_acl({'can_edit_polls': 1}) - self.poll.posted_on = timezone.now() - timedelta(days=15) self.poll.length = 5 self.poll.save() @@ -110,10 +109,9 @@ def test_no_permission_poll_closed(self): "detail": "This poll is over. You can't edit it.", }) + @patch_user_acl({"can_edit_polls": 1}) def test_no_permission_other_user_poll(self): """api validates that user has permission to edit other user poll in thread""" - self.override_acl({'can_edit_polls': 1}) - self.poll.poster = None self.poll.save() @@ -123,10 +121,10 @@ def test_no_permission_other_user_poll(self): "detail": "You can't edit other users polls in this category.", }) + @patch_user_acl({"can_edit_polls": 1}) + @patch_category_acl({"can_close_threads": False}) def test_no_permission_closed_thread(self): """api validates that user has permission to edit poll in closed thread""" - self.override_acl(category={'can_close_threads': 0}) - self.thread.is_closed = True self.thread.save() @@ -136,15 +134,20 @@ def test_no_permission_closed_thread(self): "detail": "This thread is closed. You can't edit polls in it.", }) - self.override_acl(category={'can_close_threads': 1}) + @patch_user_acl({"can_edit_polls": 1}) + @patch_category_acl({"can_close_threads": True}) + def test_closed_thread(self): + """api validates that user has permission to edit poll in closed thread""" + self.thread.is_closed = True + self.thread.save() response = self.put(self.api_link) self.assertEqual(response.status_code, 400) + @patch_user_acl({"can_edit_polls": 1}) + @patch_category_acl({"can_close_threads": False}) def test_no_permission_closed_category(self): """api validates that user has permission to edit poll in closed category""" - self.override_acl(category={'can_close_threads': 0}) - self.category.is_closed = True self.category.save() @@ -154,7 +157,12 @@ def test_no_permission_closed_category(self): "detail": "This category is closed. You can't edit polls in it.", }) - self.override_acl(category={'can_close_threads': 1}) + @patch_user_acl({"can_edit_polls": 1}) + @patch_category_acl({"can_close_threads": True}) + def test_closed_category(self): + """api validates that user has permission to edit poll in closed category""" + self.category.is_closed = True + self.category.save() response = self.put(self.api_link) self.assertEqual(response.status_code, 400) @@ -513,10 +521,9 @@ def test_poll_some_choices_edited(self): self.assertEqual(self.user.audittrail_set.count(), 1) + @patch_user_acl({"can_edit_polls": 2, "poll_edit_time": 5}) def test_moderate_user_poll(self): """api edits all poll choices out in other users poll, even if its over""" - self.override_acl({'can_edit_polls': 2, 'poll_edit_time': 5}) - self.poll.poster = None self.poll.posted_on = timezone.now() - timedelta(days=15) self.poll.length = 5 diff --git a/misago/threads/tests/test_thread_pollvotes_api.py b/misago/threads/tests/test_thread_pollvotes_api.py index 5247181d61..690680f858 100644 --- a/misago/threads/tests/test_thread_pollvotes_api.py +++ b/misago/threads/tests/test_thread_pollvotes_api.py @@ -4,7 +4,9 @@ from django.urls import reverse from django.utils import timezone +from misago.acl.test import patch_user_acl from misago.threads.models import Poll +from misago.threads.test import patch_category_acl from .test_thread_poll_api import ThreadPollApiTestCase @@ -88,10 +90,9 @@ def test_nonexistant_poll_id(self): response = self.client.get(api_link) self.assertEqual(response.status_code, 404) + @patch_user_acl({"can_always_see_poll_voters": False}) def test_no_permission(self): """api chcecks permission to see poll voters""" - self.override_acl({'can_always_see_poll_voters': False}) - self.poll.is_public = False self.poll.save() @@ -128,10 +129,9 @@ def test_get_votes(self): self.assertEqual([[v['url'] for v in c['voters']] for c in response_json][0][0], user.get_absolute_url()) + @patch_user_acl({"can_always_see_poll_voters": True}) def test_get_votes_private_poll(self): """api returns list of voters on private poll for user with permission""" - self.override_acl({'can_always_see_poll_voters': True}) - self.poll.is_public = False self.poll.save() @@ -271,10 +271,9 @@ def test_revote(self): "detail": 'Expected a list of items but got type "dict".', }) - def test_vote_in_closed_thread(self): + @patch_category_acl({"can_close_threads": False}) + def test_vote_in_closed_thread_no_permission(self): """api validates is user has permission to vote poll in closed thread""" - self.override_acl(category={'can_close_threads': 0}) - self.thread.is_closed = True self.thread.save() @@ -286,18 +285,20 @@ def test_vote_in_closed_thread(self): "detail": "This thread is closed. You can't vote in it.", }) - self.override_acl(category={'can_close_threads': 1}) + @patch_category_acl({"can_close_threads": True}) + def test_vote_in_closed_thread(self): + """api validates is user has permission to vote poll in closed thread""" + self.thread.is_closed = True + self.thread.save() + + self.delete_user_votes() response = self.post(self.api_link) self.assertEqual(response.status_code, 400) - self.assertEqual(response.json(), { - "detail": 'Expected a list of items but got type "dict".', - }) - def test_vote_in_closed_category(self): + @patch_category_acl({"can_close_threads": False}) + def test_vote_in_closed_category_no_permission(self): """api validates is user has permission to vote poll in closed category""" - self.override_acl(category={'can_close_threads': 0}) - self.category.is_closed = True self.category.save() @@ -309,13 +310,16 @@ def test_vote_in_closed_category(self): "detail": "This category is closed. You can't vote in it.", }) - self.override_acl(category={'can_close_threads': 1}) + @patch_category_acl({"can_close_threads": True}) + def test_vote_in_closed_category(self): + """api validates is user has permission to vote poll in closed category""" + self.category.is_closed = True + self.category.save() + + self.delete_user_votes() response = self.post(self.api_link) self.assertEqual(response.status_code, 400) - self.assertEqual(response.json(), { - "detail": 'Expected a list of items but got type "dict".', - }) def test_vote_in_finished_poll(self): """api valdiates if poll has finished before letting user to vote in it""" diff --git a/misago/threads/tests/test_thread_postbulkpatch_api.py b/misago/threads/tests/test_thread_postbulkpatch_api.py index 0cee70be1c..6443f872d9 100644 --- a/misago/threads/tests/test_thread_postbulkpatch_api.py +++ b/misago/threads/tests/test_thread_postbulkpatch_api.py @@ -4,10 +4,10 @@ from django.urls import reverse from django.utils import timezone -from misago.acl.testutils import override_acl from misago.categories.models import Category from misago.threads import testutils from misago.threads.models import Post, Thread +from misago.threads.test import patch_category_acl from misago.users.testutils import AuthenticatedUserTestCase @@ -35,21 +35,6 @@ def setUp(self): def patch(self, api_link, ops): return self.client.patch(api_link, json.dumps(ops), content_type="application/json") - def override_acl(self, extra_acl=None): - new_acl = self.user.acl_cache - new_acl['categories'][self.category.pk].update({ - 'can_see': 1, - 'can_browse': 1, - 'can_start_threads': 0, - 'can_reply_threads': 0, - 'can_edit_posts': 1, - }) - - if extra_acl: - new_acl['categories'][self.category.pk].update(extra_acl) - - override_acl(self.user, new_acl) - class BulkPatchSerializerTests(ThreadPostBulkPatchApiTestCase): def test_invalid_input_type(self): @@ -220,13 +205,9 @@ def test_add_acl_true(self): class BulkPostProtectApiTests(ThreadPostBulkPatchApiTestCase): + @patch_category_acl({"can_protect_posts": True, "can_edit_posts": 2}) def test_protect_post(self): """api makes it possible to protect posts""" - self.override_acl({ - 'can_protect_posts': 1, - 'can_edit_posts': 2, - }) - response = self.patch( self.api_link, { 'ids': self.ids, @@ -249,10 +230,9 @@ def test_protect_post(self): for post in Post.objects.filter(id__in=self.ids): self.assertTrue(post.is_protected) + @patch_category_acl({"can_protect_posts": False}) def test_protect_post_no_permission(self): """api validates permission to protect posts and returns errors""" - self.override_acl({'can_protect_posts': 0}) - response = self.patch( self.api_link, { 'ids': self.ids, @@ -280,6 +260,7 @@ def test_protect_post_no_permission(self): class BulkPostsApproveApiTests(ThreadPostBulkPatchApiTestCase): + @patch_category_acl({"can_approve_content": True}) def test_approve_post(self): """api resyncs thread and categories on posts approval""" for post in self.posts: @@ -291,8 +272,6 @@ def test_approve_post(self): self.assertNotIn(self.thread.last_post_id, self.ids) - self.override_acl({'can_approve_content': 1}) - response = self.patch( self.api_link, { 'ids': self.ids, diff --git a/misago/threads/tests/test_thread_postedits_api.py b/misago/threads/tests/test_thread_postedits_api.py index cffcf311a7..d43942cf30 100644 --- a/misago/threads/tests/test_thread_postedits_api.py +++ b/misago/threads/tests/test_thread_postedits_api.py @@ -1,6 +1,7 @@ from django.urls import reverse from misago.threads import testutils +from misago.threads.test import patch_category_acl from .test_threads_api import ThreadsApiTestCase @@ -19,8 +20,6 @@ def setUp(self): } ) - self.override_acl() - def mock_edit_record(self): edits_record = [ self.post.edits_record.create( @@ -135,18 +134,19 @@ def setUp(self): super().setUp() self.edits = self.mock_edit_record() - self.override_acl({'can_edit_posts': 2}) - + @patch_category_acl({"can_edit_posts": 2}) def test_empty_edit_id(self): """api handles empty edit in querystring""" response = self.client.post('%s?edit=' % self.api_link) self.assertEqual(response.status_code, 404) + @patch_category_acl({"can_edit_posts": 2}) def test_invalid_edit_id(self): """api handles invalid edit in querystring""" response = self.client.post('%s?edit=dsa67d8sa68' % self.api_link) self.assertEqual(response.status_code, 404) + @patch_category_acl({"can_edit_posts": 2}) def test_nonexistant_edit_id(self): """api handles nonexistant edit in querystring""" response = self.client.post('%s?edit=1321' % self.api_link) @@ -159,13 +159,13 @@ def test_anonymous(self): response = self.client.post('%s?edit=%s' % (self.api_link, self.edits[0].id)) self.assertEqual(response.status_code, 403) + @patch_category_acl({"can_edit_posts": 0}) def test_no_permission(self): """api validates permission to revert post""" - self.override_acl({'can_edit_posts': 0}) - response = self.client.post('%s?edit=1321' % self.api_link) self.assertEqual(response.status_code, 403) + @patch_category_acl({"can_edit_posts": 2}) def test_revert_post(self): """api reverts post to version from before specified edit""" response = self.client.post('%s?edit=%s' % (self.api_link, self.edits[0].id)) diff --git a/misago/threads/tests/test_thread_postlikes_api.py b/misago/threads/tests/test_thread_postlikes_api.py index fc6a8d63e1..89b5c49e86 100644 --- a/misago/threads/tests/test_thread_postlikes_api.py +++ b/misago/threads/tests/test_thread_postlikes_api.py @@ -1,6 +1,7 @@ from django.urls import reverse from misago.threads import testutils +from misago.threads.test import patch_category_acl from misago.threads.serializers import PostLikeSerializer from .test_threads_api import ThreadsApiTestCase @@ -20,32 +21,32 @@ def setUp(self): } ) + @patch_category_acl({"can_see_posts_likes": 0}) def test_no_permission(self): """api errors if user has no permission to see likes""" - self.override_acl({'can_see_posts_likes': 0}) - response = self.client.get(self.api_link) self.assertEqual(response.status_code, 403) self.assertEquals(response.json(), { "detail": "You can't see who liked this post." }) + @patch_category_acl({"can_see_posts_likes": 1}) def test_no_permission_to_list(self): """api errors if user has no permission to see likes, but can see likes count""" - self.override_acl({'can_see_posts_likes': 1}) - response = self.client.get(self.api_link) self.assertEqual(response.status_code, 403) self.assertEquals(response.json(), { "detail": "You can't see who liked this post." }) + @patch_category_acl({"can_see_posts_likes": 2}) def test_no_likes(self): """api returns empty list if post has no likes""" response = self.client.get(self.api_link) self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), []) + @patch_category_acl({"can_see_posts_likes": 2}) def test_likes(self): """api returns list of likes""" like = testutils.like_post(self.post, self.user) diff --git a/misago/threads/views/goto.py b/misago/threads/views/goto.py index 23819cacfb..fc2f5aa3c8 100644 --- a/misago/threads/views/goto.py +++ b/misago/threads/views/goto.py @@ -19,9 +19,13 @@ def get(self, request, pk, slug, **kwargs): thread = self.get_thread(request, pk, slug).unwrap() self.test_permissions(request, thread) - posts_queryset = exclude_invisible_posts(request.user, thread.category, thread.post_set) + posts_queryset = exclude_invisible_posts( + request.user_acl, thread.category, thread.post_set + ) - target_post = self.get_target_post(request.user, thread, posts_queryset.order_by('id'), **kwargs) + target_post = self.get_target_post( + request.user, thread, posts_queryset.order_by('id'), **kwargs + ) target_page = self.compute_post_page(target_post, posts_queryset) return self.get_redirect(thread, target_post, target_page) @@ -38,7 +42,6 @@ def get_target_post(self, user, thread, posts_queryset): def compute_post_page(self, target_post, posts_queryset): # filter out events, order queryset posts_queryset = posts_queryset.filter(is_event=False).order_by('id') - thread_length = posts_queryset.count() # is target an event? From 14ddb2fa5732e59c69d1a78224091c0b72c63022 Mon Sep 17 00:00:00 2001 From: rafalp Date: Sun, 16 Dec 2018 00:11:06 +0100 Subject: [PATCH 51/86] Make tests pass --- misago/acl/tests/test_testutils.py | 1 - misago/threads/api/postendpoints/read.py | 2 +- misago/threads/middleware.py | 4 ++-- misago/threads/tests/test_events.py | 25 +++++++++++------------- 4 files changed, 14 insertions(+), 18 deletions(-) diff --git a/misago/acl/tests/test_testutils.py b/misago/acl/tests/test_testutils.py index a56d19735a..ab9b00f757 100644 --- a/misago/acl/tests/test_testutils.py +++ b/misago/acl/tests/test_testutils.py @@ -8,5 +8,4 @@ class FakeTestDataTests(TestCase): def test_fake_post_data_for_role(self): """fake data was created for Role""" test_data = fake_post_data(Role(), {'can_fly': 1}) - self.assertIn('can_fly', test_data) diff --git a/misago/threads/api/postendpoints/read.py b/misago/threads/api/postendpoints/read.py index 6f1a15402c..c6402b3fe9 100644 --- a/misago/threads/api/postendpoints/read.py +++ b/misago/threads/api/postendpoints/read.py @@ -12,7 +12,7 @@ def post_read_endpoint(request, thread, post): thread.subscription.last_read_on = post.posted_on thread.subscription.save() - threadstracker.make_read_aware(request.user, thread) + threadstracker.make_read_aware(request.user, request.user_acl, thread) # send signal if post read marked thread as read # used in some places, eg. syncing unread thread count diff --git a/misago/threads/middleware.py b/misago/threads/middleware.py index 5a0a10867d..5057a5ffc1 100644 --- a/misago/threads/middleware.py +++ b/misago/threads/middleware.py @@ -22,8 +22,8 @@ def process_request(self, request): category = Category.objects.private_threads() threads = Thread.objects.filter(category=category, id__in=participated_threads) - new_threads = filter_read_threads_queryset(request.user, [category], 'new', threads) - unread_threads = filter_read_threads_queryset(request.user, [category], 'unread', threads) + new_threads = filter_read_threads_queryset(request, [category], 'new', threads) + unread_threads = filter_read_threads_queryset(request, [category], 'unread', threads) request.user.unread_private_threads = new_threads.count() + unread_threads.count() request.user.sync_unread_private_threads = False diff --git a/misago/threads/tests/test_events.py b/misago/threads/tests/test_events.py index 7fbeaead4b..ecd55789d1 100644 --- a/misago/threads/tests/test_events.py +++ b/misago/threads/tests/test_events.py @@ -1,25 +1,21 @@ +from unittest.mock import Mock + from django.contrib.auth import get_user_model from django.test import TestCase from django.utils import timezone -from misago.acl import add_acl +from misago.acl import add_acl, useracl from misago.categories.models import Category from misago.threads.events import record_event from misago.threads.models import Thread - -UserModel = get_user_model() - - -class MockRequest(object): - def __init__(self, user): - self.user = user - self.user_ip = '123.14.15.222' +User = get_user_model() +cache_versions = {"acl": "abcdefgh"} class EventsApiTests(TestCase): def setUp(self): - self.user = UserModel.objects.create_user("Bob", "bob@bob.com", "Pass.123") + self.user = User.objects.create_user("Bob", "bob@bob.com", "Pass.123") datetime = timezone.now() @@ -37,12 +33,13 @@ def setUp(self): self.thread.set_title("Test thread") self.thread.save() - add_acl(self.user, self.category) - add_acl(self.user, self.thread) + user_acl = useracl.get_user_acl(self.user, cache_versions) + add_acl(user_acl, self.category) + add_acl(user_acl, self.thread) def test_record_event_with_context(self): """record_event registers event with context in thread""" - request = MockRequest(self.user) + request = Mock(user=self.user, user_ip="123.14.15.222") context = {'user': 'Lorem ipsum'} event = record_event(request, self.thread, 'announcement', context) @@ -59,7 +56,7 @@ def test_record_event_with_context(self): def test_record_event_is_read(self): """record_event makes recorded event read to its author""" - request = MockRequest(self.user) + request = Mock(user=self.user, user_ip="123.14.15.222") event = record_event(request, self.thread, 'announcement') self.user.postread_set.get( From 35bee7a9c04b2ff6a48c814b7e3663ebc7b8d360 Mon Sep 17 00:00:00 2001 From: rafalp Date: Mon, 17 Dec 2018 00:19:10 +0100 Subject: [PATCH 52/86] Cleanup misago.acl a little --- misago/acl/__init__.py | 4 +- misago/acl/api.py | 65 ---------- misago/acl/apps.py | 1 + misago/acl/cache.py | 23 ++++ misago/acl/constants.py | 1 - misago/acl/migrations/0004_cache_version.py | 3 +- misago/acl/models.py | 6 +- misago/acl/objectacl.py | 18 +++ misago/acl/providers.py | 26 ++-- misago/acl/tests/test_api.py | 26 ---- misago/acl/tests/test_getting_user_acl.py | 7 ++ misago/acl/tests/test_providers.py | 119 +++++++----------- misago/acl/tests/test_serializing_user_acl.py | 23 ++++ misago/acl/useracl.py | 22 +++- misago/acl/version.py | 15 --- 15 files changed, 156 insertions(+), 203 deletions(-) delete mode 100644 misago/acl/api.py create mode 100644 misago/acl/cache.py delete mode 100644 misago/acl/constants.py create mode 100644 misago/acl/objectacl.py delete mode 100644 misago/acl/tests/test_api.py create mode 100644 misago/acl/tests/test_serializing_user_acl.py delete mode 100644 misago/acl/version.py diff --git a/misago/acl/__init__.py b/misago/acl/__init__.py index dc0f37dd08..f8a3165e74 100644 --- a/misago/acl/__init__.py +++ b/misago/acl/__init__.py @@ -1,3 +1,3 @@ -from .api import get_user_acl, add_acl, serialize_acl - default_app_config = 'misago.acl.apps.MisagoACLsConfig' + +ACL_CACHE = "acl" diff --git a/misago/acl/api.py b/misago/acl/api.py deleted file mode 100644 index e854dc8322..0000000000 --- a/misago/acl/api.py +++ /dev/null @@ -1,65 +0,0 @@ -""" -Module functions for ACLs - -Workflow for ACLs in Misago is simple: - -First, you get user ACL. Its directory that you can introspect to find out user -permissions, or if you have objects, you can use this acl to make those objects -aware of their ACLs. This gives objects themselves special "acl" attribute with -properties defined by ACL providers within their "add_acl_to_target" -""" -import copy - -from misago.core import threadstore -from misago.core.cache import cache - -from . import version -from .buildacl import build_acl -from .providers import providers - - -def get_user_acl(user): - """get ACL for User""" - acl_key = 'acl_%s' % user.acl_key - - acl_cache = threadstore.get(acl_key) - if not acl_cache: - acl_cache = cache.get(acl_key) - - if acl_cache and version.is_valid(acl_cache.get('_acl_version')): - return acl_cache - else: - new_acl = build_acl(user.get_roles()) - new_acl['_acl_version'] = version.get_version() - - threadstore.set(acl_key, new_acl) - cache.set(acl_key, new_acl) - - return new_acl - - -def add_acl(user_acl, target): - """add valid ACL to target (iterable of objects or single object)""" - if hasattr(target, '__iter__'): - for item in target: - _add_acl_to_target(user_acl, item) - else: - _add_acl_to_target(user_acl, target) - - -def _add_acl_to_target(user_acl, target): - """add valid ACL to single target, helper for add_acl function""" - target.acl = {} - - for annotator in providers.get_obj_type_annotators(target): - annotator(user_acl, target) - - -def serialize_acl(target): - """serialize authenticated user's ACL""" - serialized_acl = copy.deepcopy(target.acl_cache) - - for serializer in providers.get_obj_type_serializers(target): - serializer(serialized_acl) - - return serialized_acl diff --git a/misago/acl/apps.py b/misago/acl/apps.py index fe7fff16cc..7a3570e2c7 100644 --- a/misago/acl/apps.py +++ b/misago/acl/apps.py @@ -1,6 +1,7 @@ from django.apps import AppConfig from .providers import providers + class MisagoACLsConfig(AppConfig): name = 'misago.acl' label = 'misago_acl' diff --git a/misago/acl/cache.py b/misago/acl/cache.py new file mode 100644 index 0000000000..9ea258a076 --- /dev/null +++ b/misago/acl/cache.py @@ -0,0 +1,23 @@ +from django.core.cache import cache + +from misago.cache.versions import invalidate_cache + +from . import ACL_CACHE + + +def get(user, cache_versions): + key = get_cache_key(user, cache_versions) + return cache.get(key) + + +def set(user, cache_versions, user_acl): + key = get_cache_key(user, cache_versions) + cache.set(key, user_acl) + + +def get_cache_key(user, cache_versions): + return 'acl_%s_%s' % (user.acl_key, cache_versions[ACL_CACHE]) + + +def clear(): + invalidate_cache(ACL_CACHE) diff --git a/misago/acl/constants.py b/misago/acl/constants.py deleted file mode 100644 index c3a6c458ec..0000000000 --- a/misago/acl/constants.py +++ /dev/null @@ -1 +0,0 @@ -ACL_CACHEBUSTER = 'misago_acl' diff --git a/misago/acl/migrations/0004_cache_version.py b/misago/acl/migrations/0004_cache_version.py index 8407b8fce6..5a0d768a87 100644 --- a/misago/acl/migrations/0004_cache_version.py +++ b/misago/acl/migrations/0004_cache_version.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from django.db import migrations +from misago.acl import ACL_CACHE from misago.cache.operations import StartCacheVersioning @@ -12,5 +13,5 @@ class Migration(migrations.Migration): ] operations = [ - StartCacheVersioning("acl") + StartCacheVersioning(ACL_CACHE) ] \ No newline at end of file diff --git a/misago/acl/models.py b/misago/acl/models.py index 4968b088fb..103fc25734 100644 --- a/misago/acl/models.py +++ b/misago/acl/models.py @@ -2,7 +2,7 @@ from django.db import models from django.utils.translation import gettext as _ -from . import version as acl_version +from . import cache def permissions_default(): @@ -22,11 +22,11 @@ def __str__(self): def save(self, *args, **kwargs): if self.pk: - acl_version.invalidate() + cache.clear() return super().save(*args, **kwargs) def delete(self, *args, **kwargs): - acl_version.invalidate() + cache.clear() return super().delete(*args, **kwargs) diff --git a/misago/acl/objectacl.py b/misago/acl/objectacl.py new file mode 100644 index 0000000000..2912b29ab3 --- /dev/null +++ b/misago/acl/objectacl.py @@ -0,0 +1,18 @@ +from .providers import providers + + +def add_acl_to_obj(user_acl, obj): + """add valid ACL to obj (iterable of objects or single object)""" + if hasattr(obj, '__iter__'): + for item in obj: + _add_acl_to_obj(user_acl, item) + else: + _add_acl_to_obj(user_acl, obj) + + +def _add_acl_to_obj(user_acl, obj): + """add valid ACL to single obj, helper for add_acl function""" + obj.acl = {} + + for annotator in providers.get_obj_type_annotators(obj): + annotator(user_acl, obj) diff --git a/misago/acl/providers.py b/misago/acl/providers.py index 9ded5d95e0..87193eccbf 100644 --- a/misago/acl/providers.py +++ b/misago/acl/providers.py @@ -5,7 +5,7 @@ _NOT_INITIALIZED_ERROR = ( "PermissionProviders instance has to load providers with load() " - "before get_obj_type_annotators(), get_obj_type_serializers(), " + "before get_obj_type_annotators(), get_obj_type_user_acl_serializers(), " "list() or dict() methods will be available." ) @@ -24,14 +24,16 @@ def __init__(self): self._providers_dict = {} self._annotators = {} - self._serializers = {} + self._user_acl_serializers = [] def load(self): - if not self._initialized: - self._register_providers() - self._change_lists_to_tupes(self._annotators) - self._change_lists_to_tupes(self._serializers) - self._initialized = True + if self._initialized: + raise RuntimeError("providers are already loaded") + + self._register_providers() + self._cast_dict_values_to_tuples(self._annotators) + self._user_acl_serializers = tuple(self._user_acl_serializers) + self._initialized = True def _register_providers(self): for namespace in settings.MISAGO_ACL_EXTENSIONS: @@ -41,7 +43,7 @@ def _register_providers(self): if hasattr(self._providers_dict[namespace], 'register_with'): self._providers_dict[namespace].register_with(self) - def _change_lists_to_tupes(self, types_dict): + def _cast_dict_values_to_tuples(self, types_dict): for hashType in types_dict.keys(): types_dict[hashType] = tuple(types_dict[hashType]) @@ -50,18 +52,18 @@ def acl_annotator(self, hashable_type, func): assert not self._initialized, _ALREADY_INITIALIZED_ERROR self._annotators.setdefault(hashable_type, []).append(func) - def acl_serializer(self, hashable_type, func): + def user_acl_serializer(self, func): """registers ACL serializer for specified types""" assert not self._initialized, _ALREADY_INITIALIZED_ERROR - self._serializers.setdefault(hashable_type, []).append(func) + self._user_acl_serializers.append(func) def get_obj_type_annotators(self, obj): assert self._initialized, _NOT_INITIALIZED_ERROR return self._annotators.get(obj.__class__, []) - def get_obj_type_serializers(self, obj): + def get_user_acl_serializers(self): assert self._initialized, _NOT_INITIALIZED_ERROR - return self._serializers.get(obj.__class__, []) + return self._user_acl_serializers def list(self): assert self._initialized, _NOT_INITIALIZED_ERROR diff --git a/misago/acl/tests/test_api.py b/misago/acl/tests/test_api.py deleted file mode 100644 index 1d696bdf99..0000000000 --- a/misago/acl/tests/test_api.py +++ /dev/null @@ -1,26 +0,0 @@ -from django.contrib.auth import get_user_model -from django.test import TestCase - -from misago.acl.api import get_user_acl -from misago.users.models import AnonymousUser - - -UserModel = get_user_model() - - -class GetUserACLTests(TestCase): - def test_get_authenticated_acl(self): - """get ACL for authenticated user""" - test_user = UserModel.objects.create_user('Bob', 'bob@bob.com', 'pass123') - - acl = get_user_acl(test_user) - - self.assertTrue(acl) - self.assertEqual(acl, test_user.acl_cache) - - def test_get_anonymous_acl(self): - """get ACL for unauthenticated user""" - acl = get_user_acl(AnonymousUser()) - - self.assertTrue(acl) - self.assertEqual(acl, AnonymousUser().acl_cache) diff --git a/misago/acl/tests/test_getting_user_acl.py b/misago/acl/tests/test_getting_user_acl.py index 79d4b816c1..7d2ac7403c 100644 --- a/misago/acl/tests/test_getting_user_acl.py +++ b/misago/acl/tests/test_getting_user_acl.py @@ -29,6 +29,13 @@ def test_user_acl_includes_staff_and_superuser_false_status(self): assert acl["is_staff"] is False assert acl["is_superuser"] is False + def test_user_acl_includes_cache_versions(self): + user = User.objects.create_user('Bob', 'bob@bob.com') + acl = get_user_acl(user, cache_versions) + + assert acl + assert acl["cache_versions"] == cache_versions + def test_getter_returns_anonymous_user_acl(self): user = AnonymousUser() acl = get_user_acl(user, cache_versions) diff --git a/misago/acl/tests/test_providers.py b/misago/acl/tests/test_providers.py index 32d3e11923..9179b1ef6d 100644 --- a/misago/acl/tests/test_providers.py +++ b/misago/acl/tests/test_providers.py @@ -1,112 +1,85 @@ -from types import ModuleType - from django.test import TestCase from misago.acl.providers import PermissionProviders from misago.conf import settings -class TestType(object): - pass - - class PermissionProvidersTests(TestCase): - def test_initialization(self): - """providers manager is lazily initialized""" + def test_providers_are_not_loaded_on_container_init(self): providers = PermissionProviders() - self.assertTrue(providers._initialized is False) - self.assertTrue(not providers._providers) - self.assertTrue(not providers._providers_dict) - - # public api errors on non-loaded object - with self.assertRaises(AssertionError): - providers.get_obj_type_annotators(TestType()) - - with self.assertRaises(AssertionError): - providers.get_obj_type_serializers(TestType()) - - with self.assertRaises(AssertionError): - providers.list() - - self.assertTrue(providers._initialized is False) - self.assertTrue(not providers._providers) - self.assertTrue(not providers._providers_dict) + assert not providers._initialized + assert not providers._providers + assert not providers._annotators + assert not providers._user_acl_serializers - # load initializes providers + def test_container_loads_providers(self): providers = PermissionProviders() providers.load() - self.assertTrue(providers._initialized) - self.assertTrue(providers._providers) - self.assertTrue(providers._providers_dict) + assert providers._providers + assert providers._annotators + assert providers._user_acl_serializers - def test_list(self): - """providers manager list() returns iterable of tuples""" + def test_loading_providers_second_time_raises_runtime_error(self): providers = PermissionProviders() - - # providers.list() throws before loading providers - with self.assertRaises(AssertionError): - providers.list() - providers.load() - providers_list = providers.list() + with self.assertRaises(RuntimeError): + providers.load() + def test_container_returns_list_of_providers(self): + providers = PermissionProviders() + providers.load() + providers_setting = settings.MISAGO_ACL_EXTENSIONS - self.assertEqual(len(providers_list), len(providers_setting)) - - for extension, module in providers_list: - self.assertTrue(isinstance(extension, str)) - self.assertEqual(type(module), ModuleType) + self.assertEqual(len(providers.list()), len(providers_setting)) - def test_dict(self): - """providers manager dict() returns dict""" + def test_container_returns_dict_of_providers(self): providers = PermissionProviders() + providers.load() + + providers_setting = settings.MISAGO_ACL_EXTENSIONS + self.assertEqual(len(providers.dict()), len(providers_setting)) - # providers.dict() throws before loading providers + def test_accessing_providers_list_before_load_raises_assertion_error(self): + providers = PermissionProviders() + with self.assertRaises(AssertionError): + providers.list() + + def test_accessing_providers_dict_before_load_raises_assertion_error(self): + providers = PermissionProviders() with self.assertRaises(AssertionError): providers.dict() - providers.load() - - providers_dict = providers.dict() - - providers_setting = settings.MISAGO_ACL_EXTENSIONS - self.assertEqual(len(providers_dict), len(providers_setting)) + def test_getter_returns_registered_type_annotator(self): + class TestType(object): + pass - for extension, module in providers_dict.items(): - self.assertTrue(isinstance(extension, str)) - self.assertEqual(type(module), ModuleType) - def test_annotators(self): - """its possible to register and get annotators""" - def mock_annotator(*args): + def test_annotator(): pass + providers = PermissionProviders() - providers.acl_annotator(TestType, mock_annotator) + providers.acl_annotator(TestType, test_annotator) providers.load() - # providers.acl_annotator() throws after loading providers - with self.assertRaises(AssertionError): - providers.acl_annotator(TestType, mock_annotator) + assert test_annotator in providers.get_obj_type_annotators(TestType()) - annotators_list = providers.get_obj_type_annotators(TestType()) - self.assertEqual(annotators_list[0], mock_annotator) + def test_container_returns_list_of_user_acl_serializers(self): + providers = PermissionProviders() + providers.load() - def test_serializers(self): - """its possible to register and get annotators""" - def mock_serializer(*args): + assert providers.get_user_acl_serializers() + + def test_getter_returns_registered_user_acl_serializer(self): + def test_user_acl_serializer(): pass + providers = PermissionProviders() - providers.acl_serializer(TestType, mock_serializer) + providers.user_acl_serializer(test_user_acl_serializer) providers.load() - # providers.acl_serializer() throws after loading providers - with self.assertRaises(AssertionError): - providers.acl_serializer(TestType, mock_serializer) - - serializers_list = providers.get_obj_type_serializers(TestType()) - self.assertEqual(serializers_list[0], mock_serializer) + assert test_user_acl_serializer in providers.get_user_acl_serializers() diff --git a/misago/acl/tests/test_serializing_user_acl.py b/misago/acl/tests/test_serializing_user_acl.py new file mode 100644 index 0000000000..675562d563 --- /dev/null +++ b/misago/acl/tests/test_serializing_user_acl.py @@ -0,0 +1,23 @@ +import json + +from django.contrib.auth import get_user_model +from django.test import TestCase + +from misago.acl.useracl import get_user_acl, serialize_user_acl + +User = get_user_model() + +cache_versions = {"acl": "abcdefgh"} + + +class SerializingUserACLTests(TestCase): + def test_user_acl_is_serializeable(self): + user = User.objects.create_user('Bob', 'bob@bob.com') + acl = get_user_acl(user, cache_versions) + assert serialize_user_acl(acl) + + def test_user_acl_is_json_serializeable(self): + user = User.objects.create_user('Bob', 'bob@bob.com') + acl = get_user_acl(user, cache_versions) + serialized_acl = serialize_user_acl(acl) + assert json.dumps(serialized_acl) diff --git a/misago/acl/useracl.py b/misago/acl/useracl.py index 0423e86fd4..a9b107feb5 100644 --- a/misago/acl/useracl.py +++ b/misago/acl/useracl.py @@ -1,17 +1,29 @@ -from django.core.cache import cache +import copy -from . import buildacl +from . import buildacl, cache +from .providers import providers def get_user_acl(user, cache_versions): - cache_name = 'acl_%s_%s' % (user.acl_key, cache_versions["acl"]) - user_acl = cache.get(cache_name) + user_acl = cache.get(user, cache_versions) if user_acl is None: user_acl = buildacl.build_acl(user.get_roles()) - cache.set(cache_name, user_acl) + cache.set(user, cache_versions, user_acl) user_acl["user_id"] = user.id user_acl["is_authenticated"] = bool(user.is_authenticated) user_acl["is_anonymous"] = bool(user.is_anonymous) user_acl["is_staff"] = user.is_staff user_acl["is_superuser"] = user.is_superuser + user_acl["cache_versions"] = cache_versions.copy() return user_acl + + +def serialize_user_acl(user_acl): + """serialize authenticated user's ACL""" + serialized_acl = copy.deepcopy(user_acl) + serialized_acl.pop("cache_versions") + + for serializer in providers.get_user_acl_serializers(): + serializer(serialized_acl) + + return serialized_acl diff --git a/misago/acl/version.py b/misago/acl/version.py deleted file mode 100644 index cdda904c98..0000000000 --- a/misago/acl/version.py +++ /dev/null @@ -1,15 +0,0 @@ -from misago.core import cachebuster - -from .constants import ACL_CACHEBUSTER - - -def get_version(): - return cachebuster.get_version(ACL_CACHEBUSTER) - - -def is_valid(version): - return cachebuster.is_valid(ACL_CACHEBUSTER, version) - - -def invalidate(): - cachebuster.invalidate(ACL_CACHEBUSTER) From 3a907dd804d19388acedf514148d923b6bb51945 Mon Sep 17 00:00:00 2001 From: rafalp Date: Mon, 17 Dec 2018 02:41:19 +0100 Subject: [PATCH 53/86] Remove User.acl_cache, User.acl_ and direct imports from misago.acl --- devproject/settings.py | 5 +- misago/acl/context_processors.py | 2 + .../tests/test_user_acl_context_processor.py | 12 +++ .../management/commands/fixcategoriestree.py | 4 +- misago/categories/models.py | 4 +- misago/categories/permissions.py | 10 +-- misago/categories/utils.py | 6 +- misago/categories/views/categoriesadmin.py | 4 +- misago/categories/views/permsadmin.py | 6 +- misago/core/tests/test_errorpages.py | 26 +++--- .../tests/test_exceptionhandler_middleware.py | 30 ++++--- .../commands/createfakecategories.py | 4 +- misago/markup/flavours.py | 8 +- .../readtracker/tests/test_threadstracker.py | 4 +- misago/templates/misago/navbar.html | 2 +- misago/templates/misago/threadslist/tabs.html | 2 +- misago/threads/api/attachments.py | 4 +- misago/threads/api/pollvotecreateendpoint.py | 4 +- misago/threads/api/postendpoints/edits.py | 4 +- misago/threads/api/postendpoints/merge.py | 4 +- .../threads/api/postendpoints/patch_event.py | 4 +- .../threads/api/postendpoints/patch_post.py | 4 +- .../api/postingendpoint/attachments.py | 4 +- .../threads/api/postingendpoint/category.py | 4 +- .../api/postingendpoint/privatethread.py | 4 +- misago/threads/api/threadendpoints/editor.py | 4 +- misago/threads/api/threadendpoints/merge.py | 4 +- misago/threads/api/threadendpoints/patch.py | 7 +- misago/threads/api/threadpoll.py | 6 +- misago/threads/api/threadposts.py | 4 +- misago/threads/permissions/threads.py | 9 +- misago/threads/serializers/moderation.py | 10 +-- misago/threads/tests/test_events.py | 7 +- .../tests/test_privatethread_patch_api.py | 3 +- .../threads/tests/test_threads_editor_api.py | 5 +- .../threads/tests/test_threads_merge_api.py | 11 +-- misago/threads/viewmodels/category.py | 4 +- misago/threads/viewmodels/post.py | 4 +- misago/threads/viewmodels/posts.py | 4 +- misago/threads/viewmodels/thread.py | 8 +- misago/threads/viewmodels/threads.py | 4 +- misago/users/api/auth.py | 7 +- misago/users/api/userendpoints/signature.py | 3 +- misago/users/api/userendpoints/username.py | 81 ++++++++++-------- misago/users/api/users.py | 4 +- misago/users/context_processors.py | 7 +- misago/users/models/rank.py | 6 +- misago/users/models/user.py | 31 ++----- misago/users/namechanges.py | 70 +++++++++------- misago/users/permissions/moderation.py | 2 +- misago/users/permissions/profiles.py | 5 +- misago/users/serializers/auth.py | 16 ++-- misago/users/signatures.py | 4 +- misago/users/tests/test_bans.py | 17 ++-- misago/users/tests/test_namechanges.py | 84 +++++++++++++++---- misago/users/tests/test_signatures.py | 58 +++++++++---- misago/users/tests/test_social_pipeline.py | 5 +- misago/users/viewmodels/threads.py | 6 +- misago/users/views/admin/users.py | 6 +- misago/users/views/profile.py | 4 +- 60 files changed, 398 insertions(+), 277 deletions(-) create mode 100644 misago/acl/context_processors.py create mode 100644 misago/acl/tests/test_user_acl_context_processor.py diff --git a/devproject/settings.py b/devproject/settings.py index c7cacbe57a..d774a9c66d 100644 --- a/devproject/settings.py +++ b/devproject/settings.py @@ -286,12 +286,13 @@ 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', + 'misago.acl.context_processors.user_acl', + 'misago.conf.context_processors.settings', 'misago.core.context_processors.site_address', 'misago.core.context_processors.momentjs_locale', - 'misago.conf.context_processors.settings', + 'misago.legal.context_processors.legal_links', 'misago.search.context_processors.search_providers', 'misago.users.context_processors.user_links', - 'misago.legal.context_processors.legal_links', # Data preloaders 'misago.conf.context_processors.preload_settings_json', diff --git a/misago/acl/context_processors.py b/misago/acl/context_processors.py new file mode 100644 index 0000000000..93d70b6c76 --- /dev/null +++ b/misago/acl/context_processors.py @@ -0,0 +1,2 @@ +def user_acl(request): + return {"user_acl": request.user_acl} diff --git a/misago/acl/tests/test_user_acl_context_processor.py b/misago/acl/tests/test_user_acl_context_processor.py new file mode 100644 index 0000000000..f1148aa847 --- /dev/null +++ b/misago/acl/tests/test_user_acl_context_processor.py @@ -0,0 +1,12 @@ +from unittest.mock import Mock + +from django.test import TestCase + +from misago.acl.context_processors import user_acl + + +class ContextProcessorsTests(TestCase): + def test_context_processor_adds_request_user_acl_to_context(self): + test_acl = {"test": True} + context = user_acl(Mock(user_acl=test_acl)) + assert context == {"user_acl": test_acl} \ No newline at end of file diff --git a/misago/categories/management/commands/fixcategoriestree.py b/misago/categories/management/commands/fixcategoriestree.py index 8d9de51873..4ae47d7456 100644 --- a/misago/categories/management/commands/fixcategoriestree.py +++ b/misago/categories/management/commands/fixcategoriestree.py @@ -1,6 +1,6 @@ from django.core.management.base import BaseCommand -from misago.acl import version as acl_version +from misago.acl.cache import clear as clear_acl_cache from misago.categories.models import Category @@ -19,5 +19,5 @@ def handle(self, *args, **options): self.stdout.write("Categories tree has been rebuild.") Category.objects.clear_cache() - acl_version.invalidate() + clear_acl_cache() self.stdout.write("Caches have been cleared.") diff --git a/misago/categories/models.py b/misago/categories/models.py index 990efda47d..229d6017e5 100644 --- a/misago/categories/models.py +++ b/misago/categories/models.py @@ -3,7 +3,7 @@ from django.db import models -from misago.acl import version as acl_version +from misago.acl.cache import clear as clear_acl_cache from misago.acl.models import BaseRole from misago.conf import settings from misago.core.cache import cache @@ -115,7 +115,7 @@ def thread_type(self): def delete(self, *args, **kwargs): Category.objects.clear_cache() - acl_version.invalidate() + clear_acl_cache() return super().delete(*args, **kwargs) def synchronize(self): diff --git a/misago/categories/permissions.py b/misago/categories/permissions.py index 9e786ba83d..485227d167 100644 --- a/misago/categories/permissions.py +++ b/misago/categories/permissions.py @@ -90,9 +90,9 @@ def add_acl_to_category(user_acl, target): target.acl['can_browse'] = can_browse_category(user_acl, target) -def serialize_categories_acls(serialized_acl): +def serialize_categories_acls(user_acl): categories_acl = [] - for category, acl in serialized_acl.pop('categories').items(): + for category, acl in user_acl.pop('categories').items(): if acl['can_browse']: categories_acl.append({ 'id': category, @@ -102,14 +102,12 @@ def serialize_categories_acls(serialized_acl): 'can_hide_threads': acl.get('can_hide_threads', 0), 'can_close_threads': acl.get('can_close_threads', False), }) - serialized_acl['categories'] = categories_acl + user_acl['categories'] = categories_acl def register_with(registry): registry.acl_annotator(Category, add_acl_to_category) - - registry.acl_serializer(get_user_model(), serialize_categories_acls) - registry.acl_serializer(AnonymousUser, serialize_categories_acls) + registry.user_acl_serializer(serialize_categories_acls) def allow_see_category(user_acl, target): diff --git a/misago/categories/utils.py b/misago/categories/utils.py index 6e514736f2..1ad859237b 100644 --- a/misago/categories/utils.py +++ b/misago/categories/utils.py @@ -1,11 +1,11 @@ -from misago.acl import add_acl +from misago.acl.objectacl import add_acl_to_obj from misago.readtracker import categoriestracker from .models import Category def get_categories_tree(user, user_acl, parent=None, join_posters=False): - if not user.acl_cache['visible_categories']: + if not user_acl['visible_categories']: return [] if parent: @@ -32,7 +32,7 @@ def get_categories_tree(user, user_acl, parent=None, join_posters=False): if category.parent_id and category.level > parent_level: categories_dict[category.parent_id].subcategories.append(category) - add_acl(user_acl, categories_list) + add_acl_to_obj(user_acl, categories_list) categoriestracker.make_read_aware(user, user_acl, categories_list) for category in reversed(visible_categories): diff --git a/misago/categories/views/categoriesadmin.py b/misago/categories/views/categoriesadmin.py index 7b687872f4..339efe61fa 100644 --- a/misago/categories/views/categoriesadmin.py +++ b/misago/categories/views/categoriesadmin.py @@ -2,7 +2,7 @@ from django.shortcuts import redirect from django.utils.translation import gettext_lazy as _ -from misago.acl import version as acl_version +from misago.acl.cache import clear as clear_acl_cache from misago.admin.views import generic from misago.categories import THREADS_ROOT_NAME from misago.categories.forms import CategoryFormFactory, DeleteFormFactory @@ -88,7 +88,7 @@ def handle_form(self, form, request, target): if copied_acls: RoleCategoryACL.objects.bulk_create(copied_acls) - acl_version.invalidate() + clear_acl_cache() messages.success(request, self.message_submit % {'name': target.name}) diff --git a/misago/categories/views/permsadmin.py b/misago/categories/views/permsadmin.py index 9aa8b5d8ca..4d66d2f959 100644 --- a/misago/categories/views/permsadmin.py +++ b/misago/categories/views/permsadmin.py @@ -2,7 +2,7 @@ from django.shortcuts import redirect from django.utils.translation import gettext_lazy as _ -from misago.acl import version as acl_version +from misago.acl.cache import clear as clear_acl_cache from misago.acl.forms import get_permissions_forms from misago.acl.models import Role from misago.acl.views import RoleAdmin, RolesList @@ -128,7 +128,7 @@ def real_dispatch(self, request, target): if new_permissions: RoleCategoryACL.objects.bulk_create(new_permissions) - acl_version.invalidate() + clear_acl_cache() message = _("Category %(name)s permissions have been changed.") messages.success(request, message % {'name': target.name}) @@ -196,7 +196,7 @@ def real_dispatch(self, request, target): if new_permissions: RoleCategoryACL.objects.bulk_create(new_permissions) - acl_version.invalidate() + clear_acl_cache() message = _("Category permissions for role %(name)s have been changed.") messages.success(request, message % {'name': target.name}) diff --git a/misago/core/tests/test_errorpages.py b/misago/core/tests/test_errorpages.py index a6a97960aa..5c32bdcaf0 100644 --- a/misago/core/tests/test_errorpages.py +++ b/misago/core/tests/test_errorpages.py @@ -4,9 +4,10 @@ from django.test.client import RequestFactory from django.urls import reverse +from misago.acl.useracl import get_user_acl +from misago.users.models import AnonymousUser from misago.core.testproject.views import mock_custom_403_error_page, mock_custom_404_error_page from misago.core.utils import encode_json_html -from misago.users.models import AnonymousUser class CSRFErrorViewTests(TestCase): @@ -73,20 +74,21 @@ def test_social_auth_banned(self): self.assertContains(response, "Banned in auth!", status_code=403) +def test_request(url): + request = RequestFactory().get(url) + request.cache_versions = {"acl": "abcdefgh"} + request.user = AnonymousUser() + request.user_acl = get_user_acl(request.user, request.cache_versions) + request.include_frontend_context = True + request.frontend_context = {} + return request + + @override_settings(ROOT_URLCONF='misago.core.testproject.urlswitherrorhandlers') class CustomErrorPagesTests(TestCase): def setUp(self): - self.misago_request = RequestFactory().get(reverse('misago:index')) - self.site_request = RequestFactory().get(reverse('raise-403')) - - self.misago_request.user = AnonymousUser() - self.site_request.user = AnonymousUser() - - self.misago_request.include_frontend_context = True - self.site_request.include_frontend_context = True - - self.misago_request.frontend_context = {} - self.site_request.frontend_context = {} + self.misago_request = test_request(reverse('misago:index')) + self.site_request = test_request(reverse('raise-403')) def test_shared_403_decorator(self): """shared_403_decorator calls correct error handler""" diff --git a/misago/core/tests/test_exceptionhandler_middleware.py b/misago/core/tests/test_exceptionhandler_middleware.py index b1250ff30b..4f4887082c 100644 --- a/misago/core/tests/test_exceptionhandler_middleware.py +++ b/misago/core/tests/test_exceptionhandler_middleware.py @@ -3,27 +3,31 @@ from django.test.client import RequestFactory from django.urls import reverse -from misago.core.middleware import ExceptionHandlerMiddleware +from misago.acl.useracl import get_user_acl from misago.users.models import AnonymousUser +from misago.core.middleware import ExceptionHandlerMiddleware -class ExceptionHandlerMiddlewareTests(TestCase): - def setUp(self): - self.request = RequestFactory().get(reverse('misago:index')) - self.request.user = AnonymousUser() - self.request.include_frontend_context = True - self.request.frontend_context = {} +def test_request(): + request = RequestFactory().get(reverse('misago:index')) + request.cache_versions = {"acl": "abcdefgh"} + request.user = AnonymousUser() + request.user_acl = get_user_acl(request.user, request.cache_versions) + request.include_frontend_context = True + request.frontend_context = {} + return request + + +class ExceptionHandlerMiddlewareTests(TestCase): def test_middleware_returns_response_for_supported_exception(self): """Middleware returns HttpResponse for supported exception""" - exception = Http404() middleware = ExceptionHandlerMiddleware() - - self.assertTrue(middleware.process_exception(self.request, exception)) + exception = Http404() + assert middleware.process_exception(test_request(), exception) def test_middleware_returns_none_for_non_supported_exception(self): """Middleware returns None for non-supported exception""" - exception = TypeError() middleware = ExceptionHandlerMiddleware() - - self.assertFalse(middleware.process_exception(self.request, exception)) + exception = TypeError() + assert middleware.process_exception(test_request(), exception) is None diff --git a/misago/faker/management/commands/createfakecategories.py b/misago/faker/management/commands/createfakecategories.py index 2f92738efd..0fccc9d1d2 100644 --- a/misago/faker/management/commands/createfakecategories.py +++ b/misago/faker/management/commands/createfakecategories.py @@ -5,7 +5,7 @@ from django.core.management.base import BaseCommand -from misago.acl import version as acl_version +from misago.acl.cache import clear as clear_acl_cache from misago.categories.models import Category, RoleCategoryACL from misago.core.management.progressbar import show_progress @@ -85,7 +85,7 @@ def handle(self, *args, **options): created_count += 1 show_progress(self, created_count, items_to_create, start_time) - acl_version.invalidate() + clear_acl_cache() total_time = time.time() - start_time total_humanized = time.strftime('%H:%M:%S', time.gmtime(total_time)) diff --git a/misago/markup/flavours.py b/misago/markup/flavours.py index 6f1cf8364c..ef71a4110f 100644 --- a/misago/markup/flavours.py +++ b/misago/markup/flavours.py @@ -43,15 +43,15 @@ def limited(request, text): return result['parsed_text'] -def signature(request, owner, text): +def signature(request, owner, user_acl, text): result = parse( text, request, owner, allow_mentions=False, - allow_blocks=owner.acl_cache['allow_signature_blocks'], - allow_links=owner.acl_cache['allow_signature_links'], - allow_images=owner.acl_cache['allow_signature_images'], + allow_blocks=user_acl['allow_signature_blocks'], + allow_links=user_acl['allow_signature_links'], + allow_images=user_acl['allow_signature_images'], ) return result['parsed_text'] diff --git a/misago/readtracker/tests/test_threadstracker.py b/misago/readtracker/tests/test_threadstracker.py index e9e8a68ba7..3b1ae2d465 100644 --- a/misago/readtracker/tests/test_threadstracker.py +++ b/misago/readtracker/tests/test_threadstracker.py @@ -4,7 +4,7 @@ from django.test import TestCase from django.utils import timezone -from misago.acl import add_acl +from misago.acl.objectacl import add_acl_to_obj from misago.acl.useracl import get_user_acl from misago.categories.models import Category from misago.conf import settings @@ -28,7 +28,7 @@ def setUp(self): self.user_acl = get_user_acl(self.user, cache_versions) self.category = Category.objects.get(slug='first-category') - add_acl(self.user_acl, self.category) + add_acl_to_obj(self.user_acl, self.category) def test_falsy_value(self): """passing falsy value to readtracker causes no errors""" diff --git a/misago/templates/misago/navbar.html b/misago/templates/misago/navbar.html index 3c3a0f4699..6fac253447 100644 --- a/misago/templates/misago/navbar.html +++ b/misago/templates/misago/navbar.html @@ -93,7 +93,7 @@ group - {% if user.acl_cache.can_search %} + {% if user_acl.can_search %}
  • search diff --git a/misago/templates/misago/threadslist/tabs.html b/misago/templates/misago/threadslist/tabs.html index 164075cfb1..937ae5ccae 100644 --- a/misago/templates/misago/threadslist/tabs.html +++ b/misago/templates/misago/threadslist/tabs.html @@ -27,7 +27,7 @@ {% trans "Subscribed" %}
  • - {% if user.acl_cache.can_see_unapproved_content_lists and not hide_unapproved %} + {% if user_acl.can_see_unapproved_content_lists and not hide_unapproved %} {% trans "Unapproved" %} diff --git a/misago/threads/api/attachments.py b/misago/threads/api/attachments.py index d1a32338aa..5b776c798d 100644 --- a/misago/threads/api/attachments.py +++ b/misago/threads/api/attachments.py @@ -5,7 +5,7 @@ from django.template.defaultfilters import filesizeformat from django.utils.translation import gettext as _ -from misago.acl import add_acl +from misago.acl.objectacl import add_acl_to_obj from misago.threads.models import Attachment, AttachmentType from misago.threads.serializers import AttachmentSerializer from misago.users.audittrail import create_audit_trail @@ -52,7 +52,7 @@ def create_attachment(self, request): attachment.set_file(upload) attachment.save() - add_acl(request.user_acl, attachment) + add_acl_to_obj(request.user_acl, attachment) create_audit_trail(request, attachment) diff --git a/misago/threads/api/pollvotecreateendpoint.py b/misago/threads/api/pollvotecreateendpoint.py index 2898d83b88..187891e366 100644 --- a/misago/threads/api/pollvotecreateendpoint.py +++ b/misago/threads/api/pollvotecreateendpoint.py @@ -2,7 +2,7 @@ from rest_framework.response import Response -from misago.acl import add_acl +from misago.acl.objectacl import add_acl_to_obj from misago.threads.permissions import allow_vote_poll from misago.threads.serializers import PollSerializer, NewVoteSerializer @@ -33,7 +33,7 @@ def poll_vote_create(request, thread, poll): remove_user_votes(request.user, poll, serializer.data['choices']) set_new_votes(request, poll, serializer.data['choices']) - add_acl(request.user_acl, poll) + add_acl_to_obj(request.user_acl, poll) serialized_poll = PollSerializer(poll).data poll.choices = list(map(presave_clean_choice, deepcopy(poll.choices))) diff --git a/misago/threads/api/postendpoints/edits.py b/misago/threads/api/postendpoints/edits.py index 30bb94b14b..111f00b6ce 100644 --- a/misago/threads/api/postendpoints/edits.py +++ b/misago/threads/api/postendpoints/edits.py @@ -6,7 +6,7 @@ from django.utils import timezone from django.utils.translation import gettext as _ -from misago.acl import add_acl +from misago.acl.objectacl import add_acl_to_obj from misago.core.shortcuts import get_int_or_404 from misago.markup import common_flavour from misago.threads.checksums import update_post_checksum @@ -71,7 +71,7 @@ def revert_post_endpoint(request, post): post.is_new = False post.edits = post_edits + 1 - add_acl(request.user_acl, post) + add_acl_to_obj(request.user_acl, post) if post.poster: make_users_status_aware(request, [post.poster]) diff --git a/misago/threads/api/postendpoints/merge.py b/misago/threads/api/postendpoints/merge.py index 59afef0580..9155778a85 100644 --- a/misago/threads/api/postendpoints/merge.py +++ b/misago/threads/api/postendpoints/merge.py @@ -3,7 +3,7 @@ from django.core.exceptions import PermissionDenied from django.utils.translation import gettext as _ -from misago.acl import add_acl +from misago.acl.objectacl import add_acl_to_obj from misago.threads.serializers import MergePostsSerializer, PostSerializer @@ -55,6 +55,6 @@ def posts_merge_endpoint(request, thread): first_post.thread = thread first_post.category = thread.category - add_acl(request.user_acl, first_post) + add_acl_to_obj(request.user_acl, first_post) return Response(PostSerializer(first_post, context={'user': request.user}).data) diff --git a/misago/threads/api/postendpoints/patch_event.py b/misago/threads/api/postendpoints/patch_event.py index 6f97575d25..ffb4000923 100644 --- a/misago/threads/api/postendpoints/patch_event.py +++ b/misago/threads/api/postendpoints/patch_event.py @@ -1,7 +1,7 @@ from django.core.exceptions import PermissionDenied from django.utils.translation import gettext as _ -from misago.acl import add_acl +from misago.acl.objectacl import add_acl_to_obj from misago.core.apipatch import ApiPatch from misago.threads.moderation import posts as moderation from misago.threads.permissions import allow_hide_event, allow_unhide_event @@ -13,7 +13,7 @@ def patch_acl(request, event, value): """useful little op that updates event acl to current state""" if value: - add_acl(request.user_acl, event) + add_acl_to_obj(request.user_acl, event) return {'acl': event.acl} else: return {'acl': None} diff --git a/misago/threads/api/postendpoints/patch_post.py b/misago/threads/api/postendpoints/patch_post.py index d26b205672..b0d51e338f 100644 --- a/misago/threads/api/postendpoints/patch_post.py +++ b/misago/threads/api/postendpoints/patch_post.py @@ -4,7 +4,7 @@ from django.core.exceptions import PermissionDenied from django.utils.translation import gettext as _ -from misago.acl import add_acl +from misago.acl.objectacl import add_acl_to_obj from misago.conf import settings from misago.core.apipatch import ApiPatch from misago.threads.models import PostLike @@ -23,7 +23,7 @@ def patch_acl(request, post, value): """useful little op that updates post acl to current state""" if value: - add_acl(request.user_acl, post) + add_acl_to_obj(request.user_acl, post) return {'acl': post.acl} else: return {'acl': None} diff --git a/misago/threads/api/postingendpoint/attachments.py b/misago/threads/api/postingendpoint/attachments.py index 6ae74bb303..fbc2e9de76 100644 --- a/misago/threads/api/postingendpoint/attachments.py +++ b/misago/threads/api/postingendpoint/attachments.py @@ -3,7 +3,7 @@ from django.utils.translation import gettext as _ from django.utils.translation import ngettext -from misago.acl import add_acl +from misago.acl.objectacl import add_acl_to_obj from misago.conf import settings from misago.threads.serializers import AttachmentSerializer @@ -75,7 +75,7 @@ def get_initial_attachments(self, mode, user_acl, post): if mode == PostingEndpoint.EDIT: queryset = post.attachment_set.select_related('filetype') attachments = list(queryset) - add_acl(user_acl, attachments) + add_acl_to_obj(user_acl, attachments) return attachments def get_new_attachments(self, user, ids): diff --git a/misago/threads/api/postingendpoint/category.py b/misago/threads/api/postingendpoint/category.py index 94f7922961..05f6337ced 100755 --- a/misago/threads/api/postingendpoint/category.py +++ b/misago/threads/api/postingendpoint/category.py @@ -4,7 +4,7 @@ from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy -from misago.acl import add_acl +from misago.acl.objectacl import add_acl_to_obj from misago.categories import THREADS_ROOT_NAME from misago.categories.models import Category from misago.categories.permissions import can_browse_category, can_see_category @@ -28,7 +28,7 @@ def get_serializer(self): def pre_save(self, serializer): category = serializer.category_cache - add_acl(self.user_acl, category) + add_acl_to_obj(self.user_acl, category) # set flags for savechanges middleware category.update_all = False diff --git a/misago/threads/api/postingendpoint/privatethread.py b/misago/threads/api/postingendpoint/privatethread.py index 38212a5ce7..ab3172ecb5 100644 --- a/misago/threads/api/postingendpoint/privatethread.py +++ b/misago/threads/api/postingendpoint/privatethread.py @@ -1,4 +1,4 @@ -from misago.acl import add_acl +from misago.acl.objectacl import add_acl_to_obj from misago.categories import PRIVATE_THREADS_ROOT_NAME from misago.categories.models import Category @@ -16,7 +16,7 @@ def use_this_middleware(self): def pre_save(self, serializer): category = Category.objects.private_threads() - add_acl(self.user_acl, category) + add_acl_to_obj(self.user_acl, category) # set flags for savechanges middleware category.update_all = False diff --git a/misago/threads/api/threadendpoints/editor.py b/misago/threads/api/threadendpoints/editor.py index 18bdc56ff9..eb6ebe0486 100644 --- a/misago/threads/api/threadendpoints/editor.py +++ b/misago/threads/api/threadendpoints/editor.py @@ -3,7 +3,7 @@ from django.core.exceptions import PermissionDenied from django.utils.translation import gettext as _ -from misago.acl import add_acl +from misago.acl.objectacl import add_acl_to_obj from misago.categories import THREADS_ROOT_NAME from misago.categories.models import Category from misago.threads.permissions import can_start_thread @@ -24,7 +24,7 @@ def thread_start_editor(request): ).order_by('-lft') for category in queryset: - add_acl(request.user_acl, category) + add_acl_to_obj(request.user_acl, category) post = False if can_start_thread(request.user_acl, category): diff --git a/misago/threads/api/threadendpoints/merge.py b/misago/threads/api/threadendpoints/merge.py index fa414a2353..756503c269 100644 --- a/misago/threads/api/threadendpoints/merge.py +++ b/misago/threads/api/threadendpoints/merge.py @@ -4,7 +4,7 @@ from django.core.exceptions import PermissionDenied from django.utils.translation import gettext as _ -from misago.acl import add_acl +from misago.acl.objectacl import add_acl_to_obj from misago.threads.events import record_event from misago.threads.mergeconflict import MergeConflict from misago.threads.models import Thread @@ -189,5 +189,5 @@ def merge_threads(request, validated_data, threads, merge_conflict): new_thread.is_read = False new_thread.subscription = None - add_acl(request.user_acl, new_thread) + add_acl_to_obj(request.user_acl, new_thread) return new_thread diff --git a/misago/threads/api/threadendpoints/patch.py b/misago/threads/api/threadendpoints/patch.py index 513f3afa1c..af210f4ea3 100644 --- a/misago/threads/api/threadendpoints/patch.py +++ b/misago/threads/api/threadendpoints/patch.py @@ -7,7 +7,8 @@ from django.shortcuts import get_object_or_404 from django.utils.translation import gettext as _ -from misago.acl import add_acl, useracl +from misago.acl import useracl +from misago.acl.objectacl import add_acl_to_obj from misago.categories.models import Category from misago.categories.permissions import allow_browse_category, allow_see_category from misago.categories.serializers import CategorySerializer @@ -37,7 +38,7 @@ def patch_acl(request, thread, value): """useful little op that updates thread acl to current state""" if value: - add_acl(request.user_acl, thread) + add_acl_to_obj(request.user_acl, thread) return {'acl': thread.acl} else: return {'acl': None} @@ -96,7 +97,7 @@ def patch_move(request, thread, value): Category.objects.all_categories().select_related('parent'), pk=category_pk ) - add_acl(request.user_acl, new_category) + add_acl_to_obj(request.user_acl, new_category) allow_see_category(request.user_acl, new_category) allow_browse_category(request.user_acl, new_category) allow_start_thread(request.user_acl, new_category) diff --git a/misago/threads/api/threadpoll.py b/misago/threads/api/threadpoll.py index a411435021..03e3c0039c 100644 --- a/misago/threads/api/threadpoll.py +++ b/misago/threads/api/threadpoll.py @@ -7,7 +7,7 @@ from django.http import Http404 from django.utils.translation import gettext as _ -from misago.acl import add_acl +from misago.acl.objectacl import add_acl_to_obj from misago.core.shortcuts import get_int_or_404 from misago.threads.models import Poll from misago.threads.permissions import ( @@ -68,7 +68,7 @@ def create(self, request, thread_pk): serializer.save() - add_acl(request.user_acl, instance) + add_acl_to_obj(request.user_acl, instance) for choice in instance.choices: choice['selected'] = False @@ -91,7 +91,7 @@ def update(self, request, thread_pk, pk=None): serializer.save() - add_acl(request.user_acl, instance) + add_acl_to_obj(request.user_acl, instance) instance.make_choices_votes_aware(request.user) create_audit_trail(request, instance) diff --git a/misago/threads/api/threadposts.py b/misago/threads/api/threadposts.py index e86f8f2201..148f0efdb7 100644 --- a/misago/threads/api/threadposts.py +++ b/misago/threads/api/threadposts.py @@ -6,7 +6,7 @@ from django.db import transaction from django.utils.translation import gettext as _ -from misago.acl import add_acl +from misago.acl.objectacl import add_acl_to_obj from misago.core.shortcuts import get_int_or_404 from misago.threads.models import Post from misago.threads.permissions import allow_edit_post, allow_reply_thread @@ -192,7 +192,7 @@ def post_editor(self, request, thread_pk, pk=None): attachments = [] for attachment in post.attachment_set.order_by('-id'): - add_acl(request.user_acl, attachment) + add_acl_to_obj(request.user_acl, attachment) attachments.append(attachment) attachments_json = AttachmentSerializer( attachments, many=True, context={'user': request.user} diff --git a/misago/threads/permissions/threads.py b/misago/threads/permissions/threads.py index e2785a3f2f..71568ebc5e 100644 --- a/misago/threads/permissions/threads.py +++ b/misago/threads/permissions/threads.py @@ -5,9 +5,10 @@ from django.utils import timezone from django.utils.translation import gettext_lazy as _, ngettext -from misago.acl import add_acl, algebra +from misago.acl import algebra from misago.acl.decorators import return_boolean from misago.acl.models import Role +from misago.acl.objectacl import add_acl_to_obj from misago.admin.forms import YesNoSwitch from misago.categories.models import Category, CategoryRole from misago.categories.permissions import get_categories_roles @@ -1251,7 +1252,7 @@ def exclude_invisible_threads(user_acl, categories, queryset): show_owned_visible = [] for category in categories: - add_acl(user_acl, category) + add_acl_to_obj(user_acl, category) if not (category.acl['can_see'] and category.acl['can_browse']): continue @@ -1360,7 +1361,7 @@ def exclude_invisible_posts_in_categories(user_acl, categories, queryset): hide_invisible_events = [] for category in categories: - add_acl(user_acl, category) + add_acl_to_obj(user_acl, category) if category.acl['can_approve_content']: show_all.append(category.pk) @@ -1413,7 +1414,7 @@ def exclude_invisible_posts_in_categories(user_acl, categories, queryset): def exclude_invisible_posts_in_category(user_acl, category, queryset): - add_acl(user_acl, category) + add_acl_to_obj(user_acl, category) if not category.acl['can_approve_content']: if user_acl["is_authenticated"]: diff --git a/misago/threads/serializers/moderation.py b/misago/threads/serializers/moderation.py index f2ddeb1c46..3e44df8c07 100644 --- a/misago/threads/serializers/moderation.py +++ b/misago/threads/serializers/moderation.py @@ -4,7 +4,7 @@ from django.http import Http404 from django.utils.translation import gettext as _, gettext_lazy, ngettext -from misago.acl import add_acl +from misago.acl.objectacl import add_acl_to_obj from misago.categories import THREADS_ROOT_NAME from misago.conf import settings from misago.threads.mergeconflict import MergeConflict @@ -267,7 +267,7 @@ def validate_category(self, category_id): def validate_weight(self, weight): try: - add_acl(self.context['user_acl'], self.category) + add_acl_to_obj(self.context['user_acl'], self.category) except AttributeError: return weight # don't validate weight further if category failed @@ -284,7 +284,7 @@ def validate_weight(self, weight): def validate_is_hidden(self, is_hidden): try: - add_acl(self.context['user_acl'], self.category) + add_acl_to_obj(self.context['user_acl'], self.category) except AttributeError: return is_hidden # don't validate hidden further if category failed @@ -294,7 +294,7 @@ def validate_is_hidden(self, is_hidden): def validate_is_closed(self, is_closed): try: - add_acl(self.context['user_acl'], self.category) + add_acl_to_obj(self.context['user_acl'], self.category) except AttributeError: return is_closed # don't validate closed further if category failed @@ -523,7 +523,7 @@ def validate_threads(self, data): threads = [] for thread in threads_queryset: - add_acl(user_acl, thread) + add_acl_to_obj(user_acl, thread) if can_see_thread(user_acl, thread): threads.append(thread) diff --git a/misago/threads/tests/test_events.py b/misago/threads/tests/test_events.py index ecd55789d1..216ccfef00 100644 --- a/misago/threads/tests/test_events.py +++ b/misago/threads/tests/test_events.py @@ -4,7 +4,8 @@ from django.test import TestCase from django.utils import timezone -from misago.acl import add_acl, useracl +from misago.acl import useracl +from misago.acl.objectacl import add_acl_to_obj from misago.categories.models import Category from misago.threads.events import record_event from misago.threads.models import Thread @@ -34,8 +35,8 @@ def setUp(self): self.thread.save() user_acl = useracl.get_user_acl(self.user, cache_versions) - add_acl(user_acl, self.category) - add_acl(user_acl, self.thread) + add_acl_to_obj(user_acl, self.category) + add_acl_to_obj(user_acl, self.thread) def test_record_event_with_context(self): """record_event registers event with context in thread""" diff --git a/misago/threads/tests/test_privatethread_patch_api.py b/misago/threads/tests/test_privatethread_patch_api.py index ae6a168c8f..e46ceb5224 100644 --- a/misago/threads/tests/test_privatethread_patch_api.py +++ b/misago/threads/tests/test_privatethread_patch_api.py @@ -145,11 +145,12 @@ def test_add_no_perm_user(self): 'detail': ["BobBoberson can't participate in private threads."], }) + @patch_user_acl({"max_private_thread_participants": 3}) def test_add_too_many_users(self): """can't add user that is already participant""" ThreadParticipant.objects.set_owner(self.thread, self.user) - for i in range(self.user.acl_cache['max_private_thread_participants']): + for i in range(3): user = UserModel.objects.create_user( 'User%s' % i, 'user%s@example.com' % i, 'Pass.123' ) diff --git a/misago/threads/tests/test_threads_editor_api.py b/misago/threads/tests/test_threads_editor_api.py index 635803ddb8..85810836f8 100644 --- a/misago/threads/tests/test_threads_editor_api.py +++ b/misago/threads/tests/test_threads_editor_api.py @@ -2,7 +2,8 @@ from django.urls import reverse -from misago.acl import add_acl, useracl +from misago.acl import useracl +from misago.acl.objectacl import add_acl_to_obj from misago.categories.models import Category from misago.threads import testutils from misago.threads.models import Attachment @@ -557,7 +558,7 @@ def test_edit(self): response = self.client.get(self.api_link) user_acl = useracl.get_user_acl(self.user, cache_versions) for attachment in attachments: - add_acl(user_acl, attachment) + add_acl_to_obj(user_acl, attachment) self.assertEqual(response.status_code, 200) self.assertEqual( diff --git a/misago/threads/tests/test_threads_merge_api.py b/misago/threads/tests/test_threads_merge_api.py index 0052cd3bdf..0bb8dfde61 100644 --- a/misago/threads/tests/test_threads_merge_api.py +++ b/misago/threads/tests/test_threads_merge_api.py @@ -2,7 +2,8 @@ from django.urls import reverse -from misago.acl import add_acl, useracl +from misago.acl import useracl +from misago.acl.objectacl import add_acl_to_obj from misago.categories.models import Category from misago.readtracker import poststracker from misago.threads import testutils @@ -552,8 +553,8 @@ def test_merge(self): new_thread.subscription = None user_acl = useracl.get_user_acl(self.user, cache_versions) - add_acl(user_acl, new_thread.category) - add_acl(user_acl, new_thread) + add_acl_to_obj(user_acl, new_thread.category) + add_acl_to_obj(user_acl, new_thread) self.assertEqual(response_json, ThreadsListSerializer(new_thread).data) @@ -617,8 +618,8 @@ def test_merge_kitchensink(self): self.assertTrue(new_thread.is_hidden) user_acl = useracl.get_user_acl(self.user, cache_versions) - add_acl(user_acl, new_thread.category) - add_acl(user_acl, new_thread) + add_acl_to_obj(user_acl, new_thread.category) + add_acl_to_obj(user_acl, new_thread) self.assertEqual(response_json, ThreadsListSerializer(new_thread).data) diff --git a/misago/threads/viewmodels/category.py b/misago/threads/viewmodels/category.py index 0175df97e4..d6f782bacd 100644 --- a/misago/threads/viewmodels/category.py +++ b/misago/threads/viewmodels/category.py @@ -1,6 +1,6 @@ from django.http import Http404 -from misago.acl import add_acl +from misago.acl.objectacl import add_acl_to_obj from misago.categories.models import Category from misago.categories.permissions import allow_browse_category, allow_see_category from misago.categories.serializers import CategorySerializer @@ -15,7 +15,7 @@ class ViewModel(BaseViewModel): def __init__(self, request, **kwargs): self._categories = self.get_categories(request) - add_acl(request.user_acl, self._categories) + add_acl_to_obj(request.user_acl, self._categories) self._model = self.get_category(request, self._categories, **kwargs) diff --git a/misago/threads/viewmodels/post.py b/misago/threads/viewmodels/post.py index 7c21f94e5b..e7f6a859bc 100644 --- a/misago/threads/viewmodels/post.py +++ b/misago/threads/viewmodels/post.py @@ -1,6 +1,6 @@ from django.shortcuts import get_object_or_404 -from misago.acl import add_acl +from misago.acl.objectacl import add_acl_to_obj from misago.core.viewmodel import ViewModel as BaseViewModel from misago.threads.permissions import exclude_invisible_posts @@ -12,7 +12,7 @@ class ViewModel(BaseViewModel): def __init__(self, request, thread, pk): model = self.get_post(request, thread, pk) - add_acl(request.user_acl, model) + add_acl_to_obj(request.user_acl, model) self._model = model diff --git a/misago/threads/viewmodels/posts.py b/misago/threads/viewmodels/posts.py index 4dbf9831bc..8d1c7241eb 100644 --- a/misago/threads/viewmodels/posts.py +++ b/misago/threads/viewmodels/posts.py @@ -1,4 +1,4 @@ -from misago.acl import add_acl +from misago.acl.objectacl import add_acl_to_obj from misago.conf import settings from misago.core.shortcuts import paginate, pagination_dict from misago.readtracker.poststracker import make_read_aware @@ -61,7 +61,7 @@ def __init__(self, request, thread, page): posts.sort(key=lambda p: p.pk) # make posts and events ACL and reads aware - add_acl(request.user_acl, posts) + add_acl_to_obj(request.user_acl, posts) make_read_aware(request.user, posts) self._user = request.user diff --git a/misago/threads/viewmodels/thread.py b/misago/threads/viewmodels/thread.py index 11125b4871..474f5ffeda 100644 --- a/misago/threads/viewmodels/thread.py +++ b/misago/threads/viewmodels/thread.py @@ -1,7 +1,7 @@ from django.shortcuts import get_object_or_404 from django.utils.translation import gettext as _ -from misago.acl import add_acl +from misago.acl.objectacl import add_acl_to_obj from misago.categories import PRIVATE_THREADS_ROOT_NAME, THREADS_ROOT_NAME from misago.categories.models import Category from misago.core.shortcuts import validate_slug @@ -44,8 +44,8 @@ def __init__( if path_aware: model.path = self.get_thread_path(model.category) - add_acl(request.user_acl, model.category) - add_acl(request.user_acl, model) + add_acl_to_obj(request.user_acl, model.category) + add_acl_to_obj(request.user_acl, model) if read_aware: make_read_aware(request.user, request.user_acl, model) @@ -56,7 +56,7 @@ def __init__( try: self._poll = model.poll - add_acl(request.user_acl, self._poll) + add_acl_to_obj(request.user_acl, self._poll) if poll_votes_aware: self._poll.make_choices_votes_aware(request.user) diff --git a/misago/threads/viewmodels/threads.py b/misago/threads/viewmodels/threads.py index b79267d659..4041648e51 100644 --- a/misago/threads/viewmodels/threads.py +++ b/misago/threads/viewmodels/threads.py @@ -7,7 +7,7 @@ from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy -from misago.acl import add_acl +from misago.acl.objectacl import add_acl_to_obj from misago.conf import settings from misago.core.shortcuts import paginate, pagination_dict from misago.readtracker import threadstracker @@ -68,7 +68,7 @@ def __init__(self, request, category, list_type, page): threads = list(pinned_threads) + list(list_page.object_list) add_categories_to_items(category_model, category.categories, threads) - add_acl(request.user_acl, threads) + add_acl_to_obj(request.user_acl, threads) make_subscription_aware(request.user, threads) if list_type in ('new', 'unread'): diff --git a/misago/users/api/auth.py b/misago/users/api/auth.py index 20b386aced..37353faea2 100644 --- a/misago/users/api/auth.py +++ b/misago/users/api/auth.py @@ -56,11 +56,12 @@ def login(request): def session_user(request): """GET /auth/ will return current auth user, either User or AnonymousUser""" if request.user.is_authenticated: - UserSerializer = AuthenticatedUserSerializer + serializer = AuthenticatedUserSerializer else: - UserSerializer = AnonymousUserSerializer + serializer = AnonymousUserSerializer - return Response(UserSerializer(request.user).data) + serialized_user = serializer(request.user, context={"acl": request.user_acl}).data + return Response(serialized_user) @api_view(['GET']) diff --git a/misago/users/api/userendpoints/signature.py b/misago/users/api/userendpoints/signature.py index 50e0afe153..17f468e7ec 100644 --- a/misago/users/api/userendpoints/signature.py +++ b/misago/users/api/userendpoints/signature.py @@ -55,7 +55,8 @@ def get_signature_options(user): def edit_signature(request, user): serializer = EditSignatureSerializer(user, data=request.data) if serializer.is_valid(): - set_user_signature(request, user, serializer.validated_data['signature']) + signature = serializer.validated_data['signature'] + set_user_signature(request, user, request.user_acl, signature) user.save(update_fields=['signature', 'signature_parsed', 'signature_checksum']) return get_signature_options(user) else: diff --git a/misago/users/api/userendpoints/username.py b/misago/users/api/userendpoints/username.py index 1aae649a60..69d576cd5f 100644 --- a/misago/users/api/userendpoints/username.py +++ b/misago/users/api/userendpoints/username.py @@ -5,7 +5,7 @@ from django.utils.translation import gettext as _ from misago.conf import settings -from misago.users.namechanges import UsernameChanges +from misago.users.namechanges import get_username_options from misago.users.serializers import ChangeUsernameSerializer @@ -13,17 +13,14 @@ def username_endpoint(request): if request.method == 'POST': return change_username(request) else: - return options_response(get_username_options(request.user)) + options = get_username_options_from_request(request) + return options_response(options) -def get_username_options(user): - options = UsernameChanges(user) - return { - 'changes_left': options.left, - 'next_on': options.next_on, - 'length_min': settings.username_length_min, - 'length_max': settings.username_length_max, - } +def get_username_options_from_request(request): + return get_username_options( + settings, request.user, request.user_acl + ) def options_response(options): @@ -33,34 +30,46 @@ def options_response(options): def change_username(request): - options = get_username_options(request.user) + options = get_username_options_from_request(request) if not options['changes_left']: - return Response({ - 'detail': _("You can't change your username now."), - 'options': options - }, - status=status.HTTP_400_BAD_REQUEST) + return Response( + { + 'detail': _("You can't change your username now."), + 'options': options + }, + status=status.HTTP_400_BAD_REQUEST + ) - serializer = ChangeUsernameSerializer(data=request.data, context={'user': request.user}) + serializer = ChangeUsernameSerializer( + data=request.data, context={'user': request.user} + ) if serializer.is_valid(): try: serializer.change_username(changed_by=request.user) + updated_options = get_username_options_from_request(request) + if updated_options['next_on']: + updated_options['next_on'] = updated_options['next_on'].isoformat() + return Response({ 'username': request.user.username, 'slug': request.user.slug, - 'options': get_username_options(request.user) + 'options': updated_options, }) except IntegrityError: - return Response({ - 'detail': _("Error changing username. Please try again."), - }, - status=status.HTTP_400_BAD_REQUEST) + return Response( + { + 'detail': _("Error changing username. Please try again."), + }, + status=status.HTTP_400_BAD_REQUEST + ) else: - return Response({ - 'detail': serializer.errors['non_field_errors'][0] - }, - status=status.HTTP_400_BAD_REQUEST) + return Response( + { + 'detail': serializer.errors['non_field_errors'][0] + }, + status=status.HTTP_400_BAD_REQUEST + ) def moderate_username_endpoint(request, profile): @@ -75,15 +84,19 @@ def moderate_username_endpoint(request, profile): 'slug': profile.slug, }) except IntegrityError: - return Response({ - 'detail': _("Error changing username. Please try again."), - }, - status=status.HTTP_400_BAD_REQUEST) + return Response( + { + 'detail': _("Error changing username. Please try again."), + }, + status=status.HTTP_400_BAD_REQUEST + ) else: - return Response({ - 'detail': serializer.errors['non_field_errors'][0] - }, - status=status.HTTP_400_BAD_REQUEST) + return Response( + { + 'detail': serializer.errors['non_field_errors'][0] + }, + status=status.HTTP_400_BAD_REQUEST + ) else: return Response({ 'length_min': settings.username_length_min, diff --git a/misago/users/api/users.py b/misago/users/api/users.py index f65fde9b66..a347bb24e4 100644 --- a/misago/users/api/users.py +++ b/misago/users/api/users.py @@ -11,7 +11,7 @@ from django.shortcuts import get_object_or_404 from django.utils.translation import gettext as _ -from misago.acl import add_acl +from misago.acl.objectacl import add_acl_to_obj from misago.categories.models import Category from misago.conf import settings from misago.core.rest_permissions import IsAuthenticatedOrReadOnly @@ -84,7 +84,7 @@ def create(self, request): def retrieve(self, request, pk=None): profile = self.get_user(request, pk) - add_acl(request.user_acl, profile) + add_acl_to_obj(request.user_acl, profile) profile.status = get_user_status(request, profile) serializer = UserProfileSerializer(profile, context={'request': request}) diff --git a/misago/users/context_processors.py b/misago/users/context_processors.py index 8bcd33ab17..8c603fcef1 100644 --- a/misago/users/context_processors.py +++ b/misago/users/context_processors.py @@ -36,8 +36,11 @@ def preload_user_json(request): }) if request.user.is_authenticated: - request.frontend_context.update({'user': AuthenticatedUserSerializer(request.user).data}) + serializer = AuthenticatedUserSerializer else: - request.frontend_context.update({'user': AnonymousUserSerializer(request.user).data}) + serializer = AnonymousUserSerializer + + serialized_user = serializer(request.user, context={"acl": request.user_acl}).data + request.frontend_context.update({'user': serialized_user}) return {} diff --git a/misago/users/models/rank.py b/misago/users/models/rank.py index 59d8806f09..c4fe276d39 100644 --- a/misago/users/models/rank.py +++ b/misago/users/models/rank.py @@ -1,7 +1,7 @@ from django.db import models, transaction from django.urls import reverse -from misago.acl import version as acl_version +from misago.acl.cache import clear as clear_acl_cache from misago.core.utils import slugify @@ -39,11 +39,11 @@ def save(self, *args, **kwargs): if not self.pk: self.set_order() else: - acl_version.invalidate() + clear_acl_cache() return super().save(*args, **kwargs) def delete(self, *args, **kwargs): - acl_version.invalidate() + clear_acl_cache() return super().delete(*args, **kwargs) def get_absolute_url(self): diff --git a/misago/users/models/user.py b/misago/users/models/user.py index a15cb8cd69..e81e584f3a 100644 --- a/misago/users/models/user.py +++ b/misago/users/models/user.py @@ -11,7 +11,6 @@ from django.utils import timezone from django.utils.translation import gettext_lazy as _ -from misago.acl import get_user_acl from misago.acl.models import Role from misago.conf import settings from misago.core.pgutils import PgPartialIndex @@ -328,22 +327,6 @@ def anonymize_data(self): from misago.users.signals import anonymize_user_data anonymize_user_data.send(sender=self) - @property - def acl_cache(self): - try: - return self._acl_cache - except AttributeError: - self._acl_cache = get_user_acl(self) - return self._acl_cache - - @acl_cache.setter - def acl_cache(self, value): - raise TypeError("acl_cache can't be assigned") - - @property - def acl_(self): - raise NotImplementedError('user.acl_ property was renamed to user.acl') - @property def requires_activation_by_admin(self): return self.requires_activation == self.ACTIVATION_ADMIN @@ -401,13 +384,17 @@ def set_username(self, new_username, changed_by=None): if self.pk: changed_by = changed_by or self - self.record_name_change(changed_by, new_username, old_username) + namechange = self.record_name_change( + changed_by, new_username, old_username + ) from misago.users.signals import username_changed username_changed.send(sender=self) + return namechange + def record_name_change(self, changed_by, new_username, old_username): - self.namechanges.create( + return self.namechanges.create( new_username=new_username, old_username=old_username, changed_by=changed_by, @@ -525,11 +512,7 @@ class AnonymousUser(DjangoAnonymousUser): @property def acl_cache(self): - try: - return self._acl_cache - except AttributeError: - self._acl_cache = get_user_acl(self) - return self._acl_cache + raise Exception("AnonymousUser.acl_cache has been removed") @acl_cache.setter def acl_cache(self, value): diff --git a/misago/users/namechanges.py b/misago/users/namechanges.py index 4e19c6af00..21fcfc474f 100644 --- a/misago/users/namechanges.py +++ b/misago/users/namechanges.py @@ -8,32 +8,44 @@ from .models import UsernameChange -class UsernameChanges(object): - def __init__(self, user): - self.left = 0 - self.next_on = None - - if user.acl_cache['name_changes_allowed']: - self.count_namechanges(user) - - def count_namechanges(self, user): - name_changes_allowed = user.acl_cache['name_changes_allowed'] - name_changes_expire = user.acl_cache['name_changes_expire'] - - valid_changes_qs = user.namechanges.filter(changed_by=user) - if name_changes_expire: - cutoff = timezone.now() - timedelta(days=name_changes_expire) - valid_changes_qs = valid_changes_qs.filter(changed_on__gte=cutoff) - - used_changes = valid_changes_qs.count() - if name_changes_allowed <= used_changes: - self.left = 0 - else: - self.left = name_changes_allowed - used_changes - - if not self.left and name_changes_expire: - try: - self.next_on = valid_changes_qs.latest().changed_on - self.next_on += timedelta(days=name_changes_expire) - except UsernameChange.DoesNotExist: - pass +def get_username_options(settings, user, user_acl): + changes_left = get_left_namechanges(user, user_acl) + next_on = get_next_available_namechange(user, user_acl, changes_left) + + return { + 'changes_left': changes_left, + 'next_on': next_on, + 'length_min': settings.username_length_min, + 'length_max': settings.username_length_max, + } + + +def get_left_namechanges(user, user_acl): + name_changes_allowed = user_acl['name_changes_allowed'] + if not name_changes_allowed: + return 0 + + valid_changes = get_valid_changes_queryset(user, user_acl) + used_changes = valid_changes.count() + if name_changes_allowed <= used_changes: + left = 0 + return name_changes_allowed - used_changes + + +def get_next_available_namechange(user, user_acl, changes_left): + name_changes_expire = user_acl['name_changes_expire'] + if changes_left or not name_changes_expire: + return None + + valid_changes = get_valid_changes_queryset(user, user_acl) + name_last_changed_on = valid_changes.latest().changed_on + return name_last_changed_on + timedelta(days=name_changes_expire) + + +def get_valid_changes_queryset(user, user_acl): + name_changes_expire = user_acl['name_changes_expire'] + queryset = user.namechanges.filter(changed_by=user) + if user_acl['name_changes_expire']: + cutoff = timezone.now() - timedelta(days=name_changes_expire) + return queryset.filter(changed_on__gte=cutoff) + return queryset diff --git a/misago/users/permissions/moderation.py b/misago/users/permissions/moderation.py index 48d0e2a3a7..2293f279f0 100644 --- a/misago/users/permissions/moderation.py +++ b/misago/users/permissions/moderation.py @@ -171,7 +171,7 @@ def allow_lift_ban(user_acl, target): if not user_acl['can_lift_bans']: raise PermissionDenied(_("You can't lift bans.")) # FIXME: this will require cache version delegation - ban = get_user_ban(target) + ban = get_user_ban(target, user_ac["cache_versions"]) if not ban: raise PermissionDenied(_("This user is not banned.")) if user_acl['max_lifted_ban_length']: diff --git a/misago/users/permissions/profiles.py b/misago/users/permissions/profiles.py index 911e5d7b1c..8d3235426f 100644 --- a/misago/users/permissions/profiles.py +++ b/misago/users/permissions/profiles.py @@ -134,10 +134,7 @@ def allow_block_user(user_acl, target): raise PermissionDenied(_("You can't block administrators.")) if user_acl["user_id"] == target.id: raise PermissionDenied(_("You can't block yourself.")) - # FIXME: this will require changes in ACL checking - if not target.acl_cache['can_be_blocked'] or target.is_superuser: - message = _("%(user)s can't be blocked.") % {'user': target.username} - raise PermissionDenied(message) + # FIXME: check if user has "can be blocked" permission can_block_user = return_boolean(allow_block_user) diff --git a/misago/users/serializers/auth.py b/misago/users/serializers/auth.py index 938ab0ed10..e12e41c625 100644 --- a/misago/users/serializers/auth.py +++ b/misago/users/serializers/auth.py @@ -3,11 +3,10 @@ from django.contrib.auth import get_user_model from django.urls import reverse -from misago.acl import serialize_acl +from misago.acl.useracl import serialize_user_acl from .user import UserSerializer - UserModel = get_user_model() __all__ = [ @@ -43,7 +42,10 @@ class Meta: ] def get_acl(self, obj): - return serialize_acl(obj) + acl = self.context.get("acl") + if acl: + return serialize_user_acl(acl) + return {} def get_email(self, obj): return obj.email @@ -81,7 +83,7 @@ class AnonymousUserSerializer(serializers.Serializer, AuthFlags): is_anonymous = serializers.SerializerMethodField() def get_acl(self, obj): - if hasattr(obj, 'acl_cache'): - return serialize_acl(obj) - else: - return {} + acl = self.context.get("acl") + if acl: + return serialize_user_acl(acl) + return {} diff --git a/misago/users/signatures.py b/misago/users/signatures.py index 8ab96a9784..b37bc8315b 100644 --- a/misago/users/signatures.py +++ b/misago/users/signatures.py @@ -1,11 +1,11 @@ from misago.markup import checksums, signature_flavour -def set_user_signature(request, user, signature): +def set_user_signature(request, user, user_acl, signature): user.signature = signature if signature: - user.signature_parsed = signature_flavour(request, user, signature) + user.signature_parsed = signature_flavour(request, user, user_acl, signature) user.signature_checksum = make_signature_checksum(user.signature_parsed, user) else: user.signature_parsed = '' diff --git a/misago/users/tests/test_bans.py b/misago/users/tests/test_bans.py index 7aa8eec7d8..cf9a93ec8b 100644 --- a/misago/users/tests/test_bans.py +++ b/misago/users/tests/test_bans.py @@ -4,16 +4,15 @@ from django.test import TestCase from django.utils import timezone -from misago.cache.versions import get_cache_versions_from_db - from misago.users.bans import ( ban_ip, ban_user, get_email_ban, get_ip_ban, get_request_ip_ban, get_user_ban, get_username_ban) from misago.users.constants import BANS_CACHE from misago.users.models import Ban - UserModel = get_user_model() +cache_versions = {"bans": "abcdefgh"} + class GetBanTests(TestCase): def test_get_username_ban(self): @@ -134,7 +133,7 @@ def setUp(self): def test_no_ban(self): """user is not caught by ban""" - self.assertIsNone(get_user_ban(self.user, get_cache_versions_from_db())) + self.assertIsNone(get_user_ban(self.user, cache_versions)) self.assertFalse(self.user.ban_cache.is_banned) def test_permanent_ban(self): @@ -145,7 +144,7 @@ def test_permanent_ban(self): staff_message='Staff reason', ) - user_ban = get_user_ban(self.user, get_cache_versions_from_db()) + user_ban = get_user_ban(self.user, cache_versions) self.assertIsNotNone(user_ban) self.assertEqual(user_ban.user_message, 'User reason') self.assertEqual(user_ban.staff_message, 'Staff reason') @@ -160,7 +159,7 @@ def test_temporary_ban(self): expires_on=timezone.now() + timedelta(days=7), ) - user_ban = get_user_ban(self.user, get_cache_versions_from_db()) + user_ban = get_user_ban(self.user, cache_versions) self.assertIsNotNone(user_ban) self.assertEqual(user_ban.user_message, 'User reason') self.assertEqual(user_ban.staff_message, 'Staff reason') @@ -173,7 +172,7 @@ def test_expired_ban(self): expires_on=timezone.now() - timedelta(days=7), ) - self.assertIsNone(get_user_ban(self.user, get_cache_versions_from_db())) + self.assertIsNone(get_user_ban(self.user, cache_versions)) self.assertFalse(self.user.ban_cache.is_banned) def test_expired_non_flagged_ban(self): @@ -184,7 +183,7 @@ def test_expired_non_flagged_ban(self): ) Ban.objects.update(is_checked=True) - self.assertIsNone(get_user_ban(self.user, get_cache_versions_from_db())) + self.assertIsNone(get_user_ban(self.user, cache_versions)) self.assertFalse(self.user.ban_cache.is_banned) @@ -261,7 +260,7 @@ def test_ban_user(self): self.assertEqual(ban.user_message, 'User reason') self.assertEqual(ban.staff_message, 'Staff reason') - db_ban = get_user_ban(user, get_cache_versions_from_db()) + db_ban = get_user_ban(user, cache_versions) self.assertEqual(ban.pk, db_ban.ban_id) diff --git a/misago/users/tests/test_namechanges.py b/misago/users/tests/test_namechanges.py index 249ee33767..d6408aa70e 100644 --- a/misago/users/tests/test_namechanges.py +++ b/misago/users/tests/test_namechanges.py @@ -1,28 +1,80 @@ +from datetime import timedelta + from django.contrib.auth import get_user_model from django.test import TestCase -from misago.users.namechanges import UsernameChanges - +from misago.users.namechanges import ( + get_next_available_namechange, get_left_namechanges, get_username_options +) -UserModel = get_user_model() +User = get_user_model() class UsernameChangesTests(TestCase): - def test_username_changes_helper(self): - """username changes are tracked correctly""" - test_user = UserModel.objects.create_user('Bob', 'bob@bob.com', 'pass123') + def test_user_without_permission_to_change_name_has_no_changes_left(self): + user = User.objects.create_user('User', 'test@example.com') + user_acl = {"name_changes_allowed": 0} + assert get_left_namechanges(user, user_acl) == 0 + + def test_user_without_namechanges_has_all_changes_left(self): + user = User.objects.create_user('User', 'test@example.com') + user_acl = {"name_changes_allowed": 3, "name_changes_expire": 0} + assert get_left_namechanges(user, user_acl) == 3 + + def test_user_own_namechanges_are_subtracted_from_changes_left(self): + user = User.objects.create_user('User', 'test@example.com') + user_acl = {"name_changes_allowed": 3, "name_changes_expire": 0} + user.set_username('Changed') + + assert get_left_namechanges(user, user_acl) == 2 + + def test_user_own_recent_namechanges_subtract_from_changes_left(self): + user = User.objects.create_user('User', 'test@example.com') + user_acl = {"name_changes_allowed": 3, "name_changes_expire": 5} + + user.set_username('Changed') + + assert get_left_namechanges(user, user_acl) == 2 + + def test_user_own_expired_namechanges_dont_subtract_from_changes_left(self): + user = User.objects.create_user('User', 'test@example.com') + user_acl = {"name_changes_allowed": 3, "name_changes_expire": 5} + + username_change = user.set_username('Changed') + username_change.changed_on -= timedelta(days=10) + username_change.save() + + assert get_left_namechanges(user, user_acl) == 3 + + def test_user_namechanges_by_other_users_dont_subtract_from_changes_left(self): + user = User.objects.create_user('User', 'test@example.com') + user_acl = {"name_changes_allowed": 3, "name_changes_expire": 0} + + username_change = user.set_username('Changed') + username_change.changed_by = None + username_change.save() + + assert get_left_namechanges(user, user_acl) == 3 + + def test_user_next_available_namechange_is_none_for_user_with_changes_left(self): + user = User.objects.create_user('User', 'test@example.com') + user_acl = {"name_changes_allowed": 3, "name_changes_expire": 0} - namechanges = UsernameChanges(test_user) - self.assertEqual(namechanges.left, 2) - self.assertIsNone(namechanges.next_on) + assert get_next_available_namechange(user, user_acl, 3) is None + + def test_user_next_available_namechange_is_none_if_own_namechanges_dont_expire(self): + user = User.objects.create_user('User', 'test@example.com') + user_acl = {"name_changes_allowed": 1, "name_changes_expire": 0} + user.set_username('Changed') - self.assertEqual(test_user.namechanges.count(), 0) + assert get_next_available_namechange(user, user_acl, 0) is None - test_user.set_username('Boberson') - test_user.save(update_fields=['username', 'slug']) + def test_user_next_available_namechange_is_calculated_if_own_namechanges_expire(self): + user = User.objects.create_user('User', 'test@example.com') + user_acl = {"name_changes_allowed": 1, "name_changes_expire": 1} - namechanges = UsernameChanges(test_user) - self.assertEqual(namechanges.left, 1) - self.assertIsNone(namechanges.next_on) + username_change = user.set_username('Changed') + next_change_on = get_next_available_namechange(user, user_acl, 0) - self.assertEqual(test_user.namechanges.count(), 1) + assert next_change_on + assert next_change_on == username_change.changed_on + timedelta(days=1) \ No newline at end of file diff --git a/misago/users/tests/test_signatures.py b/misago/users/tests/test_signatures.py index 8b641bc697..175221d82f 100644 --- a/misago/users/tests/test_signatures.py +++ b/misago/users/tests/test_signatures.py @@ -1,10 +1,13 @@ +from unittest.mock import Mock + from django.contrib.auth import get_user_model from django.test import TestCase +from misago.acl.useracl import get_user_acl from misago.users import signatures - -UserModel = get_user_model() +User = get_user_model() +cache_versions = {"acl": "abcdefg"} class MockRequest(object): @@ -14,22 +17,45 @@ def get_host(self): return '127.0.0.1:8000' -class SignaturesTests(TestCase): - def test_signature_change(self): - """signature module allows for signature change""" - test_user = UserModel.objects.create_user('Bob', 'bob@bob.com', 'pass123') +class UserSignatureTests(TestCase): + def test_user_signature_and_valid_checksum_is_set(self): + user = User.objects.create_user('Bob', 'bob@bob.com') + user.signature = "Test" + user.signature_parsed = "Test" + user.signature_checksum = "Test" + user.save() + + request = Mock(scheme="http", get_host=Mock(return_value="127.0.0.1:800")) + user_acl = get_user_acl(user, cache_versions) + + signatures.set_user_signature(request, user, user_acl, "Changed") + + assert user.signature == "Changed" + assert user.signature_parsed == "

    Changed

    " + assert user.signature_checksum + assert signatures.is_user_signature_valid(user) + + def test_user_signature_is_cleared(self): + user = User.objects.create_user('Bob', 'bob@bob.com') + user.signature = "Test" + user.signature_parsed = "Test" + user.signature_checksum = "Test" + user.save() - signatures.set_user_signature(MockRequest(), test_user, '') + request = Mock(scheme="http", get_host=Mock(return_value="127.0.0.1:800")) + user_acl = get_user_acl(user, cache_versions) - self.assertEqual(test_user.signature, '') - self.assertEqual(test_user.signature_parsed, '') - self.assertEqual(test_user.signature_checksum, '') + signatures.set_user_signature(request, user, user_acl, "") - signatures.set_user_signature(MockRequest(), test_user, 'Hello, world!') + assert not user.signature + assert not user.signature_parsed + assert not user.signature_checksum - self.assertEqual(test_user.signature, 'Hello, world!') - self.assertEqual(test_user.signature_parsed, '

    Hello, world!

    ') - self.assertTrue(signatures.is_user_signature_valid(test_user)) + def test_signature_validity_check_fails_for_incorrect_signature_checksum(self): + user = User.objects.create_user('Bob', 'bob@bob.com') + user.signature = "Test" + user.signature_parsed = "Test" + user.signature_checksum = "Test" + user.save() - test_user.signature_parsed = '

    Injected evil HTML!

    ' - self.assertFalse(signatures.is_user_signature_valid(test_user)) + assert not signatures.is_user_signature_valid(user) diff --git a/misago/users/tests/test_social_pipeline.py b/misago/users/tests/test_social_pipeline.py index 8e711f5abd..e6a9db0cd8 100644 --- a/misago/users/tests/test_social_pipeline.py +++ b/misago/users/tests/test_social_pipeline.py @@ -6,6 +6,8 @@ from social_core.backends.github import GithubOAuth2 from social_django.utils import load_strategy +from misago.acl import ACL_CACHE +from misago.acl.useracl import get_user_acl from misago.core.exceptions import SocialAuthFailed, SocialAuthBanned from misago.legal.models import Agreement @@ -28,10 +30,11 @@ def create_request(user_ip='0.0.0.0', data=None): else: request = factory.post('/', data=json.dumps(data), content_type='application/json') request.include_frontend_context = True - request.cache_versions = {BANS_CACHE: "abcdefgh"} + request.cache_versions = {BANS_CACHE: "abcdefgh", ACL_CACHE: "abcdefgh"} request.frontend_context = {} request.session = {} request.user = AnonymousUser() + request.user_acl = get_user_acl(request.user, request.cache_versions) request.user_ip = user_ip return request diff --git a/misago/users/viewmodels/threads.py b/misago/users/viewmodels/threads.py index 3bff85ec6c..5c5eaeef82 100644 --- a/misago/users/viewmodels/threads.py +++ b/misago/users/viewmodels/threads.py @@ -1,4 +1,4 @@ -from misago.acl import add_acl +from misago.acl.objectacl import add_acl_to_obj from misago.conf import settings from misago.core.shortcuts import paginate, pagination_dict from misago.threads.permissions import exclude_invisible_threads @@ -33,8 +33,8 @@ def __init__(self, request, profile, page=0): add_categories_to_items(root_category.unwrap(), threads_categories, posts + threads) - add_acl(request.user_acl, threads) - add_acl(request.user_acl, posts) + add_acl_to_obj(request.user_acl, threads) + add_acl_to_obj(request.user_acl, posts) self._user = request.user diff --git a/misago/users/views/admin/users.py b/misago/users/views/admin/users.py index 99ffe5eb88..5bbd45e525 100644 --- a/misago/users/views/admin/users.py +++ b/misago/users/views/admin/users.py @@ -5,6 +5,7 @@ from django.shortcuts import redirect from django.utils.translation import gettext_lazy as _ +from misago.acl.useracl import get_user_acl from misago.admin.auth import start_admin_session from misago.admin.views import generic from misago.categories.models import Category @@ -325,7 +326,10 @@ def handle_form(self, form, request, target): target.roles.clear() target.roles.add(*form.cleaned_data['roles']) - set_user_signature(request, target, form.cleaned_data.get('signature')) + target_acl = get_user_acl(target, request.cache_versions) + set_user_signature( + request, target, target_acl, form.cleaned_data.get('signature') + ) profilefields.update_user_profile_fields(request, target, form) diff --git a/misago/users/views/profile.py b/misago/users/views/profile.py index 7e65de5734..e9ab096cf7 100644 --- a/misago/users/views/profile.py +++ b/misago/users/views/profile.py @@ -3,7 +3,7 @@ from django.shortcuts import get_object_or_404, redirect, render from django.views import View -from misago.acl import add_acl +from misago.acl.objectacl import add_acl_to_obj from misago.core.shortcuts import paginate, pagination_dict, validate_slug from misago.users.bans import get_user_ban from misago.users.online.utils import get_user_status @@ -44,7 +44,7 @@ def get_profile(self, request, pk, slug): raise Http404() validate_slug(profile, slug) - add_acl(request.user_acl, profile) + add_acl_to_obj(request.user_acl, profile) return profile From 593a0fd1e0cede696febe90caa329fc3d1c58545 Mon Sep 17 00:00:00 2001 From: rafalp Date: Mon, 17 Dec 2018 02:50:49 +0100 Subject: [PATCH 54/86] Fix allow_lift_ban permission --- misago/users/permissions/moderation.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/misago/users/permissions/moderation.py b/misago/users/permissions/moderation.py index 2293f279f0..5e08a1fbdc 100644 --- a/misago/users/permissions/moderation.py +++ b/misago/users/permissions/moderation.py @@ -170,8 +170,7 @@ def allow_ban_user(user_acl, target): def allow_lift_ban(user_acl, target): if not user_acl['can_lift_bans']: raise PermissionDenied(_("You can't lift bans.")) - # FIXME: this will require cache version delegation - ban = get_user_ban(target, user_ac["cache_versions"]) + ban = get_user_ban(target, user_acl["cache_versions"]) if not ban: raise PermissionDenied(_("This user is not banned.")) if user_acl['max_lifted_ban_length']: From e912a78520f3f828cf7ee719612475f4d38ee084 Mon Sep 17 00:00:00 2001 From: rafalp Date: Mon, 17 Dec 2018 23:07:59 +0100 Subject: [PATCH 55/86] Add assertion for testing if cache is invalidated --- misago/cache/test.py | 18 +++++++++++++ .../tests/test_assert_invalidates_cache.py | 25 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 misago/cache/test.py create mode 100644 misago/cache/tests/test_assert_invalidates_cache.py diff --git a/misago/cache/test.py b/misago/cache/test.py new file mode 100644 index 0000000000..f9aba7d57e --- /dev/null +++ b/misago/cache/test.py @@ -0,0 +1,18 @@ +from .versions import get_cache_versions_from_db + + +class assert_invalidates_cache: + def __init__(self, cache): + self.cache = cache + + def __enter__(self): + self.versions = get_cache_versions_from_db() + return self + + def __exit__(self, *_): + new_versions = get_cache_versions_from_db() + for cache, version in new_versions.items(): + if cache == self.cache: + message = "cache %s was not invalidated" % cache + assert self.versions[cache] != version, message + diff --git a/misago/cache/tests/test_assert_invalidates_cache.py b/misago/cache/tests/test_assert_invalidates_cache.py new file mode 100644 index 0000000000..b90a826282 --- /dev/null +++ b/misago/cache/tests/test_assert_invalidates_cache.py @@ -0,0 +1,25 @@ +from django.test import TestCase + +from misago.cache.models import CacheVersion +from misago.cache.test import assert_invalidates_cache +from misago.cache.versions import invalidate_cache + + +class AssertCacheVersionChangedTests(TestCase): + def test_assertion_fails_if_specified_cache_is_not_invaldiated(self): + CacheVersion.objects.create(cache="test") + with self.assertRaises(AssertionError): + with assert_invalidates_cache("test"): + pass + + def test_assertion_passess_if_specified_cache_is_invalidated(self): + CacheVersion.objects.create(cache="test") + with assert_invalidates_cache("test"): + invalidate_cache("test") + + def test_assertion_fails_if_other_cache_is_invalidated(self): + CacheVersion.objects.create(cache="test") + CacheVersion.objects.create(cache="changed_test") + with self.assertRaises(AssertionError): + with assert_invalidates_cache("test"): + invalidate_cache("changed_test") \ No newline at end of file From 27ed8cde9fc50f6e4f75242f1578fa504fdce0ab Mon Sep 17 00:00:00 2001 From: rafalp Date: Tue, 18 Dec 2018 00:31:48 +0100 Subject: [PATCH 56/86] Add assertions that ACL cache is invalidated --- misago/acl/tests/test_roleadmin_views.py | 37 ++++++++++ misago/cache/test.py | 5 +- .../tests/test_assert_invalidates_cache.py | 2 +- .../tests/test_categories_admin_views.py | 58 ++++++++++++++++ .../tests/test_fixcategoriestree.py | 7 ++ .../tests/test_permissions_admin_views.py | 67 +++++++++++++++++++ misago/users/tests/test_rankadmin_views.py | 55 +++++++++++++++ 7 files changed, 229 insertions(+), 2 deletions(-) diff --git a/misago/acl/tests/test_roleadmin_views.py b/misago/acl/tests/test_roleadmin_views.py index e4222676a9..11ed074929 100644 --- a/misago/acl/tests/test_roleadmin_views.py +++ b/misago/acl/tests/test_roleadmin_views.py @@ -1,7 +1,9 @@ from django.urls import reverse +from misago.acl import ACL_CACHE from misago.acl.models import Role from misago.acl.testutils import fake_post_data +from misago.cache.test import assert_invalidates_cache from misago.admin.testutils import AdminTestCase @@ -70,6 +72,25 @@ def test_edit_view(self): self.assertEqual(response.status_code, 200) self.assertContains(response, test_role.name) + def test_editing_role_invalidates_acl_cache(self): + self.client.post( + reverse('misago:admin:permissions:users:new'), data=fake_data({ + 'name': 'Test Role', + }) + ) + + test_role = Role.objects.get(name='Test Role') + + with assert_invalidates_cache(ACL_CACHE): + self.client.post( + reverse('misago:admin:permissions:users:edit', kwargs={ + 'pk': test_role.pk, + }), + data=fake_data({ + 'name': 'Top Lel', + }) + ) + def test_users_view(self): """users with this role view has no showstoppers""" response = self.client.post( @@ -106,3 +127,19 @@ def test_delete_view(self): self.client.get(reverse('misago:admin:permissions:users:index')) response = self.client.get(reverse('misago:admin:permissions:users:index')) self.assertNotContains(response, test_role.name) + + def test_deleting_role_invalidates_acl_cache(self): + self.client.post( + reverse('misago:admin:permissions:users:new'), data=fake_data({ + 'name': 'Test Role', + }) + ) + + test_role = Role.objects.get(name='Test Role') + + with assert_invalidates_cache(ACL_CACHE): + self.client.post( + reverse('misago:admin:permissions:users:delete', kwargs={ + 'pk': test_role.pk, + }) + ) \ No newline at end of file diff --git a/misago/cache/test.py b/misago/cache/test.py index f9aba7d57e..8e7189d169 100644 --- a/misago/cache/test.py +++ b/misago/cache/test.py @@ -9,7 +9,10 @@ def __enter__(self): self.versions = get_cache_versions_from_db() return self - def __exit__(self, *_): + def __exit__(self, exc_type, *_): + if exc_type: + return False + new_versions = get_cache_versions_from_db() for cache, version in new_versions.items(): if cache == self.cache: diff --git a/misago/cache/tests/test_assert_invalidates_cache.py b/misago/cache/tests/test_assert_invalidates_cache.py index b90a826282..5f3d1eb030 100644 --- a/misago/cache/tests/test_assert_invalidates_cache.py +++ b/misago/cache/tests/test_assert_invalidates_cache.py @@ -22,4 +22,4 @@ def test_assertion_fails_if_other_cache_is_invalidated(self): CacheVersion.objects.create(cache="changed_test") with self.assertRaises(AssertionError): with assert_invalidates_cache("test"): - invalidate_cache("changed_test") \ No newline at end of file + invalidate_cache("changed_test") diff --git a/misago/categories/tests/test_categories_admin_views.py b/misago/categories/tests/test_categories_admin_views.py index e334e61107..924413b7a2 100644 --- a/misago/categories/tests/test_categories_admin_views.py +++ b/misago/categories/tests/test_categories_admin_views.py @@ -1,6 +1,8 @@ from django.urls import reverse +from misago.acl import ACL_CACHE from misago.admin.testutils import AdminTestCase +from misago.cache.test import assert_invalidates_cache from misago.categories.models import Category from misago.threads import testutils from misago.threads.models import Thread @@ -140,6 +142,21 @@ def test_new_view(self): response = self.client.get(reverse('misago:admin:categories:nodes:index')) self.assertContains(response, 'Test Subcategory') + def test_creating_new_category_invalidates_acl_cache(self): + root = Category.objects.root_category() + + with assert_invalidates_cache(ACL_CACHE): + self.client.post( + reverse('misago:admin:categories:nodes:new'), + data={ + 'name': 'Test Category', + 'description': 'Lorem ipsum dolor met', + 'new_parent': root.pk, + 'prune_started_after': 0, + 'prune_replied_after': 0, + }, + ) + def test_edit_view(self): """edit category view has no showstoppers""" private_threads = Category.objects.private_threads() @@ -228,6 +245,35 @@ def test_edit_view(self): response = self.client.get(reverse('misago:admin:categories:nodes:index')) self.assertContains(response, 'Test Category Edited') + def test_editing_category_invalidates_acl_cache(self): + root = Category.objects.root_category() + self.client.post( + reverse('misago:admin:categories:nodes:new'), + data={ + 'name': 'Test Category', + 'description': 'Lorem ipsum dolor met', + 'new_parent': root.pk, + 'prune_started_after': 0, + 'prune_replied_after': 0, + }, + ) + + test_category = Category.objects.get(slug='test-category') + + with assert_invalidates_cache(ACL_CACHE): + self.client.post( + reverse('misago:admin:categories:nodes:edit', kwargs={ + 'pk': test_category.pk, + }), + data={ + 'name': 'Test Category Edited', + 'new_parent': root.pk, + 'role': 'category', + 'prune_started_after': 0, + 'prune_replied_after': 0, + }, + ) + def test_move_views(self): """move up/down views have no showstoppers""" root = Category.objects.root_category() @@ -522,3 +568,15 @@ def test_delete_leaf_category_and_contents(self): (self.category_e, 1, 10, 13), (self.category_f, 2, 11, 12), ]) + + def test_deleting_category_invalidates_acl_cache(self): + with assert_invalidates_cache(ACL_CACHE): + self.client.post( + reverse('misago:admin:categories:nodes:delete', kwargs={ + 'pk': self.category_d.pk, + }), + data={ + 'move_children_to': '', + 'move_threads_to': '', + } + ) diff --git a/misago/categories/tests/test_fixcategoriestree.py b/misago/categories/tests/test_fixcategoriestree.py index 53f9bb2676..1217781e4d 100644 --- a/misago/categories/tests/test_fixcategoriestree.py +++ b/misago/categories/tests/test_fixcategoriestree.py @@ -3,6 +3,8 @@ from django.core.management import call_command from django.test import TestCase +from misago.acl import ACL_CACHE +from misago.cache.test import assert_invalidates_cache from misago.categories.management.commands import fixcategoriestree from misago.categories.models import Category @@ -82,3 +84,8 @@ def test_fix_categories_tree_affected(self): (self.test_category, 1, 2, 3), (self.first_category, 1, 4, 5), ]) + + def test_fixing_categories_tree_invalidates_acl_cache(self): + with assert_invalidates_cache(ACL_CACHE): + run_command() + diff --git a/misago/categories/tests/test_permissions_admin_views.py b/misago/categories/tests/test_permissions_admin_views.py index 7968c2523e..c7ef6cf391 100644 --- a/misago/categories/tests/test_permissions_admin_views.py +++ b/misago/categories/tests/test_permissions_admin_views.py @@ -1,8 +1,10 @@ from django.urls import reverse +from misago.acl import ACL_CACHE from misago.acl.models import Role from misago.acl.testutils import fake_post_data from misago.admin.testutils import AdminTestCase +from misago.cache.test import assert_invalidates_cache from misago.categories.models import Category, CategoryRole @@ -72,6 +74,26 @@ def test_edit_view(self): response = self.client.get(reverse('misago:admin:permissions:categories:index')) self.assertContains(response, test_role.name) + def test_editing_role_invalidates_acl_cache(self): + self.client.post( + reverse('misago:admin:permissions:categories:new'), + data=fake_data({ + 'name': 'Test CategoryRole', + }), + ) + + test_role = CategoryRole.objects.get(name='Test CategoryRole') + + with assert_invalidates_cache(ACL_CACHE): + self.client.post( + reverse('misago:admin:permissions:categories:edit', kwargs={ + 'pk': test_role.pk, + }), + data=fake_data({ + 'name': 'Top Lel', + }), + ) + def test_delete_view(self): """delete role view has no showstoppers""" self.client.post( @@ -93,6 +115,23 @@ def test_delete_view(self): response = self.client.get(reverse('misago:admin:permissions:categories:index')) self.assertNotContains(response, test_role.name) + def test_deleting_role_invalidates_acl_cache(self): + self.client.post( + reverse('misago:admin:permissions:categories:new'), + data=fake_data({ + 'name': 'Test CategoryRole', + }), + ) + + test_role = CategoryRole.objects.get(name='Test CategoryRole') + + with assert_invalidates_cache(ACL_CACHE): + self.client.post( + reverse('misago:admin:permissions:categories:delete', kwargs={ + 'pk': test_role.pk, + }) + ) + def test_change_category_roles_view(self): """change category roles perms view works""" root = Category.objects.root_category() @@ -186,6 +225,20 @@ def test_change_category_roles_view(self): self.assertEqual( category_role_set.get(role=test_role_b).category_role_id, role_comments.pk ) + + # Check that ACL was invalidated + with assert_invalidates_cache(ACL_CACHE): + self.client.post( + reverse( + 'misago:admin:categories:nodes:permissions', kwargs={ + 'pk': test_category.pk, + } + ), + data={ + ('%s-category_role' % test_role_a.pk): role_full.pk, + ('%s-category_role' % test_role_b.pk): role_comments.pk, + }, + ) def test_change_role_categories_permissions_view(self): """change role categories perms view works""" @@ -323,3 +376,17 @@ def test_change_role_categories_permissions_view(self): ) self.assertEqual(categories_acls.get(category=category_c).category_role_id, role_full.pk) self.assertEqual(categories_acls.get(category=category_d).category_role_id, role_full.pk) + + # Check that ACL was invalidated + with assert_invalidates_cache(ACL_CACHE): + self.client.post( + reverse('misago:admin:permissions:users:categories', kwargs={ + 'pk': test_role.pk, + }), + data={ + ('%s-role' % category_a.pk): role_comments.pk, + ('%s-role' % category_b.pk): role_comments.pk, + ('%s-role' % category_c.pk): role_full.pk, + ('%s-role' % category_d.pk): role_full.pk, + }, + ) \ No newline at end of file diff --git a/misago/users/tests/test_rankadmin_views.py b/misago/users/tests/test_rankadmin_views.py index 9be67324ec..1928e0f2ad 100644 --- a/misago/users/tests/test_rankadmin_views.py +++ b/misago/users/tests/test_rankadmin_views.py @@ -1,7 +1,9 @@ from django.urls import reverse +from misago.acl import ACL_CACHE from misago.acl.models import Role from misago.admin.testutils import AdminTestCase +from misago.cache.test import assert_invalidates_cache from misago.users.models import Rank @@ -109,6 +111,35 @@ def test_edit_view(self): self.assertTrue(test_role_a not in test_rank.roles.all()) self.assertTrue(test_role_c not in test_rank.roles.all()) + def test_editing_rank_invalidates_acl_cache(self): + self.client.post( + reverse('misago:admin:users:ranks:new'), + data={ + 'name': 'Test Rank', + 'description': 'Lorem ipsum dolor met', + 'title': 'Test Title', + 'style': 'test', + 'is_tab': '1', + }, + ) + + test_rank = Rank.objects.get(slug='test-rank') + test_role_b = Role.objects.create(name='Test Role B') + + with assert_invalidates_cache(ACL_CACHE): + self.client.post( + reverse( + 'misago:admin:users:ranks:edit', + kwargs={ + 'pk': test_rank.pk, + }, + ), + data={ + 'name': 'Top Lel', + 'roles': [test_role_b.pk], + }, + ) + def test_default_view(self): """default rank view has no showstoppers""" self.client.post( @@ -260,6 +291,30 @@ def test_delete_view(self): self.assertNotContains(response, test_rank.name) self.assertNotContains(response, test_rank.title) + + def test_deleting_rank_invalidates_acl_cache(self): + self.client.post( + reverse('misago:admin:users:ranks:new'), + data={ + 'name': 'Test Rank', + 'description': 'Lorem ipsum dolor met', + 'title': 'Test Title', + 'style': 'test', + 'is_tab': '1', + }, + ) + + test_rank = Rank.objects.get(slug='test-rank') + + with assert_invalidates_cache(ACL_CACHE): + self.client.post( + reverse( + 'misago:admin:users:ranks:delete', + kwargs={ + 'pk': test_rank.pk, + }, + ) + ) def test_uniquess(self): """rank slug uniqueness is enforced by admin forms""" From d52d2da2f9ddb0db5a1e085a86ef7c7114e1af96 Mon Sep 17 00:00:00 2001 From: rafalp Date: Tue, 18 Dec 2018 03:26:22 +0100 Subject: [PATCH 57/86] Fix assertion error message --- misago/acl/providers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/misago/acl/providers.py b/misago/acl/providers.py index 87193eccbf..f674752ba7 100644 --- a/misago/acl/providers.py +++ b/misago/acl/providers.py @@ -5,13 +5,13 @@ _NOT_INITIALIZED_ERROR = ( "PermissionProviders instance has to load providers with load() " - "before get_obj_type_annotators(), get_obj_type_user_acl_serializers(), " + "before get_obj_type_annotators(), get_user_acl_serializers(), " "list() or dict() methods will be available." ) _ALREADY_INITIALIZED_ERROR = ( "PermissionProviders instance has already loaded providers and " - "acl_annotator or acl_serializer are no longer available." + "acl_annotator or user_acl_serializer are no longer available." ) From 3e45c7ce9c8f63b976dc450c4e4fdb9a460a3706 Mon Sep 17 00:00:00 2001 From: rafalp Date: Tue, 18 Dec 2018 03:27:31 +0100 Subject: [PATCH 58/86] Small name change --- misago/acl/providers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/misago/acl/providers.py b/misago/acl/providers.py index f674752ba7..83c80714d6 100644 --- a/misago/acl/providers.py +++ b/misago/acl/providers.py @@ -31,7 +31,7 @@ def load(self): raise RuntimeError("providers are already loaded") self._register_providers() - self._cast_dict_values_to_tuples(self._annotators) + self._coerce_dict_values_to_tuples(self._annotators) self._user_acl_serializers = tuple(self._user_acl_serializers) self._initialized = True @@ -43,7 +43,7 @@ def _register_providers(self): if hasattr(self._providers_dict[namespace], 'register_with'): self._providers_dict[namespace].register_with(self) - def _cast_dict_values_to_tuples(self, types_dict): + def _coerce_dict_values_to_tuples(self, types_dict): for hashType in types_dict.keys(): types_dict[hashType] = tuple(types_dict[hashType]) From f5a2d6051c8c5913df1dd2b057680d4a844933f8 Mon Sep 17 00:00:00 2001 From: rafalp Date: Tue, 18 Dec 2018 03:30:38 +0100 Subject: [PATCH 59/86] Clean the test --- misago/cache/tests/test_getting_cache_versions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/misago/cache/tests/test_getting_cache_versions.py b/misago/cache/tests/test_getting_cache_versions.py index 720d78f5dd..8791e1c617 100644 --- a/misago/cache/tests/test_getting_cache_versions.py +++ b/misago/cache/tests/test_getting_cache_versions.py @@ -40,5 +40,4 @@ def test_getter_sets_new_cache_if_no_cache_is_set(self, _, cache_set): @patch('django.core.cache.cache.get', return_value=True) def test_getter_is_not_setting_new_cache_if_cache_is_set(self, _, cache_set): get_cache_versions() - get_cache_versions_from_db() cache_set.assert_not_called() \ No newline at end of file From 210d552f072b9ad6703432ea87ef0bfcd2292cfc Mon Sep 17 00:00:00 2001 From: rafalp Date: Tue, 18 Dec 2018 03:45:05 +0100 Subject: [PATCH 60/86] Small change of misago.acl.cache api --- misago/acl/cache.py | 6 +++--- misago/acl/useracl.py | 7 ++++--- misago/categories/management/commands/fixcategoriestree.py | 2 +- misago/categories/models.py | 2 +- misago/categories/views/categoriesadmin.py | 2 +- misago/categories/views/permsadmin.py | 2 +- misago/faker/management/commands/createfakecategories.py | 2 +- misago/users/models/rank.py | 2 +- 8 files changed, 13 insertions(+), 12 deletions(-) diff --git a/misago/acl/cache.py b/misago/acl/cache.py index 9ea258a076..3c349c27a7 100644 --- a/misago/acl/cache.py +++ b/misago/acl/cache.py @@ -5,12 +5,12 @@ from . import ACL_CACHE -def get(user, cache_versions): +def get_acl_cache(user, cache_versions): key = get_cache_key(user, cache_versions) return cache.get(key) -def set(user, cache_versions, user_acl): +def set_acl_cache(user, cache_versions, user_acl): key = get_cache_key(user, cache_versions) cache.set(key, user_acl) @@ -19,5 +19,5 @@ def get_cache_key(user, cache_versions): return 'acl_%s_%s' % (user.acl_key, cache_versions[ACL_CACHE]) -def clear(): +def clear_acl_cache(): invalidate_cache(ACL_CACHE) diff --git a/misago/acl/useracl.py b/misago/acl/useracl.py index a9b107feb5..53135ebfac 100644 --- a/misago/acl/useracl.py +++ b/misago/acl/useracl.py @@ -1,14 +1,15 @@ import copy -from . import buildacl, cache +from . import buildacl +from .cache import get_acl_cache, set_acl_cache from .providers import providers def get_user_acl(user, cache_versions): - user_acl = cache.get(user, cache_versions) + user_acl = get_acl_cache(user, cache_versions) if user_acl is None: user_acl = buildacl.build_acl(user.get_roles()) - cache.set(user, cache_versions, user_acl) + set_acl_cache(user, cache_versions, user_acl) user_acl["user_id"] = user.id user_acl["is_authenticated"] = bool(user.is_authenticated) user_acl["is_anonymous"] = bool(user.is_anonymous) diff --git a/misago/categories/management/commands/fixcategoriestree.py b/misago/categories/management/commands/fixcategoriestree.py index 4ae47d7456..01740f7fc4 100644 --- a/misago/categories/management/commands/fixcategoriestree.py +++ b/misago/categories/management/commands/fixcategoriestree.py @@ -1,6 +1,6 @@ from django.core.management.base import BaseCommand -from misago.acl.cache import clear as clear_acl_cache +from misago.acl.cache import clear_acl_cache from misago.categories.models import Category diff --git a/misago/categories/models.py b/misago/categories/models.py index 229d6017e5..df4822cf05 100644 --- a/misago/categories/models.py +++ b/misago/categories/models.py @@ -3,7 +3,7 @@ from django.db import models -from misago.acl.cache import clear as clear_acl_cache +from misago.acl.cache import clear_acl_cache from misago.acl.models import BaseRole from misago.conf import settings from misago.core.cache import cache diff --git a/misago/categories/views/categoriesadmin.py b/misago/categories/views/categoriesadmin.py index 339efe61fa..568cbeec7f 100644 --- a/misago/categories/views/categoriesadmin.py +++ b/misago/categories/views/categoriesadmin.py @@ -2,7 +2,7 @@ from django.shortcuts import redirect from django.utils.translation import gettext_lazy as _ -from misago.acl.cache import clear as clear_acl_cache +from misago.acl.cache import clear_acl_cache from misago.admin.views import generic from misago.categories import THREADS_ROOT_NAME from misago.categories.forms import CategoryFormFactory, DeleteFormFactory diff --git a/misago/categories/views/permsadmin.py b/misago/categories/views/permsadmin.py index 4d66d2f959..5d825be0f8 100644 --- a/misago/categories/views/permsadmin.py +++ b/misago/categories/views/permsadmin.py @@ -2,7 +2,7 @@ from django.shortcuts import redirect from django.utils.translation import gettext_lazy as _ -from misago.acl.cache import clear as clear_acl_cache +from misago.acl.cache import clear_acl_cache from misago.acl.forms import get_permissions_forms from misago.acl.models import Role from misago.acl.views import RoleAdmin, RolesList diff --git a/misago/faker/management/commands/createfakecategories.py b/misago/faker/management/commands/createfakecategories.py index 0fccc9d1d2..2b8757a6e4 100644 --- a/misago/faker/management/commands/createfakecategories.py +++ b/misago/faker/management/commands/createfakecategories.py @@ -5,7 +5,7 @@ from django.core.management.base import BaseCommand -from misago.acl.cache import clear as clear_acl_cache +from misago.acl.cache import clear_acl_cache from misago.categories.models import Category, RoleCategoryACL from misago.core.management.progressbar import show_progress diff --git a/misago/users/models/rank.py b/misago/users/models/rank.py index c4fe276d39..97097bfda9 100644 --- a/misago/users/models/rank.py +++ b/misago/users/models/rank.py @@ -1,7 +1,7 @@ from django.db import models, transaction from django.urls import reverse -from misago.acl.cache import clear as clear_acl_cache +from misago.acl.cache import clear_acl_cache from misago.core.utils import slugify From 8eff6f473b74a6284201fa32005e3392888624a9 Mon Sep 17 00:00:00 2001 From: rafalp Date: Tue, 18 Dec 2018 04:03:44 +0100 Subject: [PATCH 61/86] Fix build --- misago/acl/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/misago/acl/models.py b/misago/acl/models.py index 103fc25734..88888372f6 100644 --- a/misago/acl/models.py +++ b/misago/acl/models.py @@ -2,7 +2,7 @@ from django.db import models from django.utils.translation import gettext as _ -from . import cache +from .cache import clear_acl_cache def permissions_default(): @@ -22,11 +22,11 @@ def __str__(self): def save(self, *args, **kwargs): if self.pk: - cache.clear() + clear_acl_cache() return super().save(*args, **kwargs) def delete(self, *args, **kwargs): - cache.clear() + clear_acl_cache() return super().delete(*args, **kwargs) From 4d6381002cd5bba93fcae7924924469af260ae57 Mon Sep 17 00:00:00 2001 From: rafalp Date: Sun, 2 Dec 2018 17:35:51 +0100 Subject: [PATCH 62/86] Early groundwork for new database settings proxy --- misago/conf/__init__.py | 2 + misago/conf/databasesettings.py | 70 +++++++++++++++++++ misago/conf/dbsettings.py | 7 +- misago/conf/migrations/0002_cache_version.py | 17 +++++ misago/conf/tests/__init__.py | 21 ++++++ .../conf/tests/test_getting_setting_value.py | 5 ++ .../conf/tests/test_overriding_db_settings.py | 16 +++++ misago/conf/tests/test_settings_middleware.py | 0 8 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 misago/conf/databasesettings.py create mode 100644 misago/conf/migrations/0002_cache_version.py create mode 100644 misago/conf/tests/test_getting_setting_value.py create mode 100644 misago/conf/tests/test_overriding_db_settings.py create mode 100644 misago/conf/tests/test_settings_middleware.py diff --git a/misago/conf/__init__.py b/misago/conf/__init__.py index 539698c224..16ab047551 100644 --- a/misago/conf/__init__.py +++ b/misago/conf/__init__.py @@ -1,3 +1,5 @@ from .gateway import settings, db_settings # noqa +CACHE_NAME = "settings" + default_app_config = 'misago.conf.apps.MisagoConfConfig' diff --git a/misago/conf/databasesettings.py b/misago/conf/databasesettings.py new file mode 100644 index 0000000000..b360884af0 --- /dev/null +++ b/misago/conf/databasesettings.py @@ -0,0 +1,70 @@ +from django.core.cache import cache + +from . import CACHE_NAME +from .models import Setting + + +class DatabaseSettings: + _overrides = {} + + def __init__(self, cache_versions): + cache_name = get_cache_name(cache_versions) + self._settings = cache.get(cache_name) + if self._settings is None: + self._settings = get_settings_from_db() + cache.set(cache_name, self._settings) + + def get_public_settings(self): + public_settings = {} + for name, setting in self._settings.items(): + if setting["is_public"]: + public_settings[name] = setting["value"] + return public_settings + + def get_lazy_setting_value(self, setting): + try: + if self._settings[setting]["is_lazy"]: + if setting in self._overrides: + return self._overrides[setting] + if not self._settings[setting].get("real_value"): + real_value = Setting.objects.get(setting=setting).value + self._settings[setting]["real_value"] = real_value + return self._settings[setting]["real_value"] + raise ValueError("Setting %s is not lazy" % setting) + except (KeyError, Setting.DoesNotExist): + raise AttributeError("Setting %s is not defined" % setting) + + def __getattr__(self, setting): + if setting in self._overrides: + return self._overrides[setting] + return self._settings[setting]["value"] + + @classmethod + def override_settings(cls, overrides): + cls._overrides = overrides + + @classmethod + def remove_overrides(cls): + cls._overrides = {} + + +def get_cache_name(cache_versions): + return "%s_%s" % (CACHE_NAME, cache_versions[CACHE_NAME]) + + +def get_settings_from_db(): + settings = {} + for setting in Setting.objects.iterator(): + if setting.is_lazy: + settings[setting.setting] = { + 'value': True if setting.value else None, + 'is_lazy': setting.is_lazy, + 'is_public': setting.is_public, + } + else: + settings[setting.setting] = { + 'value': setting.value, + 'is_lazy': setting.is_lazy, + 'is_public': setting.is_public, + } + return settings diff --git a/misago/conf/dbsettings.py b/misago/conf/dbsettings.py index e8c73dbdfe..7453758dda 100644 --- a/misago/conf/dbsettings.py +++ b/misago/conf/dbsettings.py @@ -1,10 +1,11 @@ -from misago.core import threadstore +from django.core.cache import cache +from misago.core import threadstore CACHE_KEY = 'misago_db_settings' -class DBSettings(object): +class DBSettingsDeprecated(object): def __init__(self): self._settings = self._read_cache() self._overrides = {} @@ -85,7 +86,7 @@ class _DBSettingsGateway(object): def get_db_settings(self): dbsettings = threadstore.get(CACHE_KEY) if not dbsettings: - dbsettings = DBSettings() + dbsettings = DBSettingsDeprecated() threadstore.set(CACHE_KEY, dbsettings) return dbsettings diff --git a/misago/conf/migrations/0002_cache_version.py b/misago/conf/migrations/0002_cache_version.py new file mode 100644 index 0000000000..70f8ad5928 --- /dev/null +++ b/misago/conf/migrations/0002_cache_version.py @@ -0,0 +1,17 @@ +# Generated by Django 1.11.16 on 2018-12-02 15:54 +from django.db import migrations + +from misago.cache.operations import StartCacheVersioning + +from misago.conf import CACHE_NAME + + +class Migration(migrations.Migration): + + dependencies = [ + ('misago_conf', '0001_initial'), + ] + + operations = [ + StartCacheVersioning(CACHE_NAME) + ] diff --git a/misago/conf/tests/__init__.py b/misago/conf/tests/__init__.py index e69de29bb2..1cdeccf9d8 100644 --- a/misago/conf/tests/__init__.py +++ b/misago/conf/tests/__init__.py @@ -0,0 +1,21 @@ +from functools import wraps + +from misago.conf.databasesettings import DatabaseSettings + + +class OverrideDatabaseSettings: + def __init__(self, **settings): + self._overrides = settings + + def __enter__(self): + DatabaseSettings.override_settings(self._overrides) + + def __exit__(self, *_): + DatabaseSettings.remove_overrides() + + def __call__(self, f): + @wraps(f) + def test_function_wrapper(*args, **kwargs): + with self as context: + return f(*args, **kwargs) + return test_function_wrapper \ No newline at end of file diff --git a/misago/conf/tests/test_getting_setting_value.py b/misago/conf/tests/test_getting_setting_value.py new file mode 100644 index 0000000000..afc1904f74 --- /dev/null +++ b/misago/conf/tests/test_getting_setting_value.py @@ -0,0 +1,5 @@ +from django.test import TestCase + + +class GettingSettingValueTests(TestCase): + pass \ No newline at end of file diff --git a/misago/conf/tests/test_overriding_db_settings.py b/misago/conf/tests/test_overriding_db_settings.py new file mode 100644 index 0000000000..bd9654201d --- /dev/null +++ b/misago/conf/tests/test_overriding_db_settings.py @@ -0,0 +1,16 @@ +from django.test import TestCase + +from misago.conf import CACHE_NAME +from misago.conf.tests import OverrideDatabaseSettings +from misago.conf.databasesettings import DatabaseSettings + + +class OverrideDatabaseSettingsTests(TestCase): + def test_as_context_manager(self): + settings = DatabaseSettings({CACHE_NAME: "abcdefgh"}) + assert settings.forum_name == "Misago" + + with OverrideDatabaseSettings(forum_name="Overrided"): + assert settings.forum_name == "Overrided" + + assert settings.forum_name == "Misago" diff --git a/misago/conf/tests/test_settings_middleware.py b/misago/conf/tests/test_settings_middleware.py new file mode 100644 index 0000000000..e69de29bb2 From 008a41ab84c6e24f67ae79b259dbf9adb13437c9 Mon Sep 17 00:00:00 2001 From: rafalp Date: Sun, 2 Dec 2018 18:25:08 +0100 Subject: [PATCH 63/86] Add override_dynamic_settings testing utility --- ...databasesettings.py => dynamicsettings.py} | 2 +- misago/conf/tests/__init__.py | 8 +++--- .../test_overridding_dynamic_settings.py | 28 +++++++++++++++++++ .../conf/tests/test_overriding_db_settings.py | 16 ----------- 4 files changed, 33 insertions(+), 21 deletions(-) rename misago/conf/{databasesettings.py => dynamicsettings.py} (98%) create mode 100644 misago/conf/tests/test_overridding_dynamic_settings.py delete mode 100644 misago/conf/tests/test_overriding_db_settings.py diff --git a/misago/conf/databasesettings.py b/misago/conf/dynamicsettings.py similarity index 98% rename from misago/conf/databasesettings.py rename to misago/conf/dynamicsettings.py index b360884af0..d7b76492d4 100644 --- a/misago/conf/databasesettings.py +++ b/misago/conf/dynamicsettings.py @@ -4,7 +4,7 @@ from .models import Setting -class DatabaseSettings: +class DynamicSettings: _overrides = {} def __init__(self, cache_versions): diff --git a/misago/conf/tests/__init__.py b/misago/conf/tests/__init__.py index 1cdeccf9d8..b38ab01f64 100644 --- a/misago/conf/tests/__init__.py +++ b/misago/conf/tests/__init__.py @@ -1,17 +1,17 @@ from functools import wraps -from misago.conf.databasesettings import DatabaseSettings +from misago.conf.dynamicsettings import DynamicSettings -class OverrideDatabaseSettings: +class override_dynamic_settings: def __init__(self, **settings): self._overrides = settings def __enter__(self): - DatabaseSettings.override_settings(self._overrides) + DynamicSettings.override_settings(self._overrides) def __exit__(self, *_): - DatabaseSettings.remove_overrides() + DynamicSettings.remove_overrides() def __call__(self, f): @wraps(f) diff --git a/misago/conf/tests/test_overridding_dynamic_settings.py b/misago/conf/tests/test_overridding_dynamic_settings.py new file mode 100644 index 0000000000..8b70a73105 --- /dev/null +++ b/misago/conf/tests/test_overridding_dynamic_settings.py @@ -0,0 +1,28 @@ +from django.test import TestCase + +from misago.conf import CACHE_NAME +from misago.conf.tests import override_dynamic_settings +from misago.conf.dynamicsettings import DynamicSettings + +cache_versions = {CACHE_NAME: "abcdefgh"} + + +class OverrideDynamicSettingsTests(TestCase): + def test_setting_can_be_overridden_using_context_manager(self): + settings = DynamicSettings(cache_versions) + assert settings.forum_name == "Misago" + + with override_dynamic_settings(forum_name="Overrided"): + assert settings.forum_name == "Overrided" + + assert settings.forum_name == "Misago" + + def test_setting_can_be_overridden_using_decorator(self): + @override_dynamic_settings(forum_name="Overrided") + def decorated_function(settings): + return settings.forum_name + + settings = DynamicSettings(cache_versions) + assert settings.forum_name == "Misago" + assert decorated_function(settings) == "Overrided" + assert settings.forum_name == "Misago" diff --git a/misago/conf/tests/test_overriding_db_settings.py b/misago/conf/tests/test_overriding_db_settings.py deleted file mode 100644 index bd9654201d..0000000000 --- a/misago/conf/tests/test_overriding_db_settings.py +++ /dev/null @@ -1,16 +0,0 @@ -from django.test import TestCase - -from misago.conf import CACHE_NAME -from misago.conf.tests import OverrideDatabaseSettings -from misago.conf.databasesettings import DatabaseSettings - - -class OverrideDatabaseSettingsTests(TestCase): - def test_as_context_manager(self): - settings = DatabaseSettings({CACHE_NAME: "abcdefgh"}) - assert settings.forum_name == "Misago" - - with OverrideDatabaseSettings(forum_name="Overrided"): - assert settings.forum_name == "Overrided" - - assert settings.forum_name == "Misago" From e853de85ef69e47a7ad22e18639573dee09fff4f Mon Sep 17 00:00:00 2001 From: rafalp Date: Sun, 2 Dec 2018 20:23:01 +0100 Subject: [PATCH 64/86] Add tests for getting/overriding dynamic setting value --- devproject/test_settings.py | 3 +- .../test_getting_dynamic_setting_value.py | 102 ++++++++++++++++++ .../conf/tests/test_getting_setting_value.py | 5 - .../test_overridding_dynamic_settings.py | 45 +++++++- 4 files changed, 145 insertions(+), 10 deletions(-) create mode 100644 misago/conf/tests/test_getting_dynamic_setting_value.py delete mode 100644 misago/conf/tests/test_getting_setting_value.py diff --git a/devproject/test_settings.py b/devproject/test_settings.py index 2dbfab942f..987d8777fa 100644 --- a/devproject/test_settings.py +++ b/devproject/test_settings.py @@ -17,8 +17,7 @@ # Use in-memory cache CACHES = { 'default': { - 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', - 'LOCATION': 'uniqu3-sn0wf14k3' + 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', } } diff --git a/misago/conf/tests/test_getting_dynamic_setting_value.py b/misago/conf/tests/test_getting_dynamic_setting_value.py new file mode 100644 index 0000000000..2728e6f821 --- /dev/null +++ b/misago/conf/tests/test_getting_dynamic_setting_value.py @@ -0,0 +1,102 @@ +from django.test import TestCase + +from misago.conf import CACHE_NAME +from misago.conf.dynamicsettings import DynamicSettings +from misago.conf.models import Setting, SettingsGroup + +from . import override_dynamic_settings + +cache_versions = {CACHE_NAME: "abcdefgh"} + + +class GettingSettingValueTests(TestCase): + def test_accessing_attr_returns_setting_value(self): + settings = DynamicSettings(cache_versions) + assert settings.forum_name == "Misago" + + def test_accessing_attr_for_undefined_setting_raises_error(self): + settings = DynamicSettings(cache_versions) + with self.assertRaises(KeyError): + settings.not_existing + + def test_accessing_attr_for_lazy_setting_without_value_returns_none(self): + settings_group = SettingsGroup.objects.create(key="test", name="Test") + setting = Setting.objects.create( + group=settings_group, + setting="lazy_setting", + name="Lazy setting", + is_lazy=True, + field_extra={}, + ) + + settings = DynamicSettings(cache_versions) + assert settings.lazy_setting is None + + def test_accessing_attr_for_lazy_setting_with_value_returns_true(self): + settings_group = SettingsGroup.objects.create(key="test", name="Test") + setting = Setting.objects.create( + group=settings_group, + setting="lazy_setting", + name="Lazy setting", + dry_value="Hello", + is_lazy=True, + field_extra={}, + ) + + settings = DynamicSettings(cache_versions) + assert settings.lazy_setting is True + + def test_lazy_setting_getter_for_lazy_setting_with_value_returns_real_value(self): + settings_group = SettingsGroup.objects.create(key="test", name="Test") + setting = Setting.objects.create( + group=settings_group, + setting="lazy_setting", + name="Lazy setting", + dry_value="Hello", + is_lazy=True, + field_extra={}, + ) + + settings = DynamicSettings(cache_versions) + assert settings.get_lazy_setting_value("lazy_setting") == "Hello" + + def test_lazy_setting_getter_for_lazy_setting_makes_db_query(self): + settings_group = SettingsGroup.objects.create(key="test", name="Test") + setting = Setting.objects.create( + group=settings_group, + setting="lazy_setting", + name="Lazy setting", + dry_value="Hello", + is_lazy=True, + field_extra={}, + ) + + settings = DynamicSettings(cache_versions) + with self.assertNumQueries(1): + settings.get_lazy_setting_value("lazy_setting") + + def test_lazy_setting_getter_for_lazy_setting_is_reusing_query_result(self): + settings_group = SettingsGroup.objects.create(key="test", name="Test") + setting = Setting.objects.create( + group=settings_group, + setting="lazy_setting", + name="Lazy setting", + dry_value="Hello", + is_lazy=True, + field_extra={}, + ) + + settings = DynamicSettings(cache_versions) + settings.get_lazy_setting_value("lazy_setting") + with self.assertNumQueries(0): + settings.get_lazy_setting_value("lazy_setting") + + def test_lazy_setting_getter_for_undefined_setting_raises_attribute_error(self): + settings = DynamicSettings(cache_versions) + with self.assertRaises(AttributeError): + settings.get_lazy_setting_value("undefined") + + def test_lazy_setting_getter_for_not_lazy_setting_raises_value_error(self): + settings = DynamicSettings(cache_versions) + with self.assertRaises(ValueError): + settings.get_lazy_setting_value("forum_name") \ No newline at end of file diff --git a/misago/conf/tests/test_getting_setting_value.py b/misago/conf/tests/test_getting_setting_value.py deleted file mode 100644 index afc1904f74..0000000000 --- a/misago/conf/tests/test_getting_setting_value.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.test import TestCase - - -class GettingSettingValueTests(TestCase): - pass \ No newline at end of file diff --git a/misago/conf/tests/test_overridding_dynamic_settings.py b/misago/conf/tests/test_overridding_dynamic_settings.py index 8b70a73105..83a9cedcee 100644 --- a/misago/conf/tests/test_overridding_dynamic_settings.py +++ b/misago/conf/tests/test_overridding_dynamic_settings.py @@ -1,14 +1,16 @@ from django.test import TestCase from misago.conf import CACHE_NAME -from misago.conf.tests import override_dynamic_settings from misago.conf.dynamicsettings import DynamicSettings +from misago.conf.models import Setting, SettingsGroup + +from . import override_dynamic_settings cache_versions = {CACHE_NAME: "abcdefgh"} class OverrideDynamicSettingsTests(TestCase): - def test_setting_can_be_overridden_using_context_manager(self): + def test_dynamic_setting_can_be_overridden_using_context_manager(self): settings = DynamicSettings(cache_versions) assert settings.forum_name == "Misago" @@ -17,7 +19,7 @@ def test_setting_can_be_overridden_using_context_manager(self): assert settings.forum_name == "Misago" - def test_setting_can_be_overridden_using_decorator(self): + def test_dynamic_setting_can_be_overridden_using_decorator(self): @override_dynamic_settings(forum_name="Overrided") def decorated_function(settings): return settings.forum_name @@ -26,3 +28,40 @@ def decorated_function(settings): assert settings.forum_name == "Misago" assert decorated_function(settings) == "Overrided" assert settings.forum_name == "Misago" + + def test_lazy_dynamic_setting_can_be_overridden_using_context_manager(self): + settings_group = SettingsGroup.objects.create(key="test", name="Test") + setting = Setting.objects.create( + group=settings_group, + setting="lazy_setting", + name="Lazy setting", + dry_value="Hello", + is_lazy=True, + field_extra={}, + ) + + settings = DynamicSettings(cache_versions) + assert settings.get_lazy_setting_value("lazy_setting") == "Hello" + with override_dynamic_settings(lazy_setting="Overrided"): + assert settings.get_lazy_setting_value("lazy_setting") == "Overrided" + assert settings.get_lazy_setting_value("lazy_setting") == "Hello" + + def test_lazy_dynamic_setting_can_be_overridden_using_decorator(self): + @override_dynamic_settings(lazy_setting="Overrided") + def decorated_function(settings): + return settings.get_lazy_setting_value("lazy_setting") + + settings_group = SettingsGroup.objects.create(key="test", name="Test") + setting = Setting.objects.create( + group=settings_group, + setting="lazy_setting", + name="Lazy setting", + dry_value="Hello", + is_lazy=True, + field_extra={}, + ) + + settings = DynamicSettings(cache_versions) + assert settings.get_lazy_setting_value("lazy_setting") == "Hello" + assert decorated_function(settings) == "Overrided" + assert settings.get_lazy_setting_value("lazy_setting") == "Hello" From 8505aefd35f34cc54d9783fba3bcc9175ee9c693 Mon Sep 17 00:00:00 2001 From: rafalp Date: Sun, 2 Dec 2018 20:47:08 +0100 Subject: [PATCH 65/86] Add tests for dynamic settings loading, improve test assertions in misago.cache --- .../tests/test_getting_cache_versions.py | 10 ++++--- .../test_getting_dynamic_setting_value.py | 28 +++++++++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/misago/cache/tests/test_getting_cache_versions.py b/misago/cache/tests/test_getting_cache_versions.py index 8791e1c617..6c9c9c236a 100644 --- a/misago/cache/tests/test_getting_cache_versions.py +++ b/misago/cache/tests/test_getting_cache_versions.py @@ -9,8 +9,8 @@ class CacheVersionsTests(TestCase): def test_db_getter_returns_cache_versions_from_db(self): - cache_versions = get_cache_versions_from_db() - assert cache_versions + with self.assertNumQueries(1): + assert get_cache_versions_from_db() @patch('django.core.cache.cache.get', return_value=True) def test_cache_getter_returns_cache_versions_from_cache(self, cache_get): @@ -19,14 +19,16 @@ def test_cache_getter_returns_cache_versions_from_cache(self, cache_get): @patch('django.core.cache.cache.get', return_value=True) def test_getter_reads_from_cache(self, cache_get): - assert get_cache_versions() is True + with self.assertNumQueries(0): + assert get_cache_versions() is True cache_get.assert_called_once_with(CACHE_NAME) @patch('django.core.cache.cache.set') @patch('django.core.cache.cache.get', return_value=None) def test_getter_reads_from_db_when_cache_is_not_available(self, cache_get, _): db_caches = get_cache_versions_from_db() - assert get_cache_versions() == db_caches + with self.assertNumQueries(1): + assert get_cache_versions() == db_caches cache_get.assert_called_once_with(CACHE_NAME) @patch('django.core.cache.cache.set') diff --git a/misago/conf/tests/test_getting_dynamic_setting_value.py b/misago/conf/tests/test_getting_dynamic_setting_value.py index 2728e6f821..4574e2cee4 100644 --- a/misago/conf/tests/test_getting_dynamic_setting_value.py +++ b/misago/conf/tests/test_getting_dynamic_setting_value.py @@ -1,3 +1,5 @@ +from unittest.mock import patch + from django.test import TestCase from misago.conf import CACHE_NAME @@ -10,6 +12,32 @@ class GettingSettingValueTests(TestCase): + @patch('django.core.cache.cache.set') + @patch('django.core.cache.cache.get', return_value=None) + def test_dynamic_settings_are_loaded_from_database_if_cache_is_not_available(self, cache_get, _): + with self.assertNumQueries(1): + DynamicSettings(cache_versions) + + @patch('django.core.cache.cache.set') + @patch('django.core.cache.cache.get', return_value={}) + def test_dynamic_settings_are_loaded_from_cache_if_it_is_not_none(self, cache_get, _): + with self.assertNumQueries(0): + DynamicSettings(cache_versions) + cache_get.assert_called_once() + + @patch('django.core.cache.cache.set') + @patch('django.core.cache.cache.get', return_value=None) + def test_dynamic_settings_cache_is_set_if_none_exists(self, _, cache_set): + DynamicSettings(cache_versions) + cache_set.assert_called_once() + + @patch('django.core.cache.cache.set') + @patch('django.core.cache.cache.get', return_value={}) + def test_dynamic_settings_cache_is_not_set_if_it_already_exists(self, _, cache_set): + with self.assertNumQueries(0): + DynamicSettings(cache_versions) + cache_set.assert_not_called() + def test_accessing_attr_returns_setting_value(self): settings = DynamicSettings(cache_versions) assert settings.forum_name == "Misago" From 8c40166e41775fc5cb98f055e562e64a05a398fa Mon Sep 17 00:00:00 2001 From: rafalp Date: Sun, 2 Dec 2018 20:57:42 +0100 Subject: [PATCH 66/86] Rename tests suite, add tests for other use-cases --- ...> test_getting_dynamic_settings_values.py} | 61 +++++++++++++++---- 1 file changed, 50 insertions(+), 11 deletions(-) rename misago/conf/tests/{test_getting_dynamic_setting_value.py => test_getting_dynamic_settings_values.py} (67%) diff --git a/misago/conf/tests/test_getting_dynamic_setting_value.py b/misago/conf/tests/test_getting_dynamic_settings_values.py similarity index 67% rename from misago/conf/tests/test_getting_dynamic_setting_value.py rename to misago/conf/tests/test_getting_dynamic_settings_values.py index 4574e2cee4..9a0692cab2 100644 --- a/misago/conf/tests/test_getting_dynamic_setting_value.py +++ b/misago/conf/tests/test_getting_dynamic_settings_values.py @@ -8,36 +8,45 @@ from . import override_dynamic_settings -cache_versions = {CACHE_NAME: "abcdefgh"} +cache_version = "abcdefgh" +cache_versions = {CACHE_NAME: cache_version} class GettingSettingValueTests(TestCase): @patch('django.core.cache.cache.set') @patch('django.core.cache.cache.get', return_value=None) - def test_dynamic_settings_are_loaded_from_database_if_cache_is_not_available(self, cache_get, _): + def test_settings_are_loaded_from_database_if_cache_is_not_available(self, cache_get, _): with self.assertNumQueries(1): DynamicSettings(cache_versions) @patch('django.core.cache.cache.set') @patch('django.core.cache.cache.get', return_value={}) - def test_dynamic_settings_are_loaded_from_cache_if_it_is_not_none(self, cache_get, _): + def test_settings_are_loaded_from_cache_if_it_is_not_none(self, cache_get, _): with self.assertNumQueries(0): DynamicSettings(cache_versions) cache_get.assert_called_once() @patch('django.core.cache.cache.set') @patch('django.core.cache.cache.get', return_value=None) - def test_dynamic_settings_cache_is_set_if_none_exists(self, _, cache_set): + def test_settings_cache_is_set_if_none_exists(self, _, cache_set): DynamicSettings(cache_versions) cache_set.assert_called_once() @patch('django.core.cache.cache.set') @patch('django.core.cache.cache.get', return_value={}) - def test_dynamic_settings_cache_is_not_set_if_it_already_exists(self, _, cache_set): + def test_settings_cache_is_not_set_if_it_already_exists(self, _, cache_set): with self.assertNumQueries(0): DynamicSettings(cache_versions) cache_set.assert_not_called() + @patch('django.core.cache.cache.set') + @patch('django.core.cache.cache.get', return_value=None) + def test_settings_cache_key_includes_cache_name_and_version(self, _, cache_set): + DynamicSettings(cache_versions) + cache_key = cache_set.call_args[0][0] + assert CACHE_NAME in cache_key + assert cache_version in cache_key + def test_accessing_attr_returns_setting_value(self): settings = DynamicSettings(cache_versions) assert settings.forum_name == "Misago" @@ -49,7 +58,7 @@ def test_accessing_attr_for_undefined_setting_raises_error(self): def test_accessing_attr_for_lazy_setting_without_value_returns_none(self): settings_group = SettingsGroup.objects.create(key="test", name="Test") - setting = Setting.objects.create( + Setting.objects.create( group=settings_group, setting="lazy_setting", name="Lazy setting", @@ -62,7 +71,7 @@ def test_accessing_attr_for_lazy_setting_without_value_returns_none(self): def test_accessing_attr_for_lazy_setting_with_value_returns_true(self): settings_group = SettingsGroup.objects.create(key="test", name="Test") - setting = Setting.objects.create( + Setting.objects.create( group=settings_group, setting="lazy_setting", name="Lazy setting", @@ -76,7 +85,7 @@ def test_accessing_attr_for_lazy_setting_with_value_returns_true(self): def test_lazy_setting_getter_for_lazy_setting_with_value_returns_real_value(self): settings_group = SettingsGroup.objects.create(key="test", name="Test") - setting = Setting.objects.create( + Setting.objects.create( group=settings_group, setting="lazy_setting", name="Lazy setting", @@ -90,7 +99,7 @@ def test_lazy_setting_getter_for_lazy_setting_with_value_returns_real_value(self def test_lazy_setting_getter_for_lazy_setting_makes_db_query(self): settings_group = SettingsGroup.objects.create(key="test", name="Test") - setting = Setting.objects.create( + Setting.objects.create( group=settings_group, setting="lazy_setting", name="Lazy setting", @@ -105,7 +114,7 @@ def test_lazy_setting_getter_for_lazy_setting_makes_db_query(self): def test_lazy_setting_getter_for_lazy_setting_is_reusing_query_result(self): settings_group = SettingsGroup.objects.create(key="test", name="Test") - setting = Setting.objects.create( + Setting.objects.create( group=settings_group, setting="lazy_setting", name="Lazy setting", @@ -127,4 +136,34 @@ def test_lazy_setting_getter_for_undefined_setting_raises_attribute_error(self): def test_lazy_setting_getter_for_not_lazy_setting_raises_value_error(self): settings = DynamicSettings(cache_versions) with self.assertRaises(ValueError): - settings.get_lazy_setting_value("forum_name") \ No newline at end of file + settings.get_lazy_setting_value("forum_name") + + def test_public_settings_getter_returns_dict_with_public_settings(self): + settings_group = SettingsGroup.objects.create(key="test", name="Test") + Setting.objects.create( + group=settings_group, + setting="public_setting", + name="Public setting", + dry_value="Hello", + is_public=True, + field_extra={}, + ) + + settings = DynamicSettings(cache_versions) + public_settings = settings.get_public_settings() + assert public_settings["public_setting"] == "Hello" + + def test_public_settings_getter_excludes_private_settings_from_dict(self): + settings_group = SettingsGroup.objects.create(key="test", name="Test") + Setting.objects.create( + group=settings_group, + setting="private_setting", + name="Private setting", + dry_value="Hello", + is_public=False, + field_extra={}, + ) + + settings = DynamicSettings(cache_versions) + public_settings = settings.get_public_settings() + assert "private_setting" not in public_settings \ No newline at end of file From e7bb86b94fb8c98cffd9bf0d0f5892b75bee4a3c Mon Sep 17 00:00:00 2001 From: rafalp Date: Sun, 2 Dec 2018 22:26:40 +0100 Subject: [PATCH 67/86] Add tests for dynamic settings middleware --- devproject/settings.py | 1 + misago/conf/middleware.py | 14 ++++++ .../tests/test_dynamic_settings_middleware.py | 50 +++++++++++++++++++ misago/conf/tests/test_settings_middleware.py | 0 4 files changed, 65 insertions(+) create mode 100644 misago/conf/middleware.py create mode 100644 misago/conf/tests/test_dynamic_settings_middleware.py delete mode 100644 misago/conf/tests/test_settings_middleware.py diff --git a/devproject/settings.py b/devproject/settings.py index d774a9c66d..4b4c9aa4a6 100644 --- a/devproject/settings.py +++ b/devproject/settings.py @@ -225,6 +225,7 @@ 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'misago.cache.middleware.cache_versions_middleware', + 'misago.conf.middleware.dynamic_settings_middleware', 'misago.users.middleware.UserMiddleware', 'misago.acl.middleware.user_acl_middleware', 'misago.core.middleware.ExceptionHandlerMiddleware', diff --git a/misago/conf/middleware.py b/misago/conf/middleware.py new file mode 100644 index 0000000000..b5c18ec98c --- /dev/null +++ b/misago/conf/middleware.py @@ -0,0 +1,14 @@ +from django.utils.functional import SimpleLazyObject + +from .dynamicsettings import DynamicSettings + + +def dynamic_settings_middleware(get_response): + """Sets request.settings attribute with DynamicSettings.""" + def middleware(request): + def get_dynamic_settings(): + return DynamicSettings(request.cache_versions) + request.settings = SimpleLazyObject(get_dynamic_settings) + return get_response(request) + + return middleware diff --git a/misago/conf/tests/test_dynamic_settings_middleware.py b/misago/conf/tests/test_dynamic_settings_middleware.py new file mode 100644 index 0000000000..198aaf0625 --- /dev/null +++ b/misago/conf/tests/test_dynamic_settings_middleware.py @@ -0,0 +1,50 @@ +from unittest.mock import Mock, PropertyMock, patch + +from django.test import TestCase +from django.utils.functional import SimpleLazyObject + +from misago.conf.dynamicsettings import DynamicSettings +from misago.conf.middleware import dynamic_settings_middleware + + +class MiddlewareTests(TestCase): + def test_middleware_sets_attr_on_request(self): + get_response = Mock() + request = Mock() + settings = PropertyMock() + type(request).settings = settings + middleware = dynamic_settings_middleware(get_response) + middleware(request) + settings.assert_called_once() + + def test_attr_set_by_middleware_on_request_is_lazy_object(self): + get_response = Mock() + request = Mock() + settings = PropertyMock() + type(request).settings = settings + middleware = dynamic_settings_middleware(get_response) + middleware(request) + attr_value = settings.call_args[0][0] + assert isinstance(attr_value, SimpleLazyObject) + + def test_middleware_calls_get_response(self): + get_response = Mock() + request = Mock() + middleware = dynamic_settings_middleware(get_response) + middleware(request) + get_response.assert_called_once() + + def test_middleware_is_not_reading_db(self): + get_response = Mock() + request = Mock() + with self.assertNumQueries(0): + middleware = dynamic_settings_middleware(get_response) + middleware(request) + + @patch('django.core.cache.cache.get') + def test_middleware_is_not_reading_cache(self, cache_get): + get_response = Mock() + request = Mock() + middleware = dynamic_settings_middleware(get_response) + middleware(request) + cache_get.assert_not_called() \ No newline at end of file diff --git a/misago/conf/tests/test_settings_middleware.py b/misago/conf/tests/test_settings_middleware.py deleted file mode 100644 index e69de29bb2..0000000000 From daf21308da71edcf75a6d6d61db7fbe871f47841 Mon Sep 17 00:00:00 2001 From: rafalp Date: Mon, 3 Dec 2018 02:21:12 +0100 Subject: [PATCH 68/86] Move context processor to request.settings, rename it to conf. --- devproject/settings.py | 2 +- misago/acl/tests/test_getting_user_acl.py | 3 +- misago/acl/tests/test_patching_user_acl.py | 3 +- misago/acl/tests/test_serializing_user_acl.py | 3 +- misago/acl/tests/test_user_acl_middleware.py | 3 +- misago/categories/tests/test_utils.py | 3 +- misago/conf/__init__.py | 4 +- misago/conf/context_processors.py | 46 +++++++++---------- misago/conf/dynamicsettings.py | 4 +- misago/conf/migrations/0002_cache_version.py | 4 +- misago/conf/tests/test_context_processors.py | 18 ++++---- .../test_getting_dynamic_settings_values.py | 7 +-- .../test_overridding_dynamic_settings.py | 5 +- misago/conftest.py | 10 ++++ misago/core/mail.py | 10 ++-- misago/core/tests/test_errorpages.py | 5 +- .../tests/test_exceptionhandler_middleware.py | 6 ++- .../tests/test_categoriestracker.py | 3 +- .../readtracker/tests/test_threadstracker.py | 3 +- misago/templates/misago/base.html | 6 +-- misago/templates/misago/categories/base.html | 24 +++++----- .../templates/misago/categories/header.html | 6 +-- misago/templates/misago/emails/base.html | 4 +- misago/templates/misago/emails/base.txt | 4 +- misago/templates/misago/footer.html | 6 +-- misago/templates/misago/index.html | 8 ++-- misago/templates/misago/navbar.html | 8 ++-- .../templates/misago/threadslist/threads.html | 22 ++++----- .../tests/test_attachments_middleware.py | 3 +- misago/threads/tests/test_events.py | 3 +- .../threads/tests/test_threads_editor_api.py | 3 +- misago/threads/tests/test_threadview.py | 3 +- misago/users/api/userendpoints/changeemail.py | 5 +- misago/users/tests/test_bans.py | 7 ++- misago/users/tests/test_signatures.py | 3 +- misago/users/tests/test_social_pipeline.py | 6 ++- 36 files changed, 150 insertions(+), 113 deletions(-) create mode 100644 misago/conftest.py diff --git a/devproject/settings.py b/devproject/settings.py index 4b4c9aa4a6..a2f4a0b925 100644 --- a/devproject/settings.py +++ b/devproject/settings.py @@ -288,10 +288,10 @@ 'django.contrib.messages.context_processors.messages', 'misago.acl.context_processors.user_acl', + 'misago.conf.context_processors.conf', 'misago.conf.context_processors.settings', 'misago.core.context_processors.site_address', 'misago.core.context_processors.momentjs_locale', - 'misago.legal.context_processors.legal_links', 'misago.search.context_processors.search_providers', 'misago.users.context_processors.user_links', diff --git a/misago/acl/tests/test_getting_user_acl.py b/misago/acl/tests/test_getting_user_acl.py index 7d2ac7403c..97c82ac25c 100644 --- a/misago/acl/tests/test_getting_user_acl.py +++ b/misago/acl/tests/test_getting_user_acl.py @@ -4,11 +4,12 @@ from django.test import TestCase from misago.acl.useracl import get_user_acl +from misago.conftest import get_cache_versions from misago.users.models import AnonymousUser User = get_user_model() -cache_versions = {"acl": "abcdefgh"} +cache_versions = get_cache_versions() class GettingUserACLTests(TestCase): diff --git a/misago/acl/tests/test_patching_user_acl.py b/misago/acl/tests/test_patching_user_acl.py index 80fd564759..6aa6d7ed9e 100644 --- a/misago/acl/tests/test_patching_user_acl.py +++ b/misago/acl/tests/test_patching_user_acl.py @@ -3,10 +3,11 @@ from misago.acl import useracl from misago.acl.test import patch_user_acl +from misago.conftest import get_cache_versions User = get_user_model() -cache_versions = {"acl": "abcdefgh"} +cache_versions = get_cache_versions() def callable_acl_patch(user, user_acl): diff --git a/misago/acl/tests/test_serializing_user_acl.py b/misago/acl/tests/test_serializing_user_acl.py index 675562d563..b65cbd1d26 100644 --- a/misago/acl/tests/test_serializing_user_acl.py +++ b/misago/acl/tests/test_serializing_user_acl.py @@ -4,10 +4,11 @@ from django.test import TestCase from misago.acl.useracl import get_user_acl, serialize_user_acl +from misago.conftest import get_cache_versions User = get_user_model() -cache_versions = {"acl": "abcdefgh"} +cache_versions = get_cache_versions() class SerializingUserACLTests(TestCase): diff --git a/misago/acl/tests/test_user_acl_middleware.py b/misago/acl/tests/test_user_acl_middleware.py index 9c7b04a43d..fd1323f64c 100644 --- a/misago/acl/tests/test_user_acl_middleware.py +++ b/misago/acl/tests/test_user_acl_middleware.py @@ -4,10 +4,11 @@ from django.test import TestCase from misago.acl.middleware import user_acl_middleware +from misago.conftest import get_cache_versions User = get_user_model() -cache_versions = {"acl": "abcdefgh"} +cache_versions = get_cache_versions() class MiddlewareTests(TestCase): diff --git a/misago/categories/tests/test_utils.py b/misago/categories/tests/test_utils.py index f6edc64efd..c96dab99b8 100644 --- a/misago/categories/tests/test_utils.py +++ b/misago/categories/tests/test_utils.py @@ -1,10 +1,11 @@ from misago.acl.useracl import get_user_acl from misago.categories.models import Category from misago.categories.utils import get_categories_tree, get_category_path +from misago.conftest import get_cache_versions from misago.core import threadstore from misago.users.testutils import AuthenticatedUserTestCase -cache_versions = {"acl": "abcdefgh"} +cache_versions = get_cache_versions() def get_patched_user_acl(user): diff --git a/misago/conf/__init__.py b/misago/conf/__init__.py index 16ab047551..985f968c9e 100644 --- a/misago/conf/__init__.py +++ b/misago/conf/__init__.py @@ -1,5 +1,5 @@ from .gateway import settings, db_settings # noqa -CACHE_NAME = "settings" - default_app_config = 'misago.conf.apps.MisagoConfConfig' + +SETTINGS_CACHE = "settings" diff --git a/misago/conf/context_processors.py b/misago/conf/context_processors.py index 440aa004cf..5787415bfb 100644 --- a/misago/conf/context_processors.py +++ b/misago/conf/context_processors.py @@ -4,46 +4,44 @@ from misago.users.social.utils import get_enabled_social_auth_sites_list -from .gateway import settings as misago_settings # noqa -from .gateway import db_settings +from .gateway import settings +BLANK_AVATAR_URL = static(settings.MISAGO_BLANK_AVATAR) -BLANK_AVATAR_URL = static(misago_settings.MISAGO_BLANK_AVATAR) - -def settings(request): +def conf(request): return { - 'DEBUG': misago_settings.DEBUG, - 'LANGUAGE_CODE_SHORT': get_language()[:2], - 'misago_settings': db_settings, 'BLANK_AVATAR_URL': BLANK_AVATAR_URL, - 'THREADS_ON_INDEX': misago_settings.MISAGO_THREADS_ON_INDEX, - 'LOGIN_REDIRECT_URL': misago_settings.LOGIN_REDIRECT_URL, - 'LOGIN_URL': misago_settings.LOGIN_URL, - 'LOGOUT_URL': misago_settings.LOGOUT_URL, + 'DEBUG': settings.DEBUG, + 'LANGUAGE_CODE_SHORT': get_language()[:2], + 'LOGIN_REDIRECT_URL': settings.LOGIN_REDIRECT_URL, + 'LOGIN_URL': settings.LOGIN_URL, + 'LOGOUT_URL': settings.LOGOUT_URL, + 'THREADS_ON_INDEX': settings.MISAGO_THREADS_ON_INDEX, + 'settings': request.settings, } def preload_settings_json(request): - preloaded_settings = db_settings.get_public_settings() + preloaded_settings = request.settings.get_public_settings() preloaded_settings.update({ - 'LOGIN_API_URL': misago_settings.MISAGO_LOGIN_API_URL, - 'LOGIN_REDIRECT_URL': reverse(misago_settings.LOGIN_REDIRECT_URL), - 'LOGIN_URL': reverse(misago_settings.LOGIN_URL), - 'LOGOUT_URL': reverse(misago_settings.LOGOUT_URL), + 'LOGIN_API_URL': settings.MISAGO_LOGIN_API_URL, + 'LOGIN_REDIRECT_URL': reverse(settings.LOGIN_REDIRECT_URL), + 'LOGIN_URL': reverse(settings.LOGIN_URL), + 'LOGOUT_URL': reverse(settings.LOGOUT_URL), 'SOCIAL_AUTH': get_enabled_social_auth_sites_list(), }) request.frontend_context.update({ - 'SETTINGS': preloaded_settings, - 'MISAGO_PATH': reverse('misago:index'), 'BLANK_AVATAR_URL': BLANK_AVATAR_URL, - 'ENABLE_DOWNLOAD_OWN_DATA': misago_settings.MISAGO_ENABLE_DOWNLOAD_OWN_DATA, - 'ENABLE_DELETE_OWN_ACCOUNT': misago_settings.MISAGO_ENABLE_DELETE_OWN_ACCOUNT, - 'STATIC_URL': misago_settings.STATIC_URL, - 'CSRF_COOKIE_NAME': misago_settings.CSRF_COOKIE_NAME, - 'THREADS_ON_INDEX': misago_settings.MISAGO_THREADS_ON_INDEX, + 'CSRF_COOKIE_NAME': settings.CSRF_COOKIE_NAME, + 'ENABLE_DELETE_OWN_ACCOUNT': settings.MISAGO_ENABLE_DELETE_OWN_ACCOUNT, + 'ENABLE_DOWNLOAD_OWN_DATA': settings.MISAGO_ENABLE_DOWNLOAD_OWN_DATA, + 'MISAGO_PATH': reverse('misago:index'), + 'SETTINGS': preloaded_settings, + 'STATIC_URL': settings.STATIC_URL, + 'THREADS_ON_INDEX': settings.MISAGO_THREADS_ON_INDEX, }) return {} diff --git a/misago/conf/dynamicsettings.py b/misago/conf/dynamicsettings.py index d7b76492d4..a430aec311 100644 --- a/misago/conf/dynamicsettings.py +++ b/misago/conf/dynamicsettings.py @@ -1,6 +1,6 @@ from django.core.cache import cache -from . import CACHE_NAME +from . import SETTINGS_CACHE from .models import Setting @@ -49,7 +49,7 @@ def remove_overrides(cls): def get_cache_name(cache_versions): - return "%s_%s" % (CACHE_NAME, cache_versions[CACHE_NAME]) + return "%s_%s" % (SETTINGS_CACHE, cache_versions[SETTINGS_CACHE]) def get_settings_from_db(): diff --git a/misago/conf/migrations/0002_cache_version.py b/misago/conf/migrations/0002_cache_version.py index 70f8ad5928..16fcd6a2c4 100644 --- a/misago/conf/migrations/0002_cache_version.py +++ b/misago/conf/migrations/0002_cache_version.py @@ -3,7 +3,7 @@ from misago.cache.operations import StartCacheVersioning -from misago.conf import CACHE_NAME +from misago.conf import SETTINGS_CACHE class Migration(migrations.Migration): @@ -13,5 +13,5 @@ class Migration(migrations.Migration): ] operations = [ - StartCacheVersioning(CACHE_NAME) + StartCacheVersioning(SETTINGS_CACHE) ] diff --git a/misago/conf/tests/test_context_processors.py b/misago/conf/tests/test_context_processors.py index 511b579f98..0491aeb25f 100644 --- a/misago/conf/tests/test_context_processors.py +++ b/misago/conf/tests/test_context_processors.py @@ -1,12 +1,12 @@ +from unittest.mock import Mock + from django.test import TestCase -from misago.conf.context_processors import settings -from misago.conf.dbsettings import db_settings +from misago.cache.versions import get_cache_versions from misago.core import threadstore - -class MockRequest(object): - pass +from misago.conf.context_processors import conf +from misago.conf.dynamicsettings import DynamicSettings class ContextProcessorsTests(TestCase): @@ -15,10 +15,10 @@ def tearDown(self): def test_db_settings(self): """DBSettings are exposed to templates""" - mock_request = MockRequest() - processor_settings = settings(mock_request)['misago_settings'], - - self.assertEqual(id(processor_settings[0]), id(db_settings)) + cache_versions = get_cache_versions() + mock_request = Mock(settings=DynamicSettings(cache_versions)) + context_settings = conf(mock_request)['settings'] + assert context_settings == mock_request.settings def test_preload_settings(self): """site configuration is preloaded by middleware""" diff --git a/misago/conf/tests/test_getting_dynamic_settings_values.py b/misago/conf/tests/test_getting_dynamic_settings_values.py index 9a0692cab2..674a8429a5 100644 --- a/misago/conf/tests/test_getting_dynamic_settings_values.py +++ b/misago/conf/tests/test_getting_dynamic_settings_values.py @@ -2,14 +2,15 @@ from django.test import TestCase -from misago.conf import CACHE_NAME +from misago.conf import SETTINGS_CACHE from misago.conf.dynamicsettings import DynamicSettings from misago.conf.models import Setting, SettingsGroup +from misago.conftest import get_cache_versions from . import override_dynamic_settings cache_version = "abcdefgh" -cache_versions = {CACHE_NAME: cache_version} +cache_versions = get_cache_versions() class GettingSettingValueTests(TestCase): @@ -44,7 +45,7 @@ def test_settings_cache_is_not_set_if_it_already_exists(self, _, cache_set): def test_settings_cache_key_includes_cache_name_and_version(self, _, cache_set): DynamicSettings(cache_versions) cache_key = cache_set.call_args[0][0] - assert CACHE_NAME in cache_key + assert SETTINGS_CACHE in cache_key assert cache_version in cache_key def test_accessing_attr_returns_setting_value(self): diff --git a/misago/conf/tests/test_overridding_dynamic_settings.py b/misago/conf/tests/test_overridding_dynamic_settings.py index 83a9cedcee..9191636b5f 100644 --- a/misago/conf/tests/test_overridding_dynamic_settings.py +++ b/misago/conf/tests/test_overridding_dynamic_settings.py @@ -1,12 +1,13 @@ from django.test import TestCase -from misago.conf import CACHE_NAME +from misago.conf import SETTINGS_CACHE from misago.conf.dynamicsettings import DynamicSettings from misago.conf.models import Setting, SettingsGroup +from misago.conftest import get_cache_versions from . import override_dynamic_settings -cache_versions = {CACHE_NAME: "abcdefgh"} +cache_versions = get_cache_versions() class OverrideDynamicSettingsTests(TestCase): diff --git a/misago/conftest.py b/misago/conftest.py new file mode 100644 index 0000000000..741a1c34e5 --- /dev/null +++ b/misago/conftest.py @@ -0,0 +1,10 @@ +from misago.acl import ACL_CACHE +from misago.conf import SETTINGS_CACHE +from misago.users.constants import BANS_CACHE + +def cache_versions(): + return { + ACL_CACHE: "abcdefgh", + BANS_CACHE: "abcdefgh", + SETTINGS_CACHE: "abcdefgh", + } \ No newline at end of file diff --git a/misago/core/mail.py b/misago/core/mail.py index 5b72f021a2..19f02196e3 100644 --- a/misago/core/mail.py +++ b/misago/core/mail.py @@ -2,7 +2,9 @@ from django.template.loader import render_to_string from django.utils.translation import get_language -from misago.conf import db_settings, settings +from misago.cache.versions import get_cache_versions +from misago.conf import settings +from misago.conf.dynamicsettings import DynamicSettings from .utils import get_host_from_address @@ -15,13 +17,15 @@ def build_mail(recipient, subject, template, sender=None, context=None): 'LANGUAGE_CODE': get_language()[:2], 'LOGIN_URL': settings.LOGIN_URL, - 'misago_settings': db_settings, - 'user': recipient, 'sender': sender, 'subject': subject, }) + if 'settings' not in context: + cache_versions = get_cache_versions() + context["settings"] = DynamicSettings(cache_versions) + message_plain = render_to_string('%s.txt' % template, context) message_html = render_to_string('%s.html' % template, context) diff --git a/misago/core/tests/test_errorpages.py b/misago/core/tests/test_errorpages.py index 5c32bdcaf0..0daca0db51 100644 --- a/misago/core/tests/test_errorpages.py +++ b/misago/core/tests/test_errorpages.py @@ -6,6 +6,8 @@ from misago.acl.useracl import get_user_acl from misago.users.models import AnonymousUser +from misago.conf.dynamicsettings import DynamicSettings +from misago.conftest import get_cache_versions from misago.core.testproject.views import mock_custom_403_error_page, mock_custom_404_error_page from misago.core.utils import encode_json_html @@ -76,7 +78,8 @@ def test_social_auth_banned(self): def test_request(url): request = RequestFactory().get(url) - request.cache_versions = {"acl": "abcdefgh"} + request.cache_versions = get_cache_versions() + request.settings = DynamicSettings(cache_versions) request.user = AnonymousUser() request.user_acl = get_user_acl(request.user, request.cache_versions) request.include_frontend_context = True diff --git a/misago/core/tests/test_exceptionhandler_middleware.py b/misago/core/tests/test_exceptionhandler_middleware.py index 4f4887082c..675f95e296 100644 --- a/misago/core/tests/test_exceptionhandler_middleware.py +++ b/misago/core/tests/test_exceptionhandler_middleware.py @@ -4,6 +4,9 @@ from django.urls import reverse from misago.acl.useracl import get_user_acl +from misago.conf.dynamicsettings import DynamicSettings +from misago.core.middleware import ExceptionHandlerMiddleware +from misago.conftest import get_cache_versions from misago.users.models import AnonymousUser from misago.core.middleware import ExceptionHandlerMiddleware @@ -11,7 +14,8 @@ def test_request(): request = RequestFactory().get(reverse('misago:index')) - request.cache_versions = {"acl": "abcdefgh"} + request.cache_versions = get_cache_versions() + request.settings = DynamicSettings(request.cache_versions) request.user = AnonymousUser() request.user_acl = get_user_acl(request.user, request.cache_versions) request.include_frontend_context = True diff --git a/misago/readtracker/tests/test_categoriestracker.py b/misago/readtracker/tests/test_categoriestracker.py index 9bea2726ce..8415d37cb7 100644 --- a/misago/readtracker/tests/test_categoriestracker.py +++ b/misago/readtracker/tests/test_categoriestracker.py @@ -7,13 +7,14 @@ from misago.acl.useracl import get_user_acl from misago.categories.models import Category from misago.conf import settings +from misago.conftest import get_cache_versions from misago.readtracker import poststracker, categoriestracker from misago.readtracker.models import PostRead from misago.threads import testutils User = get_user_model() -cache_versions = {"acl": "abcdefgh"} +cache_versions = get_cache_versions() class AnonymousUser(object): diff --git a/misago/readtracker/tests/test_threadstracker.py b/misago/readtracker/tests/test_threadstracker.py index 3b1ae2d465..84a4fd3490 100644 --- a/misago/readtracker/tests/test_threadstracker.py +++ b/misago/readtracker/tests/test_threadstracker.py @@ -8,13 +8,14 @@ from misago.acl.useracl import get_user_acl from misago.categories.models import Category from misago.conf import settings +from misago.conftest import get_cache_versions from misago.readtracker import poststracker, threadstracker from misago.readtracker.models import PostRead from misago.threads import testutils User = get_user_model() -cache_versions = {"acl": "abcdefgh"} +cache_versions = get_cache_versions() class AnonymousUser(object): diff --git a/misago/templates/misago/base.html b/misago/templates/misago/base.html index 0279063a97..ae0d044ff5 100644 --- a/misago/templates/misago/base.html +++ b/misago/templates/misago/base.html @@ -5,14 +5,14 @@ - {% spaceless %}{% block title %}{{ misago_settings.forum_name }}{% endblock %}{% endspaceless %} + {% spaceless %}{% block title %}{{ settings.forum_name }}{% endblock %}{% endspaceless %} {% spaceless %} {% block meta-extra %}{% endblock meta-extra %} {% block og-tags %} - + - + diff --git a/misago/templates/misago/categories/base.html b/misago/templates/misago/categories/base.html index af5c02f639..48ff4d1083 100644 --- a/misago/templates/misago/categories/base.html +++ b/misago/templates/misago/categories/base.html @@ -6,20 +6,20 @@ {% if THREADS_ON_INDEX %} {% trans "Categories" %} | {{ block.super }} {% else %} - {% if misago_settings.forum_index_title %} - {{ misago_settings.forum_index_title }} + {% if settings.forum_index_title %} + {{ settings.forum_index_title }} {% else %} - {{ misago_settings.forum_name }} + {{ settings.forum_name }} {% endif %} {% endif %} {% endblock title %} {% block meta-description %} - {% if not THREADS_ON_INDEX and misago_settings.forum_index_meta_description %} - {{ misago_settings.forum_index_meta_description }} + {% if not THREADS_ON_INDEX and settings.forum_index_meta_description %} + {{ settings.forum_index_meta_description }} {% else %} - {% blocktrans trimmed count categories=categories|length with forum_name=misago_settings.forum_name %} + {% blocktrans trimmed count categories=categories|length with forum_name=settings.forum_name %} There is {{ categories }} main category currenty available on the {{ forum_name }}. {% plural %} There are {{ categories }} main categories currenty available on the {{ forum_name }}. @@ -32,20 +32,20 @@ {% if THREADS_ON_INDEX %} {% trans "Categories" %} {% else %} - {% if misago_settings.forum_index_title %} - {{ misago_settings.forum_index_title }} + {% if settings.forum_index_title %} + {{ settings.forum_index_title }} {% else %} - {{ misago_settings.forum_name }} + {{ settings.forum_name }} {% endif %} {% endif %} {% endblock og-title %} {% block og-description %} - {% if not THREADS_ON_INDEX and misago_settings.forum_index_meta_description %} - {{ misago_settings.forum_index_meta_description }} + {% if not THREADS_ON_INDEX and settings.forum_index_meta_description %} + {{ settings.forum_index_meta_description }} {% else %} - {% blocktrans trimmed count categories=categories|length with forum_name=misago_settings.forum_name %} + {% blocktrans trimmed count categories=categories|length with forum_name=settings.forum_name %} There is {{ categories }} main category currenty available on the {{ forum_name }}. {% plural %} There are {{ categories }} main categories currenty available on the {{ forum_name }}. diff --git a/misago/templates/misago/categories/header.html b/misago/templates/misago/categories/header.html index 254702c391..8df91b10b6 100644 --- a/misago/templates/misago/categories/header.html +++ b/misago/templates/misago/categories/header.html @@ -3,10 +3,10 @@