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