models.py 12.5 KB
Newer Older
1
2
# -*- coding:utf-8 -*
#
Skia's avatar
Skia committed
3
# Copyright 2016,2017,2018
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# - 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
from django.db import models
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse
from django.utils import timezone
Skia's avatar
Skia committed
31
from django.utils.functional import cached_property
Skia's avatar
Skia committed
32

Skia's avatar
Skia committed
33
from datetime import datetime
Sli's avatar
Sli committed
34
from itertools import chain
Skia's avatar
Skia committed
35
36
import pytz

Krophil's avatar
Krophil committed
37
from core.models import User, Group
38
from club.models import Club
Skia's avatar
Skia committed
39

Krophil's avatar
Krophil committed
40

Skia's avatar
Skia committed
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
    owner_club = models.ForeignKey(Club, related_name="owned_forums", verbose_name=_("owner club"),
Krophil's avatar
Krophil committed
55
                                   default=settings.SITH_MAIN_CLUB_ID)
56
    edit_groups = models.ManyToManyField(Group, related_name="editable_forums", blank=True,
Krophil's avatar
Krophil committed
57
                                         default=[settings.SITH_GROUP_OLD_SUBSCRIBERS_ID])
58
    view_groups = models.ManyToManyField(Group, related_name="viewable_forums", blank=True,
Krophil's avatar
Krophil committed
59
                                         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
    _last_message = models.ForeignKey('ForumMessage', related_name="forums_where_its_last",
Krophil's avatar
Krophil committed
62
                                      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()

Krophil's avatar
Krophil committed
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
Krophil's avatar
Krophil committed
121
122
        try:
            m = Forum._club_memberships[self.id][user.id]
123
124
        except:
            m = self.owner_club.get_membership_for(user)
Krophil's avatar
Krophil committed
125
126
            try:
                Forum._club_memberships[self.id][user.id] = m
127
128
129
            except:
                Forum._club_memberships[self.id] = {}
                Forum._club_memberships[self.id][user.id] = m
130
131
132
        if m:
            return m.role > settings.SITH_MAXIMUM_FREE_ROLE
        return False
Skia's avatar
Skia committed
133
134
135
136
137
138
139
140
141
142
143
144
145
146

    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)

Sli's avatar
Sli committed
147
    def get_full_name(self):
Sli's avatar
Sli committed
148
        return '/'.join(chain.from_iterable([[parent.name for parent in self.get_parent_list()], [self.name]]))
Sli's avatar
Sli committed
149

Skia's avatar
Skia committed
150
151
152
    def get_absolute_url(self):
        return reverse('forum:view_forum', kwargs={'forum_id': self.id})

Skia's avatar
Skia committed
153
154
155
156
    @cached_property
    def parent_list(self):
        return self.get_parent_list()

Skia's avatar
Skia committed
157
158
159
160
161
162
163
164
    def get_parent_list(self):
        l = []
        p = self.parent
        while p is not None:
            l.append(p)
            p = p.parent
        return l

165
    @property
Skia's avatar
Skia committed
166
    def topic_number(self):
167
        return self._topic_number
Skia's avatar
Skia committed
168

Skia's avatar
Skia committed
169
170
171
    def get_topic_number(self):
        number = self.topics.all().count()
        for c in self.children.all():
Skia's avatar
Skia committed
172
            number += c.topic_number
Skia's avatar
Skia committed
173
174
        return number

Skia's avatar
Skia committed
175
176
    @cached_property
    def last_message(self):
177
178
179
180
181
182
183
184
        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
185

Krophil's avatar
Krophil committed
186

Skia's avatar
Skia committed
187
188
189
190
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="")
Skia's avatar
Skia committed
191
    subscribed_users = models.ManyToManyField(User, related_name='favorite_topics', verbose_name=_("subscribed users"))
192
    _last_message = models.ForeignKey('ForumMessage', related_name="+", verbose_name=_("the last message"),
Krophil's avatar
Krophil committed
193
                                      null=True, on_delete=models.SET_NULL)
194
    _title = models.CharField(_('title'), max_length=64, blank=True)
195
    _message_number = models.IntegerField(_("number of messages"), default=0)
Skia's avatar
Skia committed
196
197

    class Meta:
