From c073b40fe1d59282e73cdf0c70ade6266a46cccc Mon Sep 17 00:00:00 2001 From: Mohan Boddu Date: Nov 06 2017 14:24:34 +0000 Subject: [PATCH 1/4] Sorting on Opened, Modified, Closed, Priority, Reporter, Assignee cols Signed-off-by: Mohan Boddu --- diff --git a/pagure/lib/__init__.py b/pagure/lib/__init__.py index 47694da..fc36cdb 100644 --- a/pagure/lib/__init__.py +++ b/pagure/lib/__init__.py @@ -2094,7 +2094,8 @@ def search_issues( closed=False, tags=None, assignee=None, author=None, private=None, priority=None, milestones=None, count=False, offset=None, limit=None, search_pattern=None, custom_search=None, - updated_after=None, no_milestones=None, order='desc'): + updated_after=None, no_milestones=None, order='desc', + order_key=None): ''' Retrieve one or more issues associated to a project with the given criterias. @@ -2153,6 +2154,8 @@ def search_issues( :type None, True, or False :kwarg order: Order issues in 'asc' or 'desc' order. :type order: None, str + :kwarg order_key: Order issues by database column + :type order_key: None, str :return: A single Issue object if issueid is specified, a list of Project objects otherwise. @@ -2369,14 +2372,14 @@ def search_issues( model.Issue.title.ilike('%%%s%%' % search_pattern) ) + column = model.Issue.date_created + if order_key and order_key in model.Issue.__table__.columns.keys(): + column = getattr(model.Issue, order_key) + if order == 'asc': - query = query.order_by( - model.Issue.date_created.asc() - ) + query = query.order_by(column.asc()) else: - query = query.order_by( - model.Issue.date_created.desc() - ) + query = query.order_by(column.desc()) if issueid is not None or issueuid is not None: output = query.first() diff --git a/pagure/templates/issues.html b/pagure/templates/issues.html index 9fb915b..76e2f1b 100644 --- a/pagure/templates/issues.html +++ b/pagure/templates/issues.html @@ -156,23 +156,53 @@ - - + + {% if status and status|lower != 'open' %} - + {% else %} - + {% endif %} - - {% if not status or status|lower == 'open' %} -
IssueOpenedIssueOpenedClosedClosedModifiedModified - Priority (Priority +  (reset) - Reporter (Reporter +  (reset) - Assignee (Assignee +  ( - - - {% if status and status|lower != 'open' %} - - {% else %} - - {% endif %} - - - {% if not status or status|lower == 'open' %} - - {% endif %} + + + {% if status and status|lower != 'open' %} + + {% else %} + + {% endif %} + + + {% if not status or status|lower == 'open' %} + + {% endif %} diff --git a/pagure/ui/filters.py b/pagure/ui/filters.py index dd154a4..66b342a 100644 --- a/pagure/ui/filters.py +++ b/pagure/ui/filters.py @@ -623,3 +623,32 @@ def add_or_remove(item, items): else: items.append(item) return items + + +@APP.template_filter('table_sort_arrow') +def table_sort_arrow(column, order_key, order): + """ Outputs an arrow icon if the column is currently being sorted on + """ + arrow_html = ('') + if column == order_key: + if order == 'desc': + return arrow_html.format('bottom') + else: + return arrow_html.format('top') + return '' + + +@APP.template_filter('table_get_link_order') +def table_get_link_order(column, order_key, order): + """ Get the correct order parameter value for the table heading link + """ + if column == order_key: + # If the user is clicking on the column again, they want the + # oposite order + if order == 'desc': + return 'asc' + else: + return 'desc' + else: + # Default to descending + return 'desc' diff --git a/pagure/ui/issues.py b/pagure/ui/issues.py index 34db516..916629d 100644 --- a/pagure/ui/issues.py +++ b/pagure/ui/issues.py @@ -745,7 +745,8 @@ def view_issues(repo, username=None, namespace=None): total_page=total_page, add_report_form=pagure.forms.AddReportForm(), search_pattern=search_string, - order=order + order=order, + order_key=order_key ) From dae613302c0bc6f03197c397bb728354a2fdb2cb Mon Sep 17 00:00:00 2001 From: mprahl Date: Nov 06 2017 14:24:34 +0000 Subject: [PATCH 3/4] Remove "stupidtable" since almost all of the functionality was replaced by database sorting and it doesn't support pagination --- diff --git a/pagure/static/vendor/stupidtable/stupidtable-1.0.1.js b/pagure/static/vendor/stupidtable/stupidtable-1.0.1.js deleted file mode 100644 index 851ba4c..0000000 --- a/pagure/static/vendor/stupidtable/stupidtable-1.0.1.js +++ /dev/null @@ -1,135 +0,0 @@ -// Stupid jQuery table plugin. - -(function($) { - $.fn.stupidtable = function(sortFns) { - return this.each(function() { - var $table = $(this); - sortFns = sortFns || {}; - sortFns = $.extend({}, $.fn.stupidtable.default_sort_fns, sortFns); - $table.data('sortFns', sortFns); - - $table.on("click.stupidtable", "thead th", function() { - $(this).stupidsort(); - }); - }); - }; - - - // Expects $("#mytable").stupidtable() to have already been called. - // Call on a table header. - $.fn.stupidsort = function(force_direction){ - var $this_th = $(this); - var th_index = 0; // we'll increment this soon - var dir = $.fn.stupidtable.dir; - var $table = $this_th.closest("table"); - var datatype = $this_th.data("sort") || null; - - // No datatype? Nothing to do. - if (datatype === null) { - return; - } - - // Account for colspans - $this_th.parents("tr").find("th").slice(0, $(this).index()).each(function() { - var cols = $(this).attr("colspan") || 1; - th_index += parseInt(cols,10); - }); - - var sort_dir; - if(arguments.length == 1){ - sort_dir = force_direction; - } - else{ - sort_dir = force_direction || $this_th.data("sort-default") || dir.ASC; - if ($this_th.data("sort-dir")) - sort_dir = $this_th.data("sort-dir") === dir.ASC ? dir.DESC : dir.ASC; - } - - - $table.trigger("beforetablesort", {column: th_index, direction: sort_dir}); - - // More reliable method of forcing a redraw - $table.css("display"); - - // Run sorting asynchronously on a timout to force browser redraw after - // `beforetablesort` callback. Also avoids locking up the browser too much. - setTimeout(function() { - // Gather the elements for this column - var column = []; - var sortFns = $table.data('sortFns'); - var sortMethod = sortFns[datatype]; - var trs = $table.children("tbody").children("tr"); - - // Extract the data for the column that needs to be sorted and pair it up - // with the TR itself into a tuple. This way sorting the values will - // incidentally sort the trs. - trs.each(function(index,tr) { - var $e = $(tr).children().eq(th_index); - var sort_val = $e.data("sort-value"); - - // Store and read from the .data cache for display text only sorts - // instead of looking through the DOM every time - if(typeof(sort_val) === "undefined"){ - var txt = $e.text(); - $e.data('sort-value', txt); - sort_val = txt; - } - column.push([sort_val, tr]); - }); - - // Sort by the data-order-by value - column.sort(function(a, b) { return sortMethod(a[0], b[0]); }); - if (sort_dir != dir.ASC) - column.reverse(); - - // Replace the content of tbody with the sorted rows. Strangely - // enough, .append accomplishes this for us. - trs = $.map(column, function(kv) { return kv[1]; }); - $table.children("tbody").append(trs); - - // Reset siblings - $table.find("th").data("sort-dir", null).removeClass("sorting-desc sorting-asc"); - $this_th.data("sort-dir", sort_dir).addClass("sorting-"+sort_dir); - - $table.trigger("aftertablesort", {column: th_index, direction: sort_dir}); - $table.css("display"); - }, 10); - - return $this_th; - }; - - // Call on a sortable td to update its value in the sort. This should be the - // only mechanism used to update a cell's sort value. If your display value is - // different from your sort value, use jQuery's .text() or .html() to update - // the td contents, Assumes stupidtable has already been called for the table. - $.fn.updateSortVal = function(new_sort_val){ - var $this_td = $(this); - if($this_td.is('[data-sort-value]')){ - // For visual consistency with the .data cache - $this_td.attr('data-sort-value', new_sort_val); - } - $this_td.data("sort-value", new_sort_val); - return $this_td; - }; - - // ------------------------------------------------------------------ - // Default settings - // ------------------------------------------------------------------ - $.fn.stupidtable.dir = {ASC: "asc", DESC: "desc"}; - $.fn.stupidtable.default_sort_fns = { - "int": function(a, b) { - return parseInt(a, 10) - parseInt(b, 10); - }, - "float": function(a, b) { - return parseFloat(a) - parseFloat(b); - }, - "string": function(a, b) { - return a.localeCompare(b); - }, - "string-ins": function(a, b) { - a = a.toLocaleLowerCase(); - b = b.toLocaleLowerCase(); - return a.localeCompare(b); - } - }; -})(jQuery); diff --git a/pagure/static/vendor/stupidtable/stupidtable-1.0.1.min.js b/pagure/static/vendor/stupidtable/stupidtable-1.0.1.min.js deleted file mode 100644 index bc7bfd3..0000000 --- a/pagure/static/vendor/stupidtable/stupidtable-1.0.1.min.js +++ /dev/null @@ -1,4 +0,0 @@ -(function(c){c.fn.stupidtable=function(b){return this.each(function(){var a=c(this);b=b||{};b=c.extend({},c.fn.stupidtable.default_sort_fns,b);a.data("sortFns",b);a.on("click.stupidtable","thead th",function(){c(this).stupidsort()})})};c.fn.stupidsort=function(b){var a=c(this),g=0,f=c.fn.stupidtable.dir,e=a.closest("table"),k=a.data("sort")||null;if(null!==k){a.parents("tr").find("th").slice(0,c(this).index()).each(function(){var a=c(this).attr("colspan")||1;g+=parseInt(a,10)});var d;1==arguments.length? -d=b:(d=b||a.data("sort-default")||f.ASC,a.data("sort-dir")&&(d=a.data("sort-dir")===f.ASC?f.DESC:f.ASC));e.trigger("beforetablesort",{column:g,direction:d});e.css("display");setTimeout(function(){var b=[],l=e.data("sortFns")[k],h=e.children("tbody").children("tr");h.each(function(a,e){var d=c(e).children().eq(g),f=d.data("sort-value");"undefined"===typeof f&&(f=d.text(),d.data("sort-value",f));b.push([f,e])});b.sort(function(a,b){return l(a[0],b[0])});d!=f.ASC&&b.reverse();h=c.map(b,function(a){return a[1]}); -e.children("tbody").append(h);e.find("th").data("sort-dir",null).removeClass("sorting-desc sorting-asc");a.data("sort-dir",d).addClass("sorting-"+d);e.trigger("aftertablesort",{column:g,direction:d});e.css("display")},10);return a}};c.fn.updateSortVal=function(b){var a=c(this);a.is("[data-sort-value]")&&a.attr("data-sort-value",b);a.data("sort-value",b);return a};c.fn.stupidtable.dir={ASC:"asc",DESC:"desc"};c.fn.stupidtable.default_sort_fns={"int":function(b,a){return parseInt(b,10)-parseInt(a,10)}, -"float":function(b,a){return parseFloat(b)-parseFloat(a)},string:function(b,a){return b.localeCompare(a)},"string-ins":function(b,a){b=b.toLocaleLowerCase();a=a.toLocaleLowerCase();return b.localeCompare(a)}}})(jQuery); diff --git a/pagure/static/vendor/stupidtable/stupidtable.js b/pagure/static/vendor/stupidtable/stupidtable.js deleted file mode 120000 index eb10d05..0000000 --- a/pagure/static/vendor/stupidtable/stupidtable.js +++ /dev/null @@ -1 +0,0 @@ -stupidtable-1.0.1.js \ No newline at end of file diff --git a/pagure/static/vendor/stupidtable/stupidtable.min.js b/pagure/static/vendor/stupidtable/stupidtable.min.js deleted file mode 120000 index 8a5a092..0000000 --- a/pagure/static/vendor/stupidtable/stupidtable.min.js +++ /dev/null @@ -1 +0,0 @@ -stupidtable-1.0.1.min.js \ No newline at end of file diff --git a/pagure/templates/issues.html b/pagure/templates/issues.html index dadb503..b8c5841 100644 --- a/pagure/templates/issues.html +++ b/pagure/templates/issues.html @@ -263,10 +263,7 @@ issue.last_updated | humanize}} {% endif %} - ', + output.data, re.M | re.S) + href = ('href="/test/issues?status=Open&order_key=title&' + 'order=desc"') + self.assertIn(href, th_elements[0]) + href = ('/test/issues?status=Open&order_key=date_created&' + 'order=asc"') + self.assertIn(href, th_elements[1]) + arrow = '' + self.assertIn(arrow, th_elements[1]) + href = ('href="/test/issues?status=Open&order_key=last_updated&' + 'amp;order=desc"') + self.assertIn(href, th_elements[2]) + href = ('href="/test/issues?status=Open&order_key=priority&' + 'order=desc"') + self.assertIn(href, th_elements[3]) + href = ('href="/test/issues?status=Open&order_key=user&' + 'order=desc"') + self.assertIn(href, th_elements[4]) + href = ('href="/test/issues?status=Open&order_key=assignee&' + 'order=desc"') + self.assertIn(href, th_elements[5]) # Status = closed (all but open) output = self.app.get('/test/issues?status=cloSED') @@ -803,6 +829,166 @@ class PagureFlaskIssuestests(tests.Modeltests): self.assertIn('Issues - test - Pagure', output.data) self.assertIn('2 Open Issues (of 2)', output.data) + # Add another issue to test sorting + msg = pagure.lib.new_issue( + session=self.session, + repo=repo, + title='Big problem!', + content='I need help ASAP', + user='foo', + ticketfolder=None + ) + self.session.commit() + self.assertEqual(msg.title, 'Big problem!') + + # Sort by last_updated + output = self.app.get('/test/issues?order_key=last_updated') + tr_elements = re.findall(r'(.*?)', output.data, re.M | re.S) + self.assertEqual(output.status_code, 200) + arrowed_th = ('Modified\n ') + # First table row is the header + self.assertIn(arrowed_th, tr_elements[0]) + # Make sure that issue four is first since it was modified last + self.assertIn('href="/test/issue/4"', tr_elements[1]) + # Make sure that issue two is second since it was modified second + self.assertIn('href="/test/issue/2"', tr_elements[2]) + # Make sure that issue one is last since it was modified first + self.assertIn('href="/test/issue/1"', tr_elements[3]) + + # Modify the date of the first issue and try again + issue_one = pagure.lib.search_issues(self.session, repo, 1) + issue_one.last_updated = datetime.utcnow() + timedelta(seconds=2) + self.session.add(issue_one) + self.session.commit() + output = self.app.get('/test/issues?order_key=last_updated') + tr_elements = re.findall(r'(.*?)', output.data, re.M | re.S) + self.assertEqual(output.status_code, 200) + # Make sure that issue one is first since it was modified last + self.assertIn('href="/test/issue/1"', tr_elements[1]) + # Make sure that issue four is second since it was modified before + # last + self.assertIn('href="/test/issue/4"', tr_elements[2]) + # Make sure that issue two is last since it was modified before issue + # one and four + self.assertIn('href="/test/issue/2"', tr_elements[3]) + # Now query so that the results are ascending + output = self.app.get('/test/issues?order_key=last_updated&order=asc') + tr_elements = re.findall(r'(.*?)', output.data, re.M | re.S) + arrowed_th = ('Modified\n ') + # First table row is the header + self.assertIn(arrowed_th, tr_elements[0]) + self.assertIn('href="/test/issue/2"', tr_elements[1]) + self.assertIn('href="/test/issue/4"', tr_elements[2]) + self.assertIn('href="/test/issue/1"', tr_elements[3]) + + # Sort by title descending + output = self.app.get('/test/issues?order_key=title') + tr_elements = re.findall(r'(.*?)', output.data, re.M | re.S) + self.assertEqual(output.status_code, 200) + arrowed_th = ('Issue\n ') + # First table row is the header + self.assertIn(arrowed_th, tr_elements[0]) + self.assertIn('href="/test/issue/2"', tr_elements[1]) + self.assertIn('href="/test/issue/1"', tr_elements[2]) + self.assertIn('href="/test/issue/4"', tr_elements[3]) + + # Sort by title ascending + output = self.app.get('/test/issues?order_key=title&order=asc') + tr_elements = re.findall(r'(.*?)', output.data, re.M | re.S) + self.assertEqual(output.status_code, 200) + arrowed_th = ('Issue\n ') + # First table row is the header + self.assertIn(arrowed_th, tr_elements[0]) + self.assertIn('href="/test/issue/4"', tr_elements[1]) + self.assertIn('href="/test/issue/1"', tr_elements[2]) + self.assertIn('href="/test/issue/2"', tr_elements[3]) + + # Sort by user (reporter/author) descending + output = self.app.get('/test/issues?order_key=user&order=desc') + tr_elements = re.findall(r'(.*?)', output.data, re.M | re.S) + self.assertEqual(output.status_code, 200) + arrowed_th = ('Reporter\n ') + # First table row is the header + self.assertIn(arrowed_th, tr_elements[0]) + # Check for the name after the avatar + self.assertIn('>\n pingou', tr_elements[1]) + # We check that they are unassigned, otherwise our previous check is + # not specific enough as it can catch an assignee of "pingou" + self.assertIn('unassigned', tr_elements[1]) + self.assertIn('>\n pingou', tr_elements[2]) + self.assertIn('unassigned', tr_elements[2]) + self.assertIn('>\n foo', tr_elements[3]) + self.assertIn('unassigned', tr_elements[3]) + + # Sort by user (reporter/author) ascending + output = self.app.get('/test/issues?order_key=user&order=asc') + tr_elements = re.findall(r'(.*?)', output.data, re.M | re.S) + self.assertEqual(output.status_code, 200) + arrowed_th = ('Reporter\n ') + # First table row is the header + self.assertIn(arrowed_th, tr_elements[0]) + # Check for the name after the avatar + self.assertIn('>\n foo', tr_elements[1]) + # We check that they are unassigned, otherwise our previous check is + # not specific enough as it can catch an assignee of "foo" + self.assertIn('unassigned', tr_elements[1]) + self.assertIn('>\n pingou', tr_elements[2]) + self.assertIn('unassigned', tr_elements[2]) + self.assertIn('>\n pingou', tr_elements[3]) + self.assertIn('unassigned', tr_elements[3]) + + # Set some assignees + issues = self.session.query(pagure.lib.model.Issue).filter_by( + status='Open').order_by(pagure.lib.model.Issue.id).all() + issues[0].assignee_id = 1 + issues[1].assignee_id = 2 + issues[2].assignee_id = 1 + self.session.commit() + + # Sort by assignee descending + output = self.app.get('/test/issues?order_key=assignee&order=desc') + tr_elements = re.findall(r'(.*?)', output.data, re.M | re.S) + self.assertEqual(output.status_code, 200) + arrowed_th = ('Assignee\n ') + # First table row is the header + self.assertIn(arrowed_th, tr_elements[0]) + # This detects the assignee but keying on if a certain link is present + one = ('') + two = ('') + three = ('') + self.assertIn(one, tr_elements[1]) + self.assertIn(two, tr_elements[2]) + self.assertIn(three, tr_elements[3]) + + # Sort by assignee ascending + output = self.app.get('/test/issues?order_key=assignee&order=asc') + tr_elements = re.findall(r'(.*?)', output.data, re.M | re.S) + self.assertEqual(output.status_code, 200) + arrowed_th = ('Assignee\n ') + # First table row is the header + self.assertIn(arrowed_th, tr_elements[0]) + # This detects the assignee but keying on if a certain link is present + one = ('') + two = ('') + three = ('') + self.assertIn(one, tr_elements[1]) + self.assertIn(two, tr_elements[2]) + self.assertIn(three, tr_elements[3]) + # New issue button is shown user = tests.FakeUser() with tests.user_set(pagure.APP, user):
IssueOpenedClosedModifiedPriority -  (reset) - Reporter -  (reset) - Assignee -  (reset) - Issue + {{ 'title' | table_sort_arrow(order_key, order) | safe }}Opened + {{ 'date_created' | table_sort_arrow(order_key, order) | safe }}Closed + {{ 'closed_at' | table_sort_arrow(order_key, order) | safe }}Modified + {{ 'last_updated' | table_sort_arrow(order_key, order) | safe }}Priority + {{ 'priority' | table_sort_arrow(order_key, order) | safe }}Reporter + {{ 'user' | table_sort_arrow(order_key, order) | safe }}Assignee + {{ 'assignee' | table_sort_arrow(order_key, order) | safe }}
+ {% if issue.priority %} - - {% endblock %} From bcbadcfbbfdfad10c6d00d1a64649e37b2c62aab Mon Sep 17 00:00:00 2001 From: mprahl Date: Nov 06 2017 14:24:34 +0000 Subject: [PATCH 4/4] Add unit tests for sorting issues --- diff --git a/tests/test_pagure_flask_ui_issues.py b/tests/test_pagure_flask_ui_issues.py index 2ff9d44..dea98f4 100644 --- a/tests/test_pagure_flask_ui_issues.py +++ b/tests/test_pagure_flask_ui_issues.py @@ -22,6 +22,8 @@ try: except ImportError: pyclamd = None import tempfile +import re +from datetime import datetime, timedelta import pygit2 from mock import patch, MagicMock @@ -713,6 +715,30 @@ class PagureFlaskIssuestests(tests.Modeltests): self.assertIn( '', output.data) + # Verify that the sorting links are correct and the arrow is pointing + # down next to the Opened column + th_elements = re.findall(r'(.*?)