Modify

Opened 9 years ago

Last modified 9 years ago

#12115 new enhancement

Allow overriding of actual ticket times with user times

Reported by: fabian.freyer@… Owned by: Chris Nelson
Priority: normal Component: TracJsGanttPlugin
Severity: normal Keywords:
Cc: Trac Release:

Description

At the moment the precedence is

  1. if useActuals is set, use the actual time
  2. else if there is a cached date in the db, use that
  3. else if there is a user-supplied field set, use it
  4. otherwise compute the dates

I would like the option to change the order of precedence for these policies. Especially, it would be great to be able to override the actual ticket time with a user field, e.g. if a ticket was re-opened or closed long after its real completion, so that it doesn't mess up the gantt chart.

In my case, I would want to use the following order:

  1. Use the user-supplied field, if it is set
  2. Else if useActuals is set and the ticket was closed, use the actual date,
  3. Else if a cached date field exists, use it
  4. Else compute the dates.

Attachments (0)

Change History (6)

comment:1 Changed 9 years ago by Chris Nelson

An interesting idea. What would you suggest as a means to configure or select this?

Is a re-opened ticket your primary use case? Would it be enough for the scheduler to favor the user-supplied time for a ticket which had been closed but it no longer closed?

comment:2 Changed 9 years ago by fabian.freyer@…

As a means to configure this, I would suggest a ListOption or OrderedExtensionsOption to reorder the time override policies. Instead of the if/elif/elif/else block, I would then iterate over the option values and override the last value if the current policy applies. The default value should then be equivalent to the current order of precedence.

I'll try to whip up a patch later. Unfortunately, I don't have a nice testing environment for trac development yet, so I'll try to set that up first :)

A reopened ticket is not my primary use case, but a main one. My secondary use case is a later adjustment of actual completion dates.

comment:3 in reply to:  2 ; Changed 9 years ago by Chris Nelson

Replying to fabian.freyer@…:

As a means to configure this, I would suggest a ListOption or OrderedExtensionsOption to reorder the time override policies. Instead of the if/elif/elif/else block, I would then iterate over the option values and override the last value if the current policy applies. The default value should then be equivalent to the current order of precedence. ...

I was thinking of something like that. So, sfPrecedence (start/finish precedence) would default to:

[ actual, db, ticket, computed ]

but you might set it to

[ ticket, actual, db, computed ]

Where "ticket" is what the user specified in the ticket and "db" is the "cached" value.

And this would be used in a

for dateType in sfPrecedence:
   if dateType data available, use it and break out of loop

(I wonder if "computed" is always a fallback? Does it need to be configurable or can you just order the other three and if nothing is found, compute?)

I assume you'd want to be able to specify this in trac.ini but also per-chart (e.g., [[TracJSGanntChart(...,sfPrecedence=ticket|actual|db|computed,,,,)]])?

comment:4 in reply to:  3 Changed 9 years ago by fabian.freyer@…

Replying to ChrisNelson:

I was thinking of something like that. So, sfPrecedence (start/finish precedence) would default to:

[ actual, db, ticket, computed ]

but you might set it to

[ ticket, actual, db, computed ]

Where "ticket" is what the user specified in the ticket and "db" is the "cached" value.

yes, that sounds good. This could also deprecate useActual as you could just remove actual from sfPrecedence, i.e. the default (useActual=0) would be

sfPrecedence=[db,ticket,computed]

(I wonder if "computed" is always a fallback? Does it need to be configurable or can you just order the other three and if nothing is found, compute?)

"computed" always returns a start/finish time, right? So, as long as "computed" in sfPrecedence, no fallback would be needed. Therefore, I would suggest something like

if "computed" not in sfPrecedence:
    sfPrecedence.append("computed") # use computed as a fallback if all else fails

class DateNotSet(Exception):
    pass

for policy in sfPrecedence:
    try:
        (start,finish) = sfPolicies[policy]()
        break
    catch(DateNotSet):
        pass

