diff --git a/README.txt b/README.txt index 7f4b13d..75d40f8 100755 --- a/README.txt +++ b/README.txt @@ -649,6 +649,23 @@ to the bottom, and to the top. ['m111', 'm114', 'm112', 'm113'] +Breadcrumbs +----------- + + >>> view = NodeView(m112, TestRequest()) + >>> view.breadcrumbs() + [] + + >>> loopsRoot.options = ['showBreadcrumbs'] + >>> m114.nodeType = 'page' + >>> m114.target = cc1 + >>> view = NodeView(m114, TestRequest()) + >>> view.breadcrumbs() + [{'url': 'http://127.0.0.1/loops/views/m1', 'label': u'Menu'}, + {'url': 'http://127.0.0.1/loops/views/m1/m11', 'label': u'Zope'}, + {'url': 'http://127.0.0.1/loops/views/m1/m11/m114', 'label': u''}] + + End-user Forms and Special Views ================================ diff --git a/browser/common.py b/browser/common.py index 762fba1..659fc11 100644 --- a/browser/common.py +++ b/browser/common.py @@ -18,8 +18,6 @@ """ Common base class for loops browser view classes. - -$Id$ """ from cgi import parse_qs, parse_qsl @@ -146,6 +144,9 @@ class BaseView(GenericView, I18NView): return self.controller.getTemplateMacros('resource', resource_macros) #return resource_macros.macros + def breadcrumbs(self): + return [] + @Lazy def name(self): return getName(self.context) diff --git a/browser/lobo/standard.py b/browser/lobo/standard.py index 9acced9..1c636ac 100644 --- a/browser/lobo/standard.py +++ b/browser/lobo/standard.py @@ -139,7 +139,7 @@ class Layout(Base, ConceptView): parts = (self.options('parts') or self.typeOptions('parts') or ['h1', 'g3']) - ti = adapted(self.context.conceptType).typeInterface + #ti = adapted(self.context.conceptType).typeInterface for p in parts: viewName = 'lobo_' + p view = component.queryMultiAdapter((self.context, self.request), diff --git a/browser/loops.css b/browser/loops.css index a28fd08..e4790e5 100644 --- a/browser/loops.css +++ b/browser/loops.css @@ -455,7 +455,6 @@ div.comment { } .blogpost .description { - font-weight: bold; font-size: 90%; color: #666666; padding-top: 0.4em; diff --git a/browser/node.py b/browser/node.py index 9c4cc9b..ddc21b8 100644 --- a/browser/node.py +++ b/browser/node.py @@ -91,6 +91,10 @@ class NodeView(BaseView): self.recordAccess() return result + @Lazy + def title(self): + return self.context.title or getName(self.context) + def breadcrumbs(self): if not self.globalOptions('showBreadcrumbs'): return [] @@ -100,7 +104,13 @@ class NodeView(BaseView): if menuItem != menu.context: data.append(dict(label=menuItem.title, url=absoluteURL(menuItem, self.request))) - return data + for p in getParents(menuItem): + if p == menu.context: + break + data.insert(1, dict(label=p.title, + url=absoluteURL(p, self.request))) + if self.virtualTarget: + data.extend(self.virtualTarget.breadcrumbs()) def recordAccess(self, viewName=''): target = self.virtualTargetObject diff --git a/browser/skin/lobo/body.pt b/browser/skin/lobo/body.pt index 6f8f83c..7d22bca 100644 --- a/browser/skin/lobo/body.pt +++ b/browser/skin/lobo/body.pt @@ -29,10 +29,11 @@
+ You are here: - / + >
@@ -63,7 +64,7 @@ tal:attributes="href string:${view/topMenu/url}/impressum">Impressum
)
Powered by - loops · + loops · Zope 3 · Python · Dojo. diff --git a/browser/skin/lobo/lobo.css b/browser/skin/lobo/lobo.css index de8850b..185692d 100644 --- a/browser/skin/lobo/lobo.css +++ b/browser/skin/lobo/lobo.css @@ -13,7 +13,7 @@ /* general */ h1, h2, h3, h4, h5, h6 { - margin-bottom: 0.5em; + margin-bottom: 0.4em; } a[href]:hover { @@ -472,23 +472,37 @@ div.comment { /* blog */ +.blog h1.headline { + clear: both; + padding-top: 1em; + margin-bottom: 0.2em; + font-size: 150%; +} + .blog .description { font-size: 90%; color: #666666; } +.blog .text { + font-size: 90%; + margin-top: 0.5em; +} + +.blog img { + padding-top: 0.2em; +} + .blogpost .description { - font-weight: bold; font-size: 90%; color: #666666; - padding-top: 0.4em; + margin-top: 1em; } .blog .info, .blogpost .info { font-style: italic; font-size: 90%; color: #666666; - padding-top: 0.4em; } /* calendar, work items */ diff --git a/compound/README.txt b/compound/README.txt index 06b6e2e..58ac802 100644 --- a/compound/README.txt +++ b/compound/README.txt @@ -321,10 +321,32 @@ Micro Articles >>> from loops.compound.microart.base import MicroArt >>> from loops.compound.microart.interfaces import IMicroArt - >>> component.provideAdapter(BlogPost, provides=IMicroArt) + >>> component.provideAdapter(MicroArt, provides=IMicroArt) >>> tMicroArt = addAndConfigureObject(concepts, Concept, 'microart', - ... title=u'MicroArt', conceptType=tType) + ... title=u'MicroArt', conceptType=tType, + ... typeInterface=IMicroArt) + + >>> ma01 = addAndConfigureObject(concepts, Concept, 'ma01', + ... conceptType=tMicroArt, + ... title=u'Organizational Knowledge', + ... story=u'Systemic KM talks about organizational knowledge.', + ... insight=u'Organizational knowledge is not visible.', + ... consequences=u'Use examples. Look for strucure and rules. ' + ... u'Knowledge shows itself in actions.', + ... followUps=u'What about collective intelligence? ' + ... u'How does an organization express itself?') + + >>> ma01._insight + u'Organizational knowledge is not visible.' + >>> list(resources) + [..., u'ma01_story'] + + >>> adMa01 = adapted(ma01) + >>> adMa01.insight + u'Organizational knowledge is not visible.' + >>> adMa01.story + u'Systemic KM talks about organizational knowledge.' Fin de partie diff --git a/compound/microart/base.py b/compound/microart/base.py index 3b568be..0133ad0 100644 --- a/compound/microart/base.py +++ b/compound/microart/base.py @@ -43,25 +43,26 @@ class MicroArt(Compound): implements(IMicroArt) - _adapterAttributes = Compound._adapterAttributes + ('text',) - _noexportAttributes = ('text',) - _textIndexAttributes = ('text',) + _contextAttributes = list(IMicroArt) + _adapterAttributes = Compound._adapterAttributes + ('story',) + _noexportAttributes = ('story',) + _textIndexAttributes = ('story', 'insight', 'consequences', 'folloUps') - defaultTextContentType = 'text/restructured' + defaultTextContentType = 'text/html' textContentType = defaultTextContentType - def getText(self): + def getStory(self): res = self.getParts() if len(res) > 0: return adapted(res[0]).data return u'' - def setText(self, value): + def setStory(self, value): res = self.getParts() if len(res) > 0: res = adapted(res[0]) else: tTextDocument = self.conceptManager['textdocument'] - name = getName(self.context) + '_text' + name = getName(self.context) + '_story' res = addAndConfigureObject(self.resourceManager, Resource, name, title=self.title, contentType=self.defaultTextContentType, resourceType=tTextDocument) @@ -70,4 +71,4 @@ class MicroArt(Compound): res = adapted(res) res.data = value notify(ObjectModifiedEvent(res.context)) - text = property(getText, setText) + story = property(getStory, setStory) diff --git a/compound/microart/browser.py b/compound/microart/browser.py index f35c83a..816bc30 100755 --- a/compound/microart/browser.py +++ b/compound/microart/browser.py @@ -26,7 +26,7 @@ from zope import component from zope.app.pagetemplate import ViewPageTemplateFile from zope.cachedescriptors.property import Lazy -from loops.browser.concept import ConceptView, ConceptRelationView +from loops.browser.concept import ConceptView from loops.common import adapted from loops import util from loops.util import _ @@ -37,16 +37,31 @@ view_macros = ViewPageTemplateFile('view_macros.pt') class MicroArtView(ConceptView): + @Lazy + def contentType(self): + return 'text/restructured' + + @Lazy + def macros(self): + return self.controller.getTemplateMacros('microart.view', view_macros) + @Lazy def macro(self): - return view_macros.macros['microart'] + return self.macros['main'] - def render(self): - return self.renderText(self.data['text'], self.adapted.textContentType) + @Lazy + def story(self): + return self.renderText(self.adapted.story, self.contentType) - def resources(self): - stdPred = self.loopsRoot.getConceptManager().getDefaultPredicate() - rels = self.context.getResourceRelations([stdPred]) - for r in rels: - yield self.childViewFactory(r, self.request, contextIsSecond=True) + @Lazy + def insight(self): + return self.renderText(self.adapted.insight, self.contentType) + + @Lazy + def consequences(self): + return self.renderText(self.adapted.consequences, self.contentType) + + @Lazy + def followUps(self): + return self.renderText(self.adapted.followUps, self.contentType) diff --git a/compound/microart/interfaces.py b/compound/microart/interfaces.py index 691bff7..e85ec2b 100644 --- a/compound/microart/interfaces.py +++ b/compound/microart/interfaces.py @@ -27,6 +27,11 @@ from loops.compound.interfaces import ICompound from loops.util import _ +class HtmlField(schema.Text): + + __typeInfo__ = ('html',) + + class IMicroArt(ICompound): """ A short article with a few elements, for collecting relevant information in a knowledge management environment. @@ -34,6 +39,7 @@ class IMicroArt(ICompound): # title = Ueberschrift, Thema + #story = HtmlField( # Geschichte story = schema.Text( # Geschichte title=_(u'Story'), description=_(u'The story, i.e. the main text of your ' @@ -54,7 +60,7 @@ class IMicroArt(ICompound): followUps = schema.Text( #Anschlussfragen title=_(u'Follow-up Questions'), - description=_(u'Question for helping to solve or avoid ' + description=_(u'Questions for helping to solve or avoid ' u'similar problems in the future.'), required=False) diff --git a/compound/microart/view_macros.pt b/compound/microart/view_macros.pt index ff15b1f..7dadf5d 100755 --- a/compound/microart/view_macros.pt +++ b/compound/microart/view_macros.pt @@ -1,6 +1,8 @@ + -
@@ -9,9 +11,17 @@ tal:condition="description"> Description
-
Here comes the text...
- - - +
+
+
+
+

+
+ + + \ No newline at end of file diff --git a/expert/browser/report.py b/expert/browser/report.py index b8f5f9b..8fdb035 100644 --- a/expert/browser/report.py +++ b/expert/browser/report.py @@ -73,8 +73,7 @@ class ResultsView(NodeView): @Lazy def params(self): params = dict(self.request.form) - if 'report_execute' in params: - del params['report_execute'] + params.pop('report_execute', None) return params @Lazy @@ -85,7 +84,7 @@ class ResultsView(NodeView): def reportInstance(self): instance = component.getAdapter(self.report, IReportInstance, name=self.report.reportType) - instance.request = self.request + instance.view = self return instance #@Lazy @@ -105,3 +104,5 @@ class ResultsView(NodeView): def displayedColumns(self): return self.reportInstance.getActiveOutputFields() + def getColumnRenderer(self, name): + return self.result_macros[name] diff --git a/expert/browser/results.pt b/expert/browser/results.pt index 9c892ad..227913c 100644 --- a/expert/browser/results.pt +++ b/expert/browser/results.pt @@ -15,8 +15,44 @@
- Default Results Listing + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + diff --git a/expert/configure.zcml b/expert/configure.zcml index 03543ad..68c5d79 100644 --- a/expert/configure.zcml +++ b/expert/configure.zcml @@ -22,9 +22,9 @@ provides="loops.expert.report.IReport" trusted="True" /> + interface="loops.expert.report.IReport" /> + set_schema="loops.expert.report.IReport" /> \n" "Language-Team: loops developers \n" "MIME-Version: 1.0\n" @@ -11,6 +11,9 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: kwrite\n" +msgid "You are here:" +msgstr "Sie sind hier:" + msgid "Concept" msgstr "Begriff" @@ -80,6 +83,8 @@ msgstr "Thema bearbeiten..." msgid "Modify topic." msgstr "Thema ändern" +# blog + msgid "Edit Blog Post..." msgstr "Eintrag bearbeiten..." @@ -101,6 +106,34 @@ msgstr "Tagebucheintrag anlegen" msgid "Export Blog" msgstr "Tagebuch exportieren" +# micro article + +msgid "Story" +msgstr "Story" + +msgid "The story, i.e. the main text of your micro article. Who did what? What happend?" +msgstr "Die Geschichte, der Haupttext Ihres MikroArtikels. Wer hat was getan? Was geschah?" + +msgid "Insight" +msgstr "Einsicht" + +msgid "What can we learn from the story? What has gone wrong? What was good?" +msgstr "Was können wir aus der Geschichte lernen? Was ist schiefgegangen? Was war gut?" + +msgid "Consequences" +msgstr "Folgerungen" + +msgid "What we will do next time in a similar situation?" +msgstr "Was werden wir das nächste Mal in einer ähnlichen Situation tun?" + +msgid "Follow-up Questions" +msgstr "Anschlussfragen" + +msgid "Questions for helping to solve or avoid similar problems in the future." +msgstr "Fragen, die einem dabei helfen können, das Problem in der Zukunft zu lösen oder zu vermeiden." + +# glossary + msgid "Glossary Item" msgstr "Glossareintrag" diff --git a/organize/work/README.txt b/organize/work/README.txt index 2946e1e..2dea1a0 100644 --- a/organize/work/README.txt +++ b/organize/work/README.txt @@ -183,12 +183,57 @@ So we use the PersonWorkItems view, assigning john to the query. Work Reports ============ +First we have to make sure that there is a report concept type available. +In addition we need a predicate that connects one or more tasks with a report. + + >>> from loops.expert.report import IReport, Report + >>> component.provideAdapter(Report) + >>> tReport = addAndConfigureObject(concepts, Concept, 'report', + ... title=u'Report', conceptType=concepts.getTypeConcept(), + ... typeInterface=IReport) + >>> hasReport = addAndConfigureObject(concepts, Concept, 'hasreport', + ... title=u'has Report', conceptType=concepts.getPredicateType()) + +Now we can create a report and register it as the report for the task +used above. + + >>> workStatement = addAndConfigureObject(concepts, Concept, 'work_statement', + ... title=u'Work Statement', conceptType=tReport, + ... reportType='work_report') + >>> workStatement.assignChild(task01, hasReport) + +The executable report is a report instance that is an adapter to the +(adapted) report instance. + >>> from loops.organize.work.report import WorkReportInstance >>> from loops.expert.report import IReportInstance >>> component.provideAdapter(WorkReportInstance, ... provides=IReportInstance, ... name='work_report') +The user interface to the report is a report view, the results are presented +in a results view. + + >>> from loops.view import Node + >>> reportNode = addAndConfigureObject(home, Node, 'report', + ... title=u'Report', target=workStatement) + >>> from loops.expert.browser.report import ReportView, ResultsView + >>> resultsView = ResultsView(reportNode, TestRequest()) + + >>> results = resultsView.results()#.getResult() + >>> len(list(results)) + 1 + >>> for row in results: + ... for col in resultsView.displayedColumns: + ... print col.getDisplayValue(row), + ... print + 08/12/28 19:00 20:15 + {'url': '.../home/report/.36', 'title': u'loops Development'} + {'url': '.../home/report/.33', 'title': u'john'} 01:15 00:15 finished + + >>> results.totals.data + {'effort': 900} + Fin de partie ============= diff --git a/organize/work/report.py b/organize/work/report.py index 71ec849..7dbefdf 100644 --- a/organize/work/report.py +++ b/organize/work/report.py @@ -28,59 +28,192 @@ from cybertools.composer.report.base import Report from cybertools.composer.report.base import LeafQueryCriteria, CompoundQueryCriteria from cybertools.composer.report.field import Field from cybertools.composer.report.result import ResultSet, Row as BaseRow +from cybertools.organize.interfaces import IWorkItems +from cybertools.util.date import timeStamp2Date +from cybertools.util.format import formatDate from cybertools.util.jeep import Jeep +from loops.common import adapted, baseObject +from loops.expert.field import TargetField from loops.expert.report import ReportInstance - -results_template = ViewPageTemplateFile('results.pt') +from loops import util -task = Field('task', u'Task', - description=u'The task to which work items belong.', - executionSteps=['query', 'output', 'sort']) -work = Field('work', u'Work', - description=u'The short description of the work.', - executionSteps=['output']) -workDescription = Field('workDescription', u'Work Description', - description=u'The long description of the work.', - executionSteps=['output']) -day = Field('day', u'Day', - description=u'The day the work was done.', - executionSteps=['output', 'sort']) +class DateField(Field): + + part = 'date' + format = 'short' + renderer = 'right' + + def getValue(self, row): + value = self.getRawValue(row) + if value is None: + return None + return timeStamp2Date(value) + + def getDisplayValue(self, row): + value = self.getValue(row) + if value: + view = row.parent.context.view + return formatDate(value, self.part, self.format, + view.languageInfo.language) + return u'' + + +class TimeField(DateField): + + part = 'time' + + +class DurationField(Field): + + renderer = 'right' + + def getValue(self, row): + value = self.getRawValue(row) + if value and 'totals' in self.executionSteps: + data = row.parent.totals.data + data[self.name] = data.get(self.name, 0) + value + if value: + value /= 3600.0 + return value + + def getDisplayValue(self, row): + value = self.getValue(row) + if not value: + return u'' + return u'%02i:%02i' % divmod(value * 60, 60) + + +tasks = Field('tasks', u'Tasks', + description=u'The tasks from which work items should be selected.', + executionSteps=['query']) dayFrom = Field('dayFrom', u'Start Day', description=u'The first day from which to select work.', executionSteps=['query']) dayTo = Field('dayTo', u'End Day', description=u'The last day until which to select work.', executionSteps=['query']) +day = DateField('day', u'Day', + description=u'The day the work was done.', + executionSteps=['sort', 'output']) +timeStart = TimeField('start', u'Start', + description=u'The time the unit of work was started.', + executionSteps=['sort', 'output']) +timeEnd = TimeField('end', u'End', + description=u'The time the unit of work was finished.', + executionSteps=['output']) +task = TargetField('taskId', u'Task', + description=u'The task to which work items belong.', + executionSteps=['output']) +party = TargetField('userName', u'Party', + description=u'The party (usually a person) who did the work.', + executionSteps=['query', 'sort', 'output']) +title = Field('title', u'Title', + description=u'The short description of the work.', + executionSteps=['output']) +description = Field('description', u'Description', + description=u'The long description of the work.', + executionSteps=['x_output']) +duration = DurationField('duration', u'Duration', + description=u'The duration of the work.', + executionSteps=['output']) +effort = DurationField('effort', u'Effort', + description=u'The effort of the work.', + executionSteps=['output', 'totals']) +state = Field('state', u'State', + description=u'The state of the work.', + executionSteps=['query', 'output']) class WorkRow(BaseRow): - pass + def getRawValue(self, attr): + if attr in self.attributeHandlers: + return self.attributeHandlers[attr](self, attr) + track = self.context + if attr in track.metadata_attributes: + return getattr(track, attr) + return track.data.get(attr, u'') + + def getDay(self, attr): + return self.context.timeStamp + + def getDuration(self, attr): + value = self.context.data.get('duration') + if value is None: + value = self.getRawValue('end') - self.getRawValue('start') + return value + + def getEffort(self, attr): + value = self.context.data.get('effort') + if value is None: + value = self.getDuration(attr) + return value + + attributeHandlers = dict(day=getDay, duration=getDuration, effort=getEffort) class WorkReportInstance(ReportInstance): - type = "deliverables" - label = u'Work Report' + type = "work_statement" + label = u'Work Statement' rowFactory = WorkRow - fields = Jeep((day, dayFrom, dayTo, task, work, workDescription)) - defaultOutputFields = fields - @Lazy + fields = Jeep((dayFrom, dayTo, tasks, + day, timeStart, timeEnd, task, party, title, description, + duration, effort, state)) + + defaultOutputFields = fields + defaultSortCriteria = (day, timeStart,) + + @property def queryCriteria(self): - # TODO: take from persistent report where appropriate - crit = [LeafQueryCriteria(f.name, f.operator, None, f) - for f in self.getAllQueryFields()] + form = self.view.request.form + crit = self.context.queryCriteria or [] + if not crit and 'tasks' not in form: + f = self.fields['tasks'] + tasks = baseObject(self.context).getChildren([self.hasReportPredicate]) + tasks = [util.getUidForObject(task) for task in tasks] + crit = [LeafQueryCriteria(f.name, f.operator, tasks, f)] + for f in self.getAllQueryFields(): + if f.name in form: + crit.append(LeafQueryCriteria(f.name, f.operator, form[f.name], f)) return CompoundQueryCriteria(crit) - def getResultsRenderer(self, name, defaultMacros): - return results_template.macros[name] - def selectObjects(self, parts): - task = parts.get('task') - if not task: - return [] - return [] + result = [] + tasks = [util.getObjectForUid(t) for t in parts.pop('tasks').comparisonValue] + for t in list(tasks): + tasks.extend(self.getAllSubtasks(t)) + for t in tasks: + result.extend(self.selectWorkItems(t, parts)) + # remove parts already used for selection from parts list: + parts.pop('userName', None) + return result + def selectWorkItems(self, task, parts): + states = ['done', 'done_x', 'finished'] + kw = dict(task=util.getUidForObject(task), state=states) + if 'userName' in parts: + kw['userName'] = parts['userName'].comparisonValue + wi = self.workItems + return wi.query(**kw) + + def getAllSubtasks(self, concept): + result = [] + for c in concept.getChildren(): + if c.conceptType in self.taskTypes: + result.append(c) + result.extend(self.getAllSubtasks(c)) + return result + + @Lazy + def taskTypes(self): + return (self.conceptManager['task'], + self.conceptManager['event'], + self.conceptManager['project']) + + @Lazy + def workItems(self): + return IWorkItems(self.recordManager['work']) diff --git a/organize/work/results.pt b/organize/work/results.pt deleted file mode 100644 index 0d1cec0..0000000 --- a/organize/work/results.pt +++ /dev/null @@ -1,61 +0,0 @@ - - - -
-

Work Items

- - - - - - - - -
-
-
- - -
- Work Items Listing - - - - - - - - - - - - - - - - - - - -
Task
2009-01
2007-03-3017:3020:002:30 - Task - JohnTitle
-
- - -