Format forum

This commit is contained in:
Pierre Brunet 2017-06-12 09:58:24 +02:00
parent b3466237ca
commit 6a43c2cef6
3 changed files with 73 additions and 51 deletions

View File

@ -23,11 +23,9 @@
# #
from django.db import models from django.db import models
from django.core import validators
from django.conf import settings from django.conf import settings
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import IntegrityError, transaction
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.functional import cached_property from django.utils.functional import cached_property
@ -35,9 +33,10 @@ from django.utils.functional import cached_property
from datetime import datetime from datetime import datetime
import pytz import pytz
from core.models import User, MetaGroup, Group, SithFile from core.models import User, Group
from club.models import Club from club.models import Club
class Forum(models.Model): class Forum(models.Model):
""" """
The Forum class, made as a tree to allow nice tidy organization The Forum class, made as a tree to allow nice tidy organization
@ -52,14 +51,14 @@ class Forum(models.Model):
is_category = models.BooleanField(_('is a category'), default=False) is_category = models.BooleanField(_('is a category'), default=False)
parent = models.ForeignKey('Forum', related_name='children', null=True, blank=True) parent = models.ForeignKey('Forum', related_name='children', null=True, blank=True)
owner_club = models.ForeignKey(Club, related_name="owned_forums", verbose_name=_("owner club"), owner_club = models.ForeignKey(Club, related_name="owned_forums", verbose_name=_("owner club"),
default=settings.SITH_MAIN_CLUB_ID) default=settings.SITH_MAIN_CLUB_ID)
edit_groups = models.ManyToManyField(Group, related_name="editable_forums", blank=True, edit_groups = models.ManyToManyField(Group, related_name="editable_forums", blank=True,
default=[settings.SITH_GROUP_OLD_SUBSCRIBERS_ID]) default=[settings.SITH_GROUP_OLD_SUBSCRIBERS_ID])
view_groups = models.ManyToManyField(Group, related_name="viewable_forums", blank=True, view_groups = models.ManyToManyField(Group, related_name="viewable_forums", blank=True,
default=[settings.SITH_GROUP_PUBLIC_ID]) default=[settings.SITH_GROUP_PUBLIC_ID])
number = models.IntegerField(_("number to choose a specific forum ordering"), default=1) number = models.IntegerField(_("number to choose a specific forum ordering"), default=1)
_last_message = models.ForeignKey('ForumMessage', related_name="forums_where_its_last", _last_message = models.ForeignKey('ForumMessage', related_name="forums_where_its_last",
verbose_name=_("the last message"), null=True, on_delete=models.SET_NULL) verbose_name=_("the last message"), null=True, on_delete=models.SET_NULL)
_topic_number = models.IntegerField(_("number of topics"), default=0) _topic_number = models.IntegerField(_("number of topics"), default=0)
class Meta: class Meta:
@ -112,16 +111,18 @@ class Forum(models.Model):
self.view_groups = self.parent.view_groups.all() self.view_groups = self.parent.view_groups.all()
self.save() self.save()
_club_memberships = {} # This cache is particularly efficient: _club_memberships = {} # This cache is particularly efficient:
# divided by 3 the number of requests on the main forum page # divided by 3 the number of requests on the main forum page
# after the first load # after the first load
def is_owned_by(self, user): def is_owned_by(self, user):
if user.is_in_group(settings.SITH_GROUP_FORUM_ADMIN_ID): if user.is_in_group(settings.SITH_GROUP_FORUM_ADMIN_ID):
return True return True
try: m = Forum._club_memberships[self.id][user.id] try:
m = Forum._club_memberships[self.id][user.id]
except: except:
m = self.owner_club.get_membership_for(user) m = self.owner_club.get_membership_for(user)
try: Forum._club_memberships[self.id][user.id] = m try:
Forum._club_memberships[self.id][user.id] = m
except: except:
Forum._club_memberships[self.id] = {} Forum._club_memberships[self.id] = {}
Forum._club_memberships[self.id][user.id] = m Forum._club_memberships[self.id][user.id] = m
@ -178,12 +179,13 @@ class Forum(models.Model):
l += c.get_children_list() l += c.get_children_list()
return l return l
class ForumTopic(models.Model): class ForumTopic(models.Model):
forum = models.ForeignKey(Forum, related_name='topics') forum = models.ForeignKey(Forum, related_name='topics')
author = models.ForeignKey(User, related_name='forum_topics') author = models.ForeignKey(User, related_name='forum_topics')
description = models.CharField(_('description'), max_length=256, default="") description = models.CharField(_('description'), max_length=256, default="")
_last_message = models.ForeignKey('ForumMessage', related_name="+", verbose_name=_("the last message"), _last_message = models.ForeignKey('ForumMessage', related_name="+", verbose_name=_("the last message"),
null=True, on_delete=models.SET_NULL) null=True, on_delete=models.SET_NULL)
_title = models.CharField(_('title'), max_length=64, blank=True) _title = models.CharField(_('title'), max_length=64, blank=True)
_message_number = models.IntegerField(_("number of messages"), default=0) _message_number = models.IntegerField(_("number of messages"), default=0)
@ -192,7 +194,7 @@ class ForumTopic(models.Model):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
super(ForumTopic, self).save(*args, **kwargs) super(ForumTopic, self).save(*args, **kwargs)
self.forum.set_topic_number() # Recompute the cached value self.forum.set_topic_number() # Recompute the cached value
self.forum.set_last_message() self.forum.set_last_message()
def is_owned_by(self, user): def is_owned_by(self, user):
@ -225,6 +227,7 @@ class ForumTopic(models.Model):
def title(self): def title(self):
return self._title return self._title
class ForumMessage(models.Model): class ForumMessage(models.Model):
""" """
"A ForumMessage object represents a message in the forum" -- Cpt. Obvious "A ForumMessage object represents a message in the forum" -- Cpt. Obvious
@ -244,7 +247,7 @@ class ForumMessage(models.Model):
return "%s (%s) - %s" % (self.id, self.author, self.title) return "%s (%s) - %s" % (self.id, self.author, self.title)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self._deleted = self.is_deleted() # Recompute the cached value self._deleted = self.is_deleted() # Recompute the cached value
super(ForumMessage, self).save(*args, **kwargs) super(ForumMessage, self).save(*args, **kwargs)
if self.is_last_in_topic(): if self.is_last_in_topic():
self.topic._last_message_id = self.id self.topic._last_message_id = self.id
@ -259,15 +262,15 @@ class ForumMessage(models.Model):
def is_last_in_topic(self): def is_last_in_topic(self):
return bool(self.id == self.topic.messages.order_by('date').last().id) return bool(self.id == self.topic.messages.order_by('date').last().id)
def is_owned_by(self, user): # Anyone can create a topic: it's better to def is_owned_by(self, user): # Anyone can create a topic: it's better to
# check the rights at the forum level, since it's more controlled # check the rights at the forum level, since it's more controlled
return self.topic.forum.is_owned_by(user) or user.id == self.author.id return self.topic.forum.is_owned_by(user) or user.id == self.author.id
def can_be_edited_by(self, user): def can_be_edited_by(self, user):
return user.can_edit(self.topic.forum) return user.can_edit(self.topic.forum)
def can_be_viewed_by(self, user): def can_be_viewed_by(self, user):
return not self._deleted # No need to check the real rights since it's already done by the Topic view return not self._deleted # No need to check the real rights since it's already done by the Topic view
def can_be_moderated_by(self, user): def can_be_moderated_by(self, user):
return self.topic.forum.is_owned_by(user) or user.id == self.author.id return self.topic.forum.is_owned_by(user) or user.id == self.author.id
@ -282,10 +285,11 @@ class ForumMessage(models.Model):
return int(self.topic.messages.filter(id__lt=self.id).count() / settings.SITH_FORUM_PAGE_LENGTH) + 1 return int(self.topic.messages.filter(id__lt=self.id).count() / settings.SITH_FORUM_PAGE_LENGTH) + 1
def mark_as_read(self, user): def mark_as_read(self, user):
try: # Need the try/except because of AnonymousUser try: # Need the try/except because of AnonymousUser
if not self.is_read(user): if not self.is_read(user):
self.readers.add(user) self.readers.add(user)
except: pass except:
pass
def is_read(self, user): def is_read(self, user):
return (self.date < user.forum_infos.last_read_date) or (user in self.readers.all()) return (self.date < user.forum_infos.last_read_date) or (user in self.readers.all())
@ -296,11 +300,13 @@ class ForumMessage(models.Model):
return meta.action == "DELETE" return meta.action == "DELETE"
return False return False
MESSAGE_META_ACTIONS = [ MESSAGE_META_ACTIONS = [
('EDIT', _("Message edited by")), ('EDIT', _("Message edited by")),
('DELETE', _("Message deleted by")), ('DELETE', _("Message deleted by")),
('UNDELETE', _("Message undeleted by")), ('UNDELETE', _("Message undeleted by")),
] ]
class ForumMessageMeta(models.Model): class ForumMessageMeta(models.Model):
user = models.ForeignKey(User, related_name="forum_message_metas") user = models.ForeignKey(User, related_name="forum_message_metas")
@ -322,8 +328,7 @@ class ForumUserInfo(models.Model):
""" """
user = models.OneToOneField(User, related_name="_forum_infos") user = models.OneToOneField(User, related_name="_forum_infos")
last_read_date = models.DateTimeField(_('last read date'), default=datetime(year=settings.SITH_SCHOOL_START_YEAR, last_read_date = models.DateTimeField(_('last read date'), default=datetime(year=settings.SITH_SCHOOL_START_YEAR,
month=1, day=1, tzinfo=pytz.UTC)) month=1, day=1, tzinfo=pytz.UTC))
def __str__(self): def __str__(self):
return str(self.user) return str(self.user)

