Coverage for src/ptf_tools/models.py: 85%
80 statements
« prev ^ index » next coverage.py v7.7.0, created at 2025-04-09 14:54 +0000
« prev ^ index » next coverage.py v7.7.0, created at 2025-04-09 14:54 +0000
1import datetime
2from dataclasses import asdict, dataclass, field
4from django.db import models
5from django.http import HttpRequest
6from invitations.app_settings import app_settings as invitations_app_settings
7from invitations.models import Invitation as BaseInvitation
8from ptf.models import Collection
11class ResourceInNumdam(models.Model):
12 pid = models.CharField(max_length=64, db_index=True)
15class CollectionGroup(models.Model):
16 """
17 Overwrites original Django Group.
18 """
20 def __str__(self):
21 return self.group.name
23 group = models.OneToOneField("auth.Group", unique=True, on_delete=models.CASCADE)
24 collections = models.ManyToManyField(Collection)
25 email_alias = models.EmailField(max_length=70, blank=True, default="")
27 def get_collections(self) -> str:
28 return ", ".join([col.pid for col in self.collections.all()])
31class Invitation(BaseInvitation):
32 """
33 Invitation model. Additionally data can be stored in `extra_data`, to be used
34 when an user signs up following the invitation link.
35 Cf. signals.py
36 """
38 first_name = models.CharField("First name", max_length=150, null=False, blank=False)
39 last_name = models.CharField("Last name", max_length=150, null=False, blank=False)
40 extra_data = models.JSONField(
41 default=dict,
42 blank=True,
43 help_text="JSON field used to dynamically update the created user object when the invitation is accepted.",
44 )
46 @classmethod
47 def get_invite(cls, email: str, request: HttpRequest, invite_data: dict) -> "Invitation":
48 """
49 Gets the existing valid invitation or creates a new one and send it.
50 If there's an existing invitation but it's expired, we delete it and
51 send a new one.
53 `invite_data` must contain `first_name` and `last_name` entries. It is passed
54 as the context of the invite mail renderer.
55 """
56 try:
57 invite = cls.objects.get(email__iexact=email)
58 # Delete the invite if it's expired and create a fresh one
59 if invite.key_expired():
60 invite.delete()
61 raise cls.DoesNotExist
62 except cls.DoesNotExist:
63 first_name = invite_data["first_name"]
64 last_name = invite_data["last_name"]
66 invite = cls.create(
67 email, inviter=request.user, first_name=first_name, last_name=last_name
68 )
70 mail_template_context = {**invite_data}
71 mail_template_context["full_name"] = f"{first_name} {last_name}"
72 invite.send_invitation(request, **mail_template_context)
74 return invite
76 def date_expired(self) -> datetime.datetime:
77 return self.sent + datetime.timedelta(
78 days=invitations_app_settings.INVITATION_EXPIRY,
79 )
82@dataclass
83class InviteCommentData:
84 id: int
85 user_id: int
86 pid: str
87 doi: str
90@dataclass
91class InviteCollectionData:
92 pid: list[str]
93 user_id: int
96@dataclass
97class InviteModeratorData:
98 """
99 Interface for storing the moderator data in an invitation.
100 """
102 comments: list[InviteCommentData] = field(default_factory=list)
103 collections: list[InviteCollectionData] = field(default_factory=list)
105 def __post_init__(self):
106 try:
107 comments = self.comments
108 if not isinstance(comments, list): 108 ↛ 109line 108 didn't jump to line 109 because the condition on line 108 was never true
109 raise ValueError("'comments' must be a list")
110 self.comments = [
111 InviteCommentData(**c) if not isinstance(c, InviteCommentData) else c
112 for c in comments
113 ]
114 except Exception as e:
115 raise ValueError(f"Error while parsing provided InviteCommentData. {str(e)}")
117 try:
118 collections = self.collections
119 if not isinstance(collections, list): 119 ↛ 120line 119 didn't jump to line 120 because the condition on line 119 was never true
120 raise ValueError("'collections' must be a list")
121 self.collections = [
122 InviteCollectionData(**c) if not isinstance(c, InviteCollectionData) else c
123 for c in collections
124 ]
125 except Exception as e:
126 raise ValueError(f"Error while parsing provided InviteCollectionData. {str(e)}")
129@dataclass
130class InvitationExtraData:
131 """
132 Interface representing an invitation's extra data.
133 """
135 moderator: InviteModeratorData = field(default_factory=InviteModeratorData)
136 user_groups: list[int] = field(default_factory=list)
138 def __post_init__(self):
139 """
140 Dataclasses do not provide an effective fromdict method to deserialize
141 a dataclass (JSON to python dataclass object).
143 This enables to effectively deserialize a JSON into a InvitationExtraData object,
144 by replacing the nested dict by their actual dataclass representation.
145 Beware this might not work well with typing (?)
146 """
147 moderator = self.moderator
148 if moderator and not isinstance(moderator, InviteModeratorData):
149 try:
150 self.moderator = InviteModeratorData(**moderator)
151 except Exception as e:
152 raise ValueError(f"Error while parsing provided InviteModeratorData. {str(e)}")
154 def serialize(self) -> dict:
155 return asdict(self)