Coverage for sites/comments_site/comments_database/models.py: 97%
69 statements
« prev ^ index » next coverage.py v7.6.4, created at 2024-10-25 11:29 +0000
« prev ^ index » next coverage.py v7.6.4, created at 2024-10-25 11:29 +0000
1from typing import Optional
3from django.contrib.auth.models import AbstractUser
4from django.db import models
5from django.utils.translation import gettext_lazy as _
7from comments_api.constants import COMMENT_STATUS_CHOICES
8from comments_api.constants import MODERATION_STATUS_CHOICES
9from comments_api.constants import STATUS_SUBMITTED
10from ptf.site_register import SITE_REGISTER
13class User(AbstractUser):
14 """
15 Custom user class.
17 A regular (API) user represents a mathdoc website.
18 The username must match an entry in site_register.py for mathdoc websites.
19 """
21 first_name = None
22 last_name = None
23 url = models.URLField(_("URL"), max_length=258, unique=True, null=True, blank=True)
24 mathdoc_site = models.BooleanField(_("Mathdoc site"), default=False)
26 class Meta:
27 constraints = [
28 models.UniqueConstraint(
29 fields=["mathdoc_site", "username"], name="unique_mathdoc_site"
30 )
31 ]
33 def save(self, *args, **kwargs) -> None:
34 if self.mathdoc_site:
35 assert self.username in SITE_REGISTER, (
36 "ERROR: the user to save is flagged as a mathdoc site but its"
37 "username does not match an entry in site_register.py"
38 )
39 return super().save(*args, **kwargs)
42class Moderator(models.Model):
43 """
44 Represents a comment moderator.
45 """
47 id = models.IntegerField(_("Trammel user ID"), primary_key=True)
49 def __str__(self) -> str:
50 return f"Trammel user #{self.id}"
53class Comment(models.Model):
54 """
55 Represents a Resource comment.
56 """
58 site = models.ForeignKey(
59 User, on_delete=models.PROTECT, null=False, blank=False, related_name="comments"
60 )
61 doi = models.CharField(max_length=64, null=False, blank=False)
62 parent: "models.ForeignKey[Comment]" = models.ForeignKey(
63 "self", on_delete=models.PROTECT, null=True, blank=True, related_name="children"
64 ) # type:ignore
65 date_submitted = models.DateTimeField(blank=True)
66 date_last_modified = models.DateTimeField(blank=True)
68 # Author related data
69 # Null values are allowed for author fields, corresponding to deleted comments
70 # We do not create an author model because this data can vary for each comment
71 # of the same author (mainly provider & provider UID but also e-mail)
72 author_id = models.BigIntegerField(null=True, blank=False)
73 author_email = models.EmailField(null=True, blank=False)
74 author_first_name = models.CharField(max_length=128, null=True, blank=False)
75 author_last_name = models.CharField(max_length=128, null=True, blank=False)
76 author_provider = models.CharField(max_length=64, null=True, blank=False)
77 author_provider_uid = models.CharField(max_length=128, null=True, blank=False)
79 # We store the both the raw, unsanitized HTML value of the comment
80 # and the sanitized one.
81 # The API should always return the sanitized version of this field.
82 raw_html = models.TextField()
83 sanitized_html = models.TextField()
84 status = models.CharField(
85 _("Status"),
86 default=STATUS_SUBMITTED,
87 choices=COMMENT_STATUS_CHOICES,
88 blank=False,
89 max_length=127,
90 )
92 # Moderator related
93 moderators = models.ManyToManyField(
94 Moderator, through="ModerationRights", related_name="comments"
95 )
96 article_author_comment = models.BooleanField(default=False)
97 editorial_team_comment = models.BooleanField(default=False)
98 hide_author_name = models.BooleanField(default=False)
100 # Boolean used to flag "new" comments. The value is used and updated by a cron
101 # script responsible for sending e-mail when there are comments with is_new = True
102 is_new = models.BooleanField(default=True)
103 # Boolean used to track if a comment has already been "Validated". As comments
104 # can be re-moderated, we use it to not send an e-mail when a comment gets
105 # validated again.
106 validation_email_sent = models.BooleanField(default=False)
108 def get_base_url(self) -> str:
109 """
110 Return the base_url of the website where the comment was posted.
111 The URL is by default the one in site_register.py but it can be overriden
112 with the "URL" field in the user form (useful for local and test env).
113 """
114 url = self.site.url if self.site.url else SITE_REGISTER[self.site.username]["site_domain"]
115 if url.endswith("/"): 115 ↛ 116line 115 didn't jump to line 116 because the condition on line 115 was never true
116 url = url[:-1]
117 if not url.startswith("http"):
118 url = f"https://{url}"
119 return url
121 def __str__(self) -> str:
122 return f"Comment {self.pk} - {self.doi}"
124 def author_full_name(self) -> str | None:
125 """
126 Returns the author's actual full name.
127 """
128 if self.author_first_name and self.author_last_name:
129 return f"{self.author_first_name} {self.author_last_name}"
130 return None
132 def get_moderators(self):
133 return self.moderators.values_list("pk", flat=True)
135 def get_moderation_data(self) -> Optional["ModerationRights"]:
136 """
137 Returns the moderation data.
138 This corresponds to the published date of the comment for a validated comment.
139 """
140 return (
141 self.moderationrights_set.filter(date_moderated__isnull=False, status=self.status)
142 .order_by("date_moderated")
143 .first()
144 )
147class ModerationRights(models.Model):
148 """
149 ManyToMany model between Moderator & Comment
150 """
152 moderator = models.ForeignKey(Moderator, on_delete=models.CASCADE)
153 comment = models.ForeignKey(Comment, on_delete=models.CASCADE)
154 date_created = models.DateTimeField(null=False, blank=False)
155 date_moderated = models.DateTimeField(null=True, blank=True)
156 status = models.CharField(
157 _("Status"), choices=MODERATION_STATUS_CHOICES, blank=False, max_length=127
158 )
160 class Meta:
161 unique_together = ["moderator", "comment"]