Edgewall Software
Modify

Opened 16 years ago

Last modified 5 years ago

#7342 new enhancement

Add filter parameters to milestone view

Reported by: techtonik <techtonik@…> Owned by:
Priority: normal Milestone: unscheduled
Component: roadmap Version: 0.10.4
Severity: normal Keywords:
Cc: ivan@…, franz.mayer@… Branch:
Release Notes:
API Changes:
Internal Changes:

Description

Milestone view support only one milestone per page. Roadmap can display several milestones, but there is almost no control on order and scope of shown stats. The only option is to show completed milestones (show=all) or not.

I propose to extend "show=all" to allow queries like "show=^0.5" or "show=0.5|0.5.1", but it can create a conflict if somebody defines milestone named "all".

It can be convenient to extend milestone view as well to allow URLs like /milestone/6.0|6.2|6.4

Attached is a simple patch to select several milestones in roadmap using piped /roadmap?milestone=6.0|6.2|6.4 syntax. No advanced filtering using ~= or ^=

Attachments (4)

0.10.4.roadmap.py.txt (640 bytes ) - added by techtonik <techtonik@…> 16 years ago.
0.11.roadmap.py.txt (569 bytes ) - added by techtonik <techtonik@…> 16 years ago.
MilestoneFilter-trunk@16826.patch (4.5 KB ) - added by anonymous 6 years ago.
Screen Shot 2020-04-29 at 18.08.56.jpg (14.1 KB ) - added by Ryan J Ollos 5 years ago.

Download all attachments as: .zip

Change History (17)

by techtonik <techtonik@…>, 16 years ago

Attachment: 0.10.4.roadmap.py.txt added

by techtonik <techtonik@…>, 16 years ago

Attachment: 0.11.roadmap.py.txt added

comment:1 by Remy Blank, 16 years ago

Milestone: 2.0
Owner: Jonas Borgström removed

Why not. However, the current patch requires editing the page URL, which is not really convenient. For this to be included, some more thought should be given to the user interface side.

comment:2 by Christian Boos, 16 years ago

Component: generalroadmap

For example, the prefs form, in addition to the Show already completed milestones checkbox could have a Filter by name: field, which would accept a glob pattern. If you fill it with e.g. 0.11*, the roadmap module would show all the Milestone starting with 0.11.

comment:3 by ivan@…, 16 years ago

Cc: ivan@… added

comment:4 by john.williams@…, 15 years ago

I'm interested in this (adding this comment so I can follow it)

comment:5 by Christian Boos, 15 years ago

Milestone: 2.0unscheduled

Milestone 2.0 deleted

comment:6 by trac-delcock@…, 15 years ago

RoadmapFilterPlugin might suit your needs, but it's for 0.11 only.

comment:7 by Christian Boos, 14 years ago

Milestone: triagingunscheduled

Milestone triaging deleted

comment:8 by anonymous, 14 years ago

RoadmapPlugin might suit your needs (uses largely code of RoadmapFilterPlugin); but it's only available in English and German.

comment:9 by Franz Mayer <franz.mayer@…>, 9 years ago

Cc: franz.mayer@… added

Is there any plan to implement this feature?

When many milestones are present (like at t.e.o or at our environment) I find it very useful to filter by milestone name (like ^1.1, which displays all milestones starting with "1.1"); this feature is already implemented by RoadmapPlugin. Maybe the filter can be implemented as in reports (with Drop-Down-Box staring, contains etc).

comment:10 by anonymous, 6 years ago

Related: #2588, #11086

