On Fri, 2021-08-06 at 11:39 +1000, Daniel Axtens wrote: > Stephen Finucane <step...@that.guru> writes: > > > On Tue, 2021-08-03 at 01:27 +1000, Daniel Axtens wrote: > > > In discussions about how to make patchwork more user-friendly and > > > suitable for more projects, we realised that perhaps the current > > > ability for submitters to change their patch state to any value > > > isn't the most appropriate setting for all maintainers, especially > > > in light of increasing automation. > > > > > > Allow a project to stop a submitter from changing the state of > > > their patches. This is not the default but can be set by a patchwork > > > administrator. > > > > Couple of comments below. Unfortunately two of them are of the "I don't know > > about this so can you investigate?" variety. I can help resolve them if > > necessary but I'm hoping you've already done said investigation :-) > > > > Stephen > > > > > Signed-off-by: Daniel Axtens <d...@axtens.net> > > > --- > > > ...45_project_submitter_state_change_rules.py | 24 +++++++++++++ > > > patchwork/models.py | 36 +++++++++++++++++++ > > > patchwork/views/__init__.py | 8 +++++ > > > patchwork/views/patch.py | 14 ++++++-- > > > 4 files changed, 80 insertions(+), 2 deletions(-) > > > create mode 100644 > > > patchwork/migrations/0045_project_submitter_state_change_rules.py > > > > > > diff --git > > > a/patchwork/migrations/0045_project_submitter_state_change_rules.py > > > b/patchwork/migrations/0045_project_submitter_state_change_rules.py > > > new file mode 100644 > > > index 000000000000..9d0b2892bd5c > > > --- /dev/null > > > +++ b/patchwork/migrations/0045_project_submitter_state_change_rules.py > > > @@ -0,0 +1,24 @@ > > > +# Generated by Django 3.1.12 on 2021-08-03 00:32 > > > + > > > +from django.db import migrations, models > > > + > > > + > > > +class Migration(migrations.Migration): > > > + > > > + dependencies = [ > > > + ('patchwork', '0044_add_project_linkname_validation'), > > > + ] > > > + > > > + operations = [ > > > + migrations.AddField( > > > + model_name='project', > > > + name='submitter_state_change_rules', > > > + field=models.SmallIntegerField( > > > + choices=[ > > > + (0, 'Submitters may not change patch states'), > > > + (1, 'Submitters may set any patch state')], > > > + default=1, > > > + help_text='What state changes can patch submitters make?' > > > + ' Does not affect maintainers.'), > > > + ), > > > > This feels like a BooleanField rather than a SmallIntegerField (even if they > > resolve to the same thing on e.g. MySQL 5.x, iirc). afaict, you can pass the > > 'choices' argument for BooleanField too [1]. Any chance we could change > > this? > > > > [1] https://code.djangoproject.com/ticket/9640 > > I picked this because I want to add another mode that permits submitters > to change states but restricts the states that submitters can change > between (so for example allowing them to move around the {New, RFC, > Superseded, Not Applicable, Changes Requested} group but not to mark > their own patches as Accepted). And I don't want to do a migration then > if I can avoid it :)
Is that necessary? What's the use case? fwiw, I'd like to get rid of State objects entirely. I've a long-term goal of being able to mark a Series as open or closed and make Series more of a first class citizen ('git-pw series list' is effectively useless right now), but doing so requires Patches to also have a boolean open/closed state (assuming we don't want the series and patch states to be totally disconnected and for users to be forced to manually manage series states, that is). I've been envisioning two solutions: * Transform the 'Patch.state' field to a combination of a boolean 'Patch.resolved' field and a 'Patch.resolution' field, with the latter being set only if 'Patch.resolved' == 'True' * Transform the 'Patch.state' field to a boolean and add support for patch tags, with the current States all becoming tags I've mostly focused on the latter approach since it seems more useful in the long term. The only reason I haven't finished this work is because each time I try, I get stuck writing a migration and shims for the XML-RPC and REST APIs that don't suck. I also don't know how to integrate them nicely with the "tags" people include in patch subject lines (i.e. '[RFC]' or '[stable-2.7]'). All this is to say that I think what you've proposed right now would for this new boolean-only future but making this a tertiary thing would not, so ideally I'd like to avoid adding this. > > > > > > + ] > > > diff --git a/patchwork/models.py b/patchwork/models.py > > > index 00273da9f5bd..706b912c349a 100644 > > > --- a/patchwork/models.py > > > +++ b/patchwork/models.py > > > @@ -93,6 +93,19 @@ class Project(models.Model): > > > send_notifications = models.BooleanField(default=False) > > > use_tags = models.BooleanField(default=True) > > > + # how much can a patch submitter change? > > > + SUBMITTER_NO_STATE_CHANGES = 0 > > > + SUBMITTER_ALL_STATE_CHANGES = 1 > > > + SUBMITTER_STATE_CHANGE_CHOICES = ( > > > + (SUBMITTER_NO_STATE_CHANGES, 'Submitters may not change patch states'), > > > + (SUBMITTER_ALL_STATE_CHANGES, 'Submitters may set any patch state'), > > > + ) > > > + submitter_state_change_rules = models.SmallIntegerField( > > > + choices=SUBMITTER_STATE_CHANGE_CHOICES, > > > + default=SUBMITTER_ALL_STATE_CHANGES, > > > + help_text='What state changes can patch submitters make?' > > > + ' Does not affect maintainers.') > > > > Ditto. > > > > > + > > > def is_editable(self, user): > > > if not user.is_authenticated: > > > return False > > > @@ -518,6 +531,29 @@ class Patch(SubmissionMixin): > > > return True > > > return False > > > + def can_set_state(self, user): > > > + # an unauthenticated user can never change state > > > + if not user.is_authenticated: > > > + return False > > > + > > > + # a maintainer can always set state > > > + if self.project.is_editable(user): > > > + self._edited_by = user > > > + return True > > > + > > > + # a delegate can always set state > > > + if user == self.delegate: > > > + self._edited_by = user > > > + return True > > > + > > > + # if the state change rules prohibit it, no other user can set change > > > + if (self.project.submitter_state_change_rules == > > > + Project.SUBMITTER_NO_STATE_CHANGES): > > > + return False > > > + > > > + # otherwise, a submitter can change state > > > + return self.is_editable(user) > > > + > > > @staticmethod > > > def filter_unique_checks(checks): > > > """Filter the provided checks to generate the unique list.""" > > > diff --git a/patchwork/views/__init__.py b/patchwork/views/__init__.py > > > index 3efe90cd6929..9f5d316d18b5 100644 > > > --- a/patchwork/views/__init__.py > > > +++ b/patchwork/views/__init__.py > > > @@ -312,6 +312,14 @@ def process_multiplepatch_form(request, form, > > > action, patches, context): > > > % patch.name) > > > continue > > > + field = form.fields.get('state', None) > > > + if field and not field.is_no_change(form.cleaned_data['state']) \ > > > + and not patch.can_set_state(request.user): > > > > style nit: can we wrap this like e.g. > > > > if ( > > field and > > not field.is_no_change(form.cleaned_data['state']) and > > not patch.can_set_state(request.user) > > ): > > > > which is slightly longer vertically but more obvious, IMO. > > Totally, if flake8 will let me do it I agree that that layout makes more > sense. Yup, flake8 will be more than happy so long as you dedent the closing bracket again. > > > I surprised that Django doesn't have a way to do field-level validation. I > > did > > take a quick look and found some extensions for it, but we probably don't > > want > > to bring in those dependencies for this single use case. > > Yeah, it's a bit frustrating because of the amount of context we have to > carry around. > > > > + errors.append( > > > + "You don't have the permissions to set the state of patch '%s'" > > > + % patch.name) > > > + continue > > > + > > > changed_patches += 1 > > > form.save(patch) > > > diff --git a/patchwork/views/patch.py b/patchwork/views/patch.py > > > index 3e6874ae1e5d..72c199135cbb 100644 > > > --- a/patchwork/views/patch.py > > > +++ b/patchwork/views/patch.py > > > @@ -101,8 +101,18 @@ def patch_detail(request, project_id, msgid): > > > elif action is None: > > > form = PatchForm(data=request.POST, instance=patch) > > > if form.is_valid(): > > > - form.save() > > > - messages.success(request, 'Patch updated') > > > + old_patch = Patch.objects.get(id=patch.id) > > > + if old_patch.state != form.cleaned_data['state'] and \ > > > + not old_patch.can_set_state(request.user): > > > > ditto (wrapping) > > > > > + messages.error( > > > + request, > > > + "You don't have the permissions to set the state of " > > > + "patch '%s'" % patch.name) > > > + patch = old_patch > > > + form = PatchForm(instance=patch) > > > > I'm not certain about this, but it seems weird that 'is_valid' is returning > > True > > yet clearly the request isn't valid. Is there no way to extend the form to > > do > > this validation instead? > > > I'll have a look. > > > > + else: > > > + form.save() > > > + messages.success(request, 'Patch updated') > > > if request.user.is_authenticated: > > > context['bundles'] = request.user.bundles.all() Let me know if you want to discuss the State thing in more detail. I have about four different attempts sitting locally, all in various states of incompleteness, so I could probably push these somewhere if you needed a more in-depth view of where my head was at. Stephen > > Kind regards, > Daniel _______________________________________________ Patchwork mailing list Patchwork@lists.ozlabs.org https://lists.ozlabs.org/listinfo/patchwork