Coverage for sites/ptf_tools/ptf_tools/models.py: 87%

82 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2024-07-18 09:02 +0000

1import datetime 

2from dataclasses import asdict 

3from dataclasses import dataclass 

4from dataclasses import field 

5 

6from django.db import models 

7from django.http import HttpRequest 

8 

9from invitations.app_settings import app_settings as invitations_app_settings 

10from invitations.models import Invitation as BaseInvitation 

11from ptf.models import Collection 

12 

13 

14class ResourceInNumdam(models.Model): 

15 pid = models.CharField(max_length=64, db_index=True) 

16 

17 

18class CollectionGroup(models.Model): 

19 """ 

20 Overwrites original Django Group. 

21 """ 

22 

23 def __str__(self): 

24 return self.group.name 

25 

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="") 

29 

30 def get_collections(self) -> str: 

31 return ", ".join([col.pid for col in self.collections.all()]) 

32 

33 

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 """ 

40 

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 ) 

48 

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. 

55 

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"] 

68 

69 invite = cls.create( 

70 email, inviter=request.user, first_name=first_name, last_name=last_name 

71 ) 

72 

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) 

76 

77 return invite 

78 

79 def date_expired(self) -> datetime.datetime: 

80 return self.sent + datetime.timedelta( 

81 days=invitations_app_settings.INVITATION_EXPIRY, 

82 ) 

83 

84 

85@dataclass 

86class InviteCommentData: 

87 id: int 

88 user_id: int 

89 pid: str 

90 doi: str 

91 

92 

93@dataclass 

94class InviteCollectionData: 

95 pid: list[str] 

96 user_id: int 

97 

98 

99@dataclass 

100class InviteModeratorData: 

101 """ 

102 Interface for storing the moderator data in an invitation. 

103 """ 

104 

105 comments: list[InviteCommentData] = field(default_factory=list) 

106 collections: list[InviteCollectionData] = field(default_factory=list) 

107 

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)}") 

119 

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)}") 

130 

131 

132@dataclass 

133class InvitationExtraData: 

134 """ 

135 Interface representing an invitation's extra data. 

136 """ 

137 

138 moderator: InviteModeratorData = field(default_factory=InviteModeratorData) 

139 user_groups: list[int] = field(default_factory=list) 

140 

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). 

145 

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)}") 

156 

157 def serialize(self) -> dict: 

158 return asdict(self)