models.py 12.1 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
# -*- coding:utf-8 -*
#
# Copyright 2016,2017
# - Skia <skia@libskia.so>
#
# 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.
#
#

Skia's avatar
Skia committed
25 26 27 28 29 30 31 32
from django.db import models
from django.core import validators
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ValidationError
from django.db import IntegrityError, transaction
from django.core.urlresolvers import reverse
from django.utils import timezone
Skia's avatar
Skia committed
33
from django.utils.functional import cached_property
Skia's avatar
Skia committed
34

Skia's avatar
Skia committed
35 36 37
from datetime import datetime
import pytz

Skia's avatar
Skia committed
38
from core.models import User, MetaGroup, Group, SithFile
39
from club.models import Club
Skia's avatar
Skia committed
40 41 42 43

class Forum(models.Model):
    """
    The Forum class, made as a tree to allow nice tidy organization
Skia's avatar
Skia committed
44 45 46 47

    owner_club allows club members to moderate there own topics
    edit_groups allows to put any group as a forum admin
    view_groups allows some groups to view a forum
Skia's avatar
Skia committed
48
    """
49
    id = models.AutoField(primary_key=True, db_index=True)
Skia's avatar
Skia committed
50
    name = models.CharField(_('name'), max_length=64)
51
    description = models.CharField(_('description'), max_length=512, default="")
Skia's avatar
Skia committed
52 53
    is_category = models.BooleanField(_('is a category'), default=False)
    parent = models.ForeignKey('Forum', related_name='children', null=True, blank=True)
54 55
    owner_club = models.ForeignKey(Club, related_name="owned_forums", verbose_name=_("owner club"),
            default=settings.SITH_MAIN_CLUB_ID)
56 57 58 59
    edit_groups = models.ManyToManyField(Group, related_name="editable_forums", blank=True,
            default=[settings.SITH_GROUP_OLD_SUBSCRIBERS_ID])
    view_groups = models.ManyToManyField(Group, related_name="viewable_forums", blank=True,
            default=[settings.SITH_GROUP_PUBLIC_ID])
Skia's avatar
Skia committed
60
    number = models.IntegerField(_("number to choose a specific forum ordering"), default=1)
61 62
    _last_message = models.ForeignKey('ForumMessage', related_name="forums_where_its_last",
            verbose_name=_("the last message"), null=True, on_delete=models.SET_NULL)
63
    _topic_number = models.IntegerField(_("number of topics"), default=0)
Skia's avatar
Skia committed
64 65 66

    class Meta:
        ordering = ['number']
67

68 69 70 71 72 73 74 75 76 77 78
    def clean(self):
        self.check_loop()

    def save(self, *args, **kwargs):
        copy_rights = False
        if self.id is None:
            copy_rights = True
        super(Forum, self).save(*args, **kwargs)
        if copy_rights:
            self.copy_rights()

79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
    def set_topic_number(self):
        self._topic_number = self.get_topic_number()
        self.save()
        if self.parent:
            self.parent.set_topic_number()

    def set_last_message(self):
        topic = ForumTopic.objects.filter(forum__id=self.id).exclude(_last_message=None).order_by('-_last_message__id').first()
        forum = Forum.objects.filter(parent__id=self.id).exclude(_last_message=None).order_by('-_last_message__id').first()
        if topic and forum:
            if topic._last_message_id < forum._last_message_id:
                self._last_message_id = forum._last_message_id
            else:
                self._last_message_id = topic._last_message_id
        elif topic:
            self._last_message_id = topic._last_message_id
        elif forum:
            self._last_message_id = forum._last_message_id
        self.save()
        if self.parent:
            self.parent.set_last_message()

101 102 103 104 105 106 107 108 109 110 111 112 113 114
    def apply_rights_recursively(self):
        children = self.children.all()
        for c in children:
            c.copy_rights()
            c.apply_rights_recursively()

    def copy_rights(self):
        """Copy, if possible, the rights of the parent folder"""
        if self.parent is not None:
            self.owner_club = self.parent.owner_club
            self.edit_groups = self.parent.edit_groups.all()
            self.view_groups = self.parent.view_groups.all()
            self.save()

115 116 117
    _club_memberships = {} # This cache is particularly efficient:
                           # divided by 3 the number of requests on the main forum page
                           # after the first load
118
    def is_owned_by(self, user):
119 120
        if user.is_in_group(settings.SITH_GROUP_FORUM_ADMIN_ID):
            return True
