class PrivateTopic(models.Model): """ Topic private, containing private posts. """ class Meta: verbose_name = u'Message privé' verbose_name_plural = u'Messages privés' title = models.CharField(u'Titre', max_length=130) subtitle = models.CharField(u'Sous-titre', max_length=200, blank=True) author = models.ForeignKey(User, verbose_name=u'Auteur', related_name='author', db_index=True) participants = models.ManyToManyField(User, verbose_name=u'Participants', related_name='participants', db_index=True) last_message = models.ForeignKey('PrivatePost', null=True, related_name='last_message', verbose_name=u'Dernier message') pubdate = models.DateTimeField(u'Date de création', auto_now_add=True, db_index=True) objects = PrivateTopicManager() def __unicode__(self): """ Human-readable representation of the PrivateTopic model. :return: PrivateTopic title :rtype: unicode """ return self.title def get_absolute_url(self): """ URL of a single PrivateTopic object. :return: PrivateTopic object URL :rtype: str """ return reverse('private-posts-list', args=[self.pk, self.slug()]) def slug(self): """ PrivateTopic doesn't have a slug attribute of a private topic. To be compatible with older private topic, the slug is always re-calculate when we need one. :return: title slugify. """ return slugify(self.title) def get_post_count(self): """ Get the number of private posts in a single PrivateTopic object. :return: number of post in PrivateTopic object :rtype: int """ return PrivatePost.objects.filter(privatetopic__pk=self.pk).count() def get_last_answer(self): """ Get the last answer in the PrivateTopic written by topic's author, if exists. :return: PrivateTopic object last answer (PrivatePost) :rtype: PrivatePost object or None """ last_post = PrivatePost.objects \ .filter(privatetopic__pk=self.pk) \ .order_by('-position_in_topic') \ .first() # If the last post is the first post, there is no answer in the topic (only initial post) if last_post == self.first_post(): return None return last_post def first_post(self): """ Get the first answer in the PrivateTopic written by topic's author, if exists. :return: PrivateTopic object first answer (PrivatePost) :rtype: PrivatePost object or None """ return PrivatePost.objects \ .filter(privatetopic=self) \ .order_by('position_in_topic') \ .first() def last_read_post(self, user=None): """ Get the last PrivatePost the user has read. :param user: The user is reading the PrivateTopic. If None, the current user is used. :type user: User object :return: last PrivatePost read :rtype: PrivatePost object or None """ # If user param is not defined, we get the current user if user is None: user = get_current_user() try: post = PrivateTopicRead.objects \ .select_related() \ .filter(privatetopic=self, user=user) if len(post) == 0: return self.first_post() return post.latest('privatepost__position_in_topic').privatepost except (PrivatePost.DoesNotExist, TypeError): return self.first_post() def first_unread_post(self, user=None): """ Get the first PrivatePost the user has unread. :param user: The user is reading the PrivateTopic. If None, the current user is used. :type user: User object :return: first PrivatePost unread :rtype: PrivatePost object or None """ # If user param is not defined, we get the current user if user is None: user = get_current_user() try: last_post = PrivateTopicRead.objects \ .select_related() \ .filter(privatetopic=self, user=user) \ .latest('privatepost__position_in_topic').privatepost next_post = PrivatePost.objects.filter( privatetopic__pk=self.pk, position_in_topic__gt=last_post.position_in_topic).first() return next_post except (PrivatePost.DoesNotExist, PrivateTopicRead.DoesNotExist): return self.first_post() def alone(self): """ Check if there just one participant in the conversation (PrivateTopic). :return: True if there just one participant in PrivateTopic :rtype: bool """ return self.participants.count() == 0 def is_unread(self, user=None): """ Check if an user has never read the current PrivateTopic. :param user: an user as Django User object. If None, the current user is used. :type user: User object :return: True if the PrivateTopic was never read :rtype: bool """ # If user param is not defined, we get the current user if user is None: user = get_current_user() return is_privatetopic_unread(self, user) def is_author(self, user): """ Check if the user given is the author of the private topic. :param user: User given. :return: true if the user is the author. """ return self.author == user def is_participant(self, user): """ Check if the user given is in participants or author of the private topic. :param user: User given. :return: true if the user is in participants """ return self.author == user or user in self.participants.all() @staticmethod def has_read_permission(request): return request.user.is_authenticated() def has_object_read_permission(self, request): return PrivateTopic.has_read_permission(request) and self.is_participant(request.user) @staticmethod def has_write_permission(request): return request.user.is_authenticated() def has_object_write_permission(self, request): return PrivateTopic.has_write_permission(request) and self.is_participant(request.user) def has_object_update_permission(self, request): return PrivateTopic.has_write_permission(request) and self.is_author(request.user)
class PrivateTopic(models.Model): """ Private topic, containing private posts. We maintain the following invariants : * all participants are reachable, * no duplicate participant. A participant is either the author or a mere participant. """ class Meta: verbose_name = "Message privé" verbose_name_plural = "Messages privés" title = models.CharField("Titre", max_length=130) subtitle = models.CharField("Sous-titre", max_length=200, blank=True) author = models.ForeignKey(User, verbose_name="Auteur", related_name="author", db_index=True, on_delete=models.SET_NULL, null=True) participants = models.ManyToManyField(User, verbose_name="Participants", related_name="participants", db_index=True) last_message = models.ForeignKey("PrivatePost", null=True, related_name="last_message", verbose_name="Dernier message", on_delete=models.SET_NULL) pubdate = models.DateTimeField("Date de création", auto_now_add=True, db_index=True) objects = PrivateTopicManager() @staticmethod def create(title, subtitle, author, recipients): limit = PrivateTopic._meta.get_field("title").max_length topic = PrivateTopic() topic.title = title[:limit] topic.subtitle = subtitle topic.pubdate = datetime.now() topic.author = author topic.save() for participant in recipients: topic.add_participant(participant, silent=True) topic.save() return topic def __str__(self): """ Human-readable representation of the PrivateTopic model. :return: PrivateTopic title :rtype: unicode """ return self.title def get_absolute_url(self): """ URL of a single PrivateTopic object. :return: PrivateTopic object URL :rtype: str """ return reverse("private-posts-list", args=[self.pk, self.slug()]) def slug(self): """ PrivateTopic doesn't have a slug attribute of a private topic. To be compatible with older private topic, the slug is always re-calculated when we need one. :return: title slugify. """ return old_slugify(self.title) def get_post_count(self): """ Get the number of private posts in a single PrivateTopic object. :return: number of posts in PrivateTopic object :rtype: int """ return PrivatePost.objects.filter(privatetopic__pk=self.pk).count() def get_last_answer(self): """ Get the last answer in the PrivateTopic written by topic's author, if exists. :return: PrivateTopic object last answer (PrivatePost) :rtype: PrivatePost object or None """ last_post = PrivatePost.objects.filter( privatetopic__pk=self.pk).order_by("-position_in_topic").first() # If the last post is the first post, there is no answer in the topic (only initial post) if last_post == self.first_post(): return None return last_post def first_post(self): """ Get the first answer in the PrivateTopic written by topic's author, if exists. :return: PrivateTopic object first answer (PrivatePost) :rtype: PrivatePost object or None """ return PrivatePost.objects.filter( privatetopic=self).order_by("position_in_topic").first() def last_read_post(self, user=None): """ Get the last PrivatePost the user has read. :param user: The user is reading the PrivateTopic. If None, the current user is used. :type user: User object :return: last PrivatePost read :rtype: PrivatePost object or None """ # If user param is not defined, we get the current user if user is None: user = get_current_user() try: post = PrivateTopicRead.objects.select_related().filter( privatetopic=self, user=user) if len(post) == 0: return self.first_post() return post.latest("privatepost__position_in_topic").privatepost except (PrivatePost.DoesNotExist, TypeError): return self.first_post() def first_unread_post(self, user=None): """ Get the first PrivatePost the user has unread. :param user: The user is reading the PrivateTopic. If None, the current user is used. :type user: User object :return: first PrivatePost unread :rtype: PrivatePost object or None """ # If user param is not defined, we get the current user if user is None: user = get_current_user() try: last_post = (PrivateTopicRead.objects.select_related().filter( privatetopic=self, user=user).latest("privatepost__position_in_topic").privatepost ) next_post = PrivatePost.objects.filter( privatetopic__pk=self.pk, position_in_topic__gt=last_post.position_in_topic).first() return next_post except (PrivatePost.DoesNotExist, PrivateTopicRead.DoesNotExist): return self.first_post() def resolve_last_read_post_absolute_url(self, user=None): """resolve the url that leads to the last post the current user has read. :return: the url :rtype: str """ if user is None: user = get_current_user() try: pk, pos = self.resolve_last_post_pk_and_pos_read_by_user(user) page_nb = 1 if pos > settings.ZDS_APP["forum"]["posts_per_page"]: page_nb += (pos - 1) // settings.ZDS_APP["forum"]["posts_per_page"] return f"{self.get_absolute_url()}?page={page_nb}#p{pk}" except PrivateTopicRead.DoesNotExist: return self.first_unread_post().get_absolute_url() def resolve_last_post_pk_and_pos_read_by_user(self, user): """Determine the primary ey of position of the last post read by a user. :param user: the current (authenticated) user. Please do not try with unauthenticated user, il would lead to a \ useless request. :return: the primary key :rtype: int """ t_read = ( PrivateTopicRead.objects.select_related("privatepost").filter( privatetopic__pk=self.pk, user__pk=user.pk).latest("privatepost__position_in_topic")) if t_read: return t_read.privatepost.pk, t_read.privatepost.position_in_topic return list( PrivatePost.objects.filter( topic__pk=self.pk).order_by("position").values( "pk", "position").first().values()) def one_participant_remaining(self): """ Check if there is only one participant remaining in the private topic. :return: True if there is only one participant remaining, False otherwise. :rtype: bool """ return self.participants.count() == 0 def is_unread(self, user=None): """ Check if a user has never read the current PrivateTopic. :param user: a user as Django User object. If None, the current user is used. :type user: User object :return: True if the PrivateTopic was never read :rtype: bool """ # If user param is not defined, we get the current user if user is None: user = get_current_user() return is_privatetopic_unread(self, user) def is_author(self, user): """ Check if a user is the author of the private topic. :param user: a given user. :return: True if the user is the author, False otherwise. """ return self.author == user def set_as_author(self, user): """ Set a participant as the author of the private topic. The previous author becomes a mere participant. If the user is already the author, nothing happens. :param user: a given user. :raise NotParticipatingError: if the user is not already participating in the private topic. """ if not self.is_participant(user): raise NotParticipatingError if not self.is_author( user): # nothing to do if user is already the author self.participants.add(self.author) self.participants.remove(user) self.author = user def is_participant(self, user): """ Check if a given user is participating in the private topic. :param user: a given user. :return: True if the user is the author or a mere participant, False otherwise. """ return self.is_author(user) or user in self.participants.all() def add_participant(self, user, silent=False): """ Add a participant to the private topic. If the user is already participating, do nothing. Send the `participant_added` signal if successful. :param user: the user to add to the private topic :param silent: specify if the `participant_added` signal should be silent (e.g. no notification) :raise NotReachableError: if the user cannot receive private messages (e.g. a bot) """ if not is_reachable(user): raise NotReachableError if not self.is_participant(user): self.participants.add(user) signals.participant_added.send(sender=PrivateTopic, topic=self, silent=silent) def remove_participant(self, user): """ Remove a participant from the private topic. If the removed participant is the author, set the first mere participant as the author. If the given user is not a participant, do nothing. Send the `participant_removed` signal if successful. :param user: the user to remove from the private topic. """ if self.is_participant(user): if self.is_author(user): self.set_as_author(self.participants.first()) self.participants.remove(user) signals.participant_removed.send(sender=PrivateTopic, topic=self) @staticmethod def has_read_permission(request): return request.user.is_authenticated def has_object_read_permission(self, request): return PrivateTopic.has_read_permission( request) and self.is_participant(request.user) @staticmethod def has_write_permission(request): return request.user.is_authenticated def has_object_write_permission(self, request): return PrivateTopic.has_write_permission( request) and self.is_participant(request.user) def has_object_update_permission(self, request): return PrivateTopic.has_write_permission(request) and self.is_author( request.user)