77
88import email .parser
99
10+ from django .contrib .auth .models import User
1011from django .core .exceptions import ValidationError
1112from django .utils .text import slugify
1213from django .utils .translation import gettext_lazy as _
1516from rest_framework .generics import ListAPIView
1617from rest_framework .generics import RetrieveUpdateAPIView
1718from rest_framework .relations import RelatedField
19+ from rest_framework .response import Response
1820from rest_framework .reverse import reverse
1921from rest_framework .serializers import SerializerMethodField
2022from rest_framework import status
2830from patchwork .api .embedded import UserSerializer
2931from patchwork .api .filters import PatchFilterSet
3032from patchwork .models import Patch
33+ from patchwork .models import PatchAttentionSet
3134from patchwork .models import PatchRelation
3235from patchwork .models import State
3336from patchwork .parser import clean_subject
@@ -76,12 +79,27 @@ class PatchConflict(APIException):
7679 )
7780
7881
82+ class PatchAttentionSetSerializer (BaseHyperlinkedModelSerializer ):
83+ user = UserSerializer ()
84+
85+ class Meta :
86+ model = PatchAttentionSet
87+ fields = [
88+ 'user' ,
89+ 'last_updated' ,
90+ ]
91+
92+
7993class PatchListSerializer (BaseHyperlinkedModelSerializer ):
8094 web_url = SerializerMethodField ()
8195 project = ProjectSerializer (read_only = True )
8296 state = StateField ()
8397 submitter = PersonSerializer (read_only = True )
8498 delegate = UserSerializer (allow_null = True )
99+ attention_set = PatchAttentionSetSerializer (
100+ source = 'patchattentionset_set' ,
101+ many = True ,
102+ )
85103 mbox = SerializerMethodField ()
86104 series = SeriesSerializer (read_only = True )
87105 comments = SerializerMethodField ()
@@ -170,6 +188,7 @@ class Meta:
170188 'hash' ,
171189 'submitter' ,
172190 'delegate' ,
191+ 'attention_set' ,
173192 'mbox' ,
174193 'series' ,
175194 'comments' ,
@@ -201,6 +220,7 @@ class Meta:
201220 'list_archive_url' ,
202221 'related' ,
203222 ),
223+ '1.4' : ('attention_set' ,),
204224 }
205225 extra_kwargs = {
206226 'url' : {'view_name' : 'api-patch-detail' },
@@ -228,16 +248,7 @@ def get_headers(self, patch):
228248 def get_prefixes (self , instance ):
229249 return clean_subject (instance .name )[1 ]
230250
231- def update (self , instance , validated_data ):
232- # d-r-f cannot handle writable nested models, so we handle that
233- # specifically ourselves and let d-r-f handle the rest
234- if 'related' not in validated_data :
235- return super (PatchDetailSerializer , self ).update (
236- instance , validated_data
237- )
238-
239- related = validated_data .pop ('related' )
240-
251+ def update_related (self , instance , related ):
241252 # Validation rules
242253 # ----------------
243254 #
@@ -278,9 +289,7 @@ def update(self, instance, validated_data):
278289 if instance .related and instance .related .patches .count () == 2 :
279290 instance .related .delete ()
280291 instance .related = None
281- return super (PatchDetailSerializer , self ).update (
282- instance , validated_data
283- )
292+ return
284293
285294 # break before make
286295 relations = {patch .related for patch in patches if patch .related }
@@ -304,6 +313,14 @@ def update(self, instance, validated_data):
304313 instance .related = relation
305314 instance .save ()
306315
316+ def update (self , instance , validated_data ):
317+ # d-r-f cannot handle writable nested models, so we handle that
318+ # specifically ourselves and let d-r-f handle the rest
319+
320+ if 'related' in validated_data :
321+ related = validated_data .pop ('related' )
322+ self .update_related (instance , related )
323+
307324 return super (PatchDetailSerializer , self ).update (
308325 instance , validated_data
309326 )
@@ -367,6 +384,7 @@ def get_queryset(self):
367384 'project' ,
368385 'series__project' ,
369386 'related__patches__project' ,
387+ 'patchattentionset_set' ,
370388 )
371389 .select_related ('state' , 'submitter' , 'series' )
372390 .defer ('content' , 'diff' , 'headers' )
@@ -381,11 +399,16 @@ class PatchDetail(RetrieveUpdateAPIView):
381399 patch:
382400 Update a patch.
383401
402+ Users can set their intention to review or comment about a patch using the
403+ `attention_set` property. Users can set their intentions by adding their
404+ IDs or its negative value to the list. Maintainers can remove people from
405+ the list but only a user can add itself.
406+
407+
384408 put:
385409 Update a patch.
386410 """
387411
388- permission_classes = (PatchworkPermission ,)
389412 serializer_class = PatchDetailSerializer
390413
391414 def get_queryset (self ):
@@ -396,3 +419,62 @@ def get_queryset(self):
396419 'project' , 'state' , 'submitter' , 'delegate' , 'series'
397420 )
398421 )
422+
423+ def partial_update (self , request , * args , ** kwargs ):
424+ obj = self .get_object ()
425+ req_user_id = request .user .id
426+ is_maintainer = request .user .is_authenticated and (
427+ obj .project in request .user .profile .maintainer_projects .all ()
428+ )
429+
430+ if 'attention_set' in request .data and request .method in ('PATCH' ,):
431+ attention_set = request .data .get ('attention_set' , None )
432+ del request .data ['attention_set' ]
433+ removal_list = [
434+ - user_id for user_id in set (attention_set ) if user_id < 0
435+ ]
436+ addition_list = [
437+ user_id for user_id in set (attention_set ) if user_id > 0
438+ ]
439+
440+ if not addition_list and not removal_list :
441+ removal_list = [req_user_id ]
442+
443+ if len (addition_list ) > 1 or (
444+ addition_list and req_user_id not in addition_list
445+ ):
446+ raise PermissionDenied (
447+ detail = "Only the user can declare it's own intention of "
448+ 'reviewing a patch'
449+ )
450+
451+ if not is_maintainer :
452+ if removal_list and req_user_id not in removal_list :
453+ raise PermissionDenied (
454+ detail = "Only the user can remove it's own "
455+ 'intention of reviewing a patch'
456+ )
457+
458+ try :
459+ if addition_list :
460+ PatchAttentionSet .objects .upsert (obj , addition_list )
461+ if removal_list :
462+ PatchAttentionSet .objects .soft_delete (
463+ obj , removal_list , reason = f'removed by { request .user } '
464+ )
465+ except User .DoesNotExist :
466+ return Response (
467+ {'message' : 'Unable to find referenced user' },
468+ status = 404 ,
469+ )
470+
471+ if not is_maintainer :
472+ serializer = self .get_serializer (obj )
473+ return Response (
474+ serializer .data ,
475+ status = 200 ,
476+ )
477+
478+ return super (PatchDetail , self ).partial_update (
479+ request , * args , ** kwargs
480+ )
0 commit comments