Sith/forum/views.py

450 lines
14 KiB
Python

#
# Copyright 2016,2017,2018
# - Skia <skia@libskia.so>
# - Sli <antoine@bartuccio.fr>
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License a published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
#
import logging
import math
from functools import partial
from django import forms
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import PermissionDenied
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.db import IntegrityError
from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy
from django.utils import html, timezone
from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, ListView, RedirectView
from django.views.generic.detail import SingleObjectMixin
from django.views.generic.edit import CreateView, DeleteView, UpdateView
from haystack.query import RelatedSearchQuerySet
from honeypot.decorators import check_honeypot
from club.widgets.select import AutoCompleteSelectClub
from core.views import (
CanCreateMixin,
CanEditMixin,
CanEditPropMixin,
CanViewMixin,
can_view,
)
from core.views.widgets.markdown import MarkdownInput
from core.views.widgets.select import (
AutoCompleteSelect,
AutoCompleteSelectMultipleGroup,
)
from forum.models import Forum, ForumMessage, ForumMessageMeta, ForumTopic
class ForumSearchView(ListView):
template_name = "forum/search.jinja"
def get_queryset(self):
query = self.request.GET.get("query", "")
order_by = self.request.GET.get("order", "")
try:
queryset = (
RelatedSearchQuerySet()
.models(ForumMessage)
.autocomplete(auto=html.escape(query))
)
except TypeError:
return []
if order_by == "date":
queryset = queryset.order_by("-date")
queryset = queryset.load_all()
queryset = queryset.load_all_queryset(
ForumMessage,
ForumMessage.objects.all()
.prefetch_related("topic__forum__edit_groups")
.prefetch_related("topic__forum__view_groups")
.prefetch_related("topic__forum__owner_club"),
)
# Filter unauthorized responses
resp = []
count = 0
max_count = 30
for r in queryset:
if count >= max_count:
return resp
if can_view(r.object, self.request.user) and can_view(
r.object.topic, self.request.user
):
resp.append(r.object)
count += 1
return resp
class ForumMainView(ListView):
queryset = Forum.objects.filter(parent=None).prefetch_related(
"children___last_message__author", "children___last_message__topic"
)
template_name = "forum/main.jinja"
class ForumMarkAllAsRead(RedirectView):
permanent = False
url = reverse_lazy("forum:last_unread")
def get(self, request, *args, **kwargs):
fi = request.user.forum_infos
fi.last_read_date = timezone.now()
fi.save()
try:
for m in request.user.read_messages.filter(date__lt=fi.last_read_date):
m.readers.remove(request.user) # Clean up to keep table low in data
except IntegrityError:
pass
return super().get(request, *args, **kwargs)
class ForumFavoriteTopics(ListView):
model = ForumTopic
template_name = "forum/favorite_topics.jinja"
paginate_by = settings.SITH_FORUM_PAGE_LENGTH / 2
def get_queryset(self):
topic_list = self.request.user.favorite_topics.all()
return topic_list
class ForumLastUnread(ListView):
model = ForumTopic
template_name = "forum/last_unread.jinja"
paginate_by = settings.SITH_FORUM_PAGE_LENGTH / 2
def get_queryset(self):
topic_list = (
self.model.objects.filter(
_last_message__date__gt=self.request.user.forum_infos.last_read_date
)
.exclude(_last_message__readers=self.request.user)
.order_by("-_last_message__date")
.select_related("_last_message__author", "author")
.prefetch_related("forum__edit_groups")
)
return topic_list
class ForumNameField(forms.ModelChoiceField):
def label_from_instance(self, obj):
return obj.get_full_name()
class ForumForm(forms.ModelForm):
class Meta:
model = Forum
fields = [
"name",
"parent",
"number",
"owner_club",
"is_category",
"edit_groups",
"view_groups",
]
widgets = {
"edit_groups": AutoCompleteSelectMultipleGroup,
"view_groups": AutoCompleteSelectMultipleGroup,
"owner_club": AutoCompleteSelectClub,
}
parent = ForumNameField(
Forum.objects.all(), widget=AutoCompleteSelect, required=False
)
class ForumCreateView(CanCreateMixin, CreateView):
model = Forum
form_class = ForumForm
template_name = "core/create.jinja"
def get_initial(self):
init = super().get_initial()
parent = Forum.objects.filter(id=self.request.GET["parent"]).first()
if parent is not None:
init["parent"] = parent
init["owner_club"] = parent.owner_club
init["edit_groups"] = parent.edit_groups.all()
init["view_groups"] = parent.view_groups.all()
return init
class ForumEditForm(ForumForm):
recursive = forms.BooleanField(
label=_("Apply rights and club owner recursively"), required=False
)
class ForumEditView(CanEditPropMixin, UpdateView):
model = Forum
pk_url_kwarg = "forum_id"
form_class = ForumEditForm
template_name = "core/edit.jinja"
success_url = reverse_lazy("forum:main")
def form_valid(self, form):
ret = super().form_valid(form)
if form.cleaned_data["recursive"]:
self.object.apply_rights_recursively()
return ret
class ForumDeleteView(CanEditPropMixin, DeleteView):
model = Forum
pk_url_kwarg = "forum_id"
template_name = "core/delete_confirm.jinja"
success_url = reverse_lazy("forum:main")
class ForumDetailView(CanViewMixin, DetailView):
model = Forum
template_name = "forum/forum.jinja"
pk_url_kwarg = "forum_id"
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
qs = (
self.object.topics.order_by("-_last_message__date")
.select_related("_last_message__author", "author")
.prefetch_related("forum__edit_groups")
)
paginator = Paginator(qs, settings.SITH_FORUM_PAGE_LENGTH)
page = self.request.GET.get("topic_page")
try:
kwargs["topics"] = paginator.page(page)
except PageNotAnInteger:
kwargs["topics"] = paginator.page(1)
except EmptyPage:
kwargs["topics"] = paginator.page(paginator.num_pages)
return kwargs
class TopicForm(forms.ModelForm):
class Meta:
model = ForumMessage
fields = ["title", "message"]
widgets = {"message": MarkdownInput}
title = forms.CharField(required=True, label=_("Title"))
@method_decorator(
partial(check_honeypot, field_name=settings.HONEYPOT_FIELD_NAME_FORUM), name="post"
)
class ForumTopicCreateView(CanCreateMixin, CreateView):
model = ForumMessage
form_class = TopicForm
template_name = "forum/reply.jinja"
def dispatch(self, request, *args, **kwargs):
self.forum = get_object_or_404(
Forum, id=self.kwargs["forum_id"], is_category=False
)
if not request.user.can_view(self.forum):
raise PermissionDenied
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
topic = ForumTopic(
_title=form.instance.title, author=self.request.user, forum=self.forum
)
topic.save()
form.instance.topic = topic
form.instance.author = self.request.user
return super().form_valid(form)
class ForumTopicEditView(CanEditMixin, UpdateView):
model = ForumTopic
fields = ["forum"]
pk_url_kwarg = "topic_id"
template_name = "core/edit.jinja"
class ForumTopicSubscribeView(
LoginRequiredMixin, CanViewMixin, SingleObjectMixin, RedirectView
):
model = ForumTopic
pk_url_kwarg = "topic_id"
permanent = False
def get(self, request, *args, **kwargs):
self.object = self.get_object()
if self.object.subscribed_users.filter(id=request.user.id).exists():
self.object.subscribed_users.remove(request.user)
else:
self.object.subscribed_users.add(request.user)
return super().get(request, *args, **kwargs)
def get_redirect_url(self, *args, **kwargs):
return self.object.get_absolute_url()
class ForumTopicDetailView(CanViewMixin, DetailView):
model = ForumTopic
pk_url_kwarg = "topic_id"
template_name = "forum/topic.jinja"
context_object_name = "topic"
queryset = ForumTopic.objects.select_related("forum__parent")
def get_context_data(self, **kwargs):
topic: ForumTopic = self.object
kwargs = super().get_context_data(**kwargs)
msg = topic.get_first_unread_message(self.request.user)
if msg is None:
kwargs["first_unread_message_id"] = math.inf
else:
kwargs["first_unread_message_id"] = msg.id
paginator = Paginator(
topic.messages.select_related("author__avatar_pict", "topic__forum")
.prefetch_related("topic__forum__edit_groups", "readers")
.order_by("date"),
settings.SITH_FORUM_PAGE_LENGTH,
)
page = self.request.GET.get("page")
try:
kwargs["msgs"] = paginator.page(page)
except PageNotAnInteger:
kwargs["msgs"] = paginator.page(1)
except EmptyPage:
kwargs["msgs"] = paginator.page(paginator.num_pages)
return kwargs
class ForumMessageView(SingleObjectMixin, RedirectView):
model = ForumMessage
pk_url_kwarg = "message_id"
permanent = False
def get_redirect_url(self, *args, **kwargs):
self.object = self.get_object()
return self.object.get_url()
@method_decorator(
partial(check_honeypot, field_name=settings.HONEYPOT_FIELD_NAME_FORUM), name="post"
)
class ForumMessageEditView(CanEditMixin, UpdateView):
model = ForumMessage
form_class = forms.modelform_factory(
model=ForumMessage,
fields=["title", "message"],
widgets={"message": MarkdownInput},
)
template_name = "forum/reply.jinja"
pk_url_kwarg = "message_id"
def form_valid(self, form):
ForumMessageMeta(
message=self.object, user=self.request.user, action="EDIT"
).save()
return super().form_valid(form)
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
kwargs["topic"] = self.object.topic
return kwargs
class ForumMessageDeleteView(SingleObjectMixin, RedirectView):
model = ForumMessage
pk_url_kwarg = "message_id"
permanent = False
def get_redirect_url(self, *args, **kwargs):
self.object = self.get_object()
if self.object.can_be_moderated_by(self.request.user):
ForumMessageMeta(
message=self.object, user=self.request.user, action="DELETE"
).save()
return self.object.get_absolute_url()
class ForumMessageUndeleteView(SingleObjectMixin, RedirectView):
model = ForumMessage
pk_url_kwarg = "message_id"
permanent = False
def get_redirect_url(self, *args, **kwargs):
self.object = self.get_object()
if self.object.can_be_moderated_by(self.request.user):
ForumMessageMeta(
message=self.object, user=self.request.user, action="UNDELETE"
).save()
return self.object.get_absolute_url()
@method_decorator(
partial(check_honeypot, field_name=settings.HONEYPOT_FIELD_NAME_FORUM), name="post"
)
class ForumMessageCreateView(CanCreateMixin, CreateView):
model = ForumMessage
form_class = forms.modelform_factory(
model=ForumMessage,
fields=["title", "message"],
widgets={"message": MarkdownInput},
)
template_name = "forum/reply.jinja"
def dispatch(self, request, *args, **kwargs):
self.topic = get_object_or_404(ForumTopic, id=self.kwargs["topic_id"])
if not request.user.can_view(self.topic):
raise PermissionDenied
return super().dispatch(request, *args, **kwargs)
def get_initial(self):
init = super().get_initial()
try:
message = (
ForumMessage.objects.select_related("author")
.filter(id=self.request.GET["quote_id"])
.first()
)
init["message"] = "> ##### %s\n" % (
_("%(author)s said") % {"author": message.author.get_short_name()}
)
init["message"] += "\n".join(
["> " + line for line in message.message.split("\n")]
)
init["message"] += "\n\n"
except Exception as e:
logging.error(e)
return init
def form_valid(self, form):
form.instance.topic = self.topic
form.instance.author = self.request.user
return super().form_valid(form)
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
kwargs["topic"] = self.topic
return kwargs