Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Triple approval status that allows disapproved posts to be revised #273

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
37 changes: 37 additions & 0 deletions docs/settings.rst
Expand Up @@ -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
**********

Expand Down
3 changes: 2 additions & 1 deletion machina/apps/forum/abstract_models.py
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion machina/apps/forum/views.py
Expand Up @@ -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
Expand Down
24 changes: 21 additions & 3 deletions machina/apps/forum_conversation/abstract_models.py
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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()
Expand Down
4 changes: 3 additions & 1 deletion machina/apps/forum_conversation/managers.py
Expand Up @@ -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
10 changes: 8 additions & 2 deletions machina/apps/forum_conversation/views.py
Expand Up @@ -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
Expand Down Expand Up @@ -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')
)
Expand Down Expand Up @@ -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')
Expand Down
4 changes: 3 additions & 1 deletion machina/apps/forum_feeds/feeds.py
Expand Up @@ -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


Expand Down Expand Up @@ -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. """
Expand Down
3 changes: 2 additions & 1 deletion machina/apps/forum_member/views.py
Expand Up @@ -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
Expand Down
12 changes: 6 additions & 6 deletions machina/apps/forum_moderation/views.py
Expand Up @@ -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):
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)

Expand Down
6 changes: 4 additions & 2 deletions machina/apps/forum_search/search_indexes.py
Expand Up @@ -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

Expand Down Expand Up @@ -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')
4 changes: 3 additions & 1 deletion machina/apps/forum_tracking/handler.py
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
10 changes: 9 additions & 1 deletion machina/conf/settings.py
Expand Up @@ -9,6 +9,7 @@
"""

from django.conf import settings
from django.db.models import Q


# General
Expand Down Expand Up @@ -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(
Expand Down