11import logging
22from collections .abc import Iterable
3+ from datetime import datetime
34from typing import Optional , Type , Union
45from uuid import UUID
56
910from django .db .models .functions import Cast
1011from django .db .models .query import QuerySet
1112from django .db .utils import IntegrityError
13+ from django .utils import timezone
1214from django .utils .translation import gettext_lazy as _
1315
1416# Django-rest-framework
@@ -63,6 +65,18 @@ def __getattr__(self, attr):
6365 return rd
6466
6567
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+
6680class RoleDefinitionManager (models .Manager ):
6781 def contribute_to_class (self , cls : Type [models .Model ], name : str ) -> None :
6882 """After Django populates the model for the manager, attach the manager role manager"""
@@ -178,13 +192,13 @@ def __str__(self):
178192 managed_str = ', managed=True'
179193 return f'RoleDefinition(pk={ self .id } , name={ self .name } { managed_str } )'
180194
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 )
183197
184198 def remove_global_permission (self , actor ):
185199 return self .give_or_remove_global_permission (actor , giving = False )
186200
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 ):
188202 if giving and (self .content_type is not None ):
189203 raise ValidationError ('Role definition content type must be null to assign globally' )
190204
@@ -202,6 +216,8 @@ def give_or_remove_global_permission(self, actor, giving=True):
202216 raise RuntimeError (f'Cannot { giving and "give" or "remove" } permission for { actor } , must be a user or team' )
203217
204218 if giving :
219+ if assignment_created :
220+ kwargs ['created' ] = assignment_created
205221 assignment , _ = cls .objects .get_or_create (** kwargs )
206222 else :
207223 assignment = cls .objects .filter (** kwargs ).first ()
@@ -221,8 +237,8 @@ def give_or_remove_global_permission(self, actor, giving=True):
221237
222238 return assignment
223239
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 )
226242
227243 def remove_permission (self , actor , content_object ):
228244 return self .give_or_remove_permission (actor , content_object , giving = False )
@@ -247,7 +263,7 @@ def get_or_create_object_role(self, kwargs, defaults):
247263 object_role = ObjectRole .objects .create (** kwargs , ** defaults )
248264 return (object_role , True )
249265
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 ):
251267 "Shortcut method to do whatever needed to give user or team these permissions"
252268 validate_assignment (self , actor , content_object )
253269
@@ -278,15 +294,28 @@ def give_or_remove_permission(self, actor, content_object, giving=True, sync_act
278294
279295 update_teams , to_update = needed_updates_on_assignment (self , actor , object_role , created = created , giving = True )
280296
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+
281304 assignment = None
282305 if actor ._meta .model_name == 'user' :
283306 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 )
285311 else :
286312 object_role .users .remove (actor )
287313 elif isinstance (actor , permission_registry .team_model ):
288314 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 )
290319 else :
291320 object_role .teams .remove (actor )
292321
@@ -410,6 +439,14 @@ class AssignmentBase(ImmutableCommonModel, ObjectRoleFields):
410439 null = True , blank = True , help_text = _ ('The primary key of the object this assignment applies to; null value indicates system-wide assignment.' )
411440 )
412441 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+ )
413450
414451 # object_role is internal, and not shown in serializer
415452 # content_type does not have a link, and ResourceType will be used in lieu sometime
0 commit comments