class Topic(models.Model): """ A Topic is a thread of posts. A topic has several states, witch are all independent: - Solved: it was a question, and this question has been answered. The "solved" state is set at author's discretion. - Locked: none can write on a locked topic. - Sticky: sticky topics are displayed on top of topic lists (ex: on forum page). """ class Meta: verbose_name = 'Sujet' verbose_name_plural = 'Sujets' title = models.CharField('Titre', max_length=160) subtitle = models.CharField('Sous-titre', max_length=200, null=True, blank=True) forum = models.ForeignKey(Forum, verbose_name='Forum', db_index=True) author = models.ForeignKey(User, verbose_name='Auteur', related_name='topics', db_index=True) last_message = models.ForeignKey('Post', null=True, related_name='last_message', verbose_name='Dernier message') pubdate = models.DateTimeField('Date de création', auto_now_add=True) update_index_date = models.DateTimeField( 'Date de dernière modification pour la réindexation partielle', auto_now=True, db_index=True) is_solved = models.BooleanField('Est résolu', default=False, db_index=True) is_locked = models.BooleanField('Est verrouillé', default=False, db_index=True) is_sticky = models.BooleanField('Est en post-it', default=False, db_index=True) tags = models.ManyToManyField( Tag, verbose_name='Tags du forum', blank=True, db_index=True) objects = TopicManager() def __str__(self): return self.title def get_absolute_url(self): return reverse('topic-posts-list', args=[self.pk, self.slug()]) def slug(self): return slugify(self.title) def get_post_count(self): """ :return: the number of posts in the topic. """ return Post.objects.filter(topic__pk=self.pk).count() def get_last_post(self): """ :return: the last post in the thread. """ return self.last_message def get_last_answer(self): """ Gets the last answer in this tread, if any. Note the first post is not considered as an answer, therefore a topic with a single post (the 1st one) will return `None`. :return: the last answer in the thread, if any. """ last_post = self.get_last_post() if last_post == self.first_post(): return None else: return last_post def first_post(self): """ :return: the first post of a topic, written by topic's author. """ # we need the author prefetching as this method is widely used in templating directly or with # all the mess arround last_answer and last_read message return Post.objects\ .filter(topic=self)\ .select_related("author")\ .order_by('position')\ .first() def add_tags(self, tag_collection): """ Add all tags contained in `tag_collection` to this topic. If a tag is unknown, it is added to the system. :param tag_collection: A collection of tags. """ for tag in tag_collection: try: current_tag, created = Tag.objects.get_or_create(title=tag.lower().strip()) self.tags.add(current_tag) except ValueError as e: logging.getLogger("zds.forum").warn(e) self.save() def last_read_post(self): """ Returns the last post the current user has read in this topic. If it has never read this topic, returns the first post. Used in "last read post" balloon (base.html line 91). :return: the last post the user has read. """ try: return TopicRead.objects \ .select_related() \ .filter(topic__pk=self.pk, user__pk=get_current_user().pk) \ .latest('post__position').post except TopicRead.DoesNotExist: return self.first_post() def resolve_last_read_post_absolute_url(self): """resolve the url that leads to the last post the current user has read. If current user is \ anonymous, just lead to the thread start. :return: the url :rtype: str """ user = get_current_user() if user is None or not user.is_authenticated(): return self.first_unread_post().get_absolute_url() else: try: pk, pos = self.resolve_last_post_pk_and_pos_read_by_user(user) page_nb = 1 if pos > ZDS_APP["forum"]["posts_per_page"]: page_nb += (pos - 1) // ZDS_APP["forum"]["posts_per_page"] return '{}?page={}#p{}'.format( self.get_absolute_url(), page_nb, pk) except TopicRead.DoesNotExist: return self.first_unread_post().get_absolute_url() def resolve_last_post_pk_and_pos_read_by_user(self, user): """get the primary key and position of the last post the user read :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 = TopicRead.objects\ .select_related('post')\ .filter(topic__pk=self.pk, user__pk=user.pk) \ .latest('post__position') if t_read: return t_read.post.pk, t_read.post.position return Post.objects\ .filter(topic__pk=self.pk)\ .order_by('position')\ .values('pk', "position").first().values() def first_unread_post(self, user=None): """ Returns the first post of this topics the current user has never read, or the first post if it has never read \ this topic.\ Used in notification menu. :return: The first unread post for this topic and this user. """ try: if user is None: user = get_current_user() last_post = TopicRead.objects \ .filter(topic__pk=self.pk, user__pk=user.pk) \ .latest('post__position').post next_post = Post.objects.filter(topic__pk=self.pk, position__gt=last_post.position) \ .select_related("author").first() return next_post except (TopicRead.DoesNotExist, Post.DoesNotExist): return self.first_post() def antispam(self, user=None): """ Check if the user is allowed to post in a topic according to the `ZDS_APP['forum']['spam_limit_seconds']` value. The user can always post if someone else has posted last. If the user is the last poster and there is less than `ZDS_APP['forum']['spam_limit_seconds']` since the last post, the anti-spam is active and the user cannot post. :param user: An user. If undefined, the current user is used. :return: `True` if the anti-spam is active (user can't post), `False` otherwise. """ if user is None: user = get_current_user() last_user_post = Post.objects\ .filter(topic=self)\ .filter(author=user.pk)\ .order_by('position')\ .last() if last_user_post and last_user_post == self.get_last_post(): duration = datetime.now() - last_user_post.pubdate if duration.total_seconds() < settings.ZDS_APP['forum']['spam_limit_seconds']: return True return False def old_post_warning(self): """ Check if the last message was written a long time ago according to `ZDS_APP['forum']['old_post_limit_days']` value. :return: `True` if the post is old (users are warned), `False` otherwise. """ last_post = self.last_message if last_post is not None: t = last_post.pubdate + timedelta(days=settings.ZDS_APP['forum']['old_post_limit_days']) if t < datetime.today(): return True return False
class Topic(AbstractESDjangoIndexable): """ A Topic is a thread of posts. A topic has several states, witch are all independent: - Solved: it was a question, and this question has been answered. The "solved" state is set at author's discretion. - Locked: none can write on a locked topic. - Sticky: sticky topics are displayed on top of topic lists (ex: on forum page). """ objects_per_batch = 1000 class Meta: verbose_name = 'Sujet' verbose_name_plural = 'Sujets' title = models.CharField('Titre', max_length=160) subtitle = models.CharField('Sous-titre', max_length=200, null=True, blank=True) forum = models.ForeignKey(Forum, verbose_name='Forum', db_index=True) author = models.ForeignKey(User, verbose_name='Auteur', related_name='topics', db_index=True) last_message = models.ForeignKey('Post', null=True, related_name='last_message', verbose_name='Dernier message') pubdate = models.DateTimeField('Date de création', auto_now_add=True) update_index_date = models.DateTimeField( 'Date de dernière modification pour la réindexation partielle', auto_now=True, db_index=True) is_solved = models.BooleanField('Est résolu', default=False, db_index=True) is_locked = models.BooleanField('Est verrouillé', default=False, db_index=True) is_sticky = models.BooleanField('Est en post-it', default=False, db_index=True) github_issue = models.PositiveIntegerField('Ticket GitHub', null=True, blank=True) tags = models.ManyToManyField(Tag, verbose_name='Tags du forum', blank=True, db_index=True) objects = TopicManager() def __str__(self): return self.title def get_absolute_url(self): return reverse('topic-posts-list', args=[self.pk, self.slug()]) def slug(self): return slugify(self.title) def get_post_count(self): """ :return: the number of posts in the topic. """ return Post.objects.filter(topic__pk=self.pk).count() def get_last_post(self): """ :return: the last post in the thread. """ return self.last_message def get_last_answer(self): """ Gets the last answer in this tread, if any. Note the first post is not considered as an answer, therefore a topic with a single post (the 1st one) will return `None`. :return: the last answer in the thread, if any. """ last_post = self.get_last_post() if last_post == self.first_post(): return None else: return last_post def first_post(self): """ :return: the first post of a topic, written by topic's author. """ # we need the author prefetching as this method is widely used in templating directly or with # all the mess arround last_answer and last_read message return Post.objects\ .filter(topic=self)\ .select_related('author')\ .order_by('position')\ .first() def add_tags(self, tag_collection): """ Add all tags contained in `tag_collection` to this topic. If a tag is unknown, it is added to the system. :param tag_collection: A collection of tags. """ for tag in tag_collection: try: current_tag, created = Tag.objects.get_or_create( title=tag.lower().strip()) self.tags.add(current_tag) except ValueError as e: logging.getLogger(__name__).warn(e) self.save() signals.edit_content.send(sender=self.__class__, instance=self, action='edit_tags_and_title') def last_read_post(self): """ Returns the last post the current user has read in this topic. If it has never read this topic, returns the first post. Used in "last read post" balloon (base.html line 91). :return: the last post the user has read. """ try: return TopicRead.objects \ .select_related() \ .filter(topic__pk=self.pk, user__pk=get_current_user().pk) \ .latest('post__position').post except TopicRead.DoesNotExist: return self.first_post() def resolve_last_read_post_absolute_url(self): """resolve the url that leads to the last post the current user has read. If current user is \ anonymous, just lead to the thread start. :return: the url :rtype: str """ user = get_current_user() if user is None or not user.is_authenticated(): return self.first_unread_post().get_absolute_url() else: 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 '{}?page={}#p{}'.format(self.get_absolute_url(), page_nb, pk) except TopicRead.DoesNotExist: return self.first_unread_post().get_absolute_url() def resolve_last_post_pk_and_pos_read_by_user(self, user): """get the primary key and position of the last post the user read :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 = TopicRead.objects\ .select_related('post')\ .filter(topic__pk=self.pk, user__pk=user.pk) \ .latest('post__position') if t_read: return t_read.post.pk, t_read.post.position return list( Post.objects.filter(topic__pk=self.pk).order_by('position').values( 'pk', 'position').first().values()) def first_unread_post(self, user=None): """ Returns the first post of this topics the current user has never read, or the first post if it has never read \ this topic.\ Used in notification menu. :return: The first unread post for this topic and this user. """ try: if user is None: user = get_current_user() last_post = TopicRead.objects \ .filter(topic__pk=self.pk, user__pk=user.pk) \ .latest('post__position').post next_post = Post.objects.filter(topic__pk=self.pk, position__gt=last_post.position) \ .select_related('author').first() return next_post except (TopicRead.DoesNotExist, Post.DoesNotExist): return self.first_post() def antispam(self, user=None): """ Check if the user is allowed to post in a topic according to the `ZDS_APP['forum']['spam_limit_seconds']` value. The user can always post if someone else has posted last. If the user is the last poster and there is less than `ZDS_APP['forum']['spam_limit_seconds']` since the last post, the anti-spam is active and the user cannot post. :param user: A user. If undefined, the current user is used. :return: `True` if the anti-spam is active (user can't post), `False` otherwise. """ if user is None: user = get_current_user() last_user_post = Post.objects\ .filter(topic=self)\ .filter(author=user.pk)\ .order_by('position')\ .last() if last_user_post and last_user_post == self.get_last_post(): duration = datetime.now() - last_user_post.pubdate if duration.total_seconds( ) < settings.ZDS_APP['forum']['spam_limit_seconds']: return True return False def old_post_warning(self): """ Check if the last message was written a long time ago according to `ZDS_APP['forum']['old_post_limit_days']` value. :return: `True` if the post is old (users are warned), `False` otherwise. """ last_post = self.last_message if last_post is not None: t = last_post.pubdate + timedelta( days=settings.ZDS_APP['forum']['old_post_limit_days']) if t < datetime.today(): return True return False @classmethod def get_es_mapping(cls): es_mapping = super(Topic, cls).get_es_mapping() es_mapping.field('title', Text(boost=1.5)) es_mapping.field('tags', Text(boost=2.0)) es_mapping.field('subtitle', Text()) es_mapping.field('is_solved', Boolean()) es_mapping.field('is_locked', Boolean()) es_mapping.field('is_sticky', Boolean()) es_mapping.field('pubdate', Date()) es_mapping.field('forum_pk', Integer()) # not indexed: es_mapping.field('get_absolute_url', Keyword(index=False)) es_mapping.field('forum_title', Text(index=False)) es_mapping.field('forum_get_absolute_url', Keyword(index=False)) return es_mapping @classmethod def get_es_django_indexable(cls, force_reindexing=False): """Overridden to prefetch tags and forum """ query = super(Topic, cls).get_es_django_indexable(force_reindexing) return query.prefetch_related('tags').select_related('forum') def get_es_document_source(self, excluded_fields=None): """Overridden to handle the case of tags (M2M field) """ excluded_fields = excluded_fields or [] excluded_fields.extend( ['tags', 'forum_pk', 'forum_title', 'forum_get_absolute_url']) data = super( Topic, self).get_es_document_source(excluded_fields=excluded_fields) data['tags'] = [tag.title for tag in self.tags.all()] data['forum_pk'] = self.forum.pk data['forum_title'] = self.forum.title data['forum_get_absolute_url'] = self.forum.get_absolute_url() return data def save(self, *args, **kwargs): """Overridden to handle the displacement of the topic to another forum """ try: old_self = Topic.objects.get(pk=self.pk) except Topic.DoesNotExist: pass else: if old_self.forum.pk != self.forum.pk or old_self.title != self.title: Post.objects.filter(topic__pk=self.pk).update(es_flagged=True) return super(Topic, self).save(*args, **kwargs)
class Topic(AbstractESDjangoIndexable): """ A Topic is a thread of posts. A topic has several states, witch are all independent: - Solved: it was a question, and this question has been answered. The "solved" state is set at author's discretion. - Locked: none can write on a locked topic. - Sticky: sticky topics are displayed on top of topic lists (ex: on forum page). """ objects_per_batch = 1000 class Meta: verbose_name = "Sujet" verbose_name_plural = "Sujets" title = models.CharField("Titre", max_length=160) subtitle = models.CharField("Sous-titre", max_length=200, null=True, blank=True) # on_delete default forum? forum = models.ForeignKey(Forum, verbose_name="Forum", db_index=True, on_delete=models.CASCADE) # on_delete anonymous? author = models.ForeignKey(User, verbose_name="Auteur", related_name="topics", db_index=True, on_delete=models.CASCADE) last_message = models.ForeignKey("Post", 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) update_index_date = models.DateTimeField( "Date de dernière modification pour la réindexation partielle", auto_now=True, db_index=True) solved_by = models.ForeignKey( User, verbose_name="Utilisateur ayant noté le sujet comme résolu", db_index=True, default=None, null=True, on_delete=models.SET_NULL, ) is_locked = models.BooleanField("Est verrouillé", default=False, db_index=True) is_sticky = models.BooleanField("Est en post-it", default=False, db_index=True) github_issue = models.PositiveIntegerField("Ticket GitHub", null=True, blank=True) tags = models.ManyToManyField(Tag, verbose_name="Tags du forum", blank=True, db_index=True) objects = TopicManager() def __str__(self): return self.title @property def is_solved(self): return self.solved_by is not None @property def meta_description(self): first_post = self.first_post() if len(first_post.text) < 120: return first_post.text return Topic.__remove_greetings( first_post)[:settings.ZDS_APP["forum"]["description_size"]] @staticmethod def __remove_greetings(post): greetings = settings.ZDS_APP["forum"]["greetings"] max_size = settings.ZDS_APP["forum"]["description_size"] + 1 text = post.text for greeting in greetings: if text.strip().lower().startswith(greeting): index_of_dot = max( text.index("\n") if "\n" in text else -1, -1) index_of_dot = min( index_of_dot, text.index(".") if "." in text else max_size) index_of_dot = min( index_of_dot, text.index("!") if "!" in text else max_size) return text[index_of_dot + 1:].strip() return text def get_absolute_url(self): return reverse("topic-posts-list", args=[self.pk, self.slug()]) def slug(self): return old_slugify(self.title) def get_post_count(self): """ :return: the number of posts in the topic. """ return Post.objects.filter(topic__pk=self.pk).count() def get_last_post(self): """ :return: the last post in the thread. """ return self.last_message def get_last_answer(self): """ Gets the last answer in this tread, if any. Note the first post is not considered as an answer, therefore a topic with a single post (the 1st one) will return `None`. :return: the last answer in the thread, if any. """ last_post = self.get_last_post() if last_post == self.first_post(): return None else: return last_post def first_post(self): """ :return: the first post of a topic, written by topic's author. """ # we need the author prefetching as this method is widely used in templating directly or with # all the mess arround last_answer and last_read message return Post.objects.filter( topic=self).select_related("author").order_by("position").first() def add_tags(self, tag_collection): """ Add all tags contained in `tag_collection` to this topic. If a tag is unknown, it is added to the system. :param tag_collection: A collection of tags. """ for tag in filter(None, tag_collection): try: current_tag, created = Tag.objects.get_or_create( title=tag.lower().strip()) self.tags.add(current_tag) except ValueError as e: logging.getLogger(__name__).warning(e) self.save() signals.topic_edited.send(sender=self.__class__, topic=self) def last_read_post(self): """ Returns the last post the current user has read in this topic. If it has never read this topic, returns the first post. Used in "last read post" balloon (base.html line 91). :return: the last post the user has read. """ try: return (TopicRead.objects.select_related().filter( topic__pk=self.pk, user__pk=get_current_user().pk).latest("post__position").post) except TopicRead.DoesNotExist: return self.first_post() def resolve_last_read_post_absolute_url(self): """resolve the url that leads to the last post the current user has read. If current user is \ anonymous, just lead to the thread start. :return: the url :rtype: str """ user = get_current_user() if user is None or not user.is_authenticated: return self.first_unread_post().get_absolute_url() else: 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 "{}?page={}#p{}".format(self.get_absolute_url(), page_nb, pk) except TopicRead.DoesNotExist: return self.first_unread_post().get_absolute_url() def resolve_last_post_pk_and_pos_read_by_user(self, user): """get the primary key and position of the last post the user read :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 = (TopicRead.objects.select_related("post").filter( topic__pk=self.pk, user__pk=user.pk).latest("post__position")) if t_read: return t_read.post.pk, t_read.post.position return list( Post.objects.filter(topic__pk=self.pk).order_by("position").values( "pk", "position").first().values()) def first_unread_post(self, user=None): """ Returns the first post of this topics the current user has never read, or the first post if it has never read \ this topic.\ Used in notification menu. :return: The first unread post for this topic and this user. """ try: if user is None: user = get_current_user() last_post = TopicRead.objects.filter( topic__pk=self.pk, user__pk=user.pk).latest("post__position").post next_post = (Post.objects.filter( topic__pk=self.pk, position__gt=last_post.position).select_related( "author").first()) return next_post except (TopicRead.DoesNotExist, Post.DoesNotExist): return self.first_post() def antispam(self, user=None): """ Check if the user is allowed to post in a topic according to the `ZDS_APP['forum']['spam_limit_seconds']` value. The user can always post if someone else has posted last. If the user is the last poster and there is less than `ZDS_APP['forum']['spam_limit_seconds']` since the last post, the anti-spam is active and the user cannot post. :param user: A user. If undefined, the current user is used. :return: `True` if the anti-spam is active (user can't post), `False` otherwise. """ if user is None: user = get_current_user() last_user_post = Post.objects.filter(topic=self).filter( author=user.pk).order_by("position").last() if last_user_post and last_user_post == self.get_last_post(): duration = datetime.now() - last_user_post.pubdate if duration.total_seconds( ) < settings.ZDS_APP["forum"]["spam_limit_seconds"]: return True return False def old_post_warning(self): """ Check if the last message was written a long time ago according to `ZDS_APP['forum']['old_post_limit_days']` value. :return: `True` if the post is old (users are warned), `False` otherwise. """ last_post = self.last_message if last_post is not None: t = last_post.pubdate + timedelta( days=settings.ZDS_APP["forum"]["old_post_limit_days"]) if t < datetime.today(): return True return False @classmethod def get_es_mapping(cls): es_mapping = super(Topic, cls).get_es_mapping() es_mapping.field("title", Text(boost=1.5)) es_mapping.field("tags", Text(boost=2.0)) es_mapping.field("subtitle", Text()) es_mapping.field("is_solved", Boolean()) es_mapping.field("is_locked", Boolean()) es_mapping.field("is_sticky", Boolean()) es_mapping.field("pubdate", Date()) es_mapping.field("forum_pk", Integer()) # not indexed: es_mapping.field("get_absolute_url", Keyword(index=False)) es_mapping.field("forum_title", Text(index=False)) es_mapping.field("forum_get_absolute_url", Keyword(index=False)) return es_mapping @classmethod def get_es_django_indexable(cls, force_reindexing=False): """Overridden to prefetch tags and forum""" query = super(Topic, cls).get_es_django_indexable(force_reindexing) return query.prefetch_related("tags").select_related("forum") def get_es_document_source(self, excluded_fields=None): """Overridden to handle the case of tags (M2M field)""" excluded_fields = excluded_fields or [] excluded_fields.extend( ["tags", "forum_pk", "forum_title", "forum_get_absolute_url"]) data = super( Topic, self).get_es_document_source(excluded_fields=excluded_fields) data["tags"] = [tag.title for tag in self.tags.all()] data["forum_pk"] = self.forum.pk data["forum_title"] = self.forum.title data["forum_get_absolute_url"] = self.forum.get_absolute_url() return data def save(self, *args, **kwargs): """Overridden to handle the displacement of the topic to another forum""" try: old_self = Topic.objects.get(pk=self.pk) except Topic.DoesNotExist: pass else: if old_self.forum.pk != self.forum.pk or old_self.title != self.title: Post.objects.filter(topic__pk=self.pk).update(es_flagged=True) return super(Topic, self).save(*args, **kwargs)
class Topic(models.Model): """ A Topic is a thread of posts. A topic has several states, witch are all independent: - Solved: it was a question, and this question has been answered. The "solved" state is set at author's discretion. - Locked: none can write on a locked topic. - Sticky: sticky topics are displayed on top of topic lists (ex: on forum page). """ class Meta: verbose_name = 'Sujet' verbose_name_plural = 'Sujets' title = models.CharField('Titre', max_length=80) subtitle = models.CharField('Sous-titre', max_length=200) forum = models.ForeignKey(Forum, verbose_name='Forum', db_index=True) author = models.ForeignKey(User, verbose_name='Auteur', related_name='topics', db_index=True) last_message = models.ForeignKey('Post', null=True, related_name='last_message', verbose_name='Dernier message') pubdate = models.DateTimeField('Date de création', auto_now_add=True) is_solved = models.BooleanField('Est résolu', default=False, db_index=True) is_locked = models.BooleanField('Est verrouillé', default=False) is_sticky = models.BooleanField('Est en post-it', default=False, db_index=True) tags = models.ManyToManyField(Tag, verbose_name='Tags du forum', null=True, blank=True, db_index=True) # This attribute is the link between beta of tutorials and topic of these beta. # In Tuto logic we can found something like this: `Topic.objet.get(key=tutorial.pk)` # TODO: 1. Use a better name, 2. maybe there can be a cleaner way to do this key = models.IntegerField('cle', null=True, blank=True) objects = TopicManager() def __unicode__(self): return self.title def get_absolute_url(self): return reverse('topic-posts-list', args=[self.pk, self.slug()]) def slug(self): return slugify(self.title) def get_post_count(self): """ :return: the number of posts in the topic. """ return Post.objects.filter(topic__pk=self.pk).count() def get_last_post(self): """ :return: the last post in the thread. """ return self.last_message def get_last_answer(self): """ Gets the last answer in this tread, if any. Note the first post is not considered as an answer, therefore a topic with a single post (the 1st one) will return `None`. :return: the last answer in the thread, if any. """ last_post = self.get_last_post() if last_post == self.first_post(): return None else: return last_post def first_post(self): """ :return: the first post of a topic, written by topic's author. """ # TODO: Force relation with author here is strange. Probably linked with the `get_last_answer` function that # should compare PK and not objects return Post.objects\ .filter(topic=self)\ .select_related("author")\ .order_by('position')\ .first() def add_tags(self, tag_collection): """ Add all tags contained in `tag_collection` to this topic. If a tag is unknown, it is added to the system. :param tag_collection: A collection of tags. """ for tag in tag_collection: tag_title = smart_text(tag.strip().lower()) current_tag = Tag.objects.filter(title=tag_title).first() if current_tag is None: current_tag = Tag(title=tag_title) current_tag.save() self.tags.add(current_tag) self.save() def get_followers_by_email(self): """ :return: the set of users that follows this topic by email. """ return TopicFollowed.objects.filter(topic=self, email=True).select_related("user") def last_read_post(self): """ Returns the last post the current user has read in this topic. If it has never read this topic, returns the first post. Used in "last read post" balloon (base.html line 91). :return: the last post the user has read. """ try: return TopicRead.objects \ .select_related() \ .filter(topic__pk=self.pk, user__pk=get_current_user().pk) \ .latest('post__position').post except TopicRead.DoesNotExist: return self.first_post() def resolve_last_read_post_absolute_url(self): """resolve the url that leads to the last post the current user has read. If current user is \ anonymous, just lead to the thread start. :return: the url :rtype: str """ user = get_current_user() if user is None or not user.is_authenticated(): return self.resolve_first_post_url() else: return '{0}?page=1#p{1}'.format( self.get_absolute_url(), self.resolve_last_post_pk_read_by_user(user)) def resolve_last_post_pk_read_by_user(self, user): """get the primary key of the last post the user read :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 """ return TopicRead.objects\ .select_related('post')\ .filter(topic__pk=self.pk, user__pk=user.pk) \ .latest('post__position')\ .pk def resolve_first_post_url(self): """resolve the url that leads to this topic first post :return: the url """ pk = Post.objects\ .filter(topic__pk=self.pk)\ .order_by('-position')\ .values('pk').first() return '{0}?page=1#p{1}'.format(self.get_absolute_url(), pk['pk']) def first_unread_post(self): """ Returns the first post of this topics the current user has never read, or the first post if it has never read \ this topic.\ Used in notification menu. :return: The first unread post for this topic and this user. """ # TODO: Why 2 nearly-identical functions? What is the functional need of these 2 things? try: last_post = TopicRead.objects \ .filter(topic__pk=self.pk, user__pk=get_current_user().pk) \ .latest('post__position').post next_post = Post.objects.filter(topic__pk=self.pk, position__gt=last_post.position) \ .select_related("author").first() return next_post except (TopicRead.DoesNotExist, Post.DoesNotExist): return self.first_post() def is_followed(self, user=None): """ Checks if the user follows this topic. :param user: An user. If undefined, the current user is used. :return: `True` if the user follows this topic, `False` otherwise. """ if user is None: user = get_current_user() return TopicFollowed.objects.filter(topic=self, user=user).exists() def is_email_followed(self, user=None): """ Checks if the user follows this topic by email. :param user: An user. If undefined, the current user is used. :return: `True` if the user follows this topic by email, `False` otherwise. """ if user is None: user = get_current_user() try: TopicFollowed.objects.get(topic=self, user=user, email=True) except TopicFollowed.DoesNotExist: return False return True def antispam(self, user=None): """ Check if the user is allowed to post in a topic according to the `ZDS_APP['forum']['spam_limit_seconds']` value. The user can always post if someone else has posted last. If the user is the last poster and there is less than `ZDS_APP['forum']['spam_limit_seconds']` since the last post, the anti-spam is active and the user cannot post. :param user: An user. If undefined, the current user is used. :return: `True` if the anti-spam is active (user can't post), `False` otherwise. """ if user is None: user = get_current_user() last_user_post = Post.objects\ .filter(topic=self)\ .filter(author=user.pk)\ .order_by('position')\ .last() if last_user_post and last_user_post == self.get_last_post(): duration = datetime.now() - last_user_post.pubdate if duration.total_seconds( ) < settings.ZDS_APP['forum']['spam_limit_seconds']: return True return False def old_post_warning(self): """ Check if the last message was written a long time ago according to `ZDS_APP['forum']['old_post_limit_days']` value. :return: `True` if the post is old (users are warned), `False` otherwise. """ last_post = self.last_message if last_post is not None: t = last_post.pubdate + timedelta( days=settings.ZDS_APP['forum']['old_post_limit_days']) if t < datetime.today(): return True return False def never_read(self): return never_read(self)