comment:11 by anonymous, 6 years ago

  • trac/ticket/templates/roadmap.html

    diff -r 4f798415e628 trac/ticket/templates/roadmap.html
    a b  
    4040          <label for="hidenoduedate">${
    4141            _("Hide milestones with no due date")}</label>
    4242        </div>
     43        <div>
     44          <label for="filter">${
     45            _("Filter:")}</label>
     46          <input type="input" id="filter" name="filter" value="${filter}"/>
     47        </div>
    4348        <div class="buttons">
    4449          <input type="submit" value="${_('Update')}"/>
    4550        </div>
  • trac/ticket/roadmap.py

    diff -r 4f798415e628 trac/ticket/roadmap.py
    a b  
    2727from trac.perm import IPermissionRequestor
    2828from trac.resource import *
    2929from trac.search import ISearchSource, search_to_regexps, shorten_result
    30 from trac.util import as_bool, partition
     30from trac.util import as_bool, MultiPartFilter, partition
    3131from trac.util.datefmt import (datetime_now, format_date, format_datetime,
    3232                               from_utimestamp, get_datetime_format_hint,
    3333                               parse_date, pretty_timedelta, to_datetime,
     
    484484    def process_request(self, req):
    485485        req.perm.require('ROADMAP_VIEW')
    486486
     487        filter = req.args.get('filter', '')
    487488        show = req.args.getlist('show')
    488489        if 'all' in show:
    489490            show = ['completed']
     
    494495                          if m.due is not None or m.completed]
    495496        milestones = [m for m in milestones
    496497                      if 'MILESTONE_VIEW' in req.perm(m.resource)]
     498        if filter:
     499            f = MultiPartFilter(filter)
     500            milestones = [m for m in milestones
     501                          if f.matches(m.name)]
    497502
    498503        stats = []
    499504        queries = []
     
    524529            'milestone_stats': stats,
    525530            'queries': queries,
    526531            'show': show,
     532            'filter': filter,
    527533        }
    528534        add_stylesheet(req, 'common/css/roadmap.css')
    529535        return 'roadmap.html', data
  • trac/util/__init__.py

    diff -r 4f798415e628 trac/util/__init__.py
    a b  
    13881388    else:
    13891389        the_list[index] = item_to_add
    13901390
     1391       
     1392class Filter(object):
     1393    """A simple text filter with optional leading operator:
     1394    ^  ("starts with")
     1395    !^ ("does not start with")
     1396    $  ("ends with")
     1397    !$ ("does not end with")
     1398    ~  ("contains")
     1399    !~ ("does not contain")
     1400    !  ("does not equal")
     1401    The default operator is "equals".
     1402    """
     1403
     1404    def __init__(self, pattern):
     1405        for op in ('^', '!^', '$', '!$', '~', '!~', '!'):
     1406            if pattern.startswith(op):
     1407                self.op = op
     1408                self.needle = pattern[len(op):]
     1409                break
     1410        else:
     1411            self.op = ''
     1412            self.needle = pattern
     1413        self.func = {
     1414            '^': lambda text: text.startswith(self.needle),
     1415            '!^': lambda text: not text.startswith(self.needle),
     1416            '$': lambda text: text.endswith(self.needle),
     1417            '!$': lambda text: not text.endswith(self.needle),
     1418            '~': lambda text: self.needle in text,
     1419            '!~': lambda text: self.needle not in text,
     1420            '': lambda text: text == self.needle,
     1421            '!': lambda text: text != self.needle,
     1422        }[self.op]
     1423       
     1424    def matches(self, text):
     1425        """Returns True if the text matches this filter"""
     1426        return self.func(text)
     1427
     1428
     1429class MultiPartFilter(object):
     1430    """A text filter consisting of multiple parts."""
     1431
     1432    def __init__(self, filters):
     1433        if isinstance(filters, basestring):
     1434            filters = map(Filter, to_list(filters, ' '))
     1435        self.excludes = [f for f in filters if f.op.startswith('!')]
     1436        self.includes = [f for f in filters if not f.op.startswith('!')]
     1437
     1438    def matches(self, text):
     1439        """Returns True if the text matches this filter."""
     1440        for exclude in self.excludes:
     1441            if not exclude.matches(text):
     1442                return False
     1443        for include in self.includes:
     1444            if include.matches(text):
     1445                return True
     1446        return not any(self.includes)
     1447
    13911448
    13921449# Imports for backward compatibility (at bottom to avoid circular dependencies)
    13931450from trac.core import TracError

comment:12 by Ryan J Ollos, 6 years ago

Please submit patches as attachments, per guidelines in TracDev/SubmittingPatches, rather than pasting inline.

by anonymous, 6 years ago

by Ryan J Ollos, 5 years ago

comment:13 by Ryan J Ollos, 5 years ago

I'm sorry for the very long delay to review this.

The query filter operators look similar to TracQuery#QueryLanguage.

I think ultimately we probably want a filter like on the TicketQuery page:

Maybe the roadmap could have query controls for Name, Due Date and Completed Date, similar to the TicketQuery module.

Otherwise, we are introducing a new query language that will be difficult to remember, especially lacking autocomplete for the field.

Well, maybe it's okay as an interim, but it seems like we should support globs (comment:2). Maybe a tooltip on hover of the field or help icon would be sufficient to help with the syntax. Some form of autocomplete would probably be ideal. Is this particular query language similar to that used elsewhere?

Last edited 5 years ago by Ryan J Ollos (previous) (diff)

Modify Ticket

Change Properties
Set your email in Preferences
Action
as new The ticket will remain with no owner.
The ticket will be disowned.
as The resolution will be set. Next status will be 'closed'.
The owner will be changed from (none) to anonymous. Next status will be 'assigned'.

Add Comment


E-mail address and name can be saved in the Preferences .
 
Note: See TracTickets for help on using tickets.