From 2ac29514e2284a88382b0e02feab607f9f43f1a7 Mon Sep 17 00:00:00 2001
From: Patrick Uiterwijk <puiterwijk@redhat.com>
Date: Feb 14 2017 07:57:11 +0000
Subject: [PATCH 1/3] Parse issue search strings for key:value arguments for custom fields searching


Signed-off-by: Patrick Uiterwijk <puiterwijk@redhat.com>

---

diff --git a/pagure/lib/__init__.py b/pagure/lib/__init__.py
index 6731d21..4657093 100644
--- a/pagure/lib/__init__.py
+++ b/pagure/lib/__init__.py
@@ -3863,3 +3863,41 @@ def add_metadata_update_notif(session, issue, messages, user, ticketfolder):
     session.add(issue_comment)
     # Make sure we won't have SQLAlchemy error before we continue
     session.commit()
+
+
+def tokenize_search_string(pattern):
+    """This function tokenizes search patterns into key:value and rest.
+
+    It will also correctly parse key values between quotes.
+    """
+    if pattern is None:
+        return {}, None
+    def finalize_token(token, custom_search):
+        if ':' in token:
+            # This was a "key:value" parameter
+            key, value = token.split(':', 1)
+            custom_search[key] = value
+            return ''
+        else:
+            # This was a token without colon, thus a search pattern
+            return '%s ' % token
+
+    custom_search = {}
+    # Remaining is the remaining real search_pattern (aka, non-key:values)
+    remaining = ''
+    # Token is the current "search token" we are processing
+    token = ''
+    in_quotes = False
+    for char in pattern:
+        if char == ' ' and not in_quotes:
+            remaining += finalize_token(token, custom_search)
+            token = ''
+        elif char == '"':
+            in_quotes = not in_quotes
+        else:
+            token += char
+
+    # Parse the final token
+    remaining += finalize_token(token, custom_search)
+
+    return custom_search, remaining.strip()
diff --git a/pagure/ui/issues.py b/pagure/ui/issues.py
index 3220229..710b659 100644
--- a/pagure/ui/issues.py
+++ b/pagure/ui/issues.py
@@ -603,6 +603,11 @@ def view_issues(repo, username=None, namespace=None):
         for idx, key in enumerate(custom_keys):
             custom_search[key] = custom_values[idx]
 
+    search_string = search_pattern
+    extra_fields, search_pattern = pagure.lib.tokenize_search_string(
+        search_pattern)
+    custom_search.update(extra_fields)
+
     repo = flask.g.repo
 
     if not repo.settings.get('issue_tracker', True):
@@ -710,7 +715,7 @@ def view_issues(repo, username=None, namespace=None):
         priority=priority,
         total_page=total_page,
         add_report_form=pagure.forms.AddReportForm(),
-        search_pattern=search_pattern,
+        search_pattern=search_string,
     )
 
 

From bc617a6299d829f69f39fcdda1def52e6802fa80 Mon Sep 17 00:00:00 2001
From: Patrick Uiterwijk <puiterwijk@redhat.com>
Date: Feb 14 2017 07:57:11 +0000
Subject: [PATCH 2/3] Add test for tokenization function


Signed-off-by: Patrick Uiterwijk <puiterwijk@redhat.com>

---

diff --git a/tests/test_pagure_lib.py b/tests/test_pagure_lib.py
index 7de24ad..ddbcbc8 100644
--- a/tests/test_pagure_lib.py
+++ b/tests/test_pagure_lib.py
@@ -3214,6 +3214,25 @@ class PagureLibtests(tests.Modeltests):
         self.assertEqual(out, 'You are no longer watching this issue')
 
 
