Opened 10 years ago
Last modified 10 years ago
#12115 new enhancement
Allow overriding of actual ticket times with user times
Reported by: | Owned by: | Chris Nelson | |
---|---|---|---|
Priority: | normal | Component: | TracJsGanttPlugin |
Severity: | normal | Keywords: | |
Cc: | Trac Release: |
Description
At the moment the precedence is
- if useActuals is set, use the actual time
- else if there is a cached date in the db, use that
- else if there is a user-supplied field set, use it
- 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:
- Use the user-supplied field, if it is set
- Else if useActuals is set and the ticket was closed, use the actual date,
- Else if a cached date field exists, use it
- Else compute the dates.
Attachments (0)
Change History (6)
comment:1 Changed 10 years ago by
comment:2 follow-up: 3 Changed 10 years ago by
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 follow-up: 4 Changed 10 years ago by
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 Changed 10 years ago by
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 10 years ago by
actually, the try/catch block should be
try: (start,finish) = sfPolicies[policy]() catch(DateNotSet): pass else: break
comment:6 Changed 10 years ago by
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: 20 20 except ImportError: 21 21 from trac.util.datefmt import to_timestamp as to_utimestamp 22 22 23 from trac.config import IntOption, Option, ExtensionOption 23 from trac.config import IntOption, Option, ExtensionOption, ListOption 24 24 from trac.core import implements, Component, TracError, Interface, ExtensionPoint 25 25 from trac.env import IEnvironmentSetupParticipant 26 26 from trac.db import DatabaseManager … … class TracPM(Component): 185 185 """List of statuses for goal-type tickets that are active""") 186 186 Option(cfgSection, 'useActuals', '0', 187 187 """Use actual start, finish date for tickets""") 188 189 188 scheduler = ExtensionOption(cfgSection, 'scheduler', 190 189 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 """) 191 197 192 198 def __init__(self): 193 199 self.env.log.info('Initializing TracPM') … … class ResourceScheduler(Component): 1742 1748 SF_PROJECT = 5 1743 1749 SF_DEFAULT = 6 1744 1750 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 1748 1753 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]]), 1752 1759 SF_ACTUAL ] 1753 1760 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]]), 1759 1775 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]): 1763 1785 # Don't adjust for work week; use the explicit date. 1764 task From = self.pm.parseTaskDate(t, fromField)1765 task From = [taskFrom, SF_TASK]1786 task_date = self.pm.parseTaskDate(task, fields[direction]) 1787 task_date = [task_date, SF_TASK] 1766 1788 self._logSch('Using explicit %s: %s' % 1767 (f romField, taskFrom[0]))1768 # Otherwise, compute from date from dependencies.1789 (fields[direction], task_date[0])) 1790 return task_date 1769 1791 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)) 1771 1798 1772 1799 # The date derived from dependencies is *not* a 1773 1800 # fixed (user-specified) date. 1774 1801 if taskFrom != None: 1775 1802 self._logSch('Got %s from dependencies: %s' % 1776 (f romField, taskFrom[0]))1803 (fields["from"], taskFrom[0])) 1777 1804 taskFrom[1] = SF_DEPENDENCIES 1778 1805 # If dependencies don't give a date, use date from 1779 1806 # project. Default to today if none given. 1780 1807 else: 1781 1808 # Get user-supplied date for schedule. 1782 taskFrom = self.pm.parseDbDate(options.get(fromField)) 1809 taskFrom = self.pm.parseDbDate( 1810 options.get(fields["from"])) 1783 1811 # If none, use midnight today 1784 1812 if taskFrom == None: 1785 1813 taskFrom = datetime.today().replace(hour=0, … … class ResourceScheduler(Component): 1788 1816 microsecond=0, 1789 1817 tzinfo=localtz) 1790 1818 self._logSch('Defaulting %s: %s' % 1791 (f romField, taskFrom))1819 (fields["from"], taskFrom)) 1792 1820 taskFrom = [taskFrom, SF_DEFAULT] 1793 1821 else: 1794 1822 self._logSch('Using project %s: %s' % 1795 (f romField, taskFrom))1823 (fields["from"], taskFrom)) 1796 1824 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 } 1797 1843 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 1798 1858 1799 1859 # Check resource availability. 1800 1860 # … … class ResourceScheduler(Component): 1832 1892 # function. Specifically, when the toField is set in the 1833 1893 # task. 1834 1894 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 1864 1906 t['_calc_' + toField] = taskTo 1865 1907 1866 1908 # Adjust dates based on precedence
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?