#4097 Allow filtering the issue list by the close status
Merged 5 years ago by pingou. Opened 5 years ago by pingou.

file modified
+13
@@ -105,3 +105,16 @@ 

  ~~~~~~~~~

  https://pagure.io/SSSD/sssd/issues?status=Open&search_pattern=review%3ATrue

  https://pagure.io/pagure/issues?status=Open&search_pattern=tags%3Aeasyfix

+ 

+ 

+ Search the comments of issues

+ -----------------------------

+ 

+ One can search all the comments made on an issue tracker using

+ ``content:<keyword>`` in the search field. This is going to search all the

+ comments (including the descriptions) of all the tickets and thus can be quite

+ slow on large project. This is why this feature isn't being pushed much forward.

+ 

+ Examples:

+ ~~~~~~~~~

+ https://pagure.io/pagure/issues?status=Open&search_pattern=content%3Aeasyfix

file modified
+18
@@ -2766,6 +2766,7 @@ 

      offset=None,

      limit=None,

      search_pattern=None,

+     search_content=None,

      custom_search=None,

      updated_after=None,

      no_milestones=None,
@@ -2820,6 +2821,8 @@ 

      :type count: boolean

      :kwarg search_pattern: a string to search in issues title

      :type search_pattern: str or None

+     :kwarg search_content: a string to search in the issues comments

+     :type search_content: str or None

      :kwarg custom_search: a dictionary of key/values to be used when

          searching issues with a custom key constraint

      :type custom_search: dict or None
@@ -3009,6 +3012,21 @@ 

                  sqlalchemy.or_((const for const in constraints))

              )

  

+     if search_content is not None:

+         query = query.outerjoin(

+             model.IssueComment

+         ).filter(

+             sqlalchemy.or_(

+                 model.Issue.content.ilike("%%%s%%" % search_content),

+                 sqlalchemy.and_(

+                     model.Issue.uid == model.IssueComment.issue_uid,

+                     model.IssueComment.comment.ilike(

+                         "%%%s%%" % search_content

+                     ),

+                 ),

+             )

+         )

+ 

      query = session.query(model.Issue).filter(

          model.Issue.uid.in_(query.subquery())

      )

file modified
+89 -56
@@ -216,7 +216,7 @@ 

                                username=username,

                                namespace=repo.namespace,

                                repo=repo.name ) }}" method="GET">

-                           <input type="hidden" name="status" value="{{ status or 'all' }}" />

+                           <input type="hidden" name="status" id="hidden_status" value="{{ status or 'all' }}" />

  

                            <div class="form-group row mb-1" id="tags-filter-group">

                              <label for="tags" class="col-auto align-self-center pl-1 pr-0"><i class="text-muted fa fa-fw fa-tag"></i></label>
@@ -321,6 +321,21 @@ 

                              </div>

                            </div>

  

+                           <div class="form-group row mb-1" id="close_status-filter-group">

+                             <label for="close_status" class="col-auto align-self-center pl-1 pr-0"><i class="text-muted fa fa-fw fa-user"></i></label>

+                             <div class="col pl-1">

+                               <select name="close_status" id="close_status-selectize" placeholder="Closed as">

+                                 <option value="" {% if not close_status %}selected="selected"{% endif %}></option>

+                                 {% for p in repo.close_status | sort %}

+                                   <option value="{{ p }}" {% if p == close_status %}selected="selected"{% endif %}>{{ p }}</option>

+                                 {% endfor %}

+                               </select>

+                             </div>

+                             <div class="col-auto pl-0 pr-1 pt-1">

+                               <i class="fa fa-times fa-fw text-muted" id="close_status-selectize-reset"></i>

+                             </div>

+                           </div>

+ 

                            <input type="submit" class="btn btn-block btn-primary" value="Apply Filters" />

  

                            <a href="{{ url_for('ui_ns.view_issues',
@@ -557,14 +572,14 @@ 

          event.stopPropagation();

        })

      }

-     });

-     var milestone_selectize_control = $milestone_selectize[0].selectize;

+   });

+   var milestone_selectize_control = $milestone_selectize[0].selectize;

  

-     $("#milestone-selectize-reset").on('click', function(e){

-       milestone_selectize_control.clear();

-     });

+   $("#milestone-selectize-reset").on('click', function(e){

+     milestone_selectize_control.clear();

+   });

  

-     var $priority_selectize = $('#priority-selectize').selectize({

+   var $priority_selectize = $('#priority-selectize').selectize({

      onInitialize: function(){

        $("#priority-filter-group .selectize-control").on('click', function(event){

          event.stopPropagation();
@@ -573,63 +588,81 @@ 

          event.stopPropagation();

        })

      }

-     });

+   });

  

