SciPost Code Repository

Skip to content
Snippets Groups Projects
views.py 14.8 KiB
Newer Older
Jean-Sébastien Caux's avatar
Jean-Sébastien Caux committed
__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
__license__ = "AGPL v3"
Jean-Sébastien Caux's avatar
Jean-Sébastien Caux committed

import json
from django import forms
from django.contrib import messages
from django.contrib.auth.models import User, Group
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
Jean-Sébastien Caux's avatar
Jean-Sébastien Caux committed
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied
from django.core import serializers
from django.http import HttpResponse, HttpResponseRedirect, Http404
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse, reverse_lazy
Jean-Sébastien Caux's avatar
Jean-Sébastien Caux committed
from django.utils import timezone
Jean-Sébastien Caux's avatar
Jean-Sébastien Caux committed
from django.views.generic.detail import DetailView
from django.views.generic.edit import CreateView, UpdateView, DeleteView
Jean-Sébastien Caux's avatar
Jean-Sébastien Caux committed
from django.views.generic.list import ListView

from guardian.decorators import permission_required_or_403
from guardian.mixins import PermissionRequiredMixin
from guardian.shortcuts import (
    assign_perm,
    remove_perm,
    get_objects_for_user,
    get_perms,
    get_users_with_perms,
    get_groups_with_perms,
)
from .models import Forum, Meeting, Post, Motion
from .forms import (
    ForumForm,
    ForumGroupPermissionsForm,
    ForumOrganizationPermissionsForm,
    MeetingForm,
    PostForm,
    MotionForm,
    MotionVoteForm,
Jean-Sébastien Caux's avatar
Jean-Sébastien Caux committed

from scipost.mixins import PermissionsMixin


class ForumCreateView(PermissionsMixin, CreateView):
    permission_required = "forums.add_forum"
Jean-Sébastien Caux's avatar
Jean-Sébastien Caux committed
    model = Forum
    form_class = ForumForm
    template_name = "forums/forum_form.html"
    success_url = reverse_lazy("forums:forums")
Jean-Sébastien Caux's avatar
Jean-Sébastien Caux committed

    def get_initial(self):
        initial = super().get_initial()
        parent_model = self.kwargs.get("parent_model")
        parent_content_type = None
        parent_object_id = self.kwargs.get("parent_id")
        if parent_model == "forum":
            parent_content_type = ContentType.objects.get(
                app_label="forums", model="forum"
            )
        initial.update(
            {
                "moderators": self.request.user,
                "parent_content_type": parent_content_type,
                "parent_object_id": parent_object_id,
            }
        )
Jean-Sébastien Caux's avatar
Jean-Sébastien Caux committed
        return initial


class MeetingCreateView(ForumCreateView):
    model = Meeting
    form_class = MeetingForm


class ForumUpdateView(PermissionRequiredMixin, UpdateView):
    permission_required = "forums.update_forum"
    template_name = "forums/forum_form.html"
    def get_object(self, queryset=None):
        try:
            return Meeting.objects.get(slug=self.kwargs["slug"])
        except Meeting.DoesNotExist:
            return Forum.objects.get(slug=self.kwargs["slug"])

    def get_form(self, form_class=None):
        try:
            self.object.meeting
            return MeetingForm(**self.get_form_kwargs())
        except Meeting.DoesNotExist:
            return ForumForm(**self.get_form_kwargs())
class ForumDeleteView(PermissionRequiredMixin, DeleteView):
    permission_required = "forums.delete_forum"
    success_url = reverse_lazy("forums:forums")

    def delete(self, request, *args, **kwargs):
        """
        A Forum can only be deleted if it does not have any descendants.
        Upon deletion, all object-level permissions associated to the
        Forum are explicitly removed, to avoid orphaned permissions.
        forum = get_object_or_404(Forum, slug=self.kwargs.get("slug"))
        groups_perms_dict = get_groups_with_perms(forum, attach_perms=True)
        if forum.child_forums.all().count() > 0:
            messages.warning(request, "A Forum with descendants cannot be deleted.")
            return redirect(forum.get_absolute_url())
        for group, perms_list in groups_perms_dict.items():
            for perm in perms_list:
                remove_perm(perm, group, forum)
        return super().delete(request, *args, **kwargs)


class ForumDetailView(PermissionRequiredMixin, DetailView):
    permission_required = "forums.can_view_forum"
Jean-Sébastien Caux's avatar
Jean-Sébastien Caux committed
    model = Forum
    template_name = "forums/forum_detail.html"
    def get_queryset(self):
        qs = super().get_queryset()
        qs = qs.select_related(
            "meeting",
        )
        qs = qs.prefetch_related(
            "parent",
            "child_forums",
            "posts__motion",
            "posts__posted_by",
            "motions__posted_by",
            "motions__in_agreement",
            "motions__in_doubt",
            "motions__in_disagreement",
            "motions__in_abstain",
            "motions__eligible_for_voting",
        )
        return qs

class HX_ForumQuickLinksAllView(PermissionRequiredMixin, DetailView):
    permission_required = "forums.can_view_forum"
    model = Forum
    template_name = "forums/_hx_forum_quick_links_all.html"

    def get_queryset(self):
        qs = super().get_queryset()
        qs = qs.prefetch_related(
            "posts_all__posted_by",
            "posts__motion",
        )
        return qs


class HX_ForumQuickLinksFollowupsView(PermissionRequiredMixin, DetailView):
    permission_required = "forums.can_view_forum"
    model = Forum
    template_name = "forums/_hx_forum_quick_links_followups.html"

    def get_queryset(self):
        qs = super().get_queryset()
        qs = qs.prefetch_related(
            "posts__posted_by",
            "posts__cf_latest_followup_in_hierarchy__posted_by",
        )
        return qs


class HX_ForumPermissionsView(PermissionRequiredMixin, DetailView):
    permission_required = "forums.add_forum"
    model = Forum
    template_name = "forums/_hx_forum_permissions.html"

    def get_context_data(self, *args, **kwargs):
        context = super().get_context_data(*args, **kwargs)
        context["groups_with_perms"] = get_groups_with_perms(self.object).order_by(
            "name"
        )
        context["users_with_perms"] = get_users_with_perms(
            self.object,
            attach_perms=True,
        )
        return context


class ForumPermissionsView(PermissionRequiredMixin, UpdateView):
    permission_required = "forums.can_administer_forum"
    model = Forum
    form_class = ForumGroupPermissionsForm
    template_name = "forums/forum_permissions.html"

    def get_context_data(self, *args, **kwargs):
        context = super().get_context_data(*args, **kwargs)
            context["group"] = Group.objects.get(pk=self.kwargs.get("group_id"))
        except Group.DoesNotExist:
            pass
        return context

    def get_initial(self, *args, **kwargs):
        initial = super().get_initial(*args, **kwargs)
            group = Group.objects.get(pk=self.kwargs.get("group_id"))
            perms = get_perms(group, self.object)
            initial["groups"] = group.id
            initial["can_administer"] = "can_administer_forum" in perms
            initial["can_view"] = "can_view_forum" in perms
            initial["can_post"] = "can_post_to_forum" in perms
        except Group.DoesNotExist:
            pass
        return initial

    def form_valid(self, form):
        for group in form.cleaned_data["groups"]:
            if form.cleaned_data["can_administer"]:
                assign_perm("can_administer_forum", group, self.object)
            else:
                remove_perm("can_administer_forum", group, self.object)
            if form.cleaned_data["can_view"]:
                assign_perm("can_view_forum", group, self.object)
                remove_perm("can_view_forum", group, self.object)
            if form.cleaned_data["can_post"]:
                assign_perm("can_post_to_forum", group, self.object)
                remove_perm("can_post_to_forum", group, self.object)
        return super().form_valid(form)

class ForumListView(LoginRequiredMixin, ListView):
Jean-Sébastien Caux's avatar
Jean-Sébastien Caux committed
    model = Forum
    template_name = "forum_list.html"
    def get_queryset(self):
        queryset = get_objects_for_user(
            self.request.user, "forums.can_view_forum"
        ).anchors().select_related("meeting").prefetch_related(
            "posts" + "__followup_posts" * 3,
            "child_forums__posts" + "__followup_posts" * 7,
        )
@permission_required_or_403("forums.can_post_to_forum", (Forum, "slug", "slug"))
def _hx_post_form_button(request, slug, parent_model, parent_id, origin, target, text):
    context = {
        "slug": slug,
        "parent_model": parent_model,
        "parent_id": parent_id,
        "origin": origin,
    return render(request, "forums/_hx_post_form_button.html", context)


@permission_required_or_403("forums.can_post_to_forum", (Forum, "slug", "slug"))
def _hx_post_form(request, slug, parent_model, parent_id, origin, target, text):
    forum = get_object_or_404(Forum, slug=slug)
    context = {
        "slug": slug,
        "parent_model": parent_model,
        "parent_id": parent_id,
        "origin": origin,
        "target": target,
        "text": text,
    }
    if request.method == "POST":
        form = PostForm(request.POST, forum=forum)
        if form.is_valid():
            post = form.save()
            thread_initiator = post.get_thread_initiator()
            response = render(
                request,
                "forums/post_card.html",
                context={"forum": forum, "post": thread_initiator},
            # if the parent is a forum, then this is a new Motion or Post,
            # and we keep the requested target and swap (== beforebegin).
            # Otherwise, we retarget to the initiator post, and swap outerHTML.
            # In both cases we refocus the browser after settle.
            if parent_model in ["forum", "meeting"]:
                # trigger new post form closure
                response["HX-Trigger"] = f"newPost-{target}"
                # refocus browser on new post
                response["HX-Trigger-After-Settle"] = json.dumps(
                    {"newPost": f"{target}",}
                )
            else:
                # force rerendering of whole thread from initiator down
                response["HX-Retarget"] = f"#thread-{thread_initiator.id}"
                response["HX-Reswap"] = "outerHTML"
                # refocus browser on initiator
                response["HX-Trigger-After-Settle"] = json.dumps(
                    {"newPost": f"thread-{thread_initiator.id}",}
                )
            print(f'{response["HX-Trigger-After-Settle"] = }')
    else:
        subject = ""
        if parent_model == "forum":
            parent_content_type = ContentType.objects.get(
                app_label="forums",
                model="forum",
        elif parent_model in ["post", "motion"]:
            parent_content_type = ContentType.objects.get(
                app_label="forums",
                model=parent_model,
            )
            parent = parent_content_type.get_object_for_this_type(pk=parent_id)
            if parent.subject.startswith("Re: ..."):
                subject = parent.subject
            elif parent.subject.startswith("Re:"):
                subject = "%s%s" % ("Re: ...", parent.subject.lstrip("Re:"))
            else:
                subject = "Re: %s" % parent.subject
        else:
            raise Http404
        initial = {
            "posted_by": request.user,
            "posted_on": timezone.now(),
            "parent_content_type": parent_content_type,
            "parent_object_id": parent_id,
            "subject": subject,
        }
        form = PostForm(initial=initial, forum=forum)
    context["form"] = form
    return render(request, "forums/_hx_post_form.html", context)


@permission_required_or_403("forums.can_post_to_forum", (Forum, "slug", "slug"))
def _hx_motion_form_button(request, slug):
    context = { "slug": slug, }
    return render(request, "forums/_hx_motion_form_button.html", context)


@permission_required_or_403("forums.can_post_to_forum", (Forum, "slug", "slug"))
def _hx_motion_form(request, slug):
    forum = get_object_or_404(Forum, slug=slug)
    if request.method == "POST":
        form = MotionForm(request.POST, forum=forum)
        if form.is_valid():
            motion = form.save()
            response = render(
                request,
                "forums/post_card.html",
                context={"forum": forum, "post": motion.post},
            )
            # trigger new motion form closure
            response["HX-Trigger"] = f"newMotion"
            # refocus browser on new Motion
            response["HX-Trigger-After-Settle"] = json.dumps(
                {"newPost": f"thread-{motion.post.id}",}
            )
            return response
    else:
        parent_content_type = ContentType.objects.get(
            app_label="forums",
            model="forum",
        )
        voters = get_users_with_perms(forum)
        ineligible_ids = []
        for voter in voters.all():
            if not voter.has_perm("can_post_to_forum", forum):
                ineligible_ids.append(voter.id)
        initial = {
            "posted_by": request.user,
            "posted_on": timezone.now(),
            "parent_content_type": parent_content_type,
            "parent_object_id": forum.id,
            "eligible_for_voting": voters.exclude(id__in=ineligible_ids),
        }
        initial["voting_deadline"] = (
            (forum.meeting.date_until if forum.meeting else timezone.now()) +
            datetime.timedelta(days=7)
        )
        form = MotionForm(initial=initial, forum=forum)
    context = { "slug": slug, "form": form, }
    return render(request, "forums/_hx_motion_form.html", context)


@permission_required_or_403("forums.can_view_forum", (Forum, "slug", "slug"))
def _hx_thread_from_post(request, slug, post_id):
    forum = get_object_or_404(Forum, slug=slug)
    post = Post.objects.filter(pk=post_id).select_related(
        "motion",
        "posted_by",
    ).prefetch_related(
        "parent",
        "followup_posts",
    ).first()
    context = {
        "forum": forum,
    }
    return render(request, "forums/post_card.html", context)



@permission_required_or_403("forums.can_post_to_forum", (Forum, "slug", "slug"))
def _hx_motion_voting(request, slug, motion_id):
    forum = get_object_or_404(Forum, slug=slug)
    motion = get_object_or_404(Motion.objects.prefetch_related(
        "eligible_for_voting__contributor__user",
        "in_agreement__contributor__user",
        "in_doubt__contributor__user",
        "in_disagreement__contributor__user",
        "in_abstain__contributor__user",
    ), pk=motion_id)
    initial = {
        "user": request.user.id,
        "motion": motion.id,
    }
    form = MotionVoteForm(request.POST or None, initial=initial)
    if form.is_valid():
        form.save()
        motion.refresh_from_db()
    context = { "forum": forum, "motion": motion, "form": form }
    return render(request, "forums/_hx_motion_voting.html", context)