198
199
200
201
        ordering = ['-_last_message__date']

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

205
    def is_owned_by(self, user):
206
        return self.forum.is_owned_by(user)
207
208
209
210
211
212
213

    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
214
215
    def __str__(self):
        return "%s" % (self.title)
Skia's avatar
Skia committed
216
217
218
219

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

Skia's avatar
Skia committed
220
221
222
223
224
225
226
    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

227
228
    @cached_property
    def last_message(self):
229
        return self._last_message
230

231
    @cached_property
232
    def title(self):
233
        return self._title
234

Krophil's avatar
Krophil committed
235

Skia's avatar
Skia committed
236
237
class ForumMessage(models.Model):
    """
238
    "A ForumMessage object represents a message in the forum" -- Cpt. Obvious
Skia's avatar
Skia committed
239
240
241
242
243
244
    """
    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
245
    readers = models.ManyToManyField(User, related_name="read_messages", verbose_name=_("readers"))
246
    _deleted = models.BooleanField(_('is deleted'), default=False)
Skia's avatar
Skia committed
247
248

    class Meta:
249
        ordering = ['-date']
Skia's avatar
Skia committed
250

Skia's avatar
Skia committed
251
    def __str__(self):
252
253
254
        return "%s (%s) - %s" % (self.id, self.author, self.title)

    def save(self, *args, **kwargs):
Krophil's avatar
Krophil committed
255
        self._deleted = self.is_deleted()  # Recompute the cached value
256
257
258
        super(ForumMessage, self).save(*args, **kwargs)
        if self.is_last_in_topic():
            self.topic._last_message_id = self.id
Skia's avatar
Skia committed
259
        if self.is_first_in_topic() and self.title:
260
            self.topic._title = self.title
261
262
        self.topic._message_number = self.topic.messages.count()
        self.topic.save()
263
264
265
266
267
268

    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
269

Krophil's avatar
Krophil committed
270
271
    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
Skia's avatar
Skia committed
272
        return self.topic.forum.is_owned_by(user) or user.id == self.author.id
273
274

    def can_be_edited_by(self, user):
Skia's avatar
Skia committed
275
        return user.can_edit(self.topic.forum)
276
277

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

Skia's avatar
Skia committed
280
    def can_be_moderated_by(self, user):
281
        return self.topic.forum.is_owned_by(user) or user.id == self.author.id
Skia's avatar
Skia committed
282

Skia's avatar
Skia committed
283
    def get_absolute_url(self):
284
285
286
        return reverse('forum:view_message', kwargs={'message_id': self.id})

    def get_url(self):
Skia's avatar
Skia committed
287
288
289
290
        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
291
292

    def mark_as_read(self, user):
Krophil's avatar
Krophil committed
293
        try:  # Need the try/except because of AnonymousUser
294
295
            if not self.is_read(user):
                self.readers.add(user)
Krophil's avatar
Krophil committed
296
297
        except:
            pass
Skia's avatar
Skia committed
298

Skia's avatar
Skia committed
299
300
301
    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
302
303
304
305
306
307
    def is_deleted(self):
        meta = self.metas.exclude(action="EDIT").order_by('-date').first()
        if meta:
            return meta.action == "DELETE"
        return False

Krophil's avatar
Krophil committed
308

Skia's avatar
Skia committed
309
MESSAGE_META_ACTIONS = [
Krophil's avatar
Krophil committed
310
311
312
313
314
    ('EDIT', _("Message edited by")),
    ('DELETE', _("Message deleted by")),
    ('UNDELETE', _("Message undeleted by")),
]

Skia's avatar
Skia committed
315
316
317
318
319
320
321

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)

322
323
324
325
326
327
    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
328
class ForumUserInfo(models.Model):
Skia's avatar
Skia committed
329
330
331
332
333
    """
    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...
    """
334
    user = models.OneToOneField(User, related_name="_forum_infos")
Skia's avatar
Skia committed
335
    last_read_date = models.DateTimeField(_('last read date'), default=datetime(year=settings.SITH_SCHOOL_START_YEAR,
Krophil's avatar
Krophil committed
336
                                          month=1, day=1, tzinfo=pytz.UTC))
Skia's avatar
Skia committed
337

338
339
    def __str__(self):
        return str(self.user)