+    def test_tokenize_search_string(self):
+        """ Test the tokenize_search_string function. """
+        # These are the tests performed to make sure we tokenize correctly.
+        # This is in the form: input string, custom fields, remaining pattern
+        tests = [
+            ('test123', {}, 'test123'),
+            ('test:key test123', {'test': 'key'}, 'test123'),
+            ('test:"key with spaces" test123', {'test': 'key with spaces'},
+             'test123'),
+            ('test123 test:key test456', {'test': 'key'}, 'test123 test456'),
+            ('test123 test:"key with spaces" key2:value12 test456',
+             {'test': 'key with spaces', 'key2': 'value12'},
+             'test123 test456')
+            ]
+        for inp, flds, rem in tests:
+            self.assertEqual(pagure.lib.tokenize_search_string(inp),
+                             (flds, rem))
+
+
 if __name__ == '__main__':
     SUITE = unittest.TestLoader().loadTestsFromTestCase(PagureLibtests)
     unittest.TextTestRunner(verbosity=2).run(SUITE)

From 5ee678277e81aeddd6691172d57c752fd035bc0e Mon Sep 17 00:00:00 2001
From: Patrick Uiterwijk <puiterwijk@redhat.com>
Date: Feb 14 2017 07:57:11 +0000
Subject: [PATCH 3/3] Add test for custom issue fields searching


Signed-off-by: Patrick Uiterwijk <puiterwijk@redhat.com>

---

diff --git a/tests/test_pagure_flask_ui_issues.py b/tests/test_pagure_flask_ui_issues.py
index fcb0d26..2cc2dd6 100644
--- a/tests/test_pagure_flask_ui_issues.py
+++ b/tests/test_pagure_flask_ui_issues.py
@@ -297,8 +297,24 @@ class PagureFlaskIssuestests(tests.Modeltests):
         self.assertTrue(
             '<h2>\n      0 Open Issues' in output.data)
 
-        # Create issues to play with
         repo = pagure.lib.get_project(self.session, 'test')
+        # Create some custom fields to play with
+        msg = pagure.lib.set_custom_key_fields(
+            session=self.session,
+            project=repo,
+            fields=['test1'],
+            types=['text'],
+            data=[None]
+        )
+        self.session.commit()
+        self.assertEqual(msg, 'List of custom fields updated')
+
+        cfield = pagure.lib.get_custom_key(
+            session=self.session,
+            project=repo,
+            keyname='test1')
+
+        # Create issues to play with
         msg = pagure.lib.new_issue(
             session=self.session,
             repo=repo,
@@ -310,6 +326,14 @@ class PagureFlaskIssuestests(tests.Modeltests):
         self.session.commit()
         self.assertEqual(msg.title, 'Test issue')
 
+        msg = pagure.lib.set_custom_key_value(
+            session=self.session,
+            issue=msg,
+            key=cfield,
+            value='firstissue')
+        self.session.commit()
+        self.assertEqual(msg, 'Custom field test1 adjusted to firstissue')
+
         msg = pagure.lib.new_issue(
             session=self.session,
             repo=repo,
@@ -323,6 +347,14 @@ class PagureFlaskIssuestests(tests.Modeltests):
         self.session.commit()
         self.assertEqual(msg.title, 'Test invalid issue')
 
+        msg = pagure.lib.set_custom_key_value(
+            session=self.session,
+            issue=msg,
+            key=cfield,
+            value='second issue')
+        self.session.commit()
+        self.assertEqual(msg, 'Custom field test1 adjusted to second issue')
+
         # Whole list
         output = self.app.get('/test/issues')
         self.assertEqual(output.status_code, 200)
@@ -359,6 +391,20 @@ class PagureFlaskIssuestests(tests.Modeltests):
         self.assertTrue(
             '<h2>\n      2 Issues' in output.data)
 
+        # Custom key searching
+        output = self.app.get(
+            '/test/issues?status=all&search_pattern=test1:firstissue')
+        self.assertEqual(output.status_code, 200)
+        self.assertIn('<title>Issues - test - Pagure</title>', output.data)
+        self.assertIn('1 Issues', output.data)
+
+        # Custom key searching with space
+        output = self.app.get(
+            '/test/issues?status=all&search_pattern=test1:"second issue"')
+        self.assertEqual(output.status_code, 200)
+        self.assertIn('<title>Issues - test - Pagure</title>', output.data)
+        self.assertIn('1 Issues', output.data)
+
         # All tickets - different pagination
         before = pagure.APP.config['ITEM_PER_PAGE']
         pagure.APP.config['ITEM_PER_PAGE'] = 1