121 122 123 124 125 126 127
        try: m = Forum._club_memberships[self.id][user.id]
        except:
            m = self.owner_club.get_membership_for(user)
            try: Forum._club_memberships[self.id][user.id] = m
            except:
                Forum._club_memberships[self.id] = {}
                Forum._club_memberships[self.id][user.id] = m
128 129 130
        if m:
            return m.role > settings.SITH_MAXIMUM_FREE_ROLE
        return False
Skia's avatar
Skia committed
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147

    def check_loop(self):
        """Raise a validation error when a loop is found within the parent list"""
        objs = []
        cur = self
        while cur.parent is not None:
            if cur in objs:
                raise ValidationError(_('You can not make loops in forums'))
            objs.append(cur)
            cur = cur.parent

    def __str__(self):
        return "%s" % (self.name)

    def get_absolute_url(self):
        return reverse('forum:view_forum', kwargs={'forum_id': self.id})

Skia's avatar
Skia committed
148 149 150 151
    @cached_property
    def parent_list(self):
        return self.get_parent_list()

Skia's avatar
Skia committed
152 153 154 155 156 157 158 159
    def get_parent_list(self):
        l = []
        p = self.parent
        while p is not None:
            l.append(p)
            p = p.parent
        return l

160
    @property
Skia's avatar
Skia committed
161
    def topic_number(self):
162
        return self._topic_number
Skia's avatar
Skia committed
163

Skia's avatar
Skia committed
164 165 166
    def get_topic_number(self):
        number = self.topics.all().count()
        for c in self.children.all():
Skia's avatar
Skia committed
167
            number += c.topic_number
Skia's avatar
Skia committed
168 169
        return number

Skia's avatar
Skia committed
170 171
    @cached_property
    def last_message(self):
172 173 174 175 176 177 178 179
        return self._last_message

    def get_children_list(self):
        l = [self.id]
        for c in self.children.all():
            l.append(c.id)
            l += c.get_children_list()
        return l
Skia's avatar
Skia committed
180

Skia's avatar
Skia committed
181 182 183 184
class ForumTopic(models.Model):
    forum = models.ForeignKey(Forum, related_name='topics')
    author = models.ForeignKey(User, related_name='forum_topics')
    description = models.CharField(_('description'), max_length=256, default="")
185 186
    _last_message = models.ForeignKey('ForumMessage', related_name="+", verbose_name=_("the last message"),
            null=True, on_delete=models.SET_NULL)
187
    _title = models.CharField(_('title'), max_length=64, blank=True)
188
    _message_number = models.IntegerField(_("number of messages"), default=0)
Skia's avatar
Skia committed
189 190

    class Meta:
191 192 193 194 195 196
        ordering = ['-_last_message__date']

    def save(self, *args, **kwargs):
        super(ForumTopic, self).save(*args, **kwargs)
        self.forum.set_topic_number() # Recompute the cached value
        self.forum.set_last_message()
Skia's avatar
Skia committed
197

198
    def is_owned_by(self, user):
199
        return self.forum.is_owned_by(user)
200 201 202 203 204 205 206

    def can_be_edited_by(self, user):
        return user.can_edit(self.forum)

    def can_be_viewed_by(self, user):
        return user.can_view(self.forum)

Skia's avatar
Skia committed
207 208
    def __str__(self):
        return "%s" % (self.title)
Skia's avatar
Skia committed
209 210 211 212

    def get_absolute_url(self):
        return reverse('forum:view_topic', kwargs={'topic_id': self.id})

Skia's avatar
Skia committed
213 214 215 216 217 218 219
    def get_first_unread_message(self, user):
        try:
            msg = self.messages.exclude(readers=user).filter(date__gte=user.forum_infos.last_read_date).order_by('id').first()
            return msg
        except:
            return None

220 221
    @cached_property
    def last_message(self):
222
        return self._last_message
223

224
    @cached_property
225
    def title(self):
226
        return self._title
227

Skia's avatar
Skia committed
228 229
class ForumMessage(models.Model):
    """
230
    "A ForumMessage object represents a message in the forum" -- Cpt. Obvious
Skia's avatar
Skia committed
231 232 233 234 235 236
    """
    topic = models.ForeignKey(ForumTopic, related_name='messages')
    author = models.ForeignKey(User, related_name='forum_messages')
    title = models.CharField(_("title"), default="", max_length=64, blank=True)
    message = models.TextField(_("message"), default="")
    date = models.DateTimeField(_('date'), default=timezone.now)
