1
1
import logging
2
2
from collections .abc import Iterable
3
+ from datetime import datetime
3
4
from typing import Optional , Type , Union
4
5
from uuid import UUID
5
6
9
10
from django .db .models .functions import Cast
10
11
from django .db .models .query import QuerySet
11
12
from django .db .utils import IntegrityError
13
+ from django .utils import timezone
12
14
from django .utils .translation import gettext_lazy as _
13
15
14
16
# Django-rest-framework
@@ -63,6 +65,18 @@ def __getattr__(self, attr):
63
65
return rd
64
66
65
67
68
+ def get_created_timestamp (obj : Union [models .Model , RemoteObject ]) -> Optional [datetime ]:
69
+ """Given some obj from the users app, try to infer the created timestamp"""
70
+ if isinstance (obj , RemoteObject ):
71
+ return obj .created
72
+ for field_name in ('created' , 'created_at' ):
73
+ if hasattr (obj , field_name ):
74
+ val = getattr (obj , field_name )
75
+ if isinstance (val , datetime ):
76
+ return val
77
+ return None
78
+
79
+
66
80
class RoleDefinitionManager (models .Manager ):
67
81
def contribute_to_class (self , cls : Type [models .Model ], name : str ) -> None :
68
82
"""After Django populates the model for the manager, attach the manager role manager"""
@@ -178,13 +192,13 @@ def __str__(self):
178
192
managed_str = ', managed=True'
179
193
return f'RoleDefinition(pk={ self .id } , name={ self .name } { managed_str } )'
180
194
181
- def give_global_permission (self , actor ):
182
- return self .give_or_remove_global_permission (actor , giving = True )
195
+ def give_global_permission (self , actor , assignment_created = None ):
196
+ return self .give_or_remove_global_permission (actor , giving = True , assignment_created = assignment_created )
183
197
184
198
def remove_global_permission (self , actor ):
185
199
return self .give_or_remove_global_permission (actor , giving = False )
186
200
187
- def give_or_remove_global_permission (self , actor , giving = True ):
201
+ def give_or_remove_global_permission (self , actor , giving = True , assignment_created = None ):
188
202
if giving and (self .content_type is not None ):
189
203
raise ValidationError ('Role definition content type must be null to assign globally' )
190
204
@@ -202,6 +216,8 @@ def give_or_remove_global_permission(self, actor, giving=True):
202
216
raise RuntimeError (f'Cannot { giving and "give" or "remove" } permission for { actor } , must be a user or team' )
203
217
204
218
if giving :
219
+ if assignment_created :
220
+ kwargs ['created' ] = assignment_created
205
221
assignment , _ = cls .objects .get_or_create (** kwargs )
206
222
else :
207
223
assignment = cls .objects .filter (** kwargs ).first ()
@@ -221,8 +237,8 @@ def give_or_remove_global_permission(self, actor, giving=True):
221
237
222
238
return assignment
223
239
224
- def give_permission (self , actor , content_object ):
225
- return self .give_or_remove_permission (actor , content_object , giving = True )
240
+ def give_permission (self , actor , content_object , assignment_created = None ):
241
+ return self .give_or_remove_permission (actor , content_object , giving = True , assignment_created = assignment_created )
226
242
227
243
def remove_permission (self , actor , content_object ):
228
244
return self .give_or_remove_permission (actor , content_object , giving = False )
@@ -247,7 +263,7 @@ def get_or_create_object_role(self, kwargs, defaults):
247
263
object_role = ObjectRole .objects .create (** kwargs , ** defaults )
248
264
return (object_role , True )
249
265
250
- def give_or_remove_permission (self , actor , content_object , giving = True , sync_action = False ):
266
+ def give_or_remove_permission (self , actor , content_object , giving = True , sync_action = False , assignment_created = None ):
251
267
"Shortcut method to do whatever needed to give user or team these permissions"
252
268
validate_assignment (self , actor , content_object )
253
269
@@ -278,15 +294,28 @@ def give_or_remove_permission(self, actor, content_object, giving=True, sync_act
278
294
279
295
update_teams , to_update = needed_updates_on_assignment (self , actor , object_role , created = created , giving = True )
280
296
297
+ assignment_defaults = {}
298
+ object_created = get_created_timestamp (content_object )
299
+ if object_created :
300
+ assignment_defaults ['object_created' ] = object_created
301
+ if assignment_created :
302
+ assignment_defaults ['created' ] = assignment_created
303
+
281
304
assignment = None
282
305
if actor ._meta .model_name == 'user' :
283
306
if giving :
284
- assignment , created = RoleUserAssignment .objects .get_or_create (user = actor , object_role = object_role )
307
+ try :
308
+ assignment = RoleUserAssignment .objects .get (user = actor , object_role = object_role )
309
+ except RoleUserAssignment .DoesNotExist :
310
+ assignment = RoleUserAssignment .objects .create (user = actor , object_role = object_role , ** assignment_defaults )
285
311
else :
286
312
object_role .users .remove (actor )
287
313
elif isinstance (actor , permission_registry .team_model ):
288
314
if giving :
289
- assignment , created = RoleTeamAssignment .objects .get_or_create (team = actor , object_role = object_role )
315
+ try :
316
+ assignment = RoleTeamAssignment .objects .get (team = actor , object_role = object_role )
317
+ except RoleTeamAssignment .DoesNotExist :
318
+ assignment = RoleTeamAssignment .objects .create (team = actor , object_role = object_role , ** assignment_defaults )
290
319
else :
291
320
object_role .teams .remove (actor )
292
321
@@ -410,6 +439,14 @@ class AssignmentBase(ImmutableCommonModel, ObjectRoleFields):
410
439
null = True , blank = True , help_text = _ ('The primary key of the object this assignment applies to; null value indicates system-wide assignment.' )
411
440
)
412
441
content_type = models .ForeignKey (DABContentType , on_delete = models .CASCADE , null = True , help_text = _ ("The content type this applies to." ))
442
+ # The object_created field can be used for a checksum-like purpose to verify nothing strange happened with the related object
443
+ object_created = models .DateTimeField (help_text = _ ("The created timestamp of related object, if applicable." ), null = True )
444
+ # Define this with default to make it possible to backdate if necessary, for sync
445
+ created = models .DateTimeField (
446
+ default = timezone .now ,
447
+ editable = False ,
448
+ help_text = _ ("The date/time this resource was created." ),
449
+ )
413
450
414
451
# object_role is internal, and not shown in serializer
415
452
# content_type does not have a link, and ResourceType will be used in lieu sometime
0 commit comments