@@ -503,6 +503,9 @@ class Patch(SubmissionMixin):
503503 null = True ,
504504 on_delete = models .CASCADE ,
505505 )
506+ attention_set = models .ManyToManyField (
507+ User , through = 'PatchAttentionSet' , related_name = 'attention_set'
508+ )
506509 state = models .ForeignKey (State , null = True , on_delete = models .CASCADE )
507510 archived = models .BooleanField (default = False )
508511 hash = HashField (null = True , blank = True , db_index = True )
@@ -827,6 +830,104 @@ class Meta:
827830 ]
828831
829832
833+ class PatchAttentionSetManager (models .Manager ):
834+ def get_queryset (self ):
835+ return super ().get_queryset ().filter (removed = False )
836+
837+ def upsert (self , patch , users ):
838+ """Add or updates deleted attention set entries
839+
840+ :param patch: patch object to be updated
841+ :type patch: Patch
842+ :param users: list of users to be added to the attention set list
843+ :type users: list[int]
844+ """
845+ qs = super ().get_queryset ().filter (patch = patch )
846+
847+ existing = {
848+ obj .user .id : obj for obj in qs .filter (user__in = users ).all ()
849+ }
850+ update_list = []
851+ for obj in existing .values ():
852+ if obj .removed :
853+ obj .removed = False
854+ obj .removed_reason = ''
855+ update_list .append (obj )
856+ insert_list = [user for user in users if user not in existing .keys ()]
857+
858+ qs .bulk_create (
859+ [PatchAttentionSet (patch = patch , user_id = id ) for id in insert_list ]
860+ )
861+ qs .bulk_update (update_list , ['removed' , 'removed_reason' ])
862+
863+ def soft_delete (self , patch , users , reason = '' ):
864+ """Mark attention set entries as deleted
865+
866+ :param patch: patch object to be updated
867+ :type patch: Patch
868+ :param users: list of users to be added to the attention set list
869+ :type users: list[int]
870+ :param reason: reason for removal
871+ :type reason: string
872+ """
873+ qs = super ().get_queryset ().filter (patch = patch )
874+
875+ existing = {
876+ obj .user .id : obj for obj in qs .filter (user__in = users ).all ()
877+ }
878+ update_list = []
879+ for obj in existing .values ():
880+ if not obj .removed :
881+ obj .removed = True
882+ obj .removed_reason = reason
883+ update_list .append (obj )
884+
885+ self .bulk_update (update_list , ['removed' , 'removed_reason' ])
886+
887+
888+ class PatchAttentionSet (models .Model ):
889+ patch = models .ForeignKey (Patch , on_delete = models .CASCADE )
890+ user = models .ForeignKey (User , on_delete = models .CASCADE )
891+ last_updated = models .DateTimeField (auto_now = True )
892+ removed = models .BooleanField (default = False )
893+ removed_reason = models .CharField (max_length = 50 , blank = True )
894+
895+ objects = PatchAttentionSetManager ()
896+ raw_objects = models .Manager ()
897+
898+ def delete (self ):
899+ """Soft deletes an user from the patch attention set"""
900+ self .removed = True
901+ self .removed_reason = 'reviewed or commented on the patch'
902+ self .save ()
903+
904+ def __str__ (self ):
905+ return f'<{ self .user } - { self .user .email } >'
906+
907+ class Meta :
908+ unique_together = [('patch' , 'user' )]
909+
910+
911+ def _remove_user_from_patch_attention_set (sender , instance , created , ** kwargs ):
912+ if created :
913+ submitter = instance .submitter
914+ patch = instance .patch
915+ if submitter .user :
916+ try :
917+ # Don't use the RelatedManager since it will execute a hard
918+ # delete
919+ PatchAttentionSet .objects .get (
920+ patch = patch , user = submitter .user
921+ ).delete ()
922+ except PatchAttentionSet .DoesNotExist :
923+ pass
924+
925+
926+ models .signals .post_save .connect (
927+ _remove_user_from_patch_attention_set , sender = PatchComment
928+ )
929+
930+
830931class Series (FilenameMixin , models .Model ):
831932 """A collection of patches."""
832933
0 commit comments