diff --git a/docs/settings.rst b/docs/settings.rst index d793bf24a..efa5cb409 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -290,6 +290,43 @@ Default: ``15`` The number of posts displayed inside one page of a forum member's posts list. +``MACHINA_TRIPLE_APPROVAL_STATUS`` +----------------------------------------- + +Default: ``False`` + +By default, machina employes a two-state approval status for posts: `True` (approved) or `False` +(disapproved or pending approval). Posts with approval status `False` will not be displayed, and +will be approved or deleted during moderation. + +If this option is set to `True`, posts will have three approval states `True` (approved), `None` +(pending approval), and `False` (disapproved). Posts with approval status `None` will be moderated, +and be assigned to states `True` or `False`. Disapproved posts are not deleted, allowing them to be +revised and re-posted. + +``MACHINA_DEFAULT_APPROVAL_STATUS`` +----------------------------------------- + +Default: ``True`` if `MACHINA_TRIPLE_APPROVAL_STATUS` is `False`, `None` otherwise + +The default approval state for posts when it is not explicitly specified during the creation of posts. +It can be `True` (default) or `False` if `MACHINA_TRIPLE_APPROVAL_STATUS` is `False` (default). +Otherwise it can be `True`, `None` (default), or `False`. + + +``MACHINA_PENDING_POSTS_AS_APPROVED`` +----------------------------------------- + +Default: ``True`` + +If pending posts (`approved=None`) will be treated as approved or disapproved when +`MACHINA_TRIPLE_APPROVAL_STATUS` is set to `True`. If this option is set to `True`, pending posts will +be counted towards `posts_count` and be displayed. Otherwise, pending posts will not be displayed until +they are approved. + +This option is only valid when `MACHINA_TRIPLE_APPROVAL_STATUS` is set to `True`. + + Permission ********** diff --git a/machina/apps/forum/abstract_models.py b/machina/apps/forum/abstract_models.py index d2d642383..8bf20aaa9 100644 --- a/machina/apps/forum/abstract_models.py +++ b/machina/apps/forum/abstract_models.py @@ -174,7 +174,8 @@ def save(self, *args, **kwargs): def update_trackers(self): """ Updates the denormalized trackers associated with the forum instance. """ - direct_approved_topics = self.topics.filter(approved=True).order_by('-last_post_on') + direct_approved_topics = self.topics.filter(machina_settings.APPROVED_FILTER).order_by( + '-last_post_on') # Compute the direct topics count and the direct posts count. self.direct_topics_count = direct_approved_topics.count() diff --git a/machina/apps/forum/views.py b/machina/apps/forum/views.py index 5038f7237..dc185f2dd 100644 --- a/machina/apps/forum/views.py +++ b/machina/apps/forum/views.py @@ -83,7 +83,7 @@ def get_queryset(self): qs = ( self.forum.topics .exclude(type=Topic.TOPIC_ANNOUNCE) - .exclude(approved=False) + .filter(machina_settings.APPROVED_FILTER) .select_related('poster', 'last_post', 'last_post__poster') ) return qs diff --git a/machina/apps/forum_conversation/abstract_models.py b/machina/apps/forum_conversation/abstract_models.py index 023033b04..d31e29305 100644 --- a/machina/apps/forum_conversation/abstract_models.py +++ b/machina/apps/forum_conversation/abstract_models.py @@ -204,9 +204,9 @@ def delete(self, using=None): def update_trackers(self): """ Updates the denormalized trackers associated with the topic instance. """ - self.posts_count = self.posts.filter(approved=True).count() + self.posts_count = self.posts.filter(machina_settings.APPROVED_FILTER).count() first_post = self.posts.all().order_by('created').first() - last_post = self.posts.filter(approved=True).order_by('-created').first() + last_post = self.posts.filter(machina_settings.APPROVED_FILTER).order_by('-created').first() self.first_post = first_post self.last_post = last_post self.last_post_on = last_post.created if last_post else None @@ -246,7 +246,8 @@ class AbstractPost(DatedModel): username = models.CharField(max_length=155, blank=True, null=True, verbose_name=_('Username')) # A post can be approved before publishing ; defaults to True - approved = models.BooleanField(default=True, db_index=True, verbose_name=_('Approved')) + approved = models.BooleanField(default=machina_settings.DEFAULT_APPROVAL_STATUS, null=True, + db_index=True, verbose_name=_('Approved')) # The user can choose if they want to display their signature with the content of the post enable_signature = models.BooleanField( @@ -303,6 +304,23 @@ def position(self): position = self.topic.posts.filter(Q(created__lt=self.created) | Q(id=self.id)).count() return position + def approve(self): + if self.approved: + return + self.approved = True + self.save(update_fields=['approved']) + self.topic.update_trackers() + + def disapprove(self): + if machina_settings.TRIPLE_APPROVAL_STATUS: + if self.approved is False: + return + self.approved = False + self.save(update_fields=['approved']) + self.topic.update_trackers() + else: + self.delete() + def clean(self): """ Validates the post instance. """ super().clean() diff --git a/machina/apps/forum_conversation/managers.py b/machina/apps/forum_conversation/managers.py index e0c0d32ef..c2c1855df 100644 --- a/machina/apps/forum_conversation/managers.py +++ b/machina/apps/forum_conversation/managers.py @@ -8,10 +8,12 @@ from django.db import models +from machina.conf import settings as machina_settings + class ApprovedManager(models.Manager): def get_queryset(self): """ Returns all the approved topics or posts. """ qs = super().get_queryset() - qs = qs.filter(approved=True) + qs = qs.filter(machina_settings.APPROVED_FILTER) return qs diff --git a/machina/apps/forum_conversation/views.py b/machina/apps/forum_conversation/views.py index 48426554a..35088ddc6 100644 --- a/machina/apps/forum_conversation/views.py +++ b/machina/apps/forum_conversation/views.py @@ -8,6 +8,7 @@ from django.contrib import messages from django.core.exceptions import ObjectDoesNotExist +from django.db.models import Q from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404 from django.urls import reverse @@ -80,11 +81,16 @@ def get_topic(self): def get_queryset(self): """ Returns the list of items for this view. """ + cond = machina_settings.APPROVED_FILTER + if self.request.user.is_authenticated and machina_settings.TRIPLE_APPROVAL_STATUS: + # in triple approval status, disapproved posts can be viewed by their posters + cond |= Q(poster=self.request.user) + self.topic = self.get_topic() qs = ( self.topic.posts .all() - .exclude(approved=False) + .filter(cond) .select_related('poster', 'updated_by') .prefetch_related('attachments', 'poster__forum_profile') ) @@ -658,7 +664,7 @@ def get_context_data(self, **kwargs): # Add the previous posts to the context previous_posts = ( - topic.posts.filter(approved=True) + topic.posts.filter(machina_settings.APPROVED_FILTER) .select_related('poster', 'updated_by') .prefetch_related('attachments', 'poster__forum_profile') .order_by('-created') diff --git a/machina/apps/forum_feeds/feeds.py b/machina/apps/forum_feeds/feeds.py index e4c724e5b..d3e9f4cae 100644 --- a/machina/apps/forum_feeds/feeds.py +++ b/machina/apps/forum_feeds/feeds.py @@ -12,6 +12,7 @@ from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ +from machina.conf import settings as machina_settings from machina.core.db.models import get_model @@ -53,7 +54,8 @@ def get_object(self, request, *args, **kwargs): def items(self): """ Returns the items to include into the feed. """ - return Topic.objects.filter(forum__in=self.forums, approved=True).order_by('-last_post_on') + return Topic.objects.filter(forum__in=self.forums).filter( + machina_settings.APPROVED_FILTER).order_by('-last_post_on') def item_link(self, item): """ Generates a link for a specific item of the feed. """ diff --git a/machina/apps/forum_member/views.py b/machina/apps/forum_member/views.py index ed1624491..4aabbe87c 100644 --- a/machina/apps/forum_member/views.py +++ b/machina/apps/forum_member/views.py @@ -92,7 +92,8 @@ def get_context_data(self, **kwargs): # Computes the number of topics added by the considered member context['topics_count'] = ( - Topic.objects.filter(approved=True, poster=self.object.user).count() + Topic.objects.filter(machina_settings.APPROVED_FILTER).filter(poster=self.object.user) + .count() ) # Fetches the recent posts added by the considered user diff --git a/machina/apps/forum_moderation/views.py b/machina/apps/forum_moderation/views.py index fb127b4ce..d6cd974b3 100644 --- a/machina/apps/forum_moderation/views.py +++ b/machina/apps/forum_moderation/views.py @@ -337,7 +337,8 @@ def get_queryset(self): self.request.user, ) qs = super().get_queryset() - qs = qs.filter(topic__forum__in=forums, approved=False) + qs = qs.filter(topic__forum__in=forums, approved=None if + machina_settings.TRIPLE_APPROVAL_STATUS else False) return qs.order_by('-created') def perform_permissions_check(self, user, obj, perms): @@ -371,8 +372,8 @@ def get_context_data(self, **kwargs): if not post.is_topic_head: # Add the topic review previous_posts = ( - topic.posts - .filter(approved=True, created__lte=post.created) + topic.posts.filter(machina_settings.APPROVED_FILTER) + .filter(created__lte=post.created) .select_related('poster', 'updated_by') .prefetch_related('attachments', 'poster__forum_profile') .order_by('-created') @@ -403,8 +404,7 @@ def approve(self, request, *args, **kwargs): """ Approves the considered post and retirects the user to the success URL. """ self.object = self.get_object() success_url = self.get_success_url() - self.object.approved = True - self.object.save() + self.object.approve() messages.success(self.request, self.success_message) return HttpResponseRedirect(success_url) @@ -445,7 +445,7 @@ def disapprove(self, request, *args, **kwargs): """ Disapproves the considered post and retirects the user to the success URL. """ self.object = self.get_object() success_url = self.get_success_url() - self.object.delete() + self.object.disapprove() messages.success(self.request, self.success_message) return HttpResponseRedirect(success_url) diff --git a/machina/apps/forum_search/search_indexes.py b/machina/apps/forum_search/search_indexes.py index e7f5c35d4..fe6515cee 100644 --- a/machina/apps/forum_search/search_indexes.py +++ b/machina/apps/forum_search/search_indexes.py @@ -8,6 +8,7 @@ from haystack import indexes +from machina.conf import settings as machina_settings from machina.core.db.models import get_model from machina.core.loading import get_class @@ -57,7 +58,8 @@ def prepare_topic_subject(self, obj): return obj.topic.subject def index_queryset(self, using=None): - return Post.objects.all().exclude(approved=False) + return Post.objects.all().filter(machina_settings.APPROVED_FILTER) def read_queryset(self, using=None): - return Post.objects.all().exclude(approved=False).select_related('topic', 'poster') + return Post.objects.all().filter(machina_settings.APPROVED_FILTER).select_related('topic', + 'poster') diff --git a/machina/apps/forum_tracking/handler.py b/machina/apps/forum_tracking/handler.py index c8c5909f6..1d6e836cc 100644 --- a/machina/apps/forum_tracking/handler.py +++ b/machina/apps/forum_tracking/handler.py @@ -9,6 +9,7 @@ from django.db.models import F, Q +from machina.conf import settings as machina_settings from machina.core.db.models import get_model from machina.core.loading import get_class @@ -151,7 +152,8 @@ def mark_topic_read(self, topic, user): not unread_topics.exists() and ( forum_track is not None or - forum_topic_tracks.count() == forum.topics.filter(approved=True).count() + forum_topic_tracks.count() == forum.topics.filter( + machina_settings.APPROVED_FILTER).count() ) ): # The topics that are marked as read inside the forum for the given user will be diff --git a/machina/conf/settings.py b/machina/conf/settings.py index d450cc521..85d0eb522 100644 --- a/machina/conf/settings.py +++ b/machina/conf/settings.py @@ -9,6 +9,7 @@ """ from django.conf import settings +from django.db.models import Q # General @@ -95,7 +96,14 @@ PROFILE_RECENT_POSTS_NUMBER = getattr(settings, 'MACHINA_PROFILE_RECENT_POSTS_NUMBER', 15) PROFILE_POSTS_NUMBER_PER_PAGE = getattr(settings, 'MACHINA_PROFILE_POSTS_NUMBER_PER_PAGE', 15) - +TRIPLE_APPROVAL_STATUS = getattr(settings, 'MACHINA_TRIPLE_APPROVAL_STATUS', False) +DEFAULT_APPROVAL_STATUS = getattr(settings, 'MACHINA_DEFAULT_APPROVAL_STATUS', None if + TRIPLE_APPROVAL_STATUS else True) +PENDING_POSTS_AS_APPROVED = getattr(settings, 'MACHINA_PENDING_POSTS_AS_APPROVED', True) + +APPROVED_FILTER = Q(approved=True) +if TRIPLE_APPROVAL_STATUS and PENDING_POSTS_AS_APPROVED: + APPROVED_FILTER |= Q(approved=None) # Permission DEFAULT_AUTHENTICATED_USER_FORUM_PERMISSIONS = getattr(