-     var priority_selectize_control = $priority_selectize[0].selectize;

+   var priority_selectize_control = $priority_selectize[0].selectize;

  

-     $("#priority-selectize-reset").on('click', function(e){

-       priority_selectize_control.clear();

-     });

+   $("#priority-selectize-reset").on('click', function(e){

+     priority_selectize_control.clear();

+   });

  

-     var $assignee_selectize = $('#assignee-selectize').selectize({

-       valueField: 'user',

-       labelField: 'user',

-       searchField: 'user',

-       maxItems: 1,

-       create: false,

-       load: function(query, callback) {

-         if (!query.length) return callback();

-         $.getJSON(

-           "{{ url_for('api_ns.api_users') }}", {

-             pattern: "*"+query+"*"

-           },

-           function( data ) {

-             callback( data.users.map(function(x) { return { user: x }; }) );

-           }

-         );

-       }

-     });

+   var $close_status_selectize = $('#close_status-selectize').selectize({

+     onInitialize: function(){

+       $("#close_status-filter-group .selectize-control").on('click', function(event){

+         event.stopPropagation();

+       })

+       $("#filters-dropdown").on('click', function(event){

+         event.stopPropagation();

+       })

+     }

+   });

  

-     var assignee_selectize_control = $assignee_selectize[0].selectize;

+   var close_status_selectize_control = $close_status_selectize[0].selectize;

  

-     $("#assignee-selectize-reset").on('click', function(e){

-       assignee_selectize_control.clear();

-     });

+   $("#close_status-selectize-reset").on('click', function(e){

+     close_status_selectize_control.clear();

+     $("#hidden_status").val("Open");

+   });

  

- var $author_selectize = $('#author-selectize').selectize({

-   valueField: 'user',

-   labelField: 'user',

-   searchField: 'user',

-   maxItems: 1,

-   create: false,

-   load: function(query, callback) {

-     if (!query.length) return callback();

-     $.getJSON(

-       "{{ url_for('api_ns.api_users') }}", {

-         pattern: "*"+query+"*"

-       },

-       function( data ) {

-         callback( data.users.map(function(x) { return { user: x }; }) );

-       }

-     );

-   }

- });

+   var $assignee_selectize = $('#assignee-selectize').selectize({

+     valueField: 'user',

+     labelField: 'user',

+     searchField: 'user',

+     maxItems: 1,

+     create: false,

+     load: function(query, callback) {

+       if (!query.length) return callback();

+       $.getJSON(

+         "{{ url_for('api_ns.api_users') }}", {

+           pattern: "*"+query+"*"

+         },

+         function( data ) {

+           callback( data.users.map(function(x) { return { user: x }; }) );

+         }

+       );

+     }

+   });

  

-     var author_selectize_control = $author_selectize[0].selectize;

+   var assignee_selectize_control = $assignee_selectize[0].selectize;

  

- $("#author-selectize-reset").on('click', function(e){

-   author_selectize_control.clear();

- });

+   $("#assignee-selectize-reset").on('click', function(e){

+     assignee_selectize_control.clear();

+   });

+ 

+   var $author_selectize = $('#author-selectize').selectize({

+     valueField: 'user',

+     labelField: 'user',

+     searchField: 'user',

+     maxItems: 1,

+     create: false,

+     load: function(query, callback) {

+       if (!query.length) return callback();

+       $.getJSON(

+         "{{ url_for('api_ns.api_users') }}", {

+           pattern: "*"+query+"*"

+         },

+         function( data ) {

+           callback( data.users.map(function(x) { return { user: x }; }) );

+         }

+       );

+     }

+   });

+ 

+   var author_selectize_control = $author_selectize[0].selectize;

+ 