Skia's avatar
Skia committed
237
    readers = models.ManyToManyField(User, related_name="read_messages", verbose_name=_("readers"))
238
    _deleted = models.BooleanField(_('is deleted'), default=False)
Skia's avatar
Skia committed
239 240

    class Meta:
241
        ordering = ['-date']
Skia's avatar
Skia committed
242

Skia's avatar
Skia committed
243
    def __str__(self):
244 245 246 247 248 249 250
        return "%s (%s) - %s" % (self.id, self.author, self.title)

    def save(self, *args, **kwargs):
        self._deleted = self.is_deleted() # Recompute the cached value
        super(ForumMessage, self).save(*args, **kwargs)
        if self.is_last_in_topic():
            self.topic._last_message_id = self.id
Skia's avatar
Skia committed
251
        if self.is_first_in_topic() and self.title:
252
            self.topic._title = self.title
253 254
        self.topic._message_number = self.topic.messages.count()
        self.topic.save()
255 256 257 258 259 260

    def is_first_in_topic(self):
        return bool(self.id == self.topic.messages.order_by('date').first().id)

    def is_last_in_topic(self):
        return bool(self.id == self.topic.messages.order_by('date').last().id)
Skia's avatar
Skia committed
261

Skia's avatar
Skia committed
262 263 264
    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
        return self.topic.forum.is_owned_by(user) or user.id == self.author.id
265 266

    def can_be_edited_by(self, user):
Skia's avatar
Skia committed
267
        return user.can_edit(self.topic.forum)
268 269

    def can_be_viewed_by(self, user):
270
        return not self._deleted # No need to check the real rights since it's already done by the Topic view
271

Skia's avatar
Skia committed
272
    def can_be_moderated_by(self, user):
273
        return self.topic.forum.is_owned_by(user) or user.id == self.author.id
Skia's avatar
Skia committed
274

Skia's avatar
Skia committed
275
    def get_absolute_url(self):
276 277 278
        return reverse('forum:view_message', kwargs={'message_id': self.id})

    def get_url(self):
Skia's avatar
Skia committed
279 280 281 282
        return self.topic.get_absolute_url() + "?page=" + str(self.get_page()) + "#msg_" + str(self.id)

    def get_page(self):
        return int(self.topic.messages.filter(id__lt=self.id).count() / settings.SITH_FORUM_PAGE_LENGTH) + 1
Skia's avatar
Skia committed
283 284

    def mark_as_read(self, user):
Skia's avatar
Skia committed
285
        try: # Need the try/except because of AnonymousUser
286 287
            if not self.is_read(user):
                self.readers.add(user)
Skia's avatar
Skia committed
288
        except: pass
Skia's avatar
Skia committed
289

Skia's avatar
Skia committed
290 291 292
    def is_read(self, user):
        return (self.date < user.forum_infos.last_read_date) or (user in self.readers.all())

Skia's avatar
Skia committed
293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310
    def is_deleted(self):
        meta = self.metas.exclude(action="EDIT").order_by('-date').first()
        if meta:
            return meta.action == "DELETE"
        return False

MESSAGE_META_ACTIONS = [
        ('EDIT', _("Message edited by")),
        ('DELETE', _("Message deleted by")),
        ('UNDELETE', _("Message undeleted by")),
        ]

class ForumMessageMeta(models.Model):
    user = models.ForeignKey(User, related_name="forum_message_metas")
    message = models.ForeignKey(ForumMessage, related_name="metas")
    date = models.DateTimeField(_('date'), default=timezone.now)
    action = models.CharField(_("action"), choices=MESSAGE_META_ACTIONS, max_length=16)

311 312 313 314 315 316
    def save(self, *args, **kwargs):
        super(ForumMessageMeta, self).save(*args, **kwargs)
        self.message._deleted = self.message.is_deleted()
        self.message.save()


Skia's avatar
Skia committed
317
class ForumUserInfo(models.Model):
Skia's avatar
Skia committed
318 319 320 321 322
    """
    This currently stores only the last date a user clicked "Mark all as read".
    However, this can be extended with lot of user preferences dedicated to a
    user, such as the favourite topics, the signature, and so on...
    """
323
    user = models.OneToOneField(User, related_name="_forum_infos")
Skia's avatar
Skia committed
324 325
    last_read_date = models.DateTimeField(_('last read date'), default=datetime(year=settings.SITH_SCHOOL_START_YEAR,
        month=1, day=1, tzinfo=pytz.UTC))
Skia's avatar
Skia committed
326

327 328 329
    def __str__(self):
        return str(self.user)