From 67fbf46a24834f1868ba1234bad4c81c0c87b9fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Pito=C5=84?= Date: Thu, 5 Sep 2024 21:43:25 +0200 Subject: [PATCH] Reduce test fails (#1807) --- misago/middleware/privatethreads.py | 42 ++++++++ misago/middleware/tests/__init__.py | 0 .../test_sync_user_unread_private_threads.py | 98 +++++++++++++++++++ misago/readtracker/privatethreads.py | 19 +++- misago/readtracker/signals.py | 36 ++----- ....py => test_get_unread_private_threads.py} | 86 +++++++--------- .../test_unread_private_threads_exist.py | 89 +++++++++++++++++ misago/settings.py | 2 +- .../category_row_last_thread.html | 16 ++- misago/threads/api/postendpoints/merge.py | 2 - misago/threads/middleware.py | 36 ------- misago/threads/tests/test_events.py | 9 -- .../tests/test_sync_unread_private_threads.py | 57 ----------- misago/threads/views/list.py | 4 +- misago/threads/views/replies.py | 4 +- 15 files changed, 305 insertions(+), 195 deletions(-) create mode 100644 misago/middleware/privatethreads.py create mode 100644 misago/middleware/tests/__init__.py create mode 100644 misago/middleware/tests/test_sync_user_unread_private_threads.py rename misago/readtracker/tests/{test_are_private_threads_read.py => test_get_unread_private_threads.py} (68%) create mode 100644 misago/readtracker/tests/test_unread_private_threads_exist.py delete mode 100644 misago/threads/middleware.py delete mode 100644 misago/threads/tests/test_sync_unread_private_threads.py diff --git a/misago/middleware/privatethreads.py b/misago/middleware/privatethreads.py new file mode 100644 index 0000000000..682e12875b --- /dev/null +++ b/misago/middleware/privatethreads.py @@ -0,0 +1,42 @@ +from typing import TYPE_CHECKING + +from django.http import HttpRequest + +from ..categories.enums import CategoryTree +from ..categories.models import Category +from ..readtracker.privatethreads import get_unread_private_threads +from ..readtracker.tracker import annotate_categories_read_time + +if TYPE_CHECKING: + from ..users.models import User + + +def sync_user_unread_private_threads(get_response): + def middleware(request): + if request.user.is_authenticated and request.user.sync_unread_private_threads: + user = request.user + category = get_private_threads_category(user) + queryset = get_unread_private_threads(request, category, category.read_time) + update_user_unread_private_threads(user, queryset.count()) + + return get_response(request) + + return middleware + + +def get_private_threads_category(user: "User") -> Category: + return annotate_categories_read_time( + user, + Category.objects.filter(tree_id=CategoryTree.PRIVATE_THREADS), + ).first() + + +def update_user_unread_private_threads(user: "User", unread_count: int): + user.unread_private_threads = unread_count + user.sync_unread_private_threads = False + user.save( + update_fields=[ + "unread_private_threads", + "sync_unread_private_threads", + ], + ) diff --git a/misago/middleware/tests/__init__.py b/misago/middleware/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/misago/middleware/tests/test_sync_user_unread_private_threads.py b/misago/middleware/tests/test_sync_user_unread_private_threads.py new file mode 100644 index 0000000000..db315f6c96 --- /dev/null +++ b/misago/middleware/tests/test_sync_user_unread_private_threads.py @@ -0,0 +1,98 @@ +from datetime import timedelta +from unittest.mock import Mock + +from django.utils import timezone + +from ...categories.proxy import CategoriesProxy +from ...permissions.proxy import UserPermissionsProxy +from ...threads.models import ThreadParticipant +from ...threads.test import post_thread +from ...readtracker.models import ReadThread +from ..privatethreads import sync_user_unread_private_threads + + +def test_sync_user_unread_private_threads_middleware_updates_user_with_unread_threads( + dynamic_settings, cache_versions, user, user_private_thread +): + user.unread_private_threads = 0 + user.sync_unread_private_threads = True + user.joined_on = user.joined_on.replace(year=2010) + user.save() + + user_permissions = UserPermissionsProxy(user, cache_versions) + request = Mock( + categories=CategoriesProxy(user_permissions, cache_versions), + settings=dynamic_settings, + user=user, + user_permissions=user_permissions, + ) + + middleware = sync_user_unread_private_threads(Mock()) + middleware(request) + + user.refresh_from_db() + assert user.unread_private_threads == 1 + assert not user.sync_unread_private_threads + + +def test_sync_user_unread_private_threads_middleware_updates_user_without_unread_threads( + dynamic_settings, cache_versions, user +): + user.unread_private_threads = 100 + user.sync_unread_private_threads = True + user.joined_on = user.joined_on.replace(year=2010) + user.save() + + user_permissions = UserPermissionsProxy(user, cache_versions) + request = Mock( + categories=CategoriesProxy(user_permissions, cache_versions), + settings=dynamic_settings, + user=user, + user_permissions=user_permissions, + ) + + middleware = sync_user_unread_private_threads(Mock()) + middleware(request) + + user.refresh_from_db() + assert user.unread_private_threads == 0 + assert not user.sync_unread_private_threads + + +def test_sync_user_unread_private_threads_middleware_skips_anonymous_user( + dynamic_settings, cache_versions, anonymous_user +): + user_permissions = UserPermissionsProxy(anonymous_user, cache_versions) + request = Mock( + categories=CategoriesProxy(user_permissions, cache_versions), + settings=dynamic_settings, + user=anonymous_user, + user_permissions=user_permissions, + ) + + middleware = sync_user_unread_private_threads(Mock()) + middleware(request) + + +def test_sync_user_unread_private_threads_middleware_skips_user_without_sync_flag( + dynamic_settings, cache_versions, user +): + user.unread_private_threads = 100 + user.sync_unread_private_threads = False + user.joined_on = user.joined_on.replace(year=2010) + user.save() + + user_permissions = UserPermissionsProxy(user, cache_versions) + request = Mock( + categories=CategoriesProxy(user_permissions, cache_versions), + settings=dynamic_settings, + user=user, + user_permissions=user_permissions, + ) + + middleware = sync_user_unread_private_threads(Mock()) + middleware(request) + + user.refresh_from_db() + assert user.unread_private_threads == 100 + assert not user.sync_unread_private_threads diff --git a/misago/readtracker/privatethreads.py b/misago/readtracker/privatethreads.py index ebfc00ef08..88215bb8dd 100644 --- a/misago/readtracker/privatethreads.py +++ b/misago/readtracker/privatethreads.py @@ -9,15 +9,26 @@ from .tracker import annotate_threads_read_time -def are_private_threads_read( - request: HttpRequest, category: Category, category_read_time: datetime | None +def unread_private_threads_exist( + request: HttpRequest, + category: Category, + category_read_time: datetime | None, ) -> bool: + queryset = get_unread_private_threads(request, category, category_read_time) + return queryset.exists() + + +def get_unread_private_threads( + request: HttpRequest, + category: Category, + category_read_time: datetime | None, +): read_time = get_default_read_time(request.settings, request.user) if category_read_time: read_time = max(read_time, category_read_time) - queryset = ( + return ( filter_private_threads_queryset( request.user_permissions, annotate_threads_read_time( @@ -27,5 +38,3 @@ def are_private_threads_read( .filter(last_post_on__gt=read_time) .filter(Q(last_post_on__gt=F("read_time")) | Q(read_time__isnull=True)) ) - - return not queryset.exists() diff --git a/misago/readtracker/signals.py b/misago/readtracker/signals.py index 3e0d6d2964..0655e940f8 100644 --- a/misago/readtracker/signals.py +++ b/misago/readtracker/signals.py @@ -1,51 +1,29 @@ from django.dispatch import Signal, receiver -from ..categories import PRIVATE_THREADS_ROOT_NAME from ..categories.signals import delete_category_content, move_category_content -from ..threads.signals import merge_post, merge_thread, move_post, move_thread +from ..threads.signals import merge_thread, move_thread thread_read = Signal() @receiver(delete_category_content) def delete_category_threads(sender, **kwargs): - sender.postread_set.all().delete() + sender.readthread_set.all().delete() + sender.readcategory_set.all().delete() @receiver(move_category_content) def move_category_tracker(sender, **kwargs): - sender.postread_set.update(category=kwargs["new_category"]) + sender.readthread_set.update(category=kwargs["new_category"]) + sender.readcategory_set.update(category=kwargs["new_category"]) @receiver(merge_thread) def merge_thread_tracker(sender, **kwargs): other_thread = kwargs["other_thread"] - other_thread.postread_set.update(category=sender.category, thread=sender) + other_thread.readthread_set.all().delete() @receiver(move_thread) def move_thread_tracker(sender, **kwargs): - sender.postread_set.update(category=sender.category, thread=sender) - - -@receiver(merge_post) -def merge_post_delete_tracker(sender, **kwargs): - sender.postread_set.all().delete() - - -@receiver(move_post) -def move_post_delete_tracker(sender, **kwargs): - sender.postread_set.all().delete() - - -@receiver(thread_read) -def decrease_unread_private_count(sender, **kwargs): - user = sender - thread = kwargs["thread"] - - if thread.category.thread_type.root_name != PRIVATE_THREADS_ROOT_NAME: - return - - if user.unread_private_threads: - user.unread_private_threads -= 1 - user.save(update_fields=["unread_private_threads"]) + sender.readthread_set.update(category=sender.category, thread=sender) diff --git a/misago/readtracker/tests/test_are_private_threads_read.py b/misago/readtracker/tests/test_get_unread_private_threads.py similarity index 68% rename from misago/readtracker/tests/test_are_private_threads_read.py rename to misago/readtracker/tests/test_get_unread_private_threads.py index 58a51d9057..3be9373658 100644 --- a/misago/readtracker/tests/test_are_private_threads_read.py +++ b/misago/readtracker/tests/test_get_unread_private_threads.py @@ -6,12 +6,12 @@ from ...categories.proxy import CategoriesProxy from ...permissions.proxy import UserPermissionsProxy from ...threads.models import ThreadParticipant -from ...threads.test import post_thread +from ...threads.test import post_thread, reply_thread from ..models import ReadThread -from ..privatethreads import are_private_threads_read +from ..privatethreads import get_unread_private_threads -def test_are_private_threads_read_returns_true_for_empty_category( +def test_get_unread_private_threads_returns_nothing_for_empty_category( dynamic_settings, cache_versions, user, private_threads_category ): user_permissions = UserPermissionsProxy(user, cache_versions) @@ -22,10 +22,10 @@ def test_are_private_threads_read_returns_true_for_empty_category( user_permissions=user_permissions, ) - assert are_private_threads_read(request, private_threads_category, None) + assert not get_unread_private_threads(request, private_threads_category, None) -def test_are_private_threads_read_returns_false_for_unread_thread( +def test_get_unread_private_threads_returns_unread_thread( dynamic_settings, cache_versions, user, private_threads_category ): user.joined_on = user.joined_on.replace(year=2010) @@ -45,10 +45,12 @@ def test_are_private_threads_read_returns_false_for_unread_thread( user_permissions=user_permissions, ) - assert not are_private_threads_read(request, private_threads_category, None) + assert list( + get_unread_private_threads(request, private_threads_category, None) + ) == [thread] -def test_are_private_threads_read_returns_true_for_old_unread_thread( +def test_get_unread_private_threads_excludes_unread_thread_older_than_tracking_period( dynamic_settings, cache_versions, user, private_threads_category ): user.joined_on = user.joined_on.replace(year=2010) @@ -70,10 +72,10 @@ def test_are_private_threads_read_returns_true_for_old_unread_thread( user_permissions=user_permissions, ) - assert are_private_threads_read(request, private_threads_category, None) + assert not get_unread_private_threads(request, private_threads_category, None) -def test_are_private_threads_read_returns_true_for_unread_thread_older_than_user( +def test_get_unread_private_threads_excludes_unread_thread_older_than_user( dynamic_settings, cache_versions, user, private_threads_category ): thread = post_thread( @@ -92,10 +94,10 @@ def test_are_private_threads_read_returns_true_for_unread_thread_older_than_user user_permissions=user_permissions, ) - assert are_private_threads_read(request, private_threads_category, None) + assert not get_unread_private_threads(request, private_threads_category, None) -def test_are_private_threads_read_returns_true_for_read_thread( +def test_get_unread_private_threads_excludes_read_thread( dynamic_settings, cache_versions, user, private_threads_category ): user.joined_on = user.joined_on.replace(year=2010) @@ -122,10 +124,10 @@ def test_are_private_threads_read_returns_true_for_read_thread( user_permissions=user_permissions, ) - assert are_private_threads_read(request, private_threads_category, None) + assert not get_unread_private_threads(request, private_threads_category, None) -def test_are_private_threads_read_returns_true_for_one_read_and_one_unread_thread( +def test_get_unread_private_threads_includes_read_thread_with_unread_reply( dynamic_settings, cache_versions, user, private_threads_category ): user.joined_on = user.joined_on.replace(year=2010) @@ -141,9 +143,9 @@ def test_are_private_threads_read_returns_true_for_one_read_and_one_unread_threa read_time=thread.last_post_on, ) - unread_thread = post_thread(private_threads_category) + reply_thread(thread) - private_threads_category.last_post_on = unread_thread.last_post_on + private_threads_category.last_post_on = thread.last_post_on private_threads_category.save() user_permissions = UserPermissionsProxy(user, cache_versions) @@ -154,10 +156,12 @@ def test_are_private_threads_read_returns_true_for_one_read_and_one_unread_threa user_permissions=user_permissions, ) - assert are_private_threads_read(request, private_threads_category, None) + assert list( + get_unread_private_threads(request, private_threads_category, None) + ) == [thread] -def test_are_private_threads_read_returns_true_for_one_read_and_one_invisible_unread_thread( +def test_get_unread_private_threads_excludes_thread_in_read_category( dynamic_settings, cache_versions, user, private_threads_category ): user.joined_on = user.joined_on.replace(year=2010) @@ -166,15 +170,6 @@ def test_are_private_threads_read_returns_true_for_one_read_and_one_invisible_un thread = post_thread(private_threads_category) ThreadParticipant.objects.create(user=user, thread=thread) - ReadThread.objects.create( - user=user, - category=private_threads_category, - thread=thread, - read_time=thread.last_post_on, - ) - - post_thread(private_threads_category) - private_threads_category.last_post_on = thread.last_post_on private_threads_category.save() @@ -186,24 +181,23 @@ def test_are_private_threads_read_returns_true_for_one_read_and_one_invisible_un user_permissions=user_permissions, ) - assert are_private_threads_read(request, private_threads_category, None) + assert not get_unread_private_threads( + request, private_threads_category, timezone.now() + ) -def test_are_private_threads_read_returns_true_for_read_both_read_threads( +def test_get_unread_private_threads_includes_thread_in_read_category_with_unread_reply( dynamic_settings, cache_versions, user, private_threads_category ): user.joined_on = user.joined_on.replace(year=2010) user.save() - old_thread = post_thread( - private_threads_category, started_on=timezone.now() - timedelta(minutes=30) - ) - ThreadParticipant.objects.create(user=user, thread=old_thread) + thread = post_thread(private_threads_category) + ThreadParticipant.objects.create(user=user, thread=thread) - recent_thread = post_thread(private_threads_category) - ThreadParticipant.objects.create(user=user, thread=recent_thread) + reply = reply_thread(thread) - private_threads_category.last_post_on = recent_thread.last_post_on + private_threads_category.last_post_on = thread.last_post_on private_threads_category.save() user_permissions = UserPermissionsProxy(user, cache_versions) @@ -214,24 +208,22 @@ def test_are_private_threads_read_returns_true_for_read_both_read_threads( user_permissions=user_permissions, ) - assert are_private_threads_read(request, private_threads_category, timezone.now()) + assert list( + get_unread_private_threads( + request, private_threads_category, reply.posted_on - timedelta(minutes=1) + ) + ) == [thread] -def test_are_private_threads_read_returns_false_for_read_one_read_and_one_unread_thread( +def test_get_unread_private_threads_excludes_unread_thread_user_is_not_invited_to( dynamic_settings, cache_versions, user, private_threads_category ): user.joined_on = user.joined_on.replace(year=2010) user.save() - thread = post_thread( - private_threads_category, started_on=timezone.now() - timedelta(minutes=30) - ) - ThreadParticipant.objects.create(user=user, thread=thread) - - unread_thread = post_thread(private_threads_category) - ThreadParticipant.objects.create(user=user, thread=unread_thread) + thread = post_thread(private_threads_category) - private_threads_category.last_post_on = unread_thread.last_post_on + private_threads_category.last_post_on = thread.last_post_on private_threads_category.save() user_permissions = UserPermissionsProxy(user, cache_versions) @@ -242,6 +234,4 @@ def test_are_private_threads_read_returns_false_for_read_one_read_and_one_unread user_permissions=user_permissions, ) - assert not are_private_threads_read( - request, private_threads_category, timezone.now() - timedelta(minutes=15) - ) + assert not get_unread_private_threads(request, private_threads_category, None) diff --git a/misago/readtracker/tests/test_unread_private_threads_exist.py b/misago/readtracker/tests/test_unread_private_threads_exist.py new file mode 100644 index 0000000000..82024fd3f4 --- /dev/null +++ b/misago/readtracker/tests/test_unread_private_threads_exist.py @@ -0,0 +1,89 @@ +from datetime import timedelta +from unittest.mock import Mock + +from django.utils import timezone + +from ...categories.proxy import CategoriesProxy +from ...permissions.proxy import UserPermissionsProxy +from ...threads.models import ThreadParticipant +from ...threads.test import post_thread +from ..models import ReadThread +from ..privatethreads import unread_private_threads_exist + + +def test_unread_private_threads_exist_returns_true_for_unread_thread( + dynamic_settings, cache_versions, user, private_threads_category +): + user.joined_on = user.joined_on.replace(year=2010) + user.save() + + thread = post_thread(private_threads_category) + ThreadParticipant.objects.create(user=user, thread=thread) + + private_threads_category.last_post_on = thread.last_post_on + private_threads_category.save() + + user_permissions = UserPermissionsProxy(user, cache_versions) + request = Mock( + categories=CategoriesProxy(user_permissions, cache_versions), + settings=dynamic_settings, + user=user, + user_permissions=user_permissions, + ) + + assert unread_private_threads_exist(request, private_threads_category, None) + + +def test_unread_private_threads_exist_returns_false_for_read_thread( + dynamic_settings, cache_versions, user, private_threads_category +): + user.joined_on = user.joined_on.replace(year=2010) + user.save() + + thread = post_thread(private_threads_category) + ThreadParticipant.objects.create(user=user, thread=thread) + + ReadThread.objects.create( + user=user, + category=private_threads_category, + thread=thread, + read_time=thread.last_post_on, + ) + + private_threads_category.last_post_on = thread.last_post_on + private_threads_category.save() + + user_permissions = UserPermissionsProxy(user, cache_versions) + request = Mock( + categories=CategoriesProxy(user_permissions, cache_versions), + settings=dynamic_settings, + user=user, + user_permissions=user_permissions, + ) + + assert not unread_private_threads_exist(request, private_threads_category, None) + + +def test_unread_private_threads_exist_returns_false_for_thread_in_read_category( + dynamic_settings, cache_versions, user, private_threads_category +): + user.joined_on = user.joined_on.replace(year=2010) + user.save() + + thread = post_thread(private_threads_category) + ThreadParticipant.objects.create(user=user, thread=thread) + + private_threads_category.last_post_on = thread.last_post_on + private_threads_category.save() + + user_permissions = UserPermissionsProxy(user, cache_versions) + request = Mock( + categories=CategoriesProxy(user_permissions, cache_versions), + settings=dynamic_settings, + user=user, + user_permissions=user_permissions, + ) + + assert not unread_private_threads_exist( + request, private_threads_category, timezone.now() + ) diff --git a/misago/settings.py b/misago/settings.py index e279945a84..56f16dcdd6 100644 --- a/misago/settings.py +++ b/misago/settings.py @@ -139,7 +139,7 @@ "misago.core.middleware.ExceptionHandlerMiddleware", "misago.users.middleware.OnlineTrackerMiddleware", "misago.admin.middleware.AdminAuthMiddleware", - "misago.threads.middleware.UnreadThreadsCountMiddleware", + "misago.middleware.privatethreads.sync_user_unread_private_threads", ] MISAGO_DEFAULT_OG_IMAGE = "misago/img/og-image.jpg" diff --git a/misago/templates/misago/categories_list/category_row_last_thread.html b/misago/templates/misago/categories_list/category_row_last_thread.html index 7e554e346f..cd2e77f184 100644 --- a/misago/templates/misago/categories_list/category_row_last_thread.html +++ b/misago/templates/misago/categories_list/category_row_last_thread.html @@ -3,7 +3,11 @@ {% if last_thread %}
{% if last_thread.last_poster %} - + {% else %} @@ -14,7 +18,11 @@
{% if last_thread.is_visible %} - + {{ last_thread.title }} {% else %} @@ -39,7 +47,7 @@ - bool: - return are_private_threads_read(request, category, category_read_time) + return not unread_private_threads_exist(request, category, category_read_time) def mark_category_read( self,