By adding a `subject_prefix` settings to Project. Mail will be assigned to project if List-Id match and prefix is present.
Signed-off-by: Philippe Pepiot <p...@philpep.org> --- patchwork/migrations/0020_auto_20170527_1119.py | 37 ++++++++++++++++++++ patchwork/models.py | 6 +++- patchwork/parser.py | 36 +++++++++++++------- patchwork/tests/test_parser.py | 45 +++++++++++++++++++++---- 4 files changed, 104 insertions(+), 20 deletions(-) create mode 100644 patchwork/migrations/0020_auto_20170527_1119.py diff --git a/patchwork/migrations/0020_auto_20170527_1119.py b/patchwork/migrations/0020_auto_20170527_1119.py new file mode 100644 index 0000000..59165e3 --- /dev/null +++ b/patchwork/migrations/0020_auto_20170527_1119.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-05-27 11:19 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('patchwork', '0019_userprofile_show_ids'), + ] + + operations = [ + migrations.AlterModelOptions( + name='patch', + options={'base_manager_name': 'objects', 'verbose_name_plural': 'Patches'}, + ), + migrations.AlterModelOptions( + name='series', + options={'ordering': ('date',), 'verbose_name_plural': 'Series'}, + ), + migrations.AddField( + model_name='project', + name='subject_prefix', + field=models.CharField(blank=True, help_text=b'An optional prefix that mail subject must match. This allow to handle multiple project under the same list ID', max_length=255), + ), + migrations.AlterField( + model_name='project', + name='listid', + field=models.CharField(max_length=255), + ), + migrations.AlterUniqueTogether( + name='project', + unique_together=set([('listid', 'subject_prefix')]), + ), + ] diff --git a/patchwork/models.py b/patchwork/models.py index ee66ace..148c1f7 100644 --- a/patchwork/models.py +++ b/patchwork/models.py @@ -67,8 +67,11 @@ 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_prefix = models.CharField(max_length=255, blank=True, help_text=( + 'An optional prefix that mail subject must match. This allow to ' + 'handle multiple project under the same list ID')) # url metadata @@ -97,6 +100,7 @@ class Project(models.Model): class Meta: ordering = ['linkname'] + unique_together = (('listid', 'subject_prefix')) @python_2_unicode_compatible diff --git a/patchwork/parser.py b/patchwork/parser.py index 6d4640a..e5af55b 100644 --- a/patchwork/parser.py +++ b/patchwork/parser.py @@ -136,17 +136,26 @@ 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: - pass - return project +def find_project_by_id(list_id, prefixes): + """Find a `project` object with given `list_id` and optionally matching + given `prefixes`. + + If no prefixes matches, return the project with blank prefix. + If multiple prefixes matches, display a warning and return the project + matching the first prefix. + """ + prefixes = prefixes + [''] + projects = Project.objects.filter(listid=list_id, + subject_prefix__in=prefixes) + projects = sorted(projects, key=lambda p: prefixes.index(p.subject_prefix)) + if not projects: + return + if len([p for p in projects if p.subject_prefix != '']) > 1: + logger.warning('Find multiple projects matching prefixes %s', prefixes) + return projects[0] -def find_project_by_header(mail): +def find_project_by_header(mail, prefixes): project = None listid_res = [re.compile(r'.*<([^>]+)>.*', re.S), re.compile(r'^([\S]+)$', re.S)] @@ -164,7 +173,7 @@ def find_project_by_header(mail): listid = match.group(1) - project = find_project_by_id(listid) + project = find_project_by_id(listid, prefixes) if project: break @@ -789,10 +798,12 @@ def parse_mail(mail, list_id=None): logger.debug("Ignoring email due to 'ignore' hint") return + subject = mail.get('Subject') + prefixes = clean_subject(subject)[1] if list_id: - project = find_project_by_id(list_id) + project = find_project_by_id(list_id, prefixes) else: - project = find_project_by_header(mail) + project = find_project_by_header(mail, prefixes) if project is None: logger.error('Failed to find a project for email') @@ -802,7 +813,6 @@ def parse_mail(mail, list_id=None): msgid = mail.get('Message-Id').strip() author = find_author(mail) - subject = mail.get('Subject') name, prefixes = clean_subject(subject, [project.linkname]) is_comment = subject_check(subject) x, n = parse_series_marker(prefixes) diff --git a/patchwork/tests/test_parser.py b/patchwork/tests/test_parser.py index 63231fa..0430f52 100644 --- a/patchwork/tests/test_parser.py +++ b/patchwork/tests/test_parser.py @@ -477,6 +477,39 @@ class MultipleProjectPatchCommentTest(MultipleProjectPatchTest): Comment.objects.filter(submission=patch).count(), 1) +class MultipleProjectSameListTest(TestCase): + """Test that multiple projects can be handled by the same mailing-list with + `subject_prefix` setting""" + + def setUp(self): + self.p1 = create_project(listid='test.example.com') + self.p2 = create_project(listid='test.example.com', + subject_prefix='foo') + self.p3 = create_project(listid='test.example.com', + subject_prefix='bar') + + @staticmethod + def _parse_mail(subject): + email = create_email(content=SAMPLE_DIFF, subject=subject) + return parse_mail(email) + + def test_prefix_matching(self): + patch = self._parse_mail('[PATCH foo foobar] foo bar') + self.assertEqual(patch.project, self.p2) + patch = self._parse_mail('[PATCH foobar bar] foo bar') + self.assertEqual(patch.project, self.p3) + + def test_prefix_not_matching(self): + patch = self._parse_mail('[PATCH foobar] foo bar') + self.assertEqual(patch.project, self.p1) + + def test_multiple_matching(self): + patch = self._parse_mail('[PATCH foo foobar bar] meep') + self.assertEqual(patch.project, self.p2) + patch = self._parse_mail('[PATCH bar foobar foo] meep') + self.assertEqual(patch.project, self.p3) + + class ListIdHeaderTest(TestCase): """Test that we parse List-Id headers from mails correctly.""" @@ -485,25 +518,25 @@ class ListIdHeaderTest(TestCase): def test_no_list_id(self): email = MIMEText('') - project = find_project_by_header(email) + project = find_project_by_header(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_by_header(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_by_header(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_by_header(email, []) self.assertEqual(project, None) def test_short_list_id(self): @@ -511,13 +544,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_by_header(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_by_header(email, []) self.assertEqual(project, self.project) -- 2.1.4 _______________________________________________ Patchwork mailing list Patchwork@lists.ozlabs.org https://lists.ozlabs.org/listinfo/patchwork