View File

@ -22,7 +22,7 @@
# #
# #
from django.conf.urls import url, include from django.conf.urls import url
from forum.views import * from forum.views import *

View File

@ -22,30 +22,30 @@
# #
# #
from django.shortcuts import render, get_object_or_404 from django.shortcuts import get_object_or_404
from django.views.generic import ListView, DetailView, RedirectView from django.views.generic import ListView, DetailView, RedirectView
from django.views.generic.edit import UpdateView, CreateView, DeleteView from django.views.generic.edit import UpdateView, CreateView, DeleteView
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.core.urlresolvers import reverse, reverse_lazy from django.core.urlresolvers import reverse_lazy
from django.utils import timezone from django.utils import timezone
from django.conf import settings from django.conf import settings
from django import forms from django import forms
from django.db import models
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from ajax_select import make_ajax_form, make_ajax_field from ajax_select import make_ajax_field
from core.views import CanViewMixin, CanEditMixin, CanEditPropMixin, CanCreateMixin, TabedViewMixin from core.views import CanViewMixin, CanEditMixin, CanEditPropMixin, CanCreateMixin
from core.views.forms import MarkdownInput from core.views.forms import MarkdownInput
from core.models import Page
from forum.models import Forum, ForumMessage, ForumTopic, ForumMessageMeta from forum.models import Forum, ForumMessage, ForumTopic, ForumMessageMeta
class ForumMainView(ListView): class ForumMainView(ListView):
queryset = Forum.objects.filter(parent=None).prefetch_related("children___last_message__author", "children___last_message__topic") queryset = Forum.objects.filter(parent=None).prefetch_related("children___last_message__author", "children___last_message__topic")
template_name = "forum/main.jinja" template_name = "forum/main.jinja"
class ForumMarkAllAsRead(RedirectView): class ForumMarkAllAsRead(RedirectView):
permanent = False permanent = False
url = reverse_lazy('forum:last_unread') url = reverse_lazy('forum:last_unread')
@ -56,10 +56,12 @@ class ForumMarkAllAsRead(RedirectView):
fi.last_read_date = timezone.now() fi.last_read_date = timezone.now()
fi.save() fi.save()
for m in request.user.read_messages.filter(date__lt=fi.last_read_date): 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 m.readers.remove(request.user) # Clean up to keep table low in data
except: pass except:
pass
return super(ForumMarkAllAsRead, self).get(request, *args, **kwargs) return super(ForumMarkAllAsRead, self).get(request, *args, **kwargs)
class ForumLastUnread(ListView): class ForumLastUnread(ListView):
model = ForumTopic model = ForumTopic
template_name = "forum/last_unread.jinja" template_name = "forum/last_unread.jinja"
@ -67,12 +69,13 @@ class ForumLastUnread(ListView):
def get_queryset(self): def get_queryset(self):
topic_list = self.model.objects.filter(_last_message__date__gt=self.request.user.forum_infos.last_read_date)\ 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)\ .exclude(_last_message__readers=self.request.user)\
.order_by('-_last_message__date')\ .order_by('-_last_message__date')\
.select_related('_last_message__author', 'author')\ .select_related('_last_message__author', 'author')\
.prefetch_related('forum__edit_groups') .prefetch_related('forum__edit_groups')
return topic_list return topic_list
class ForumForm(forms.ModelForm): class ForumForm(forms.ModelForm):
class Meta: class Meta:
model = Forum model = Forum
@ -80,6 +83,7 @@ class ForumForm(forms.ModelForm):
edit_groups = make_ajax_field(Forum, 'edit_groups', 'groups', help_text="") edit_groups = make_ajax_field(Forum, 'edit_groups', 'groups', help_text="")
view_groups = make_ajax_field(Forum, 'view_groups', 'groups', help_text="") view_groups = make_ajax_field(Forum, 'view_groups', 'groups', help_text="")
class ForumCreateView(CanCreateMixin, CreateView): class ForumCreateView(CanCreateMixin, CreateView):
model = Forum model = Forum
form_class = ForumForm form_class = ForumForm
@ -93,12 +97,15 @@ class ForumCreateView(CanCreateMixin, CreateView):
init['owner_club'] = parent.owner_club init['owner_club'] = parent.owner_club
init['edit_groups'] = parent.edit_groups.all() init['edit_groups'] = parent.edit_groups.all()
init['view_groups'] = parent.view_groups.all() init['view_groups'] = parent.view_groups.all()
except: pass except:
pass
return init return init
class ForumEditForm(ForumForm): class ForumEditForm(ForumForm):
recursive = forms.BooleanField(label=_("Apply rights and club owner recursively"), required=False) recursive = forms.BooleanField(label=_("Apply rights and club owner recursively"), required=False)
class ForumEditView(CanEditPropMixin, UpdateView): class ForumEditView(CanEditPropMixin, UpdateView):
model = Forum model = Forum
pk_url_kwarg = "forum_id" pk_url_kwarg = "forum_id"
@ -112,12 +119,14 @@ class ForumEditView(CanEditPropMixin, UpdateView):
self.object.apply_rights_recursively() self.object.apply_rights_recursively()
return ret return ret
class ForumDeleteView(CanEditPropMixin, DeleteView): class ForumDeleteView(CanEditPropMixin, DeleteView):
model = Forum model = Forum
pk_url_kwarg = "forum_id" pk_url_kwarg = "forum_id"
template_name = "core/delete_confirm.jinja" template_name = "core/delete_confirm.jinja"
success_url = reverse_lazy('forum:main') success_url = reverse_lazy('forum:main')
class ForumDetailView(CanViewMixin, DetailView): class ForumDetailView(CanViewMixin, DetailView):
model = Forum model = Forum
template_name = "forum/forum.jinja" template_name = "forum/forum.jinja"
@ -126,10 +135,10 @@ class ForumDetailView(CanViewMixin, DetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs = super(ForumDetailView, self).get_context_data(**kwargs) kwargs = super(ForumDetailView, self).get_context_data(**kwargs)
qs = self.object.topics.order_by('-_last_message__date')\ qs = self.object.topics.order_by('-_last_message__date')\
.select_related('_last_message__author', 'author')\ .select_related('_last_message__author', 'author')\
.prefetch_related("forum__edit_groups") .prefetch_related("forum__edit_groups")
paginator = Paginator(qs, paginator = Paginator(qs,
settings.SITH_FORUM_PAGE_LENGTH) settings.SITH_FORUM_PAGE_LENGTH)
page = self.request.GET.get('topic_page') page = self.request.GET.get('topic_page')
try: try:
kwargs["topics"] = paginator.page(page) kwargs["topics"] = paginator.page(page)
@ -139,15 +148,17 @@ class ForumDetailView(CanViewMixin, DetailView):
kwargs["topics"] = paginator.page(paginator.num_pages) kwargs["topics"] = paginator.page(paginator.num_pages)
return kwargs return kwargs
class TopicForm(forms.ModelForm): class TopicForm(forms.ModelForm):
class Meta: class Meta:
model = ForumMessage model = ForumMessage
fields = ['title', 'message'] fields = ['title', 'message']
widgets = { widgets = {
'message': MarkdownInput, 'message': MarkdownInput,
} }
title = forms.CharField(required=True, label=_("Title")) title = forms.CharField(required=True, label=_("Title"))
class ForumTopicCreateView(CanCreateMixin, CreateView): class ForumTopicCreateView(CanCreateMixin, CreateView):
model = ForumMessage model = ForumMessage
form_class = TopicForm form_class = TopicForm
@ -166,12 +177,14 @@ class ForumTopicCreateView(CanCreateMixin, CreateView):
form.instance.author = self.request.user form.instance.author = self.request.user
return super(ForumTopicCreateView, self).form_valid(form) return super(ForumTopicCreateView, self).form_valid(form)
class ForumTopicEditView(CanEditMixin, UpdateView): class ForumTopicEditView(CanEditMixin, UpdateView):
model = ForumTopic model = ForumTopic
fields = ['forum'] fields = ['forum']
pk_url_kwarg = "topic_id" pk_url_kwarg = "topic_id"
template_name = "core/edit.jinja" template_name = "core/edit.jinja"
class ForumTopicDetailView(CanViewMixin, DetailView): class ForumTopicDetailView(CanViewMixin, DetailView):
model = ForumTopic model = ForumTopic
pk_url_kwarg = "topic_id" pk_url_kwarg = "topic_id"
@ -186,9 +199,9 @@ class ForumTopicDetailView(CanViewMixin, DetailView):
kwargs['first_unread_message_id'] = msg.id kwargs['first_unread_message_id'] = msg.id
except: except:
kwargs['first_unread_message_id'] = float("inf") kwargs['first_unread_message_id'] = float("inf")
paginator = Paginator(self.object.messages.select_related('author__avatar_pict')\ paginator = Paginator(self.object.messages.select_related('author__avatar_pict')
.prefetch_related('topic__forum__edit_groups', 'readers').order_by('date'), .prefetch_related('topic__forum__edit_groups', 'readers').order_by('date'),
settings.SITH_FORUM_PAGE_LENGTH) settings.SITH_FORUM_PAGE_LENGTH)
page = self.request.GET.get('page') page = self.request.GET.get('page')
try: try:
kwargs["msgs"] = paginator.page(page) kwargs["msgs"] = paginator.page(page)
@ -198,6 +211,7 @@ class ForumTopicDetailView(CanViewMixin, DetailView):
kwargs["msgs"] = paginator.page(paginator.num_pages) kwargs["msgs"] = paginator.page(paginator.num_pages)
return kwargs return kwargs
class ForumMessageView(SingleObjectMixin, RedirectView): class ForumMessageView(SingleObjectMixin, RedirectView):
model = ForumMessage model = ForumMessage
pk_url_kwarg = "message_id" pk_url_kwarg = "message_id"
@ -207,9 +221,10 @@ class ForumMessageView(SingleObjectMixin, RedirectView):
self.object = self.get_object() self.object = self.get_object()
return self.object.get_url() return self.object.get_url()
class ForumMessageEditView(CanEditMixin, UpdateView): class ForumMessageEditView(CanEditMixin, UpdateView):
model = ForumMessage model = ForumMessage
form_class = forms.modelform_factory(model=ForumMessage, fields=['title', 'message',], widgets={'message': MarkdownInput}) form_class = forms.modelform_factory(model=ForumMessage, fields=['title', 'message', ], widgets={'message': MarkdownInput})
template_name = "forum/reply.jinja" template_name = "forum/reply.jinja"
pk_url_kwarg = "message_id" pk_url_kwarg = "message_id"
@ -222,6 +237,7 @@ class ForumMessageEditView(CanEditMixin, UpdateView):
kwargs['topic'] = self.object.topic kwargs['topic'] = self.object.topic
return kwargs return kwargs
class ForumMessageDeleteView(SingleObjectMixin, RedirectView): class ForumMessageDeleteView(SingleObjectMixin, RedirectView):
model = ForumMessage model = ForumMessage
pk_url_kwarg = "message_id" pk_url_kwarg = "message_id"
@ -233,6 +249,7 @@ class ForumMessageDeleteView(SingleObjectMixin, RedirectView):
ForumMessageMeta(message=self.object, user=self.request.user, action="DELETE").save() ForumMessageMeta(message=self.object, user=self.request.user, action="DELETE").save()
return self.object.get_absolute_url() return self.object.get_absolute_url()
class ForumMessageUndeleteView(SingleObjectMixin, RedirectView): class ForumMessageUndeleteView(SingleObjectMixin, RedirectView):
model = ForumMessage model = ForumMessage
pk_url_kwarg = "message_id" pk_url_kwarg = "message_id"
@ -244,9 +261,10 @@ class ForumMessageUndeleteView(SingleObjectMixin, RedirectView):
ForumMessageMeta(message=self.object, user=self.request.user, action="UNDELETE").save() ForumMessageMeta(message=self.object, user=self.request.user, action="UNDELETE").save()
return self.object.get_absolute_url() return self.object.get_absolute_url()
class ForumMessageCreateView(CanCreateMixin, CreateView): class ForumMessageCreateView(CanCreateMixin, CreateView):
model = ForumMessage model = ForumMessage
form_class = forms.modelform_factory(model=ForumMessage, fields=['title', 'message',], widgets={'message': MarkdownInput}) form_class = forms.modelform_factory(model=ForumMessage, fields=['title', 'message', ], widgets={'message': MarkdownInput})
template_name = "forum/reply.jinja" template_name = "forum/reply.jinja"
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
@ -262,7 +280,7 @@ class ForumMessageCreateView(CanCreateMixin, CreateView):
init['message'] = "> ##### %s\n" % (_("%(author)s said") % {'author': message.author.get_short_name()}) init['message'] = "> ##### %s\n" % (_("%(author)s said") % {'author': message.author.get_short_name()})
init['message'] += "\n".join([ init['message'] += "\n".join([
"> " + line for line in message.message.split('\n') "> " + line for line in message.message.split('\n')
]) ])
init['message'] += "\n\n" init['message'] += "\n\n"
except Exception as e: except Exception as e:
print(repr(e)) print(repr(e))
@ -277,4 +295,3 @@ class ForumMessageCreateView(CanCreateMixin, CreateView):
kwargs = super(ForumMessageCreateView, self).get_context_data(**kwargs) kwargs = super(ForumMessageCreateView, self).get_context_data(**kwargs)
kwargs['topic'] = self.topic kwargs['topic'] = self.topic
return kwargs return kwargs