On Wed, 2018-02-14 at 14:34 +0100, vkaba...@redhat.com wrote: > From: Veronika Kabatova <vkaba...@redhat.com> > > Sometimes, multiple projects reside at the same mailing list. So far, > Patchwork only allowed a single project per mailing list, which made it > impossible for these projects to use Patchwork (unless they did some > dirty hacks). > > Add a new property `subject_match` to projects and implement filtering > on (list_id, subject_match) match instead of solely list_id. Instance > admin can specify a regex on a per-project basis when the project is > created. > > Signed-off-by: Veronika Kabatova <vkaba...@redhat.com>
Both of these make perfect sense to me. I'm still not sure about using a regex, due to the security implications of same, but this is an admin-only interface so meh. Reviewed-by: Stephen Finucane <step...@that.guru> and applied. > --- > Changes since v1: > - allow default project (with empty subject match), have help string > reflect that > - add testcase (patch 2/2) > - --list-id option now respects subject matching too -- unify > find_project_by_id() and find_project_by_header() functions > --- > docs/api.yaml | 3 ++ > patchwork/api/project.py | 5 +-- > .../0022_add_subject_match_to_project.py | 29 +++++++++++++++ > patchwork/models.py | 10 +++++- > patchwork/parser.py | 41 > ++++++++++++++-------- > patchwork/tests/test_parser.py | 14 ++++---- > .../notes/list-filtering-4643d98b4064367a.yaml | 11 ++++++ > 7 files changed, 88 insertions(+), 25 deletions(-) > create mode 100644 patchwork/migrations/0022_add_subject_match_to_project.py > create mode 100644 releasenotes/notes/list-filtering-4643d98b4064367a.yaml > > diff --git a/docs/api.yaml b/docs/api.yaml > index 3e79f0b..3373226 100644 > --- a/docs/api.yaml > +++ b/docs/api.yaml > @@ -374,6 +374,9 @@ definitions: > list_id: > type: string > description: Mailing list identifier for project. > + subject_match: > + type: string > + description: Regex used for email filtering. > list_email: > type: string > description: Mailing list email address for project. > diff --git a/patchwork/api/project.py b/patchwork/api/project.py > index 446c473..597f605 100644 > --- a/patchwork/api/project.py > +++ b/patchwork/api/project.py > @@ -39,8 +39,9 @@ class ProjectSerializer(HyperlinkedModelSerializer): > class Meta: > model = Project > fields = ('id', 'url', 'name', 'link_name', 'list_id', 'list_email', > - 'web_url', 'scm_url', 'webscm_url', 'maintainers') > - read_only_fields = ('name', 'maintainers') > + 'web_url', 'scm_url', 'webscm_url', 'maintainers', > + 'subject_match') > + read_only_fields = ('name', 'maintainers', 'subject_match') > extra_kwargs = { > 'url': {'view_name': 'api-project-detail'}, > } > diff --git a/patchwork/migrations/0022_add_subject_match_to_project.py > b/patchwork/migrations/0022_add_subject_match_to_project.py > new file mode 100644 > index 0000000..cef3fb6 > --- /dev/null > +++ b/patchwork/migrations/0022_add_subject_match_to_project.py > @@ -0,0 +1,29 @@ > +# -*- coding: utf-8 -*- > +# Generated by Django 1.10.8 on 2018-01-19 18:16 > +from __future__ import unicode_literals > + > +from django.db import migrations, models > + > + > +class Migration(migrations.Migration): > + > + dependencies = [ > + ('patchwork', '0021_django_1_10_fixes'), > + ] > + > + operations = [ > + migrations.AddField( > + model_name='project', > + name='subject_match', > + field=models.CharField(blank=True, default=b'', > help_text=b'Regex to match the subject against if only part of emails sent to > the list belongs to this project. Will be used with IGNORECASE and MULTILINE > flags. If rules for more projects match the first one returned from DB is > chosen; empty field serves as a default for every email which has no other > match.', max_length=64), > + ), > + migrations.AlterField( > + model_name='project', > + name='listid', > + field=models.CharField(max_length=255), > + ), > + migrations.AlterUniqueTogether( > + name='project', > + unique_together=set([('listid', 'subject_match')]), > + ), > + ] > diff --git a/patchwork/models.py b/patchwork/models.py > index a8bb015..581dbf7 100644 > --- a/patchwork/models.py > +++ b/patchwork/models.py > @@ -71,8 +71,15 @@ class Project(models.Model): > > linkname = models.CharField(max_length=255, unique=True) > name = models.CharField(max_length=255, unique=True) > - listid = models.CharField(max_length=255, unique=True) > + listid = models.CharField(max_length=255) > listemail = models.CharField(max_length=200) > + subject_match = models.CharField( > + max_length=64, blank=True, default='', help_text='Regex to match the > ' > + 'subject against if only part of emails sent to the list belongs to ' > + 'this project. Will be used with IGNORECASE and MULTILINE flags. If ' > + 'rules for more projects match the first one returned from DB is ' > + 'chosen; empty field serves as a default for every email which has > no ' > + 'other match.') > > # url metadata > > @@ -100,6 +107,7 @@ class Project(models.Model): > return self.name > > class Meta: > + unique_together = (('listid', 'subject_match'),) > ordering = ['linkname'] > > > diff --git a/patchwork/parser.py b/patchwork/parser.py > index 2cabb3c..9935397 100644 > --- a/patchwork/parser.py > +++ b/patchwork/parser.py > @@ -150,17 +150,30 @@ def clean_header(header): > return normalise_space(header_str) > > > -def find_project_by_id(list_id): > - """Find a `project` object with given `list_id`.""" > - project = None > - try: > - project = Project.objects.get(listid=list_id) > - except Project.DoesNotExist: > - logger.debug("'%s' if not a valid project list-id", list_id) > - return project > +def find_project_by_id_and_subject(list_id, subject): > + """Find a `project` object based on `list_id` and subject match. > + Since empty `subject_match` field matches everything, project with > + given `list_id` and empty `subject_match` field serves as a default > + (in case it exists) if no other match is found. > + """ > + projects = Project.objects.filter(listid=list_id) > + default = None > + for project in projects: > + if not project.subject_match: > + default = project > + elif re.search(project.subject_match, subject, > + re.MULTILINE | re.IGNORECASE): > + return project > + > + return default > > > -def find_project_by_header(mail): > +def find_project(mail, list_id=None): > + clean_subject = clean_header(mail.get('Subject', '')) > + > + if list_id: > + return find_project_by_id_and_subject(list_id, clean_subject) > + > project = None > listid_res = [re.compile(r'.*<([^>]+)>.*', re.S), > re.compile(r'^([\S]+)$', re.S)] > @@ -181,12 +194,13 @@ def find_project_by_header(mail): > > listid = match.group(1) > > - project = find_project_by_id(listid) > + project = find_project_by_id_and_subject(listid, clean_subject) > if project: > break > > if not project: > - logger.debug("Could not find a list-id in mail headers") > + logger.debug("Could not find a valid project for given list-id and " > + "subject.") > > return project > > @@ -923,10 +937,7 @@ def parse_mail(mail, list_id=None): > logger.debug("Ignoring email due to 'ignore' hint") > return > > - if list_id: > - project = find_project_by_id(list_id) > - else: > - project = find_project_by_header(mail) > + project = find_project(mail, list_id) > > if project is None: > logger.error('Failed to find a project for email') > diff --git a/patchwork/tests/test_parser.py b/patchwork/tests/test_parser.py > index 20d70af..abe11ad 100644 > --- a/patchwork/tests/test_parser.py > +++ b/patchwork/tests/test_parser.py > @@ -36,7 +36,7 @@ from patchwork.models import State > from patchwork.parser import clean_subject > from patchwork.parser import find_author > from patchwork.parser import find_patch_content as find_content > -from patchwork.parser import find_project_by_header > +from patchwork.parser import find_project > from patchwork.parser import find_series > from patchwork.parser import parse_mail as _parse_mail > from patchwork.parser import parse_pull_request > @@ -496,25 +496,25 @@ class ListIdHeaderTest(TestCase): > > def test_no_list_id(self): > email = MIMEText('') > - project = find_project_by_header(email) > + project = find_project(email) > self.assertEqual(project, None) > > def test_blank_list_id(self): > email = MIMEText('') > email['List-Id'] = '' > - project = find_project_by_header(email) > + project = find_project(email) > self.assertEqual(project, None) > > def test_whitespace_list_id(self): > email = MIMEText('') > email['List-Id'] = ' ' > - project = find_project_by_header(email) > + project = find_project(email) > self.assertEqual(project, None) > > def test_substring_list_id(self): > email = MIMEText('') > email['List-Id'] = 'example.com' > - project = find_project_by_header(email) > + project = find_project(email) > self.assertEqual(project, None) > > def test_short_list_id(self): > @@ -522,13 +522,13 @@ class ListIdHeaderTest(TestCase): > is only the list ID itself (without enclosing angle-brackets). """ > email = MIMEText('') > email['List-Id'] = self.project.listid > - project = find_project_by_header(email) > + project = find_project(email) > self.assertEqual(project, self.project) > > def test_long_list_id(self): > email = MIMEText('') > email['List-Id'] = 'Test text <%s>' % self.project.listid > - project = find_project_by_header(email) > + project = find_project(email) > self.assertEqual(project, self.project) > > > diff --git a/releasenotes/notes/list-filtering-4643d98b4064367a.yaml > b/releasenotes/notes/list-filtering-4643d98b4064367a.yaml > new file mode 100644 > index 0000000..0f849a7 > --- /dev/null > +++ b/releasenotes/notes/list-filtering-4643d98b4064367a.yaml > @@ -0,0 +1,11 @@ > +--- > +features: > + - | > + Allow list filtering into multiple projects (and email dropping) based on > + subject prefixes. Enable by specifying a regular expression which needs > to > + be matched in the subject on a per-project basis (field `subject_match`). > + Project with empty `subject_match` field (and matching `list_id`) serves > as > + a default in case of no match. > +api: > + - | > + Expose `subject_match` in REST API. _______________________________________________ Patchwork mailing list Patchwork@lists.ozlabs.org https://lists.ozlabs.org/listinfo/patchwork