[RFC PATCH 3/4] api: modularize patch relations handling
Raxel Gutierrez
raxel at google.com
Tue Aug 24 08:49:48 AEST 2021
Made a mistake with making update_patch_relations modular. Tests are
failing. The following changes should be made to this patch to fix:
- patches = set(related_patches) if related_patches else set()
+ patches = set(related_patches['patches']) if related_patches else set()
if patch.related is not None:
patches = patches.union(patch.related.patches.all())
check_user_maintains_all(user, patches)
# handle deletion
- if not related_patches:
+ if not related_patches['patches']:
On Mon, Aug 23, 2021 at 10:58 AM Raxel Gutierrez <raxel at google.com> wrote:
>
> Move function that handles updates for patch relations for REST API
> calls outside of DRF serialized so that it can be reused by the patch
> relations table through form submission. The function is used in an
> upcoming patch that deals with the manual modification of the patch
> relations of a given patch.
>
> Signed-off-by: Raxel Gutierrez <raxel at google.com>
> ---
> patchwork/api/patch.py | 161 +++++++++++++++++++++--------------------
> 1 file changed, 84 insertions(+), 77 deletions(-)
>
> diff --git a/patchwork/api/patch.py b/patchwork/api/patch.py
> index a97a8820..ff7640b8 100644
> --- a/patchwork/api/patch.py
> +++ b/patchwork/api/patch.py
> @@ -70,6 +70,87 @@ class PatchConflict(APIException):
> )
>
>
> +def check_user_maintains_all(user, patches):
> + maintains = user.maintainer_projects.all()
> + if any(s.project not in maintains for s in patches):
> + detail = (
> + 'At least one patch is part of a project you are not '
> + 'maintaining.'
> + )
> + raise PermissionDenied(detail=detail)
> + return True
> +
> +
> +def update_patch_relations(user, patch, data):
> + # Validation rules
> + # ----------------
> + #
> + # Permissions: to change a relation:
> + # for all patches in the relation, current and proposed,
> + # the user must be maintainer of the patch's project
> + # Note that this has a ratchet effect: if you add a cross-project
> + # relation, only you or another maintainer across both projects can
> + # modify that relationship in _any way_.
> + #
> + # Break before Make: a patch must be explicitly removed from a
> + # relation before being added to another
> + #
> + # No Read-Modify-Write for deletion:
> + # to delete a patch from a relation, clear _its_ related patch,
> + # don't modify one of the patches that are to remain.
> + #
> + # (As a consequence of those two, operations are additive:
> + # if 1 is in a relation with [1,2,3], then
> + # patching 1 with related=[2,4] gives related=[1,2,3,4])
> +
> + # Permissions:
> + # Must be maintainer of:
> + # - current patch
> + check_user_maintains_all(user, [patch])
> + # - all patches currently in relation
> + # - all patches proposed to be in relation
> + errors = []
> + related_patches = data.pop('related')
> + patches = set(related_patches) if related_patches else set()
> + if patch.related is not None:
> + patches = patches.union(patch.related.patches.all())
> + check_user_maintains_all(user, patches)
> +
> + # handle deletion
> + if not related_patches:
> + # do not allow relations with a single patch
> + if patch.related and patch.related.patches.count() == 2:
> + patch.related.delete()
> + patch.related = None
> + patch.save()
> + return errors
> +
> + # break before make
> + relations = {patch.related for patch in patches if patch.related}
> + if len(relations) > 1:
> + return [PatchConflict()]
> + if relations:
> + relation = relations.pop()
> + else:
> + relation = None
> + if relation and patch.related is not None:
> + if patch.related != relation:
> + return [PatchConflict()]
> +
> + # apply
> + if relation is None:
> + relation = PatchRelation()
> + relation.save()
> +
> + for rp in patches:
> + rp.related = relation
> + rp.save()
> + patch.related = relation
> + patch.save()
> +
> + return errors
> +
> +
> class PatchListSerializer(BaseHyperlinkedModelSerializer):
>
> web_url = SerializerMethodField()
> @@ -184,87 +265,13 @@ class PatchDetailSerializer(PatchListSerializer):
> return super(PatchDetailSerializer, self).update(
> instance, validated_data)
>
> - related = validated_data.pop('related')
> -
> - # Validation rules
> - # ----------------
> - #
> - # Permissions: to change a relation:
> - # for all patches in the relation, current and proposed,
> - # the user must be maintainer of the patch's project
> - # Note that this has a ratchet effect: if you add a cross-project
> - # relation, only you or another maintainer across both projects can
> - # modify that relationship in _any way_.
> - #
> - # Break before Make: a patch must be explicitly removed from a
> - # relation before being added to another
> - #
> - # No Read-Modify-Write for deletion:
> - # to delete a patch from a relation, clear _its_ related patch,
> - # don't modify one of the patches that are to remain.
> - #
> - # (As a consequence of those two, operations are additive:
> - # if 1 is in a relation with [1,2,3], then
> - # patching 1 with related=[2,4] gives related=[1,2,3,4])
> -
> - # Permissions:
> - # Because we're in a serializer, not a view, this is a bit clunky
> user = self.context['request'].user.profile
> - # Must be maintainer of:
> - # - current patch
> - self.check_user_maintains_all(user, [instance])
> - # - all patches currently in relation
> - # - all patches proposed to be in relation
> - patches = set(related['patches']) if related else {}
> - if instance.related is not None:
> - patches = patches.union(instance.related.patches.all())
> - self.check_user_maintains_all(user, patches)
> -
> - # handle deletion
> - if not related['patches']:
> - # do not allow relations with a single patch
> - if instance.related and instance.related.patches.count() == 2:
> - instance.related.delete()
> - instance.related = None
> - return super(PatchDetailSerializer, self).update(
> - instance, validated_data)
> -
> - # break before make
> - relations = {patch.related for patch in patches if patch.related}
> - if len(relations) > 1:
> - raise PatchConflict()
> - if relations:
> - relation = relations.pop()
> - else:
> - relation = None
> - if relation and instance.related is not None:
> - if instance.related != relation:
> - raise PatchConflict()
> -
> - # apply
> - if relation is None:
> - relation = PatchRelation()
> - relation.save()
> - for patch in patches:
> - patch.related = relation
> - patch.save()
> - instance.related = relation
> - instance.save()
> -
> + errors = update_patch_relations(user, instance, validated_data)
> + if errors:
> + raise errors.pop()
> return super(PatchDetailSerializer, self).update(
> instance, validated_data)
>
> - @staticmethod
> - def check_user_maintains_all(user, patches):
> - maintains = user.maintainer_projects.all()
> - if any(s.project not in maintains for s in patches):
> - detail = (
> - 'At least one patch is part of a project you are not '
> - 'maintaining.'
> - )
> - raise PermissionDenied(detail=detail)
> - return True
> -
> class Meta:
> model = Patch
> fields = PatchListSerializer.Meta.fields + (
> --
> 2.33.0.rc2.250.ged5fa647cd-goog
>
More information about the Patchwork
mailing list