diff --git a/misago/permissions/hooks/__init__.py b/misago/permissions/hooks/__init__.py index 271efc7fc8..d3b2dd94ea 100644 --- a/misago/permissions/hooks/__init__.py +++ b/misago/permissions/hooks/__init__.py @@ -1,8 +1,14 @@ from .build_user_category_permissions import build_user_category_permissions_hook from .build_user_permissions import build_user_permissions_hook from .check_browse_category_permission import check_browse_category_permission_hook +from .check_post_in_closed_category_permission import ( + check_post_in_closed_category_permission_hook, +) from .check_private_threads_permission import check_private_threads_permission_hook from .check_see_category_permission import check_see_category_permission_hook +from .check_start_thread_in_category_permission import ( + check_start_thread_in_category_permission_hook, +) from .copy_category_permissions import copy_category_permissions_hook from .copy_group_permissions import copy_group_permissions_hook from .filter_private_threads_queryset import filter_private_threads_queryset_hook @@ -22,8 +28,10 @@ "build_user_category_permissions_hook", "build_user_permissions_hook", "check_browse_category_permission_hook", + "check_post_in_closed_category_permission_hook", "check_private_threads_permission_hook", "check_see_category_permission_hook", + "check_start_thread_in_category_permission_hook", "copy_category_permissions_hook", "copy_group_permissions_hook", "filter_private_threads_queryset_hook", diff --git a/misago/permissions/hooks/check_browse_category_permission.py b/misago/permissions/hooks/check_browse_category_permission.py index 82fd8204d3..e52cfced26 100644 --- a/misago/permissions/hooks/check_browse_category_permission.py +++ b/misago/permissions/hooks/check_browse_category_permission.py @@ -53,7 +53,7 @@ class CheckBrowseCategoryPermissionHookFilter(Protocol): It raises Django's `Http404` if they can't see it or `PermissionDenied` with an error message if they can't browse it. - Browse the [action](#action) section for details. + See the [action](#action) section for details. ## `user_permissions: UserPermissionsProxy` diff --git a/misago/permissions/hooks/check_post_in_closed_category_permission.py b/misago/permissions/hooks/check_post_in_closed_category_permission.py new file mode 100644 index 0000000000..abfe5a43a7 --- /dev/null +++ b/misago/permissions/hooks/check_post_in_closed_category_permission.py @@ -0,0 +1,121 @@ +from typing import TYPE_CHECKING, Protocol + +from ...categories.models import Category +from ...plugins.hooks import FilterHook + +if TYPE_CHECKING: + from ..proxy import UserPermissionsProxy + + +class CheckPostInClosedCategoryPermissionHookAction(Protocol): + """ + A standard Misago function used to check if the user has permission to + post in a closed category. It raises Django's `PermissionDenied` with an + error message if category is closed and they can't post in it. + + # Arguments + + ## `user_permissions: UserPermissionsProxy` + + A proxy object with the current user's permissions. + + ## `category: Category` + + A category to check permissions for. + """ + + def __call__( + self, + permissions: "UserPermissionsProxy", + category: Category, + ) -> None: ... + + +class CheckPostInClosedCategoryPermissionHookFilter(Protocol): + """ + A function implemented by a plugin that can be registered in this hook. + + # Arguments + + ## `action: CheckPostInClosedCategoryPermissionHookAction` + + A standard Misago function used to check if the user has permission to + post in a closed category. It raises Django's `PermissionDenied` with an + error message if category is closed and they can't post in it. + + See the [action](#action) section for details. + + ## `user_permissions: UserPermissionsProxy` + + A proxy object with the current user's permissions. + + ## `category: Category` + + A category to check permissions for. + """ + + def __call__( + self, + action: CheckPostInClosedCategoryPermissionHookAction, + permissions: "UserPermissionsProxy", + category: Category, + ) -> None: ... + + +class CheckPostInClosedCategoryPermissionHook( + FilterHook[ + CheckPostInClosedCategoryPermissionHookAction, + CheckPostInClosedCategoryPermissionHookFilter, + ] +): + """ + This hook wraps the standard function that Misago uses to check if the user + has permission to post in a closed category. It raises Django's `PermissionDenied` + with an error message if category is closed and they can't post in it. + + # Example + + The code below implements a custom filter function that permits a user to + post in the specific category if they have a custom flag set on their account. + + ```python + from misago.categories.models import Category + from misago.permissions.hooks import check_post_in_closed_category_permission_hook + from misago.permissions.proxy import UserPermissionsProxy + + @check_post_in_closed_category_permission_hook.append_filter + def check_user_can_post_in_closed_category( + action, + permissions: UserPermissionsProxy, + category: Category, + ) -> None: + user = permissions.user + if user.is_authenticated: + post_in_closed_categories = ( + user.plugin_data.get("post_in_closed_categories") or [] + ) + else: + post_in_closed_categories = None + + if ( + not post_in_closed_categories + or category.id not in post_in_closed_categories + ): + action(permissions, category) + ``` + """ + + __slots__ = FilterHook.__slots__ + + def __call__( + self, + action: CheckPostInClosedCategoryPermissionHookAction, + permissions: "UserPermissionsProxy", + category: Category, + ) -> None: + return super().__call__(action, permissions, category) + + +check_post_in_closed_category_permission_hook = ( + CheckPostInClosedCategoryPermissionHook() +) diff --git a/misago/permissions/hooks/check_start_thread_in_category_permission.py b/misago/permissions/hooks/check_start_thread_in_category_permission.py new file mode 100644 index 0000000000..32f57b3897 --- /dev/null +++ b/misago/permissions/hooks/check_start_thread_in_category_permission.py @@ -0,0 +1,123 @@ +from typing import TYPE_CHECKING, Protocol + +from ...categories.models import Category +from ...plugins.hooks import FilterHook + +if TYPE_CHECKING: + from ..proxy import UserPermissionsProxy + + +class CheckStartThreadInCategoryPermissionHookAction(Protocol): + """ + A standard Misago function used to check if the user has permission to + start a new thread in a category. It raises Django's `PermissionDenied` with + an error message if they can't start thread in a category. + + # Arguments + + ## `user_permissions: UserPermissionsProxy` + + A proxy object with the current user's permissions. + + ## `category: Category` + + A category to check permissions for. + """ + + def __call__( + self, + permissions: "UserPermissionsProxy", + category: Category, + ) -> None: ... + + +class CheckStartThreadInCategoryPermissionHookFilter(Protocol): + """ + A function implemented by a plugin that can be registered in this hook. + + # Arguments + + ## `action: CheckStartThreadInCategoryPermissionHookAction` + + A standard Misago function used to check if the user has permission to + start a new thread in a category. It raises Django's `PermissionDenied` with + an error message if they can't start thread in a category. + + See the [action](#action) section for details. + + ## `user_permissions: UserPermissionsProxy` + + A proxy object with the current user's permissions. + + ## `category: Category` + + A category to check permissions for. + """ + + def __call__( + self, + action: CheckStartThreadInCategoryPermissionHookAction, + permissions: "UserPermissionsProxy", + category: Category, + ) -> None: ... + + +class CheckStartThreadInCategoryPermissionHook( + FilterHook[ + CheckStartThreadInCategoryPermissionHookAction, + CheckStartThreadInCategoryPermissionHookFilter, + ] +): + """ + This hook wraps the standard Misago function used to check if the user has + permission to start a new thread in a category. It raises Django's + `PermissionDenied` with an error message if they can't start thread in a category. + + # Example + + The code below implements a custom filter function that prevents the user from + starting a thread in category if their account is newer than 7 days. + + ```python + from datetime import timedelta + + from django.core.exceptions import PermissionDenied + from django.utils import timezone + from misago.categories.models import Category + from misago.permissions.hooks import check_start_thread_in_category_permission_hook + from misago.permissions.proxy import UserPermissionsProxy + + @check_start_thread_in_category_permission_hook.append_filter + def check_user_can_start_thread( + action, + permissions: UserPermissionsProxy, + category: Category, + ) -> None: + action(permissions, category) + + user = permissions.user + if ( + user.is_authenticated + and user.joined_on > timezone.now() - timedelta(days=7): + ): + raise PermissionDenied( + "Your account was created less than 7 days ago. " + "You can't start threads yet." + ) + ``` + """ + + __slots__ = FilterHook.__slots__ + + def __call__( + self, + action: CheckStartThreadInCategoryPermissionHookAction, + permissions: "UserPermissionsProxy", + category: Category, + ) -> None: + return super().__call__(action, permissions, category) + + +check_start_thread_in_category_permission_hook = ( + CheckStartThreadInCategoryPermissionHook() +) diff --git a/misago/permissions/threads/checks.py b/misago/permissions/threads/checks.py index dc655e9e6e..cdb0456902 100644 --- a/misago/permissions/threads/checks.py +++ b/misago/permissions/threads/checks.py @@ -3,11 +3,25 @@ from ...categories.models import Category from ..enums import CategoryPermission +from ..hooks import ( + check_post_in_closed_category_permission_hook, + check_start_thread_in_category_permission_hook, +) from ..proxy import UserPermissionsProxy def check_post_in_closed_category_permission( permissions: UserPermissionsProxy, category: Category +): + check_post_in_closed_category_permission_hook( + _check_post_in_closed_category_permission_action, + permissions, + category, + ) + + +def _check_post_in_closed_category_permission_action( + permissions: UserPermissionsProxy, category: Category ): if category.is_closed and not ( permissions.is_global_moderator @@ -23,6 +37,16 @@ def check_post_in_closed_category_permission( def check_start_thread_in_category_permission( permissions: UserPermissionsProxy, category: Category +): + check_start_thread_in_category_permission_hook( + _check_start_thread_in_category_permission_action, + permissions, + category, + ) + + +def _check_start_thread_in_category_permission_action( + permissions: UserPermissionsProxy, category: Category ): if category.id not in permissions.categories[CategoryPermission.START]: raise PermissionDenied(