I assume you'd want to be able to specify this in trac.ini but also per-chart (e.g., [[TracJSGanntChart(...,sfPrecedence=ticket|actual|db|computed,,,,)]])?

I hadn't thought of that, but it sounds like a useful feature. This could also make things like "planned" and "actual" gantt charts possible.

comment:5 Changed 9 years ago by anonymous

actually, the try/catch block should be

    try:
        (start,finish) = sfPolicies[policy]()
    catch(DateNotSet):
        pass
    else:
        break

comment:6 Changed 9 years ago by fabian.freyer@…

I have created this patch, which should add this option. However the feature of being able to set sfPrecedence per-chart is not implemented yet.

I was able to test this with the "actual" date policy. However, I don't have a test environment ready. Could you confirm this works as expected?

  • tracjsgantt/tracpm.py

    From 5ab06b22b17f8630d0914d11b48bf3323072b060 Mon Sep 17 00:00:00 2001
    From: Fabian Freyer <fabian.freyer@physik.tu-berlin.de>
    Date: Tue, 16 Dec 2014 19:48:33 +0100
    Subject: [PATCH] Ticket #12115: add sfPolicies setting.
    
    ---
     tracjsgantt/tracpm.py |  150 +++++++++++++++++++++++++++++++------------------
     1 file changed, 96 insertions(+), 54 deletions(-)
    
    diff --git a/tracjsgantt/tracpm.py b/tracjsgantt/tracpm.py
    index e87f26d..36b4244 100644
    a b try: 
    2020except ImportError:
    2121    from trac.util.datefmt import to_timestamp as to_utimestamp
    2222
    23 from trac.config import IntOption, Option, ExtensionOption
     23from trac.config import IntOption, Option, ExtensionOption, ListOption
    2424from trac.core import implements, Component, TracError, Interface, ExtensionPoint
    2525from trac.env import IEnvironmentSetupParticipant
    2626from trac.db import DatabaseManager
    class TracPM(Component): 
    185185           """List of statuses for goal-type tickets that are active""")
    186186    Option(cfgSection, 'useActuals', '0',
    187187           """Use actual start, finish date for tickets""")
    188 
    189188    scheduler = ExtensionOption(cfgSection, 'scheduler',
    190189                                ITaskScheduler, 'ResourceScheduler')
     190    sfPrecedence = ListOption(cfgSection, 'sfPrecedence',
     191        default=['db', 'ticket', 'computed'], sep=",",
     192        doc="""
     193        Order of precedence of date policies.
     194        This is a comma-separated list, first values will be prioritized.
     195        Valid policies are: actual, db, ticket, computed.
     196        """)
    191197
    192198    def __init__(self):
    193199        self.env.log.info('Initializing TracPM')
    class ResourceScheduler(Component): 
    17421748            SF_PROJECT = 5
    17431749            SF_DEFAULT = 6
    17441750
    1745             # If we haven't scheduled this yet, do it now.
    1746             if t.get('_calc_' + fromField) == None:
    1747                 self._logSch('Scheduling %s' % t['id'])
     1751            class DateNotSet(Exception):
     1752                pass
    17481753
    1749                 # Use actual dates, if requested.
    1750                 if t.get('_actual_' + fromField) and options.get('useActuals'):
    1751                     taskFrom = [ to_datetime(t['_actual_' + fromField]),
     1754            def date_actual(task, fields, direction):
     1755                """ Use actual dates, if requested."""
     1756                if task.get('_actual_' + fields[direction]):
     1757                    task_date = [ to_datetime(task['_actual_'
     1758                                 + fields[direction]]),
    17521759                                 SF_ACTUAL ]
    17531760                    self._logSch('Using actual %s:%s' %
    1754                                  (fromField, taskFrom[0]))
    1755                 # If there is a precomputed date in the database,
    1756                 # use it unless we're forcing a schedule calculation.
    1757                 elif t.get('_sched_' + fromField) and not options.get('force'):
    1758                     taskFrom = [ to_datetime(t['_sched_' + fromField]),
     1761                                 (fields[direction], task_date[0]))
     1762                    return task_date
     1763                else:
     1764                    raise DateNotSet()
     1765
     1766            def date_cached(task, fields, direction):
     1767                """
     1768                If there is a precomputed date in the database,
     1769                use it unless we're forcing a schedule calculation.
     1770                """
     1771                if task.get('_sched_' + fields[direction]) \
     1772                  and not options.get('force'):
     1773                    task_date = [ to_datetime(task['_sched_'
     1774                                 + fields[direction]]),
    17591775                                 SF_SCHEDULE ]
    1760                     self._logSch('Using db %s: %s' % (fromField, taskFrom[0]))
    1761                 # If there is a user-supplied date set, use it
    1762                 elif self.pm.isSet(t, fromField):
     1776                    self._logSch('Using db %s: %s' %
     1777                        (fields[direction], task_date[0]))
     1778                    return task_date
     1779                else:
     1780                    raise DateNotSet()
     1781
     1782            def date_user(task, fields, direction):
     1783                """ If there is a user-supplied date set, use it """
     1784                if self.pm.isSet(task, fields[direction]):
    17631785                    # Don't adjust for work week; use the explicit date.
    1764                     taskFrom = self.pm.parseTaskDate(t, fromField)
    1765                     taskFrom = [taskFrom, SF_TASK]
     1786                    task_date = self.pm.parseTaskDate(task, fields[direction])
     1787                    task_date = [task_date, SF_TASK]
    17661788                    self._logSch('Using explicit %s: %s' %
    1767                                  (fromField, taskFrom[0]))
    1768                 # Otherwise, compute from date from dependencies.
     1789                                 (fields[direction], task_date[0]))
     1790                    return task_date
    17691791                else:
    1770                     taskFrom = dependentLimit(t, ancestorLimit(t))
     1792                    raise DateNotSet()
     1793
     1794            def date_computed(task, fields, direction):
     1795                """ compute date from dependencies """
     1796                if direction == "from":
     1797                    taskFrom = dependentLimit(task, ancestorLimit(task))
    17711798
    17721799                    # The date derived from dependencies is *not* a
    17731800                    # fixed (user-specified) date.
    17741801                    if taskFrom != None:
    17751802                        self._logSch('Got %s from dependencies: %s' %
    1776                                      (fromField, taskFrom[0]))
     1803                                     (fields["from"], taskFrom[0]))
    17771804                        taskFrom[1] = SF_DEPENDENCIES
    17781805                    # If dependencies don't give a date, use date from
    17791806                    # project.  Default to today if none given.
    17801807                    else:
    17811808                        # Get user-supplied date for schedule.
    1782                         taskFrom = self.pm.parseDbDate(options.get(fromField))
     1809                        taskFrom = self.pm.parseDbDate(
     1810                            options.get(fields["from"]))
    17831811                        # If none, use midnight today
    17841812                        if taskFrom == None:
    17851813                            taskFrom = datetime.today().replace(hour=0,
    class ResourceScheduler(Component): 
    17881816                                                              microsecond=0,
    17891817                                                              tzinfo=localtz)
    17901818                            self._logSch('Defaulting %s: %s' %
    1791                                          (fromField, taskFrom))
     1819                                         (fields["from"], taskFrom))
    17921820                            taskFrom = [taskFrom, SF_DEFAULT]
    17931821                        else:
    17941822                            self._logSch('Using project %s: %s' %
    1795                                          (fromField, taskFrom))
     1823                                         (fields["from"], taskFrom))
    17961824                            taskFrom = [taskFrom, SF_PROJECT]
     1825                    return taskFrom
     1826                else:
     1827                    hours = self.pm.workHours(task)
     1828                    taskTo = task['_calc_' + fields["from"]][0] + \
     1829                        _calendarOffset(task,
     1830                                        dir * hours,
     1831                                        task['_calc_' + fields["from"]][0])
     1832                    taskTo = [taskTo, task['_calc_' + fields["from"]][1]]
     1833                    self._logSch('Computed %s from %s, work: %s' %
     1834                                 (fields["to"], fields["from"], taskTo[0]))
     1835                    return taskTo
     1836
     1837            sfPolicies = {
     1838                'actual': date_actual,
     1839                'db': date_cached,
     1840                'ticket': date_user,
     1841                'computed': date_computed
     1842            }
    17971843
     1844            # If we haven't scheduled this yet, do it now.
     1845            if t.get('_calc_' + fromField) == None:
     1846                self._logSch('Scheduling %s' % t['id'])
     1847                for policy in self.pm.sfPrecedence:
     1848                    try:
     1849                        if policy not in sfPolicies:
     1850                            raise TracError("Invalid policy %s. Check sfPolicies setting." % policy)
     1851                        taskFrom = sfPolicies[policy](t,
     1852                            {"from": fromField, "to": toField},
     1853                            "from")
     1854                    except DateNotSet:
     1855                        pass
     1856                    else:
     1857                        break
    17981858
    17991859                # Check resource availability.
    18001860                #
    class ResourceScheduler(Component): 
    18321892            # function.  Specifically, when the toField is set in the
    18331893            # task.
    18341894            if t.get('_calc_' + toField) == None:
    1835                 # Use actual dates, if requested.
    1836                 if t.get('_actual_' + toField) and options.get('useActuals'):
    1837                     taskTo = [ to_datetime(t['_actual_' + toField]), SF_ACTUAL ]
    1838                     self._logSch('Using actual %s: %s' %
    1839                                  (toField, taskTo[0]))
    1840                 # If there is a precomputed date in the database,
    1841                 # use it unless we're forcing a schedule calculation.
    1842                 elif t.get('_sched_' + toField) and not options.get('force'):
    1843                     taskTo = [ to_datetime(t['_sched_' + toField]),
    1844                                SF_SCHEDULE ]
    1845                     self._logSch('Using db %s: %s' % (toField, taskTo[0]))
    1846                 # If there is a user-supplied date set, use it
    1847                 elif self.pm.isSet(t, toField):
    1848                     taskTo = self.pm.parseTaskDate(t, toField)
    1849                     taskTo = [taskTo, SF_TASK]
    1850                     self._logSch('Using explicit %s: %s' %
    1851                                  (toField, taskTo[0]))
    1852                 # Otherwise, the to date is based on the from date and
    1853                 # the work to be done.
    1854                 else:
    1855                     hours = self.pm.workHours(t)
    1856                     taskTo = t['_calc_' + fromField][0] + \
    1857                         _calendarOffset(t,
    1858                                         dir * hours,
    1859                                         t['_calc_' + fromField][0])
    1860                     taskTo = [taskTo, t['_calc_' + fromField][1]]
    1861                     self._logSch('Computed %s from %s, work: %s' %
    1862                                  (toField, fromField, taskTo[0]))
    1863 
     1895                for policy in self.pm.sfPrecedence:
     1896                    try:
     1897                        if policy not in sfPolicies:
     1898                            raise TracError("Invalid policy %s. Check sfPolicies setting." % policy)
     1899                        taskTo = sfPolicies[policy](t,
     1900                            {"from": fromField, "to": toField},
     1901                            "to")
     1902                    except DateNotSet:
     1903                        pass
     1904                    else:
     1905                        break
    18641906                t['_calc_' + toField] = taskTo
    18651907
    18661908            # Adjust dates based on precedence

Modify Ticket

Change Properties
Set your email in Preferences
Action
as new The owner will remain Chris Nelson.

Add Comment


E-mail address and name can be saved in the Preferences.

 
Note: See TracTickets for help on using tickets.