+   $("#author-selectize-reset").on('click', function(e){

+     author_selectize_control.clear();

+   });

  

    $( "#filters_form" ).submit(function( event ) {

  

file modified
+8 -3
@@ -486,14 +486,13 @@ 

      """

  

      status = flask.request.args.get("status", "Open")

+     status = flask.request.args.get("close_status") or status

      priority = flask.request.args.get("priority", None)

      tags = flask.request.args.getlist("tags")

      tags = [tag.strip() for tag in tags if tag.strip()]

      assignee = flask.request.args.get("assignee", None)

      author = flask.request.args.get("author", None)

-     search_pattern = flask.request.args.get("search_pattern", None)

-     if search_pattern == "":

-         search_pattern = None

+     search_pattern = flask.request.args.get("search_pattern") or None

      milestones = flask.request.args.getlist("milestone", None)

      order = flask.request.args.get("order", "desc")

      order_key = flask.request.args.get("order_key", "date_created")
@@ -518,6 +517,7 @@ 

          "assignee": assignee,

          "author": author,

          "milestones": milestones,

+         "search_content": None,

      }

  

      no_stone = None
@@ -530,6 +530,10 @@ 

          search_pattern

      )

  

+     if "content" in extra_fields:

+         extra_fields["search_content"] = extra_fields["content"]

+         del (extra_fields["content"])

+ 

      for field in fields:

          if field in extra_fields:

              fields[field] = extra_fields[field]
@@ -691,6 +695,7 @@ 

          search_pattern=search_string,

          order=order,

          order_key=order_key,

+         close_status=flask.request.args.get("close_status"),

          status=status,

          total_open=total_open,

          total_closed=total_closed,

@@ -695,6 +695,15 @@ 

          self.session.commit()

          self.assertEqual(msg.title, 'Tést íssüé with milestone')

  

+         # Add a comment to that ticket

+         pagure.lib.query.add_issue_comment(

+             session=self.session,

+             issue=msg,

+             comment='How about nóã!',

+             user='foo',

+         )

+         self.session.commit()

+ 

          msg = pagure.lib.query.new_issue(

              session=self.session,

              repo=repo,
@@ -776,6 +785,26 @@ 

          self.assertIn('<title>Issues - test - Pagure</title>', output_text)

          self.assertIn('0 Open &amp; Closed Issues', output_text)

  

+         # Content search - description

+         output = self.app.get(

+             '/test/issues?status=all&search_pattern=content:work')

+         self.assertEqual(output.status_code, 200)

+         output_text = output.get_data(as_text=True)

+         self.assertIn(

+             '<title>Issues - test - Pagure</title>',

+             output_text)

+         self.assertIn('1 Open &amp; Closed Issues', output_text)

+ 

+         # Content search - comment

+         output = self.app.get(

+             '/test/issues?status=all&search_pattern=content:nóã')

+         self.assertEqual(output.status_code, 200)

+         output_text = output.get_data(as_text=True)

+         self.assertIn(

+             '<title>Issues - test - Pagure</title>',

+             output_text)

+         self.assertIn('1 Open &amp; Closed Issues', output_text)

+ 

          # Custom key searching

          output = self.app.get(

              '/test/issues?status=all&search_pattern=test1:firstissue')

This is being proposed for review/input but I'd like to add some basic tests before it gets merged :)

1 new commit added

  • Allow searching the content of the comments on an issue tracker
5 years ago

2 new commits added

  • Allow searching the content of the comments on an issue tracker
  • Allow filtering the issue list by the close status
5 years ago

rebased onto 502d4b67c4363868fe61d05f30ca865049094d52

5 years ago

The following commit Allow filtering the issue list by the close status looks good to me

Postgresql support full text search and this is also supported in sqlalchemy, it might be more performant to use it here, since the the column used for search can be indexed.

SQLalchemy docs --> https://docs.sqlalchemy.org/en/latest/dialects/postgresql.html#full-text-search

Postgresql support full text search and this is also supported in sqlalchemy, it might be more performant to use it here, since the the column used for search can be indexed.

It can be indexed in postgresql but not in mysql/mariadb which doesn't support indexing TEXT field (only VARCHAR can be), so that would make pagure officially not compatible with mysql/mariadb (which may make @arrfab sad)

Postgresql support full text search and this is also supported in sqlalchemy, it might be more performant to use it here, since the the column used for search can be indexed.

It can be indexed in postgresql but not in mysql/mariadb which doesn't support indexing TEXT field (only VARCHAR can be), so that would make pagure officially not compatible with mysql/mariadb (which may make @arrfab sad)

Ha good point

This will allow to search the descriptions of the issues also not only comments.

Ok second commit LGTM too :)

Thanks for the review, I'll add some tests before merging this one though :)

rebased onto ca9a01b0a12b0f17fa5e8874252e2d1f607a0d20

5 years ago

rebased onto a1a8ce6586c92b60130763001b95f1a1d9a8f1b4

5 years ago

rebased onto 0329382f02aee4a5657b7a964601e987e5864564

5 years ago

Thanks for the reviews folks! :)

rebased onto a05e6b7

5 years ago

Pull-Request has been merged by pingou

5 years ago