add cco.work to loops-ext, + minor Python3 fixes
This commit is contained in:
parent
55af2b5f75
commit
812bd28bc2
13 changed files with 685 additions and 0 deletions
97
cco/work/README.rst
Normal file
97
cco/work/README.rst
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
|
||||||
|
cco.work - cyberconcepts.org: project and task management stuff
|
||||||
|
===============================================================
|
||||||
|
|
||||||
|
>>> from zope.publisher.browser import TestRequest
|
||||||
|
>>> from logging import getLogger
|
||||||
|
>>> log = getLogger('cco.work')
|
||||||
|
|
||||||
|
>>> from loops.setup import addAndConfigureObject, addObject
|
||||||
|
>>> from loops.concept import Concept
|
||||||
|
>>> from loops.common import adapted, baseObject
|
||||||
|
|
||||||
|
>>> concepts = loopsRoot['concepts']
|
||||||
|
>>> len(list(concepts.keys()))
|
||||||
|
14
|
||||||
|
|
||||||
|
>>> project = concepts['project']
|
||||||
|
>>> task = concepts['task']
|
||||||
|
|
||||||
|
>>> from loops.browser.node import NodeView
|
||||||
|
>>> home = loopsRoot['views']['home']
|
||||||
|
>>> homeView = NodeView(home, TestRequest())
|
||||||
|
|
||||||
|
|
||||||
|
Projects
|
||||||
|
--------
|
||||||
|
|
||||||
|
### Basic operations ###
|
||||||
|
|
||||||
|
We start with creating a project and assigning an estimated effort to it.
|
||||||
|
|
||||||
|
>>> from loops.concept import Concept
|
||||||
|
>>> from loops.setup import addAndConfigureObject
|
||||||
|
|
||||||
|
>>> proj01 = adapted(addAndConfigureObject(concepts, Concept, 'project01',
|
||||||
|
... title=u'Project #1', conceptType=project))
|
||||||
|
|
||||||
|
When counting all assoctioated tasks we get just 1, the project itself.
|
||||||
|
|
||||||
|
>>> len(proj01.getAllTasks())
|
||||||
|
1
|
||||||
|
|
||||||
|
We now add a task to the project so that we now count two tasks
|
||||||
|
(including the project itself).
|
||||||
|
|
||||||
|
>>> task01 = adapted(addAndConfigureObject(concepts, Concept, 'task01',
|
||||||
|
... title=u'Task #1', conceptType=task))
|
||||||
|
>>> baseObject(task01).assignParent(baseObject(proj01))
|
||||||
|
>>> len(proj01.getAllTasks())
|
||||||
|
2
|
||||||
|
|
||||||
|
The actual effort for the project is 0.0 because there aren't any work items
|
||||||
|
assigned to any of the tasks.
|
||||||
|
|
||||||
|
>>> proj01.actualEffort
|
||||||
|
0.0
|
||||||
|
|
||||||
|
u'0:00'
|
||||||
|
|
||||||
|
So if we add work items the corresponding efforts are summed up.
|
||||||
|
|
||||||
|
>>> task01.addWorkItem('4711', 'work', effort=15 * 3600)
|
||||||
|
>>> proj01.actualEffort
|
||||||
|
54000.0
|
||||||
|
|
||||||
|
u'15:00'
|
||||||
|
|
||||||
|
>>> task02 = adapted(addAndConfigureObject(concepts, Concept, 'task02',
|
||||||
|
... title=u'Task #2', conceptType=task))
|
||||||
|
>>> baseObject(task02).assignParent(baseObject(proj01))
|
||||||
|
>>> task02.addWorkItem('4711', 'work', effort=8 * 3600)
|
||||||
|
>>> proj01.actualEffort
|
||||||
|
82800.0
|
||||||
|
|
||||||
|
u'23:00'
|
||||||
|
|
||||||
|
The estimated and charged effords for tasks may be stored in
|
||||||
|
corresponding fields. Their sums are shown in the enclosing project.
|
||||||
|
|
||||||
|
>>> task01.estimatedEffort = 100
|
||||||
|
>>> task02.estimatedEffort = 15
|
||||||
|
|
||||||
|
>>> proj01.estimatedEffort
|
||||||
|
115.0
|
||||||
|
|
||||||
|
Reporting
|
||||||
|
---------
|
||||||
|
|
||||||
|
### Tasks overview ###
|
||||||
|
|
||||||
|
>>> from cco.work.browser import TasksOverview
|
||||||
|
>>> view = TasksOverview(proj01, TestRequest())
|
||||||
|
|
||||||
|
>>> ri = view.reportInstance
|
||||||
|
>>> ri
|
||||||
|
<cco.work.report.TasksOverview object ...>
|
||||||
|
|
2
cco/work/__init__.py
Normal file
2
cco/work/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
""" package cco.work
|
||||||
|
"""
|
28
cco/work/browser.py
Normal file
28
cco/work/browser.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2017 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
View class(es) for the cco.work package.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from loops.expert.browser.report import ReportConceptView
|
||||||
|
|
||||||
|
|
||||||
|
class TasksOverview(ReportConceptView):
|
||||||
|
|
||||||
|
reportName = 'tasks_overview'
|
57
cco/work/configure.zcml
Normal file
57
cco/work/configure.zcml
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
<configure
|
||||||
|
xmlns:zope="http://namespaces.zope.org/zope"
|
||||||
|
xmlns:browser="http://namespaces.zope.org/browser"
|
||||||
|
xmlns:i18n="http://namespaces.zope.org/i18n"
|
||||||
|
i18n_domain="cco.work">
|
||||||
|
|
||||||
|
<i18n:registerTranslations directory="locales" />
|
||||||
|
|
||||||
|
<!-- concept adapter classes -->
|
||||||
|
|
||||||
|
<zope:adapter factory="cco.work.task.Project" trusted="True" />
|
||||||
|
<zope:class class="cco.work.task.Project">
|
||||||
|
<require permission="zope.View"
|
||||||
|
interface="cco.work.interfaces.IProject" />
|
||||||
|
<require permission="zope.ManageContent"
|
||||||
|
set_schema="cco.work.interfaces.IProject" />
|
||||||
|
</zope:class>
|
||||||
|
|
||||||
|
<zope:adapter factory="cco.work.task.Task" trusted="True" />
|
||||||
|
<zope:class class="cco.work.task.Task">
|
||||||
|
<require permission="zope.View"
|
||||||
|
interface="cco.work.interfaces.ITask" />
|
||||||
|
<require permission="zope.ManageContent"
|
||||||
|
set_schema="cco.work.interfaces.ITask" />
|
||||||
|
</zope:class>
|
||||||
|
|
||||||
|
<!-- application views -->
|
||||||
|
|
||||||
|
<zope:adapter
|
||||||
|
name="cco.work.tasks_overview.html"
|
||||||
|
for="loops.interfaces.IConcept
|
||||||
|
zope.publisher.interfaces.browser.IBrowserRequest"
|
||||||
|
provides="zope.interface.Interface"
|
||||||
|
factory="cco.work.browser.TasksOverview"
|
||||||
|
permission="loops.ViewRestricted" />
|
||||||
|
|
||||||
|
<!-- field instances -->
|
||||||
|
|
||||||
|
<zope:adapter
|
||||||
|
name="duration"
|
||||||
|
factory="cco.work.interfaces.DurationFieldInstance" />
|
||||||
|
|
||||||
|
<!-- report instances -->
|
||||||
|
|
||||||
|
<zope:adapter
|
||||||
|
name="cco.work.tasks_overview"
|
||||||
|
factory="cco.work.report.TasksOverview"
|
||||||
|
provides="loops.expert.report.IReportInstance"
|
||||||
|
trusted="True" />
|
||||||
|
<zope:class class="cco.work.report.TasksOverview">
|
||||||
|
<require permission="zope.View"
|
||||||
|
interface="loops.expert.report.IReportInstance" />
|
||||||
|
<require permission="zope.ManageContent"
|
||||||
|
set_schema="loops.expert.report.IReportInstance" />
|
||||||
|
</zope:class>
|
||||||
|
|
||||||
|
</configure>
|
95
cco/work/interfaces.py
Normal file
95
cco/work/interfaces.py
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2017 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Interfaces for organizational stuff like persons and addresses.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from zope.i18nmessageid import MessageFactory
|
||||||
|
from zope.interface import Interface, Attribute
|
||||||
|
from zope import interface, component, schema
|
||||||
|
|
||||||
|
from cybertools.composer.schema.field import FieldInstance
|
||||||
|
from cybertools.composer.schema.interfaces import FieldType
|
||||||
|
from cybertools.organize.interfaces import ITask
|
||||||
|
from loops.interfaces import ILoopsAdapter, IConceptSchema
|
||||||
|
|
||||||
|
|
||||||
|
_ = MessageFactory('cco.work')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Duration(schema.Int):
|
||||||
|
|
||||||
|
__typeInfo__ = ('duration',
|
||||||
|
FieldType('textline',
|
||||||
|
u'A field representing a duration in time.',
|
||||||
|
instanceName='duration'))
|
||||||
|
|
||||||
|
|
||||||
|
class DurationFieldInstance(FieldInstance):
|
||||||
|
|
||||||
|
def display(self, value):
|
||||||
|
if value is None:
|
||||||
|
return ''
|
||||||
|
if isinstance(value, basestring):
|
||||||
|
value = value.replace(',', '.')
|
||||||
|
try:
|
||||||
|
value = float(value)
|
||||||
|
except ValueError:
|
||||||
|
value = 0.0
|
||||||
|
return u'%02i:%02i' % divmod(value * self.factor / 60.0, 60)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def factor(self):
|
||||||
|
return getattr(self.context.baseField, 'factor', 1)
|
||||||
|
|
||||||
|
|
||||||
|
# project and task management
|
||||||
|
|
||||||
|
class IProject(ILoopsAdapter):
|
||||||
|
|
||||||
|
estimatedEffort = Duration(
|
||||||
|
title=_(u'label_estimatedEffort'),
|
||||||
|
description=_(u'desc_estimatedEffort'),
|
||||||
|
readonly=True,)
|
||||||
|
chargedEffort = Duration(
|
||||||
|
title=_(u'label_chargedEffort'),
|
||||||
|
description=_(u'desc_chargedEffort'),
|
||||||
|
readonly=True,)
|
||||||
|
actualEffort = Duration(
|
||||||
|
title=_(u'label_actualEffort'),
|
||||||
|
description=_(u'desc_actualEffort'),
|
||||||
|
readonly=True)
|
||||||
|
|
||||||
|
estimatedEffort.factor = chargedEffort.factor = 3600
|
||||||
|
|
||||||
|
|
||||||
|
class ITask(IConceptSchema, ITask, IProject):
|
||||||
|
|
||||||
|
estimatedEffort = Duration(
|
||||||
|
title=_(u'label_estimatedEffort'),
|
||||||
|
description=_(u'desc_estimatedEffort'),
|
||||||
|
required=False,)
|
||||||
|
chargedEffort = Duration(
|
||||||
|
title=_(u'label_chargedEffort'),
|
||||||
|
description=_(u'desc_chargedEffort'),
|
||||||
|
required=False,)
|
||||||
|
|
||||||
|
estimatedEffort.factor = chargedEffort.factor = 3600
|
||||||
|
|
42
cco/work/locales/cco.work.pot
Normal file
42
cco/work/locales/cco.work.pot
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
|
||||||
|
"Project-Id-Version: 1.0\n"
|
||||||
|
"POT-Creation-Date: 2017-10-23 12:00 CET\n"
|
||||||
|
"PO-Revision-Date: 2017-12-07 12:00 CET\n"
|
||||||
|
"Last-Translator: Helmut Merz <helmutm@cy55.de>\n"
|
||||||
|
"Language-Team: cyberconcepts.org team <team@cyberconcepts.org>\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Generated-By: sublime_text\n"
|
||||||
|
|
||||||
|
# forms
|
||||||
|
|
||||||
|
msgid "label_estimatedEffort"
|
||||||
|
msgstr "Estimated Effort (hours)"
|
||||||
|
|
||||||
|
msgid "desc_estimatedEffort"
|
||||||
|
msgstr "The estimated effort in hours for completing this project."
|
||||||
|
|
||||||
|
msgid "label_chargedEffort"
|
||||||
|
msgstr "Charged Effort (hours)"
|
||||||
|
|
||||||
|
msgid "desc_chargedEffort"
|
||||||
|
msgstr "The effort in hours for completing this project as charged to the customer."
|
||||||
|
|
||||||
|
msgid "label_actualEffort"
|
||||||
|
msgstr "Actual Effort (hours)"
|
||||||
|
|
||||||
|
msgid "desc_actualEffort"
|
||||||
|
msgstr "The actual effort in hours."
|
||||||
|
|
||||||
|
# reports
|
||||||
|
|
||||||
|
msgid "colheader_estimatedEffort"
|
||||||
|
msgstr "Estimated Effort"
|
||||||
|
|
||||||
|
msgid "colheader_chargedEffort"
|
||||||
|
msgstr "Charged Effort"
|
||||||
|
|
||||||
|
msgid "colheader_actualEffort"
|
||||||
|
msgstr "Actual Effort"
|
||||||
|
|
BIN
cco/work/locales/de/LC_MESSAGES/cco.work.mo
Normal file
BIN
cco/work/locales/de/LC_MESSAGES/cco.work.mo
Normal file
Binary file not shown.
47
cco/work/locales/de/LC_MESSAGES/cco.work.po
Normal file
47
cco/work/locales/de/LC_MESSAGES/cco.work.po
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
"Project-Id-Version: 1.0\n"
|
||||||
|
"POT-Creation-Date: 2017-10-23 12:00 CET\n"
|
||||||
|
"PO-Revision-Date: 2017-11-14 12:00 CET\n"
|
||||||
|
"Last-Translator: Helmut Merz <helmutm@cy55.de>\n"
|
||||||
|
"Language-Team: cyberconcepts.org team <team@cyberconcepts.org>\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Generated-By: sublime_text\n"
|
||||||
|
|
||||||
|
# forms
|
||||||
|
|
||||||
|
msgid "label_estimatedEffort"
|
||||||
|
msgstr "Geschätzter Aufwand (Stunden)"
|
||||||
|
|
||||||
|
msgid "desc_estimatedEffort"
|
||||||
|
msgstr "Der geschätzte Aufwand für das Projekt in Stunden."
|
||||||
|
|
||||||
|
msgid "label_chargedEffort"
|
||||||
|
msgstr "Aufwand lt. Rechnung (Stunden)"
|
||||||
|
|
||||||
|
msgid "desc_chargedEffort"
|
||||||
|
msgstr "Der dem Kunden in Rechnung gestellte Aufwand in Stunden."
|
||||||
|
|
||||||
|
msgid "label_actualEffort"
|
||||||
|
msgstr "Tatsächlicher Aufwand (Stunden)"
|
||||||
|
|
||||||
|
msgid "desc_actualEffort"
|
||||||
|
msgstr "Der tatsächliche Aufwand in Stunden für das Projekt."
|
||||||
|
|
||||||
|
# reports
|
||||||
|
|
||||||
|
msgid "colheader_task"
|
||||||
|
msgstr "Projekt/Aufgabe"
|
||||||
|
|
||||||
|
msgid "colheader_estimatedEffort"
|
||||||
|
msgstr "Geschätzter Aufwand"
|
||||||
|
|
||||||
|
msgid "colheader_chargedEffort"
|
||||||
|
msgstr "Aufwand lt. Rechnung"
|
||||||
|
|
||||||
|
msgid "colheader_actualEffort"
|
||||||
|
msgstr "Tatsächlicher Aufwand"
|
||||||
|
|
BIN
cco/work/locales/en/LC_MESSAGES/cco.work.mo
Normal file
BIN
cco/work/locales/en/LC_MESSAGES/cco.work.mo
Normal file
Binary file not shown.
47
cco/work/locales/en/LC_MESSAGES/cco.work.po
Normal file
47
cco/work/locales/en/LC_MESSAGES/cco.work.po
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
"Project-Id-Version: 1.0\n"
|
||||||
|
"POT-Creation-Date: 2017-10-23 12:00 CET\n"
|
||||||
|
"PO-Revision-Date: 2017-12-07 12:00 CET\n"
|
||||||
|
"Last-Translator: Helmut Merz <helmutm@cy55.de>\n"
|
||||||
|
"Language-Team: cyberconcepts.org team <team@cyberconcepts.org>\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Generated-By: sublime_text\n"
|
||||||
|
|
||||||
|
# forms
|
||||||
|
|
||||||
|
msgid "label_estimatedEffort"
|
||||||
|
msgstr "Estimated Effort (hours)"
|
||||||
|
|
||||||
|
msgid "desc_estimatedEffort"
|
||||||
|
msgstr "The estimated effort in hours for completing this project."
|
||||||
|
|
||||||
|
msgid "label_chargedEffort"
|
||||||
|
msgstr "Charged Effort (hours)"
|
||||||
|
|
||||||
|
msgid "desc_quotedEffort"
|
||||||
|
msgstr "The effort in hours for completing this project as given in a quote or offering."
|
||||||
|
|
||||||
|
msgid "label_actualEffort"
|
||||||
|
msgstr "Actual Effort (hours)"
|
||||||
|
|
||||||
|
msgid "desc_actualEffort"
|
||||||
|
msgstr "The actual effort in hours."
|
||||||
|
|
||||||
|
# reports
|
||||||
|
|
||||||
|
msgid "colheader_task"
|
||||||
|
msgstr "Project/Task"
|
||||||
|
|
||||||
|
msgid "colheader_estimatedEffort"
|
||||||
|
msgstr "Estimated Effort"
|
||||||
|
|
||||||
|
msgid "colheader_chargedEffort"
|
||||||
|
msgstr "Charged Effort"
|
||||||
|
|
||||||
|
msgid "colheader_actualEffort"
|
||||||
|
msgstr "Actual Effort"
|
||||||
|
|
60
cco/work/report.py
Normal file
60
cco/work/report.py
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2017 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Report class(es) for the cco.work package.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from cybertools.util.jeep import Jeep
|
||||||
|
from loops.common import adapted, baseObject
|
||||||
|
from loops.expert.field import Field, DecimalField, TargetField, UrlField
|
||||||
|
from loops.expert.report import ReportInstance
|
||||||
|
from loops.organize.work.report import DurationField
|
||||||
|
from cco.work.interfaces import IProject, ITask, _
|
||||||
|
|
||||||
|
|
||||||
|
task = UrlField('title', _(u'colheader_task'),
|
||||||
|
executionSteps=['sort', 'output'])
|
||||||
|
estimatedEffort = DurationField(
|
||||||
|
'estimatedEffort', _(u'colheader_estimatedEffort'),
|
||||||
|
factor = 3600,
|
||||||
|
executionSteps=['output', 'totals'])
|
||||||
|
chargedEffort = DurationField('chargedEffort', _(u'colheader_chargedEffort'),
|
||||||
|
factor = 3600,
|
||||||
|
executionSteps=['output', 'totals'])
|
||||||
|
actualEffort = DurationField('actualEffort', _(u'colheader_actualEffort'),
|
||||||
|
executionSteps=['output', 'totals'])
|
||||||
|
|
||||||
|
|
||||||
|
class TasksOverview(ReportInstance):
|
||||||
|
|
||||||
|
type = 'cco.work.tasks_overview'
|
||||||
|
label = u'Tasks Overview'
|
||||||
|
|
||||||
|
fields = Jeep((task, estimatedEffort, chargedEffort, actualEffort))
|
||||||
|
#userSettings = (dayFrom, dayTo, activity)
|
||||||
|
defaultOutputFields = fields
|
||||||
|
defaultSortCriteria = (task,)
|
||||||
|
|
||||||
|
def selectObjects(self, parts):
|
||||||
|
result = []
|
||||||
|
for c in baseObject(self.view.adapted).getChildren():
|
||||||
|
obj = adapted(c)
|
||||||
|
if IProject.providedBy(obj) or ITask.providedBy(obj):
|
||||||
|
result.append(obj)
|
||||||
|
return result
|
115
cco/work/task.py
Normal file
115
cco/work/task.py
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
# cco.work.task
|
||||||
|
|
||||||
|
""" Implementation of cco.work concepts.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from zope.cachedescriptors.property import Lazy
|
||||||
|
from zope.interface import implementer
|
||||||
|
|
||||||
|
from cybertools.organize.interfaces import IWorkItems
|
||||||
|
from loops.common import AdapterBase, adapted, baseObject
|
||||||
|
#from loops.organize.task import Task
|
||||||
|
from loops.type import TypeInterfaceSourceList
|
||||||
|
from loops import util
|
||||||
|
from cco.work.interfaces import IProject, ITask
|
||||||
|
|
||||||
|
#from cco.storage.loops import common
|
||||||
|
|
||||||
|
|
||||||
|
TypeInterfaceSourceList.typeInterfaces += (IProject, ITask)
|
||||||
|
|
||||||
|
|
||||||
|
#class TaskBase(common.AdapterBase):
|
||||||
|
class TaskBase(AdapterBase):
|
||||||
|
|
||||||
|
_contextAttributes = list(ITask)
|
||||||
|
|
||||||
|
#start = end = None
|
||||||
|
|
||||||
|
defaultStates = ['done', 'done_x', 'finished', 'finished_x']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def actualEffort(self):
|
||||||
|
result = 0.0
|
||||||
|
for t in self.getAllTasks():
|
||||||
|
for wi in t.getWorkItems():
|
||||||
|
result += wi.effort
|
||||||
|
return result
|
||||||
|
|
||||||
|
def getSubTasks(self):
|
||||||
|
if self.__is_dummy__:
|
||||||
|
return []
|
||||||
|
result = []
|
||||||
|
for c in baseObject(self).getChildren():
|
||||||
|
obj = adapted(c)
|
||||||
|
if IProject.providedBy(obj) or ITask.providedBy(obj):
|
||||||
|
result.append(obj)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def getAllTasks(self):
|
||||||
|
if self.__is_dummy__:
|
||||||
|
return []
|
||||||
|
result = set([self])
|
||||||
|
for t1 in self.getSubTasks():
|
||||||
|
if t1 not in result:
|
||||||
|
for t2 in t1.getAllTasks():
|
||||||
|
if t2 not in result:
|
||||||
|
result.add(t2)
|
||||||
|
return result
|
||||||
|
|
||||||
|
@Lazy
|
||||||
|
def workItems(self):
|
||||||
|
rm = self.getLoopsRoot().getRecordManager()
|
||||||
|
return IWorkItems(rm['work'])
|
||||||
|
|
||||||
|
def addWorkItem(self, party, action='plan', **kw):
|
||||||
|
wi = self.workItems.add(util.getUidForObject(baseObject(self)), party)
|
||||||
|
wi.doAction(action, party, **kw)
|
||||||
|
|
||||||
|
def getWorkItems(self, crit={}):
|
||||||
|
kw = dict(task=util.getUidForObject(baseObject(self)),
|
||||||
|
state=crit.get('states') or self.defaultStates)
|
||||||
|
return self.workItems.query(**kw)
|
||||||
|
|
||||||
|
|
||||||
|
@implementer(IProject)
|
||||||
|
class Project(TaskBase):
|
||||||
|
""" Adapter for concepts of a project type
|
||||||
|
with additional fields for planning/controlling.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_adapterAttributes = AdapterBase._adapterAttributes + (
|
||||||
|
'estimatedEffort', 'chargedEffort', 'actualEffort',)
|
||||||
|
_contextAttributes = list(IProject)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def estimatedEffort(self):
|
||||||
|
return sum(self.tofloat(t.estimatedEffort or 0.0)
|
||||||
|
for t in self.getSubTasks())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def chargedEffort(self):
|
||||||
|
return sum(self.tofloat(t.chargedEffort or 0.0)
|
||||||
|
for t in self.getSubTasks())
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def tofloat(v):
|
||||||
|
if isinstance(v, str):
|
||||||
|
v = v.replace(',', '.')
|
||||||
|
try:
|
||||||
|
return float(v)
|
||||||
|
except ValueError:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
@implementer(ITask)
|
||||||
|
class Task(TaskBase):
|
||||||
|
""" Adapter for concepts of a task type
|
||||||
|
with additional fields for planning/controlling.
|
||||||
|
"""
|
||||||
|
|
||||||
|
#estimatedEffort = chargedEffort = 0.0
|
||||||
|
|
||||||
|
_adapterAttributes = AdapterBase._adapterAttributes + ('actualEffort',)
|
||||||
|
_contextAttributes = list(ITask)
|
||||||
|
|
95
cco/work/tests.py
Normal file
95
cco/work/tests.py
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
#! /usr/bin/python
|
||||||
|
|
||||||
|
"""
|
||||||
|
Tests for the 'cco.work' package.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import unittest, doctest
|
||||||
|
from zope import component
|
||||||
|
from zope.app.testing.setup import placefulSetUp, placefulTearDown
|
||||||
|
|
||||||
|
from cybertools.organize.work import workItemStates
|
||||||
|
from cybertools.tracking.btree import TrackingStorage
|
||||||
|
from loops.concept import Concept
|
||||||
|
from loops.expert.report import IReport, IReportInstance, Report
|
||||||
|
from loops.organize.work.base import WorkItem, WorkItems
|
||||||
|
from loops.setup import addAndConfigureObject
|
||||||
|
from loops.tests.setup import TestSite
|
||||||
|
from cco.work.interfaces import IProject, ITask
|
||||||
|
from cco.work.report import TasksOverview
|
||||||
|
from cco.work.task import Project, Task
|
||||||
|
|
||||||
|
|
||||||
|
def setupComponents(loopsRoot):
|
||||||
|
component.provideAdapter(WorkItems)
|
||||||
|
component.provideUtility(workItemStates(), name='organize.workItemStates')
|
||||||
|
component.provideAdapter(Project)
|
||||||
|
component.provideAdapter(Report, provides=IReport)
|
||||||
|
component.provideAdapter(Task)
|
||||||
|
component.provideAdapter(TasksOverview, provides=IReportInstance,
|
||||||
|
name='cco.work.tasks_overview')
|
||||||
|
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
site = placefulSetUp(True)
|
||||||
|
t = TestSite(site)
|
||||||
|
concepts, resources, views = t.setup()
|
||||||
|
loopsRoot = site['loops']
|
||||||
|
self.globs['loopsRoot'] = loopsRoot
|
||||||
|
setupComponents(loopsRoot)
|
||||||
|
records = loopsRoot.getRecordManager()
|
||||||
|
if 'work' not in records:
|
||||||
|
records['work'] = TrackingStorage(trackFactory=WorkItem)
|
||||||
|
setupObjects(concepts)
|
||||||
|
|
||||||
|
def setupObjects(concepts):
|
||||||
|
addAndConfigureObject(concepts, Concept, 'project',
|
||||||
|
title='Project', conceptType=concepts['type'],
|
||||||
|
typeInterface=IProject)
|
||||||
|
addAndConfigureObject(concepts, Concept, 'task',
|
||||||
|
title='Task', conceptType=concepts['type'],
|
||||||
|
typeInterface=ITask)
|
||||||
|
addAndConfigureObject(concepts, Concept, 'report',
|
||||||
|
title='Task', conceptType=concepts['type'],
|
||||||
|
typeInterface=IReport)
|
||||||
|
addAndConfigureObject(concepts, Concept, 'tasks_overview',
|
||||||
|
title='Tasks Overview', conceptType=concepts['report'],
|
||||||
|
reportType='cco.work.tasks_overview')
|
||||||
|
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
placefulTearDown()
|
||||||
|
|
||||||
|
|
||||||
|
class StorageTest(unittest.TestCase):
|
||||||
|
"Basic tests."
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
site = placefulSetUp(True)
|
||||||
|
t = TestSite(site)
|
||||||
|
self.concepts, resources, views = t.setup()
|
||||||
|
loopsRoot = site['loops']
|
||||||
|
setupComponents(loopsRoot)
|
||||||
|
setupObjects(self.concepts)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
placefulTearDown()
|
||||||
|
|
||||||
|
def testStorage(self):
|
||||||
|
self.setUp()
|
||||||
|
#print('***', self.concepts)
|
||||||
|
self.tearDown()
|
||||||
|
|
||||||
|
|
||||||
|
def test_suite():
|
||||||
|
flags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS
|
||||||
|
loader = unittest.TestLoader()
|
||||||
|
return unittest.TestSuite((
|
||||||
|
doctest.DocFileSuite('README.rst', optionflags=flags,
|
||||||
|
setUp=setUp, tearDown=tearDown),
|
||||||
|
loader.loadTestsFromTestCase(StorageTest),
|
||||||
|
))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main(defaultTest='test_suite')
|
Loading…
Add table
Reference in a new issue