Merge branch 'master' of ssh://git.cy55.de/home/git/loops
This commit is contained in:
		
						commit
						4178829685
					
				
					 59 changed files with 1377 additions and 512 deletions
				
			
		|  | @ -49,7 +49,7 @@ from zope.security import canAccess | |||
| from zope.security.interfaces import ForbiddenAttribute, Unauthorized | ||||
| from zope.security.proxy import removeSecurityProxy | ||||
| from zope.traversing.browser import absoluteURL | ||||
| from zope.traversing.api import getName, getParent | ||||
| from zope.traversing.api import getName, getParent, traverse | ||||
| 
 | ||||
| from cybertools.ajax.dojo import dojoMacroTemplate | ||||
| from cybertools.browser.view import GenericView | ||||
|  | @ -70,7 +70,7 @@ from loops.organize.tracking import access | |||
| from loops.resource import Resource | ||||
| from loops.security.common import checkPermission | ||||
| from loops.security.common import canAccessObject, canListObject, canWriteObject | ||||
| from loops.type import ITypeConcept | ||||
| from loops.type import ITypeConcept, LoopsTypeInfo | ||||
| from loops import util | ||||
| from loops.util import _, saveRequest | ||||
| from loops import version | ||||
|  | @ -481,7 +481,7 @@ class BaseView(GenericView, I18NView): | |||
|             return absoluteURL(provider, self.request) | ||||
|         return None | ||||
| 
 | ||||
|     def renderText(self, text, contentType): | ||||
|     def renderText(self, text, contentType='text/restructured'): | ||||
|         text = util.toUnicode(text) | ||||
|         typeKey = util.renderingFactories.get(contentType, None) | ||||
|         if typeKey is None: | ||||
|  | @ -531,11 +531,29 @@ class BaseView(GenericView, I18NView): | |||
|     def conceptTypes(self): | ||||
|         return util.KeywordVocabulary(self.listTypes(('concept',), ('hidden',))) | ||||
| 
 | ||||
|     def parentTypesFromOtherSites(self): | ||||
|         result = [] | ||||
|         typeNames = self.typeOptions('foreign_parent_types') or [] | ||||
|         for path in self.typeOptions('foreign_parent_sites') or []: | ||||
|             site = traverse(self.loopsRoot, path, None) | ||||
|             if site is None: | ||||
|                 continue | ||||
|             cm = site.getConceptManager() | ||||
|             for tname in typeNames: | ||||
|                 t = cm.get(tname) | ||||
|                 if t is not None: | ||||
|                     type = LoopsTypeInfo(t) | ||||
|                     type.isForeignReference = True | ||||
|                     result.append(type) | ||||
|         return result | ||||
| 
 | ||||
|     def listTypesForSearch(self, include=None, exclude=None, sortOn='title'): | ||||
|         types = [dict(token=t.tokenForSearch, title=t.title) | ||||
|                     for t in ITypeManager(self.context).listTypes(include, exclude)] | ||||
|         if sortOn: | ||||
|             types.sort(key=lambda x: x[sortOn]) | ||||
|         for t in self.parentTypesFromOtherSites(): | ||||
|             types.append(dict(token=t.tokenForSearch, title=t.title)) | ||||
|         return types | ||||
| 
 | ||||
|     def typesForSearch(self): | ||||
|  |  | |||
							
								
								
									
										4
									
								
								browser/compound/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								browser/compound/__init__.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,4 @@ | |||
| """ | ||||
| package loops.browser.compound | ||||
| """ | ||||
| 
 | ||||
							
								
								
									
										14
									
								
								browser/compound/configure.zcml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								browser/compound/configure.zcml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,14 @@ | |||
| <configure | ||||
|    xmlns:zope="http://namespaces.zope.org/zope" | ||||
|    xmlns:browser="http://namespaces.zope.org/browser" | ||||
|    i18n_domain="loops"> | ||||
| 
 | ||||
|   <zope:adapter | ||||
|       name="compound.html" | ||||
|       for="loops.interfaces.IConcept | ||||
|            zope.publisher.interfaces.browser.IBrowserRequest" | ||||
|       provides="zope.interface.Interface" | ||||
|       factory="loops.browser.compound.standard.CompoundView" | ||||
|       permission="zope.View" /> | ||||
| 
 | ||||
| </configure> | ||||
							
								
								
									
										54
									
								
								browser/compound/standard.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								browser/compound/standard.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,54 @@ | |||
| # | ||||
| #  Copyright (c) 2013 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 | ||||
| # | ||||
| 
 | ||||
| """ | ||||
| Definition of compound views. | ||||
| """ | ||||
| 
 | ||||
| from zope import interface, component | ||||
| from zope.app.pagetemplate import ViewPageTemplateFile | ||||
| from zope.cachedescriptors.property import Lazy | ||||
| 
 | ||||
| from loops.browser.concept import ConceptView | ||||
| from loops.util import _ | ||||
| 
 | ||||
| 
 | ||||
| compound_macros = ViewPageTemplateFile('view_macros.pt') | ||||
| 
 | ||||
| 
 | ||||
| class CompoundView(ConceptView): | ||||
| 
 | ||||
|     @Lazy | ||||
|     def macro(self): | ||||
|         return compound_macros.macros['standard'] | ||||
| 
 | ||||
|     def getParts(self): | ||||
|         parts = (self.options('view_parts') or self.typeOptions('view_parts') or []) | ||||
|         return self.getPartViews(parts) | ||||
| 
 | ||||
|     def getPartViews(self, parts): | ||||
|         result = [] | ||||
|         for p in parts: | ||||
|             view = component.queryMultiAdapter((self.adapted, self.request), name=p) | ||||
|             if view is None: | ||||
|                 view = component.queryMultiAdapter((self.context, self.request), name=p) | ||||
|                 if view is not None: | ||||
|                     view.parent = self | ||||
|                     result.append(view) | ||||
|         return result | ||||
| 
 | ||||
							
								
								
									
										13
									
								
								browser/compound/view_macros.pt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								browser/compound/view_macros.pt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | |||
| <html i18n:domain="loops"> | ||||
| 
 | ||||
| 
 | ||||
| <metal:data define-macro="standard"> | ||||
|   <tal:part repeat="item item/getParts"> | ||||
|     <tal:check condition="item/checkPermissions"> | ||||
|       <metal:part use-macro="item/macro" /> | ||||
|     </tal:check> | ||||
|   </tal:part> | ||||
| </metal:data> | ||||
| 
 | ||||
| 
 | ||||
| </html> | ||||
|  | @ -1,5 +1,5 @@ | |||
| # | ||||
| #  Copyright (c) 2012 Helmut Merz helmutm@cy55.de | ||||
| #  Copyright (c) 2013 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 | ||||
|  | @ -50,6 +50,7 @@ from cybertools.meta.interfaces import IOptions | |||
| from cybertools.typology.interfaces import IType, ITypeManager | ||||
| from cybertools.util.jeep import Jeep | ||||
| from loops.browser.common import EditForm, BaseView, LoopsTerms, concept_macros | ||||
| from loops.browser.common import ViewMode | ||||
| from loops.common import adapted | ||||
| from loops.concept import Concept, ConceptTypeSourceList, PredicateSourceList | ||||
| from loops.i18n.browser import I18NView | ||||
|  | @ -196,6 +197,12 @@ class BaseRelationView(BaseView): | |||
|             return u'' | ||||
|         return self.predicateTitle | ||||
| 
 | ||||
|     @Lazy | ||||
|     def relationInfo(self): | ||||
|         predInfo = ', ' .join(p.title for p in self.predicates  | ||||
|                                 if p != self.defaultPredicate) | ||||
|         return ' | '.join(t for t in (self.description, predInfo) if t) | ||||
| 
 | ||||
| 
 | ||||
| class ConceptView(BaseView): | ||||
| 
 | ||||
|  | @ -228,6 +235,7 @@ class ConceptView(BaseView): | |||
|                         subMacro=concept_macros.macros['parents'], | ||||
|                         priority=20, info=self) | ||||
| 
 | ||||
|     # the part-based layout is now implemented in loops.browser.compound | ||||
|     def getParts(self): | ||||
|         parts = (self.params.get('parts') or [])   # deprecated! | ||||
|         if not parts: | ||||
|  | @ -740,3 +748,30 @@ class ListTypeInstances(ListChildren): | |||
|                                     noDuplicates, useFilter, [self.typePredicate]): | ||||
|                 yield c | ||||
| 
 | ||||
| 
 | ||||
| class TabbedPage(ConceptView): | ||||
| 
 | ||||
|     @Lazy | ||||
|     def subpagePredicates(self): | ||||
|         pred = self.conceptManager.get('issubpage') | ||||
|         if pred is None: | ||||
|             pred = self.isPartOfPredicate | ||||
|         return [pred] | ||||
| 
 | ||||
|     def viewModes(self): | ||||
|         modes = Jeep() | ||||
|         for s in self.getSiblings(self.subpagePredicates): | ||||
|             url = self.nodeView.getUrlForTarget(s) | ||||
|             modes.append(ViewMode(getName(s), s.title, url)) | ||||
|         if not modes: | ||||
|             return modes | ||||
|         modes[getName(self.context)].active = True | ||||
|         return modes | ||||
| 
 | ||||
|     def getSiblings(self, preds): | ||||
|         for p in self.context.getParents(preds): | ||||
|             parent = p | ||||
|             break | ||||
|         else: | ||||
|             return [] | ||||
|         return p.getChildren(preds) | ||||
|  |  | |||
|  | @ -62,9 +62,10 @@ | |||
|           string:$resourceBase/cybertools.icons/table.png" /> | ||||
|       </a> | ||||
|     </h1> | ||||
|   <metal:block use-macro="view/concept_macros/filter_input" /> | ||||
|     <metal:block use-macro="view/concept_macros/filter_input" /> | ||||
|   </metal:title> | ||||
|   <p tal:define="description description|item/renderedDescription" | ||||
|   <p metal:define-macro="conceptdescription" | ||||
|      tal:define="description description|item/renderedDescription" | ||||
|      tal:condition="description"> | ||||
|     <i tal:content="structure description">Description</i></p> | ||||
| </metal:title> | ||||
|  | @ -158,11 +159,8 @@ | |||
|                  tal:attributes="dojoType python: | ||||
|                         item.editable and 'dojo.dnd.Source' or ''"> | ||||
|           <tal:items repeat="related children"> | ||||
|             <tal:item define="class python: repeat['related'].odd() and 'even' or 'odd'; | ||||
|                               description related/description; | ||||
|                               predicate related/predicateTitle; | ||||
|                               info python: ' | '.join( | ||||
|                                     t for t in (description, predicate) if t)"> | ||||
|             <tal:item define="class python:  | ||||
|                         repeat['related'].odd() and 'even' or 'odd';"> | ||||
|               <tr tal:attributes="class string:$class dojoDndItem dojoDndHandle; | ||||
|                                   id related/uniqueId"> | ||||
|                 <td tal:condition="item/showCheckboxes|nothing" | ||||
|  | @ -172,7 +170,7 @@ | |||
|                          tal:attributes="value uid;" /></td> | ||||
|                 <td valign="top"> | ||||
|                   <a tal:attributes="href python: view.getUrlForTarget(related); | ||||
|                                      title info"> | ||||
|                                      title related/relationInfo"> | ||||
|                     <span tal:replace="related/title">Resource Title</span> | ||||
|                   </a> | ||||
|                 </td> | ||||
|  | @ -241,11 +239,8 @@ | |||
|                  tal:attributes="dojoType python: | ||||
|                         item.editable and 'dojo.dnd.Source' or ''"> | ||||
|           <tal:items repeat="related resources"> | ||||
|             <tal:item define="class python: repeat['related'].odd() and 'even' or 'odd'; | ||||
|                               description related/description; | ||||
|                               predicate related/predicateTitle; | ||||
|                               info python: ' | '.join( | ||||
|                                     t for t in (description, predicate) if t)"> | ||||
|             <tal:item define="class python:  | ||||
|                         repeat['related'].odd() and 'even' or 'odd';"> | ||||
|               <tr tal:attributes="class string:$class dojoDndItem dojoDndHandle; | ||||
|                                   id related/uniqueId"> | ||||
|                 <td tal:condition="item/showCheckboxes|nothing" | ||||
|  | @ -262,7 +257,7 @@ | |||
|                     <img tal:attributes="src icon/src" /> | ||||
|                   </a> | ||||
|                   <a tal:attributes="href python: view.getUrlForTarget(related); | ||||
|                                      title info"> | ||||
|                                      title related/relationInfo"> | ||||
|                     <div tal:content="related/title">Resource Title</div> | ||||
|                   </a> | ||||
|                 </td> | ||||
|  |  | |||
|  | @ -553,6 +553,14 @@ | |||
|       factory="loops.browser.concept.ListTypeInstances" | ||||
|       permission="zope.View" /> | ||||
| 
 | ||||
|   <zope:adapter | ||||
|       name="tabbed_page.html" | ||||
|       for="loops.interfaces.IConcept | ||||
|            zope.publisher.interfaces.browser.IBrowserRequest" | ||||
|       provides="zope.interface.Interface" | ||||
|       factory="loops.browser.concept.TabbedPage" | ||||
|       permission="zope.View" /> | ||||
| 
 | ||||
|   <!-- dialogs/forms (end-user views) --> | ||||
| 
 | ||||
|   <page | ||||
|  | @ -757,6 +765,7 @@ | |||
|       attribute="cleanup" | ||||
|       permission="zope.ManageSite" /> | ||||
| 
 | ||||
|   <include package=".compound" /> | ||||
|   <include package=".skin" /> | ||||
|   <include package=".lobo" /> | ||||
|   <include package=".mobile" /> | ||||
|  |  | |||
|  | @ -51,6 +51,8 @@ | |||
|           </th> | ||||
|         </tr></tbody> | ||||
| 
 | ||||
|         <tbody metal:define-slot="custom_header" /> | ||||
| 
 | ||||
|         <tbody><tr><td colspan="5" style="padding-right: 15px"> | ||||
|               <div id="form.fields"> | ||||
|                 <metal:fields use-macro="view/fieldRenderers/fields" /> | ||||
|  | @ -59,7 +61,7 @@ | |||
| 
 | ||||
|         <tbody> | ||||
|           <tr metal:use-macro="view/template/macros/assignments" /> | ||||
|           <tal:custom define="customMacro view/customMacro" | ||||
|           <tal:custom define="customMacro view/customMacro|nothing" | ||||
|                       condition="customMacro"> | ||||
|             <tr metal:use-macro="customMacro" /> | ||||
|           </tal:custom> | ||||
|  | @ -119,6 +121,8 @@ | |||
|                      tal:attributes="value typeToken" /> | ||||
|         </th></tr></tbody> | ||||
| 
 | ||||
|         <tbody metal:define-slot="custom_header" /> | ||||
| 
 | ||||
|         <tbody><tr><td colspan="5"> | ||||
|               <div id="form.fields"> | ||||
|                 <metal:fields use-macro="view/fieldRenderers/fields" /> | ||||
|  | @ -127,7 +131,7 @@ | |||
| 
 | ||||
|         <tbody> | ||||
|           <tr metal:use-macro="view/template/macros/assignments" /> | ||||
|           <tal:custom define="customMacro view/customMacro" | ||||
|           <tal:custom define="customMacro view/customMacro|nothing" | ||||
|                       condition="customMacro"> | ||||
|             <tr metal:use-macro="customMacro" /> | ||||
|           </tal:custom> | ||||
|  |  | |||
|  | @ -117,6 +117,7 @@ | |||
|   <tal:img condition="cell/img"> | ||||
|     <a dojoType="dojox.image.Lightbox" group="mediasset" | ||||
|        i18n:attributes="title" | ||||
|        tal:omit-tag="python:part.imageSize in ('large',)" | ||||
|        tal:attributes="href cell/img/fullImageUrl; | ||||
|                        title python: cell.img['description'] or cell.img['title']"> | ||||
|       <img tal:condition="showImageLink|python:False" | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| /* $Id$ */ | ||||
| /* loops.js */ | ||||
| 
 | ||||
| function openEditWindow(url) { | ||||
|     zmi = window.open(url, 'zmi'); | ||||
|  | @ -19,6 +19,12 @@ function toggleCheckBoxes(toggle, fieldName) { | |||
|     for (i in w) w[i].checked=toggle.checked; | ||||
| } | ||||
| 
 | ||||
| function setRadioButtons(value) { | ||||
|     dojo.forEach(dojo.query('input[type="radio"][value="' + value + '"]'),  | ||||
|                         function(n) { | ||||
|                             n.checked = true;}) | ||||
| } | ||||
| 
 | ||||
| function validate(nodeName, required) { | ||||
|     // (work in progress) - may be used for onBlur event handler
 | ||||
|     var w = dojo.byId(nodeName); | ||||
|  |  | |||
|  | @ -195,7 +195,11 @@ class NodeView(BaseView): | |||
|                         subMacro=calendar_macros.macros['main'], | ||||
|                         priority=90) | ||||
|         # force early portlet registrations by target by setting up target view | ||||
|         self.virtualTarget | ||||
|         if self.virtualTarget is not None: | ||||
|             std = self.virtualTarget.typeOptions('portlet_states') | ||||
|             if std: | ||||
|                 from loops.organize.stateful.browser import registerStatesPortlet | ||||
|                 registerStatesPortlet(self.controller, self.virtualTarget, std) | ||||
| 
 | ||||
|     @Lazy | ||||
|     def usersPresent(self): | ||||
|  | @ -381,6 +385,8 @@ class NodeView(BaseView): | |||
|             ht = super(NodeView, self).headTitle | ||||
|             if ht not in parts: | ||||
|                 parts.append(ht) | ||||
|         if self.globalOptions('reverseHeadTitle'): | ||||
|             parts.reverse() | ||||
|         return ' - ' .join(parts) | ||||
| 
 | ||||
|     @Lazy | ||||
|  |  | |||
|  | @ -314,8 +314,13 @@ | |||
| 
 | ||||
| 
 | ||||
| <metal:login define-macro="login"> | ||||
|     <div><a href="login.html" | ||||
|     <div> | ||||
|       <a href="login.html" | ||||
|             i18n:translate="">Log in</a></div> | ||||
|     <div tal:define="register python:view.globalOptions('provideLogin')" | ||||
|          tal:condition="register"> | ||||
|       <a tal:attributes="href python:register[0]" | ||||
|          i18n:translate="">Register new member</a></div> | ||||
| </metal:login> | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -40,6 +40,7 @@ from zope.traversing.browser import absoluteURL | |||
| from cybertools.browser.action import actions | ||||
| from cybertools.meta.interfaces import IOptions | ||||
| from cybertools.typology.interfaces import IType | ||||
| from cybertools.util.html import extractFirstPart | ||||
| from cybertools.xedit.browser import ExternalEditorView, fromUnicode | ||||
| from loops.browser.action import DialogAction, TargetAction | ||||
| from loops.browser.common import EditForm, BaseView | ||||
|  | @ -131,6 +132,9 @@ class ResourceView(BaseView): | |||
|     def macro(self): | ||||
|         if 'image/' in self.context.contentType: | ||||
|             return self.template.macros['image'] | ||||
|         #elif 'audio/' in self.context.contentType: | ||||
|         #    self.registerDojoAudio() | ||||
|         #    return self.template.macros['audio'] | ||||
|         else: | ||||
|             return self.template.macros['download'] | ||||
| 
 | ||||
|  | @ -252,6 +256,12 @@ class ResourceView(BaseView): | |||
|             #return util.toUnicode(wp.render(self.request)) | ||||
|         return super(ResourceView, self).renderText(text, contentType) | ||||
| 
 | ||||
|     def renderShortText(self): | ||||
|         return self.renderDescription() or self.createShortText(self.render()) | ||||
| 
 | ||||
|     def createShortText(self, text=None): | ||||
|         return extractFirstPart(text or self.render()) | ||||
| 
 | ||||
|     def download(self): | ||||
|         """ Force download, e.g. of a PDF file """ | ||||
|         return self.show(True) | ||||
|  |  | |||
|  | @ -30,7 +30,13 @@ | |||
|         <metal:tabs use-macro="views/node_macros/breadcrumbs" /> | ||||
|       </metal:breadcrumbs> | ||||
|       <div metal:define-slot="actions"></div> | ||||
|       <div metal:define-slot="message"></div> | ||||
|       <metal:message define-slot="message"> | ||||
|         <div class="message" | ||||
|              i18n:translate="" | ||||
|              tal:define="msg request/loops.message|nothing" | ||||
|              tal:condition="msg" | ||||
|              tal:content="msg" /> | ||||
|       </metal:message> | ||||
|       <metal:tabs use-macro="views/node_macros/view_modes" /> | ||||
|       <metal:content define-slot="content"> | ||||
|         <tal:content define="item nocall:view/item; | ||||
|  |  | |||
|  | @ -62,6 +62,10 @@ h1, h2, h3, h4, h5, h6 { | |||
|     margin-bottom: 0.4em; | ||||
| } | ||||
| 
 | ||||
| a { | ||||
|     text-decoration: none; | ||||
| } | ||||
| 
 | ||||
| a[href]:hover { | ||||
|     text-decoration: none; | ||||
|     color: #6060c0; | ||||
|  | @ -120,6 +124,10 @@ thead th { | |||
|     margin-bottom: 0.3em; | ||||
| } | ||||
| 
 | ||||
| .infotext { | ||||
|     font-size: 90%; | ||||
| } | ||||
| 
 | ||||
| .fields td { | ||||
|     vertical-align: top; | ||||
| } | ||||
|  | @ -163,6 +171,10 @@ table.listing td { | |||
|     border-bottom: 1px dotted #dddddd; | ||||
| } | ||||
| 
 | ||||
| table.listing tr.vpad td { | ||||
|     padding: 7px 2px 7px 2px; | ||||
| } | ||||
| 
 | ||||
| fieldset.box table.listing td { | ||||
|     padding: 0 1px 0 1px; | ||||
| } | ||||
|  | @ -267,7 +279,8 @@ fieldset.box td { | |||
| 
 | ||||
| .top-actions { | ||||
|     position: absolute; | ||||
|     top: 30px; | ||||
|     top: 40px; | ||||
|     margin-left: 10px; | ||||
| } | ||||
| 
 | ||||
| .quicksearch input { | ||||
|  | @ -281,7 +294,7 @@ fieldset.box td { | |||
| 
 | ||||
| .page-actions { | ||||
|     position: absolute; | ||||
|     top: 55px; | ||||
|     top: 75px; | ||||
|     margin-left: 210px; | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -113,6 +113,9 @@ class AdapterBase(object): | |||
|         self.context = context | ||||
|         self.__parent__ = context # to get the permission stuff right | ||||
| 
 | ||||
|     def __hash__(self): | ||||
|         return hash(self.context) | ||||
| 
 | ||||
|     def __getattr__(self, attr): | ||||
|         self.checkAttr(attr) | ||||
|         return getattr(self.context, '_' + attr, None) | ||||
|  |  | |||
|  | @ -355,7 +355,7 @@ Books, Sections, and Pages | |||
|   >>> importPath = os.path.join(os.path.dirname(__file__), 'book') | ||||
|   >>> importData(loopsRoot, importPath, 'loops_book_de.dmp') | ||||
| 
 | ||||
|   >>> from loops.compound.book.browser import PageLayout | ||||
|   >>> from loops.compound.book.browser import BookView, SectionView, TopicView | ||||
| 
 | ||||
| 
 | ||||
| Fin de partie | ||||
|  |  | |||
|  | @ -1,52 +0,0 @@ | |||
| # | ||||
| #  Copyright (c) 2012 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 | ||||
| # | ||||
| 
 | ||||
| """ | ||||
| Implementation of book and book components | ||||
| """ | ||||
| 
 | ||||
| from zope.cachedescriptors.property import Lazy | ||||
| from zope.interface import implements | ||||
| from zope.traversing.api import getName | ||||
| 
 | ||||
| from loops.compound.base import Compound | ||||
| from loops.compound.book.interfaces import IPage | ||||
| from loops.type import TypeInterfaceSourceList | ||||
| 
 | ||||
| 
 | ||||
| TypeInterfaceSourceList.typeInterfaces += (IPage,) | ||||
| 
 | ||||
| 
 | ||||
| class Page(Compound): | ||||
| 
 | ||||
|     implements(IPage) | ||||
| 
 | ||||
|     compoundPredicateNames = ['ispartof', 'standard'] | ||||
| 
 | ||||
|     @Lazy | ||||
|     def documentType(self): | ||||
|         return self.context.getConceptManager()['documenttype'] | ||||
| 
 | ||||
|     def getParts(self): | ||||
|         result = {} | ||||
|         for r in super(Page, self).getParts(): | ||||
|             for parent in r.getParents(): | ||||
|                 if parent.conceptType == self.documentType: | ||||
|                     item = result.setdefault(getName(parent), []) | ||||
|                     item.append(r) | ||||
|         return result | ||||
|  | @ -1,5 +1,5 @@ | |||
| # | ||||
| #  Copyright (c) 2012 Helmut Merz helmutm@cy55.de | ||||
| #  Copyright (c) 2013 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 | ||||
|  | @ -26,6 +26,7 @@ from zope.app.pagetemplate import ViewPageTemplateFile | |||
| from zope.cachedescriptors.property import Lazy | ||||
| from zope.traversing.api import getName | ||||
| 
 | ||||
| from cybertools.meta.interfaces import IOptions | ||||
| from cybertools.typology.interfaces import IType | ||||
| from loops.browser.lobo import standard | ||||
| from loops.browser.concept import ConceptView | ||||
|  | @ -45,10 +46,22 @@ class Base(object): | |||
|     def book_macros(self): | ||||
|         return book_template.macros | ||||
| 
 | ||||
|     @Lazy | ||||
|     def documentTypeType(self): | ||||
|         return self.conceptManager['documenttype'] | ||||
| 
 | ||||
|     @Lazy | ||||
|     def sectionType(self): | ||||
|         return self.conceptManager['section'] | ||||
| 
 | ||||
|     @Lazy | ||||
|     def isPartOfPredicate(self): | ||||
|         return self.conceptManager['ispartof'] | ||||
| 
 | ||||
|     @Lazy | ||||
|     def showNavigation(self): | ||||
|         return self.typeOptions.show_navigation | ||||
| 
 | ||||
|     @Lazy | ||||
|     def breadcrumbsParent(self): | ||||
|         for p in self.context.getParents([self.isPartOfPredicate]): | ||||
|  | @ -82,34 +95,8 @@ class Base(object): | |||
|         if self.editable: | ||||
|             return 'index.html' | ||||
| 
 | ||||
| 
 | ||||
| class BookOverview(Base, ConceptView): | ||||
| 
 | ||||
|     @Lazy | ||||
|     def macro(self): | ||||
|         return book_template.macros['book'] | ||||
| 
 | ||||
| 
 | ||||
| class SectionView(Base, ConceptView): | ||||
| 
 | ||||
|     @Lazy | ||||
|     def macro(self): | ||||
|         return book_template.macros['section'] | ||||
| 
 | ||||
|     @Lazy | ||||
|     def documentTypeType(self): | ||||
|         return self.conceptManager['documenttype'] | ||||
| 
 | ||||
|     @Lazy | ||||
|     def showNavigation(self): | ||||
|         return self.typeOptions.show_navigation | ||||
| 
 | ||||
|     @Lazy | ||||
|     def sectionType(self): | ||||
|         return self.conceptManager['section'] | ||||
| 
 | ||||
|     def getResources(self): | ||||
|         relViews = super(SectionView, self).getResources() | ||||
|         relViews = super(Base, self).getResources() | ||||
|         return relViews | ||||
| 
 | ||||
|     @Lazy | ||||
|  | @ -132,11 +119,36 @@ class SectionView(Base, ConceptView): | |||
|                 self.images[idx].append(img) | ||||
|         return result | ||||
| 
 | ||||
|     def getCssClassForResource(self, r): | ||||
|     def getDocumentTypeForResource(self, r): | ||||
|         for c in r.context.getConcepts([self.defaultPredicate]): | ||||
|             if c.conceptType == self.documentTypeType: | ||||
|                 return getName(c) | ||||
|         return 'textelement' | ||||
|                 return c | ||||
| 
 | ||||
|     def getOptionsForResource(self, r, name): | ||||
|         dt = self.getDocumentTypeForResource(r) | ||||
|         if dt is not None: | ||||
|             return IOptions(adapted(dt))(name) | ||||
| 
 | ||||
|     def getTitleForResource(self, r): | ||||
|         if self.getOptionsForResource(r, 'showtitle'): | ||||
|             return r.title | ||||
| 
 | ||||
|     def getIconForResource(self, r): | ||||
|         icon = self.getOptionsForResource(r, 'icon') | ||||
|         if icon: | ||||
|             return '/'.join((self.controller.resourceBase, icon[0])) | ||||
| 
 | ||||
|     def getCssClassForResource(self, r): | ||||
|         dt = self.getDocumentTypeForResource(r) | ||||
|         if dt is None: | ||||
|             return 'textelement' | ||||
|         css = IOptions(adapted(dt))('cssclass') | ||||
|         if css: | ||||
|             return css | ||||
|         return getName(dt) | ||||
| 
 | ||||
|     def getMacroForResource(self, r): | ||||
|         return self.book_macros['default_text'] | ||||
| 
 | ||||
|     def getParentsForResource(self, r): | ||||
|         for c in r.context.getConcepts([self.defaultPredicate]): | ||||
|  | @ -144,64 +156,23 @@ class SectionView(Base, ConceptView): | |||
|                 yield c | ||||
| 
 | ||||
| 
 | ||||
| # layout parts - probably obsolete: | ||||
| class BookView(Base, ConceptView): | ||||
| 
 | ||||
| class PageLayout(Base, standard.Layout): | ||||
| 
 | ||||
|     def getParts(self): | ||||
|         parts = ['headline', 'keyquestions', 'quote', 'maintext',  | ||||
|                  'story', 'tip', 'usecase'] | ||||
|         return self.getPartViews(parts) | ||||
|     @Lazy | ||||
|     def macro(self): | ||||
|         return book_template.macros['book'] | ||||
| 
 | ||||
| 
 | ||||
| class PagePart(object): | ||||
| class SectionView(Base, ConceptView): | ||||
| 
 | ||||
|     template = book_template | ||||
|     templateName = 'compound.book' | ||||
|     macroName = 'text' | ||||
|     partName = None     # define in subclass | ||||
|     gridPattern = ['span-4'] | ||||
| 
 | ||||
|     def getResources(self): | ||||
|         result = [] | ||||
|         res = self.adapted.getParts().get(self.partName) or [] | ||||
|         for idx, r in enumerate(res): | ||||
|             result.append(standard.ResourceView( | ||||
|                                 r, self.request, parent=self, idx=idx)) | ||||
|         return result | ||||
|     @Lazy | ||||
|     def macro(self): | ||||
|         return book_template.macros['section'] | ||||
| 
 | ||||
| 
 | ||||
| class Headline(PagePart, standard.Header2): | ||||
| class TopicView(Base, ConceptView): | ||||
| 
 | ||||
|     macroName = 'headline' | ||||
|     @Lazy | ||||
|     def macro(self): | ||||
|         return book_template.macros['topic'] | ||||
| 
 | ||||
| 
 | ||||
| class MainText(PagePart, standard.BasePart): | ||||
| 
 | ||||
|     partName = 'maintext' | ||||
| 
 | ||||
| 
 | ||||
| class KeyQuestions(PagePart, standard.BasePart): | ||||
| 
 | ||||
|     partName = 'keyquestions' | ||||
| 
 | ||||
| 
 | ||||
| class Story(PagePart, standard.BasePart): | ||||
| 
 | ||||
|     partName = 'story' | ||||
| 
 | ||||
| 
 | ||||
| class Tip(PagePart, standard.BasePart): | ||||
| 
 | ||||
|     partName = 'tip' | ||||
| 
 | ||||
| 
 | ||||
| class UseCase(PagePart, standard.BasePart): | ||||
| 
 | ||||
|     partName = 'usecase' | ||||
| 
 | ||||
| 
 | ||||
| class Quote(PagePart, standard.BasePart): | ||||
| 
 | ||||
|     partName = 'quote' | ||||
|     gridPattern = ['span-2 last'] | ||||
|  |  | |||
|  | @ -3,18 +3,6 @@ | |||
|    xmlns:browser="http://namespaces.zope.org/browser" | ||||
|    i18n_domain="loops"> | ||||
| 
 | ||||
|   <!-- type adapters --> | ||||
| 
 | ||||
|   <zope:adapter factory="loops.compound.book.base.Page"  | ||||
|                 provides="loops.compound.book.interfaces.IPage" | ||||
|                 trusted="True" /> | ||||
|   <zope:class class="loops.compound.book.base.Page"> | ||||
|     <require permission="zope.View" | ||||
|              interface="loops.compound.book.interfaces.IPage" /> | ||||
|     <require permission="zope.ManageContent" | ||||
|              set_schema="loops.compound.book.interfaces.IPage" /> | ||||
|   </zope:class> | ||||
| 
 | ||||
|   <!-- Views --> | ||||
| 
 | ||||
|   <zope:adapter | ||||
|  | @ -22,7 +10,7 @@ | |||
|         for="loops.interfaces.IConcept | ||||
|              loops.browser.skin.Lobo" | ||||
|         provides="zope.interface.Interface" | ||||
|         factory="loops.compound.book.browser.BookOverview" | ||||
|         factory="loops.compound.book.browser.BookView" | ||||
|         permission="zope.View" /> | ||||
| 
 | ||||
|   <zope:adapter | ||||
|  | @ -34,69 +22,11 @@ | |||
|         permission="zope.View" /> | ||||
| 
 | ||||
|   <zope:adapter | ||||
|         name="page_layout" | ||||
|         name="book_topic_view" | ||||
|         for="loops.interfaces.IConcept | ||||
|              loops.browser.skin.Lobo" | ||||
|         provides="zope.interface.Interface" | ||||
|         factory="loops.compound.book.browser.PageLayout" | ||||
|         permission="zope.View" /> | ||||
| 
 | ||||
|   <!-- parts --> | ||||
| 
 | ||||
|   <zope:adapter | ||||
|         name="lobo_headline" | ||||
|         for="loops.compound.book.interfaces.IPage | ||||
|              loops.browser.skin.Lobo" | ||||
|         provides="zope.interface.Interface" | ||||
|         factory="loops.compound.book.browser.Headline" | ||||
|         permission="zope.View" /> | ||||
| 
 | ||||
|   <zope:adapter | ||||
|         name="lobo_keyquestions" | ||||
|         for="loops.compound.book.interfaces.IPage | ||||
|              loops.browser.skin.Lobo" | ||||
|         provides="zope.interface.Interface" | ||||
|         factory="loops.compound.book.browser.KeyQuestions" | ||||
|         permission="zope.View" /> | ||||
| 
 | ||||
|   <zope:adapter | ||||
|         name="lobo_maintext" | ||||
|         for="loops.compound.book.interfaces.IPage | ||||
|              loops.browser.skin.Lobo" | ||||
|         provides="zope.interface.Interface" | ||||
|         factory="loops.compound.book.browser.MainText" | ||||
|         permission="zope.View" /> | ||||
| 
 | ||||
|   <zope:adapter | ||||
|         name="lobo_story" | ||||
|         for="loops.compound.book.interfaces.IPage | ||||
|              loops.browser.skin.Lobo" | ||||
|         provides="zope.interface.Interface" | ||||
|         factory="loops.compound.book.browser.Story" | ||||
|         permission="zope.View" /> | ||||
| 
 | ||||
|   <zope:adapter | ||||
|         name="lobo_tip" | ||||
|         for="loops.compound.book.interfaces.IPage | ||||
|              loops.browser.skin.Lobo" | ||||
|         provides="zope.interface.Interface" | ||||
|         factory="loops.compound.book.browser.Tip" | ||||
|         permission="zope.View" /> | ||||
| 
 | ||||
|   <zope:adapter | ||||
|         name="lobo_usecase" | ||||
|         for="loops.compound.book.interfaces.IPage | ||||
|              loops.browser.skin.Lobo" | ||||
|         provides="zope.interface.Interface" | ||||
|         factory="loops.compound.book.browser.UseCase" | ||||
|         permission="zope.View" /> | ||||
| 
 | ||||
|   <zope:adapter | ||||
|         name="lobo_quote" | ||||
|         for="loops.compound.book.interfaces.IPage | ||||
|              loops.browser.skin.Lobo" | ||||
|         provides="zope.interface.Interface" | ||||
|         factory="loops.compound.book.browser.Quote" | ||||
|         factory="loops.compound.book.browser.TopicView" | ||||
|         permission="zope.View" /> | ||||
| 
 | ||||
| </configure> | ||||
|  |  | |||
|  | @ -1,32 +0,0 @@ | |||
| # | ||||
| #  Copyright (c) 2012 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 | ||||
| # | ||||
| 
 | ||||
| """ | ||||
| Books, sections, pages... | ||||
| """ | ||||
| 
 | ||||
| from zope.interface import Interface, Attribute | ||||
| from zope import interface, component, schema | ||||
| 
 | ||||
| from loops.compound.interfaces import ICompound | ||||
| from loops.util import _ | ||||
| 
 | ||||
| 
 | ||||
| class IPage(ICompound): | ||||
| 
 | ||||
|     pass | ||||
|  | @ -1,14 +1,15 @@ | |||
| type(u'documenttype', u'Dokumentenart', options=u'qualifier:assign', | ||||
|     typeInterface=u'loops.interfaces.IOptions', | ||||
|     viewName=u'') | ||||
| 
 | ||||
| # book types | ||||
| type(u'book', u'Buch', viewName=u'book_overview', typeInterface=u'', | ||||
|     options=u'action.portlet:create_subtype,edit_concept') | ||||
| #type(u'page', u'Seite', viewName=u'page_layout',  | ||||
| #    typeInterface=u'loops.compound.book.interfaces.IPage', | ||||
| #    options=u'action.portlet:edit_concept') | ||||
| type(u'section', u'Kapitel', viewName=u'section_view', typeInterface=u'', | ||||
|     options=u'action.portlet:create_subtype,edit_concept') | ||||
| #type(u'topic', u'Thema', viewName=u'book_topic_view',  | ||||
| #    typeInterface=u'loops.knowledge.interfaces.ITopic', | ||||
| #    options=u'action.portlet:create_topic,edit_topic') | ||||
| 
 | ||||
| concept(u'system', u'System', u'domain') | ||||
| 
 | ||||
|  | @ -26,8 +27,8 @@ concept(u'quote', u'Zitat', u'documenttype') | |||
| concept(u'story', u'Geschichte', u'documenttype') | ||||
| concept(u'tip', u'Tipp', u'documenttype') | ||||
| concept(u'usecase', u'Fallbeispiel', u'documenttype') | ||||
| concept(u'warning', u'Warnung', u'documenttype') | ||||
| 
 | ||||
| # book structure | ||||
| child(u'book', u'section', u'issubtype', usePredicate=u'ispartof') | ||||
| child(u'section', u'section', u'issubtype', usePredicate=u'ispartof') | ||||
| #child(u'section', u'page', u'issubtype', usePredicate=u'ispartof') | ||||
|  |  | |||
|  | @ -1,14 +1,22 @@ | |||
| <html i18n:domain="loops"> | ||||
| 
 | ||||
| 
 | ||||
| <metal:book define-macro="book"> | ||||
|   <metal:info use-macro="view/concept_macros/concepttitle" /> | ||||
|   <div tal:repeat="related item/children"> | ||||
| <metal:children define-macro="children"> | ||||
|   <div tal:repeat="related item/children" | ||||
|        tal:define="level python:level + 1" | ||||
|        tal:attributes="class string:content-$level"> | ||||
|     <h3> | ||||
|       <a tal:attributes="href python:view.getUrlForTarget(related)" | ||||
|          tal:content="related/title" /></h3> | ||||
|          tal:content="related/title" /> | ||||
|     </h3> | ||||
|     <div tal:content="structure related/renderedDescription" /> | ||||
|     <!-- TODO: show next level (+/-) --> | ||||
|   </div> | ||||
| </metal:children> | ||||
| 
 | ||||
| <metal:book define-macro="book"> | ||||
|   <metal:info use-macro="view/concept_macros/concepttitle" /> | ||||
|   <metal:info use-macro="item/book_macros/children" /> | ||||
| </metal:book> | ||||
| 
 | ||||
| 
 | ||||
|  | @ -32,71 +40,93 @@ | |||
|     </div> | ||||
|   </metal:navigation> | ||||
|   <metal:info use-macro="view/concept_macros/concepttitle" /> | ||||
|   <div tal:repeat="related item/textResources"> | ||||
|     <div class="span-4"> | ||||
|       <div tal:attributes="class python: | ||||
|                   item.getCssClassForResource(related)" | ||||
|            tal:content="structure related/render" /> | ||||
|     </div> | ||||
|     <div class="span-2 last" style="padding-top: 0.4em"> | ||||
|       <div class="object-actions" style="padding-top: 0" | ||||
|            tal:define="url python:view.getUrlForTarget(related.context)" | ||||
|            tal:condition="related/editable"> | ||||
|         <a i18n:translate="" i18n:attributes="title" | ||||
|            title="Edit" | ||||
|            tal:define="targetUid python:view.getUidForObject(related.context); | ||||
|                        url  | ||||
|                   string:$url/edit_object.html?version=this&targetUid=$targetUid" | ||||
|            tal:attributes="href url; | ||||
|                            onclick string:objectDialog('edit', '$url');;  | ||||
|                                           return false"> | ||||
|           <img tal:attributes="src  | ||||
|                     string:$resourceBase/cybertools.icons/vcard_edit.png" /></a> | ||||
|         <a i18n:translate="" i18n:attributes="title" | ||||
|            title="Edit with external editor." | ||||
|            xxtal:condition="related/xeditable" | ||||
|            tal:condition="nothing" | ||||
|            tal:attributes="href string:$url/external_edit?version=this"> | ||||
|           <img tal:attributes="src  | ||||
|             string:$resourceBase/cybertools.icons/application_edit.png" /></a> | ||||
|   <metal:info use-macro="item/book_macros/children" /> | ||||
|   <metal:text define-macro="textresources"> | ||||
|     <div style="clear: both" | ||||
|          tal:repeat="related item/textResources"> | ||||
|       <div class="span-4"> | ||||
|         <div metal:define-macro="default_text" | ||||
|              tal:attributes="class python: | ||||
|                       item.getCssClassForResource(related)"> | ||||
|           <h3 tal:define="ttitle python:item.getTitleForResource(related)" | ||||
|               tal:condition="ttitle" | ||||
|               tal:content="ttitle" /> | ||||
|           <img class="flow-left" style="padding-top: 5px" | ||||
|                tal:define="icon python:item.getIconForResource(related)" | ||||
|                tal:condition="icon" | ||||
|                tal:attributes="src icon" /> | ||||
|           <span tal:content="structure related/render" /> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div tal:repeat="parent python:item.getParentsForResource(related)"> | ||||
|         <a tal:content="parent/title" | ||||
|            tal:attributes="href python:view.getUrlForTarget(parent)" /> | ||||
|       </div> | ||||
|       <div tal:repeat="image python: | ||||
|                           item.images[repeat['related'].index() + 1]"> | ||||
|         <a dojoType="dojox.image.Lightbox" group="mediasset" | ||||
|            i18n:attributes="title" | ||||
|            tal:attributes="href image/fullImageUrl; | ||||
|                            title image/title"> | ||||
|           <img tal:attributes="src image/src; | ||||
|                                alt image/title" /></a> | ||||
|       <div class="span-2 last" style="padding-top: 0.4em"> | ||||
|         <div class="object-actions" style="padding-top: 0" | ||||
|              tal:define="url python:view.getUrlForTarget(related.context)" | ||||
|              tal:condition="related/editable"> | ||||
|           <a i18n:translate="" i18n:attributes="title" | ||||
|              title="Edit" | ||||
|              tal:define="targetUid python:view.getUidForObject(related.context); | ||||
|                          url  | ||||
|                     string:$url/edit_object.html?version=this&targetUid=$targetUid" | ||||
|              tal:attributes="href url; | ||||
|                              onclick string:objectDialog('edit', '$url');;  | ||||
|                                             return false"> | ||||
|             <img tal:attributes="src  | ||||
|                       string:$resourceBase/cybertools.icons/vcard_edit.png" /></a> | ||||
|           <a i18n:translate="" i18n:attributes="title" | ||||
|              title="Edit with external editor." | ||||
|              xxtal:condition="related/xeditable" | ||||
|              tal:condition="nothing" | ||||
|              tal:attributes="href string:$url/external_edit?version=this"> | ||||
|             <img tal:attributes="src  | ||||
|               string:$resourceBase/cybertools.icons/application_edit.png" /></a> | ||||
|         </div> | ||||
|         <div tal:repeat="parent python:item.getParentsForResource(related)"> | ||||
|           <a tal:content="parent/title" | ||||
|              tal:attributes="href python:view.getUrlForTarget(parent)" /> | ||||
|         </div> | ||||
|         <div tal:repeat="image python: | ||||
|                             item.images[repeat['related'].index() + 1]"> | ||||
|           <a dojoType="dojox.image.Lightbox" group="mediasset" | ||||
|              i18n:attributes="title" | ||||
|              tal:attributes="href image/fullImageUrl; | ||||
|                              title image/title"> | ||||
|             <img tal:attributes="src image/src; | ||||
|                                  alt image/title" /></a> | ||||
|         </div> | ||||
|         <!-- TODO: links to files --> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|   </metal:text> | ||||
|   <br style="clear: both" /> | ||||
|   <metal:navigation use-macro="item/book_macros/navigation" /> | ||||
|   <br /> | ||||
| </metal:section> | ||||
| 
 | ||||
| 
 | ||||
| <!-- layout part macros - obsolete? --> | ||||
| 
 | ||||
| <metal:part define-macro="headline"> | ||||
|   <div tal:define="cell part/getView"> | ||||
|     <metal:headline use-macro="item/macros/headline" /> | ||||
|   </div> | ||||
| </metal:part> | ||||
| 
 | ||||
| <metal:part define-macro="text"> | ||||
|   <tal:cell repeat="cell part/getResources"> | ||||
|     <div tal:attributes="class cell/cssClass"> | ||||
|       <h3 tal:content="cell/title" /> | ||||
|       <span tal:content="structure cell/view/render" /> | ||||
| <metal:topic define-macro="topic"> | ||||
|   <metal:info use-macro="view/concept_macros/concepttitle" /> | ||||
|   <h2 i18n:translate="" | ||||
|       tal:condition="python: list(item.children())">Children</h2> | ||||
|   <metal:children use-macro="item/book_macros/children" /> | ||||
|   <h2 i18n:translate="" | ||||
|       tal:condition="item/textResources">Text Elements</h2> | ||||
|   <div> | ||||
|     <div tal:repeat="related item/textResources" | ||||
|          tal:define="level python:level + 1" | ||||
|          tal:attributes="class string:content-$level"> | ||||
|       <h3> | ||||
|         <a tal:attributes="href python:view.getUrlForTarget(related.context)" | ||||
|            tal:content="related/title" /> | ||||
|       </h3> | ||||
|       <div> | ||||
|         <div tal:replace="structure related/renderShortText" /> | ||||
|         <p> | ||||
|           <a i18n:translate="" | ||||
|               tal:attributes="href python:view.getUrlForTarget(related.context)"> | ||||
|             more...</a></p> | ||||
|     </div> | ||||
|   </tal:cell> | ||||
| </metal:part> | ||||
|   </div> | ||||
| </metal:topic> | ||||
| 
 | ||||
| 
 | ||||
| </html> | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| # | ||||
| #  Copyright (c) 2011 Helmut Merz helmutm@cy55.de | ||||
| #  Copyright (c) 2013 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 | ||||
|  | @ -19,14 +19,12 @@ | |||
| """ | ||||
| Definition of basic view classes and other browser related stuff for the | ||||
| loops.expert package. | ||||
| 
 | ||||
| $Id$ | ||||
| """ | ||||
| 
 | ||||
| from zope import interface, component | ||||
| from zope.app.pagetemplate import ViewPageTemplateFile | ||||
| from zope.cachedescriptors.property import Lazy | ||||
| from zope.traversing.api import getName, getParent | ||||
| from zope.traversing.api import getName, getParent, traverse | ||||
| 
 | ||||
| from cybertools.browser.form import FormController | ||||
| from cybertools.stateful.interfaces import IStateful, IStatesDefinition | ||||
|  | @ -150,12 +148,16 @@ class Search(ConceptView): | |||
|             if not isinstance(types, (list, tuple)): | ||||
|                 types = [types] | ||||
|             for type in types: | ||||
|                 site = self.loopsRoot | ||||
|                 if type.startswith('/'): | ||||
|                     parts = type.split(':') | ||||
|                     site = traverse(self.loopsRoot, parts[0], site) | ||||
|                 result = self.executeQuery(title=title or None, type=type, | ||||
|                                                  exclude=('hidden',)) | ||||
|                 fv = FilterView(self.context, self.request) | ||||
|                 result = fv.apply(result) | ||||
|                 for o in result: | ||||
|                     if o.getLoopsRoot() == self.loopsRoot: | ||||
|                     if o.getLoopsRoot() == site: | ||||
|                         adObj = adapted(o, self.languageInfo) | ||||
|                         if filterMethod is not None and not filterMethod(adObj): | ||||
|                             continue | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| # | ||||
| #  Copyright (c) 2008 Helmut Merz helmutm@cy55.de | ||||
| #  Copyright (c) 2013 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 | ||||
|  | @ -18,8 +18,6 @@ | |||
| 
 | ||||
| """ | ||||
| Query concepts management stuff. | ||||
| 
 | ||||
| $Id$ | ||||
| """ | ||||
| 
 | ||||
| from BTrees.IOBTree import IOBTree | ||||
|  | @ -29,6 +27,7 @@ from zope.interface import Interface, Attribute, implements | |||
| from zope.app.catalog.interfaces import ICatalog | ||||
| from zope.app.intid.interfaces import IIntIds | ||||
| from zope.cachedescriptors.property import Lazy | ||||
| from zope.traversing.api import traverse | ||||
| 
 | ||||
| from cybertools.typology.interfaces import IType | ||||
| from loops.common import AdapterBase | ||||
|  | @ -66,6 +65,11 @@ class BaseQuery(object): | |||
|         return self.context.context.getLoopsRoot() | ||||
| 
 | ||||
|     def queryConcepts(self, title=None, type=None, **kw): | ||||
|         site = self.loopsRoot | ||||
|         if type.startswith('/'): | ||||
|             parts = type.split(':') | ||||
|             site = traverse(self.loopsRoot, parts[0], site) | ||||
|             type = 'loops:' + ':'.join(parts[1:]) | ||||
|         if type.endswith('*'): | ||||
|             start = type[:-1] | ||||
|             end = start + '\x7f' | ||||
|  | @ -76,7 +80,7 @@ class BaseQuery(object): | |||
|             result = cat.searchResults(loops_type=(start, end), loops_title=title) | ||||
|         else: | ||||
|             result = cat.searchResults(loops_type=(start, end)) | ||||
|         result = set(r for r in result if r.getLoopsRoot() == self.loopsRoot | ||||
|         result = set(r for r in result if r.getLoopsRoot() == site | ||||
|                      and canListObject(r)) | ||||
|         if 'exclude' in kw: | ||||
|             r1 = set() | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| # | ||||
| #  Copyright (c) 2012 Helmut Merz helmutm@cy55.de | ||||
| #  Copyright (c) 2013 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 | ||||
|  | @ -78,28 +78,28 @@ class OfficeFile(ExternalFileAdapter): | |||
|     @Lazy | ||||
|     def docPropertyDom(self): | ||||
|         fn = self.docFilename | ||||
|         dummy = dict(core=[], custom=[]) | ||||
|         result = dict(core=[], custom=[]) | ||||
|         root, ext = os.path.splitext(fn) | ||||
|         if not ext.lower() in self.fileExtensions: | ||||
|             return dummy | ||||
|             return result | ||||
|         try: | ||||
|             zf = ZipFile(fn, 'r') | ||||
|         except IOError, e: | ||||
|             from logging import getLogger | ||||
|             self.logger.warn(e) | ||||
|             return dummy | ||||
|             return result | ||||
|         if self.corePropFileName not in zf.namelist(): | ||||
|             self.logger.warn('Core properties not found in file %s.' % | ||||
|                              self.externalAddress) | ||||
|         else: | ||||
|             result['core'] = etree.fromstring(zf.read(self.corePropFileName)) | ||||
|         if self.propFileName not in zf.namelist(): | ||||
|             self.logger.warn('Custom properties not found in file %s.' % | ||||
|                              self.externalAddress) | ||||
|         propsXml = zf.read(self.propFileName) | ||||
|         corePropsXml = zf.read(self.corePropFileName) | ||||
|         # TODO: read core.xml, return both trees in dictionary | ||||
|         else: | ||||
|             result['custom'] = etree.fromstring(zf.read(self.propFileName)) | ||||
|         zf.close() | ||||
|         return {'custom': etree.fromstring(propsXml), | ||||
|                 'core': etree.fromstring(corePropsXml)} | ||||
|         return result | ||||
| 
 | ||||
|     def getDocProperty(self, pname): | ||||
|         for p in self.docPropertyDom['custom']: | ||||
|  |  | |||
|  | @ -690,9 +690,21 @@ class IIndexAttributes(Interface): | |||
|         """ | ||||
| 
 | ||||
| 
 | ||||
| # reusable interface elements | ||||
| 
 | ||||
| class IOptions(Interface): | ||||
| 
 | ||||
|     options = schema.List( | ||||
|         title=_(u'Options'), | ||||
|         description=_(u'Additional settings.'), | ||||
|         value_type=schema.TextLine(), | ||||
|         default=[], | ||||
|         required=False) | ||||
| 
 | ||||
| 
 | ||||
| # types stuff | ||||
| 
 | ||||
| class ITypeConcept(IConceptSchema, ILoopsAdapter): | ||||
| class ITypeConcept(IConceptSchema, ILoopsAdapter, IOptions): | ||||
|     """ Concepts of type 'type' should be adaptable to this interface. | ||||
|     """ | ||||
| 
 | ||||
|  | @ -725,13 +737,6 @@ class ITypeConcept(IConceptSchema, ILoopsAdapter): | |||
|         default=u'', | ||||
|         required=False) | ||||
| 
 | ||||
|     options = schema.List( | ||||
|         title=_(u'Options'), | ||||
|         description=_(u'Additional settings.'), | ||||
|         value_type=schema.TextLine(), | ||||
|         default=[], | ||||
|         required=False) | ||||
| 
 | ||||
|     # storage = schema.Choice() | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -26,6 +26,7 @@ from zope.component import adapts | |||
| from zope.interface import implementer, implements | ||||
| 
 | ||||
| from loops.common import AdapterBase | ||||
| from loops.interfaces import IConcept | ||||
| from loops.knowledge.qualification.interfaces import ICompetence | ||||
| from loops.type import TypeInterfaceSourceList | ||||
| 
 | ||||
|  |  | |||
|  | @ -4,7 +4,14 @@ | |||
|     i18n_domain="loops"> | ||||
| 
 | ||||
|   <zope:adapter | ||||
|         factory="loops.knowledge.qualification.base.Competence" /> | ||||
|         factory="loops.knowledge.qualification.base.Competence" | ||||
|         trusted="True" /> | ||||
|   <zope:class class="loops.knowledge.qualification.base.Competence"> | ||||
|     <require permission="zope.View" | ||||
|              interface="loops.knowledge.qualification.interfaces.ICompetence" /> | ||||
|     <require permission="zope.ManageContent" | ||||
|              set_schema="loops.knowledge.qualification.interfaces.ICompetence" /> | ||||
|   </zope:class> | ||||
| 
 | ||||
|   <!-- views --> | ||||
| 
 | ||||
|  |  | |||
|  | @ -23,11 +23,11 @@ Interfaces for knowledge management and elearning with loops. | |||
| from zope.interface import Interface, Attribute | ||||
| from zope import interface, component, schema | ||||
| 
 | ||||
| from loops.interfaces import IConceptSchema | ||||
| from loops.interfaces import IConceptSchema, ILoopsAdapter | ||||
| from loops.util import _ | ||||
| 
 | ||||
| 
 | ||||
| class ICompetence(IConceptSchema): | ||||
| class ICompetence(ILoopsAdapter): | ||||
|     """ The competence of a person. | ||||
| 
 | ||||
|         Maybe assigned to the person via a 'knows' relation or | ||||
|  |  | |||
|  | @ -62,7 +62,7 @@ class QuestionGroup(AdapterBase, QuestionGroup): | |||
| 
 | ||||
|     _contextAttributes = list(IQuestionGroup) | ||||
|     _adapterAttributes = AdapterBase._adapterAttributes + ( | ||||
|                 'questionnaire', 'questions', 'feedbackItems',) | ||||
|                 'questionnaire', 'questions', 'feedbackItems') | ||||
|     _noexportAttributes = _adapterAttributes | ||||
| 
 | ||||
|     @property | ||||
|  | @ -109,9 +109,6 @@ class Question(AdapterBase, Question): | |||
|     def questionnaire(self): | ||||
|         return self.questionGroup.questionnaire | ||||
| 
 | ||||
|     def __hash__(self): | ||||
|         return hash(self.context) | ||||
| 
 | ||||
| 
 | ||||
| class FeedbackItem(AdapterBase, FeedbackItem): | ||||
| 
 | ||||
|  | @ -125,4 +122,3 @@ class FeedbackItem(AdapterBase, FeedbackItem): | |||
|     @property | ||||
|     def text(self): | ||||
|         return self.context.description | ||||
| 
 | ||||
|  |  | |||
|  | @ -21,26 +21,40 @@ Definition of view classes and other browser related stuff for | |||
| surveys and self-assessments. | ||||
| """ | ||||
| 
 | ||||
| import csv | ||||
| from cStringIO import StringIO | ||||
| from zope.app.pagetemplate import ViewPageTemplateFile | ||||
| from zope.cachedescriptors.property import Lazy | ||||
| from zope.i18n import translate | ||||
| 
 | ||||
| from cybertools.knowledge.survey.questionnaire import Response | ||||
| from cybertools.util.date import formatTimeStamp | ||||
| from loops.browser.concept import ConceptView | ||||
| from loops.browser.node import NodeView | ||||
| from loops.common import adapted | ||||
| from loops.knowledge.survey.response import Responses | ||||
| from loops.organize.party import getPersonForUser | ||||
| from loops.util import getObjectForUid | ||||
| from loops.util import _ | ||||
| 
 | ||||
| 
 | ||||
| template = ViewPageTemplateFile('view_macros.pt') | ||||
| 
 | ||||
| class SurveyView(ConceptView): | ||||
| 
 | ||||
|     tabview = 'index.html' | ||||
|     data = None | ||||
|     errors = None | ||||
| 
 | ||||
|     @Lazy | ||||
|     def macro(self): | ||||
|         self.registerDojo() | ||||
|         return template.macros['survey'] | ||||
| 
 | ||||
|     @Lazy | ||||
|     def tabview(self): | ||||
|         if self.editable: | ||||
|             return 'index.html' | ||||
| 
 | ||||
|     def results(self): | ||||
|         result = [] | ||||
|         response = None | ||||
|  | @ -52,22 +66,114 @@ class SurveyView(ConceptView): | |||
|                 if key.startswith('question_'): | ||||
|                     uid = key[len('question_'):] | ||||
|                     question = adapted(self.getObjectForUid(uid)) | ||||
|                     value = int(value) | ||||
|                     self.data[uid] = value | ||||
|                     response.values[question] = value | ||||
|             # TODO: store self.data in track | ||||
|         # else: | ||||
|         #     get response from track | ||||
|                     if value != 'none': | ||||
|                         value = int(value) | ||||
|                         self.data[uid] = value | ||||
|                         response.values[question] = value | ||||
|             Responses(self.context).save(self.data) | ||||
|             self.errors = self.check(response) | ||||
|             if self.errors: | ||||
|                 return [] | ||||
|         if response is not None: | ||||
|             result = response.getGroupedResult() | ||||
|         return [dict(category=r[0].title, text=r[1].text,  | ||||
|                             score=int(round(r[2] * 100))) | ||||
|                         for r in result] | ||||
| 
 | ||||
|     def getValues(self, question): | ||||
|         setting = 0 | ||||
|         if self.data is not None: | ||||
|             setting = self.data.get(question.uid) or 0 | ||||
|         return [dict(value=i, checked=(i == setting))  | ||||
|                     for i in range(question.answerRange)] | ||||
|     def check(self, response): | ||||
|         errors = [] | ||||
|         values = response.values | ||||
|         for qu in self.adapted.questions: | ||||
|             if qu.required and qu not in values: | ||||
|                 errors.append('Please answer the obligatory questions.') | ||||
|                 break | ||||
|         qugroups = {} | ||||
|         for qugroup in self.adapted.questionGroups: | ||||
|             qugroups[qugroup] = 0 | ||||
|         for qu in values: | ||||
|             qugroups[qu.questionGroup] += 1 | ||||
|         for qugroup, count in qugroups.items(): | ||||
|             minAnswers = qugroup.minAnswers | ||||
|             if minAnswers in (u'', None): | ||||
|                 minAnswers = len(qugroup.questions) | ||||
|             if count < minAnswers: | ||||
|                 errors.append('Please answer the minimum number of questions.') | ||||
|                 break | ||||
|         return errors | ||||
| 
 | ||||
|     def getInfoText(self, qugroup): | ||||
|         lang = self.languageInfo.language | ||||
|         text = qugroup.description | ||||
|         info = None | ||||
|         if qugroup.minAnswers in (u'', None): | ||||
|             info = translate(_(u'Please answer all questions.'), target_language=lang) | ||||
|         elif qugroup.minAnswers > 0: | ||||
|             info = translate(_(u'Please answer at least $minAnswers questions.', | ||||
|                                mapping=dict(minAnswers=qugroup.minAnswers)), | ||||
|                              target_language=lang) | ||||
|         if info: | ||||
|             text = u'<i>%s</i><br />(%s)' % (text, info) | ||||
|         return text | ||||
| 
 | ||||
|     def getValues(self, question): | ||||
|         setting = None | ||||
|         if self.data is None: | ||||
|             self.data = Responses(self.context).load() | ||||
|         if self.data: | ||||
|             setting = self.data.get(question.uid) | ||||
|         noAnswer = [dict(value='none', checked=(setting == None), | ||||
|                          radio=(not question.required))] | ||||
|         return noAnswer + [dict(value=i, checked=(setting == i), radio=True)  | ||||
|                                 for i in reversed(range(question.answerRange))] | ||||
| 
 | ||||
| 
 | ||||
| class SurveyCsvExport(NodeView): | ||||
| 
 | ||||
|     encoding = 'ISO8859-15' | ||||
| 
 | ||||
|     def encode(self, text): | ||||
|         text.encode(self.encoding) | ||||
| 
 | ||||
|     @Lazy | ||||
|     def questions(self): | ||||
|         result = [] | ||||
|         for idx1, qug in enumerate(adapted(self.virtualTargetObject).questionGroups): | ||||
|             for idx2, qu in enumerate(qug.questions): | ||||
|                 result.append((idx1, idx2, qug, qu)) | ||||
|         return result | ||||
| 
 | ||||
|     @Lazy | ||||
|     def columns(self): | ||||
|         infoCols = ['Name', 'Timestamp'] | ||||
|         dataCols = ['%02i-%02i' % (item[0], item[1]) for item in self.questions] | ||||
|         return infoCols + dataCols | ||||
| 
 | ||||
|     def getRows(self): | ||||
|         for tr in Responses(self.virtualTargetObject).getAllTracks(): | ||||
|             p = adapted(getObjectForUid(tr.userName)) | ||||
|             name = p and p.title or u'???' | ||||
|             ts = formatTimeStamp(tr.timeStamp) | ||||
|             cells = [tr.data.get(qu.uid, -1)  | ||||
|                         for (idx1, idx2, qug, qu) in self.questions] | ||||
|             yield [name, ts] + cells | ||||
| 
 | ||||
|     def __call__(self): | ||||
|         f = StringIO() | ||||
|         writer = csv.writer(f, delimiter=',') | ||||
|         writer.writerow(self.columns) | ||||
|         for row in self.getRows(): | ||||
|             writer.writerow(row) | ||||
|         text = f.getvalue() | ||||
|         self.setDownloadHeader(text) | ||||
|         return text | ||||
| 
 | ||||
|     def setDownloadHeader(self, text): | ||||
|         response = self.request.response | ||||
|         filename = 'survey_data.csv' | ||||
|         response.setHeader('Content-Disposition', | ||||
|                                'attachment; filename=%s' % filename) | ||||
|         response.setHeader('Cache-Control', '') | ||||
|         response.setHeader('Pragma', '') | ||||
|         response.setHeader('Content-Length', len(text)) | ||||
|         response.setHeader('Content-Type', 'text/csv') | ||||
| 
 | ||||
|  |  | |||
|  | @ -7,19 +7,56 @@ | |||
| 
 | ||||
|   <zope:adapter | ||||
|         factory="loops.knowledge.survey.base.Questionnaire" | ||||
|         provides="loops.knowledge.survey.interfaces.IQuestionnaire" /> | ||||
|         provides="loops.knowledge.survey.interfaces.IQuestionnaire" | ||||
|         trusted="True" /> | ||||
|   <zope:class class="loops.knowledge.survey.base.Questionnaire"> | ||||
|     <require permission="zope.View" | ||||
|              interface="loops.knowledge.survey.interfaces.IQuestionnaire" /> | ||||
|     <require permission="zope.ManageContent" | ||||
|              set_schema="loops.knowledge.survey.interfaces.IQuestionnaire" /> | ||||
|   </zope:class> | ||||
| 
 | ||||
|   <zope:adapter | ||||
|         factory="loops.knowledge.survey.base.QuestionGroup" | ||||
|         provides="loops.knowledge.survey.interfaces.IQuestionGroup" /> | ||||
|         provides="loops.knowledge.survey.interfaces.IQuestionGroup" | ||||
|         trusted="True" /> | ||||
|   <zope:class class="loops.knowledge.survey.base.QuestionGroup"> | ||||
|     <require permission="zope.View" | ||||
|              interface="loops.knowledge.survey.interfaces.IQuestionGroup" /> | ||||
|     <require permission="zope.ManageContent" | ||||
|              set_schema="loops.knowledge.survey.interfaces.IQuestionGroup" /> | ||||
|   </zope:class> | ||||
| 
 | ||||
|   <zope:adapter | ||||
|         factory="loops.knowledge.survey.base.Question" | ||||
|         provides="loops.knowledge.survey.interfaces.IQuestion" /> | ||||
|         provides="loops.knowledge.survey.interfaces.IQuestion" | ||||
|         trusted="True" /> | ||||
|   <zope:class class="loops.knowledge.survey.base.Question"> | ||||
|     <require permission="zope.View" | ||||
|              interface="loops.knowledge.survey.interfaces.IQuestion" /> | ||||
|     <require permission="zope.ManageContent" | ||||
|              set_schema="loops.knowledge.survey.interfaces.IQuestion" /> | ||||
|   </zope:class> | ||||
| 
 | ||||
|   <zope:adapter | ||||
|         factory="loops.knowledge.survey.base.FeedbackItem" | ||||
|         provides="loops.knowledge.survey.interfaces.IFeedbackItem" /> | ||||
|         provides="loops.knowledge.survey.interfaces.IFeedbackItem" | ||||
|         trusted="True" /> | ||||
|   <zope:class class="loops.knowledge.survey.base.FeedbackItem"> | ||||
|     <require permission="zope.View" | ||||
|              interface="loops.knowledge.survey.interfaces.IFeedbackItem" /> | ||||
|     <require permission="zope.ManageContent" | ||||
|              set_schema="loops.knowledge.survey.interfaces.IFeedbackItem" /> | ||||
|   </zope:class> | ||||
| 
 | ||||
|   <!-- track --> | ||||
| 
 | ||||
|   <zope:class class="loops.knowledge.survey.response.Response"> | ||||
|     <require permission="zope.View" | ||||
|              interface="cybertools.tracking.interfaces.ITrack" /> | ||||
|     <require permission="zope.ManageContent" | ||||
|              set_schema="cybertools.tracking.interfaces.ITrack" /> | ||||
|   </zope:class> | ||||
| 
 | ||||
|   <!-- views --> | ||||
| 
 | ||||
|  | @ -31,4 +68,9 @@ | |||
|       factory="loops.knowledge.survey.browser.SurveyView" | ||||
|       permission="zope.View" /> | ||||
| 
 | ||||
|   <browser:page name="survey_data.csv" | ||||
|         for="loops.interfaces.IView" | ||||
|         class="loops.knowledge.survey.browser.SurveyCsvExport" | ||||
|         permission="zope.View" /> | ||||
| 
 | ||||
| </configure> | ||||
|  |  | |||
|  | @ -24,7 +24,7 @@ from zope.interface import Interface, Attribute | |||
| from zope import interface, component, schema | ||||
| 
 | ||||
| from cybertools.knowledge.survey import interfaces | ||||
| from loops.interfaces import IConceptSchema | ||||
| from loops.interfaces import IConceptSchema, ILoopsAdapter | ||||
| from loops.util import _ | ||||
| 
 | ||||
| 
 | ||||
|  | @ -38,16 +38,43 @@ class IQuestionnaire(IConceptSchema, interfaces.IQuestionnaire): | |||
|         default=4, | ||||
|         required=True) | ||||
| 
 | ||||
|     feedbackHeader = schema.Text( | ||||
|         title=_(u'Feedback Header'), | ||||
|         description=_(u'Text that will appear at the top of the feedback page.'), | ||||
|         default=u'', | ||||
|         missing_value=u'', | ||||
|         required=False) | ||||
| 
 | ||||
|     feedbackFooter = schema.Text( | ||||
|         title=_(u'Feedback Footer'), | ||||
|         description=_(u'Text that will appear at the end of the feedback page.'), | ||||
|         default=u'', | ||||
|         missing_value=u'', | ||||
|         required=False) | ||||
| 
 | ||||
| 
 | ||||
| class IQuestionGroup(IConceptSchema, interfaces.IQuestionGroup): | ||||
|     """ A group of questions within a questionnaire. | ||||
|     """ | ||||
| 
 | ||||
|     minAnswers = schema.Int( | ||||
|         title=_(u'Minimum Number of Answers'), | ||||
|         description=_(u'Minumum number of questions that have to be answered. ' | ||||
|             'Empty means all questions have to be answered.'), | ||||
|         default=None, | ||||
|         required=False) | ||||
| 
 | ||||
| 
 | ||||
| class IQuestion(IConceptSchema, interfaces.IQuestion): | ||||
|     """ A single question within a questionnaire. | ||||
|     """ | ||||
| 
 | ||||
|     required = schema.Bool( | ||||
|         title=_(u'Required'), | ||||
|         description=_(u'Question must be answered.'), | ||||
|         default=False, | ||||
|         required=False) | ||||
| 
 | ||||
|     revertAnswerOptions = schema.Bool( | ||||
|         title=_(u'Negative'), | ||||
|         description=_(u'Value inversion: High selection means low value.'), | ||||
|  |  | |||
|  | @ -29,20 +29,34 @@ from loops.knowledge.survey.interfaces import IResponse, IResponses | |||
| from loops.organize.tracking.base import BaseRecordManager | ||||
| 
 | ||||
| 
 | ||||
| class Responses(BaseRecordManager): | ||||
| 
 | ||||
|     implements(IResponses) | ||||
| 
 | ||||
|     storageName = 'survey_responses' | ||||
| 
 | ||||
|     def __init__(self, context): | ||||
|         self.context = context | ||||
| 
 | ||||
|     def save(self, data): | ||||
|         if self.personId: | ||||
|             self.storage.saveUserTrack(self.uid, 0, self.personId, data,  | ||||
|                                         update=True, overwrite=True) | ||||
| 
 | ||||
|     def load(self): | ||||
|         if self.personId: | ||||
|             tracks = self.storage.getUserTracks(self.uid, 0, self.personId) | ||||
|             if tracks: | ||||
|                 return tracks[0].data | ||||
|         return {} | ||||
| 
 | ||||
|     def getAllTracks(self): | ||||
|         return self.storage.query(taskId=self.uid) | ||||
| 
 | ||||
| 
 | ||||
| class Response(Track): | ||||
|     """ A survey response. | ||||
|     """ | ||||
| 
 | ||||
|     implements(IResponse) | ||||
| 
 | ||||
|     typeName = 'Response' | ||||
|     typeInterface = IResponse | ||||
| 
 | ||||
| 
 | ||||
| class Responses(BaseRecordManager): | ||||
|     """ A tracking storage adapter for survey responses. | ||||
|     """ | ||||
| 
 | ||||
|     implements(IResponses) | ||||
|     adapts(ITrackingStorage) | ||||
| 
 | ||||
|  |  | |||
|  | @ -3,11 +3,18 @@ | |||
| 
 | ||||
| 
 | ||||
| <metal:block define-macro="survey" | ||||
|               tal:define="feedback item/results"> | ||||
|   <metal:title use-macro="item/conceptMacros/concepttitle" /> | ||||
|               tal:define="feedback item/results; | ||||
|                           errors item/errors"> | ||||
|   <metal:title use-macro="item/conceptMacros/concepttitle_only" /> | ||||
|   <tal:description condition="not:feedback"> | ||||
|     <metal:title use-macro="item/conceptMacros/conceptdescription" /> | ||||
|   </tal:description> | ||||
|   <div tal:condition="feedback"> | ||||
|     <h3 i18n:translate="">Feedback</h3> | ||||
|     <table> | ||||
|     <div tal:define="header item/adapted/feedbackHeader" | ||||
|          tal:condition="header" | ||||
|          tal:content="structure python:item.renderText(header, 'text/restructured')" /> | ||||
|     <table class="listing"> | ||||
|       <tr> | ||||
|         <th i18n:translate="">Category</th> | ||||
|         <th i18n:translate="">Response</th> | ||||
|  | @ -19,42 +26,74 @@ | |||
|         <td tal:content="fbitem/score" /> | ||||
|       </tr> | ||||
|     </table> | ||||
|     <br /> | ||||
|     <div class="button" id="show_questionnaire"> | ||||
|       <a href="" onclick="back(); return false" | ||||
|          i18n:translate=""> | ||||
|         Back to Questionnaire</a> | ||||
|       <br /> | ||||
|     </div> | ||||
|     <div tal:define="footer item/adapted/feedbackFooter" | ||||
|          tal:condition="footer" | ||||
|          tal:content="structure python:item.renderText(footer, 'text/restructured')" /> | ||||
|   </div> | ||||
|   <div id="questionnaire" | ||||
|        tal:condition="not:feedback"> | ||||
|     <h3 i18n:translate="">Questionnaire</h3> | ||||
|     <div class="error" | ||||
|          tal:condition="errors"> | ||||
|       <div tal:repeat="error errors"> | ||||
|         <span i18n:translate="" | ||||
|               tal:content="error" /> | ||||
|       </div> | ||||
|     </div> | ||||
|     <form method="post"> | ||||
|       <table class="listing"> | ||||
|         <tal:qugroup repeat="qugroup item/adapted/questionGroups"> | ||||
|           <tr><td colspan="6"> </td></tr> | ||||
|           <tr class="vpad"> | ||||
|             <td tal:define="infoText python:item.getInfoText(qugroup)"> | ||||
|               <b tal:content="qugroup/title" /> | ||||
|               <div class="infotext" | ||||
|                    tal:condition="infoText"> | ||||
|                 <span tal:content="structure infoText" /> | ||||
|               </div> | ||||
|             </td> | ||||
|             <td style="text-align: center" | ||||
|                 i18n:translate="">No answer</td> | ||||
|             <td colspan="2" | ||||
|                 i18n:translate="">Fully applies</td> | ||||
|             <td colspan="2" | ||||
|                 style="text-align: right" | ||||
|                 i18n:translate="">Does not apply</td> | ||||
|           </tr> | ||||
|           <tr class="vpad" | ||||
|               tal:repeat="question qugroup/questions"> | ||||
|             <td tal:content="question/text" /> | ||||
|             <td style="white-space: nowrap; text-align: center" | ||||
|                 tal:repeat="value python:item.getValues(question)"> | ||||
|                 <input type="radio" | ||||
|                        i18n:attributes="title" | ||||
|                        tal:condition="value/radio"       | ||||
|                        tal:attributes=" | ||||
|                             name string:question_${question/uid}; | ||||
|                             value value/value; | ||||
|                             checked value/checked; | ||||
|                             title string:survey_value_${value/value}" /> | ||||
|                 <span tal:condition="not:value/radio" | ||||
|                       title="Obligatory question, must be answered" | ||||
|                       i18n:attributes="title">*** | ||||
|                 </span> | ||||
|             </td> | ||||
|           </tr> | ||||
|         </tal:qugroup> | ||||
|       </table> | ||||
|       <input type="submit" name="submit" value="Evaluate Questionnaire" | ||||
|              i18n:attributes="value" /> | ||||
|       <input type="button" name="reset_responses" value="Reset Responses Entered" | ||||
|              i18n:attributes="value" | ||||
|              onclick="setRadioButtons('none'); return false" /> | ||||
|     </form> | ||||
|   </div> | ||||
|   <h3 i18n:translate="">Questionnaire</h3> | ||||
|   <form method="post"> | ||||
|     <table> | ||||
|       <tr> | ||||
|         <th></th> | ||||
|         <th> | ||||
|           <table> | ||||
|             <tr> | ||||
|               <td i18n:translate="">Does not apply</td> | ||||
|               <td style="text-align: right" | ||||
|                   i18n:translate="">Fully applies</td> | ||||
|             </tr> | ||||
|           </table> | ||||
|         </th> | ||||
|       </tr> | ||||
|       <tr tal:repeat="question item/adapted/questions"> | ||||
|         <td tal:content="question/text" /> | ||||
|         <td style="white-space: nowrap"> | ||||
|           <span tal:repeat="value python:item.getValues(question)">  | ||||
|             <input type="radio" | ||||
|                    i18n:attributes="title"          | ||||
|                    tal:attributes=" | ||||
|                         name string:question_${question/uid}; | ||||
|                         value value/value; | ||||
|                         checked value/checked; | ||||
|                         title string:survey_value_${value/value}" />  | ||||
|           </span> | ||||
|         </td> | ||||
|       </tr> | ||||
|     </table> | ||||
|     <br /> | ||||
|     <input type="submit" name="submit" value="Evaluate Questionnaire" | ||||
|            i18n:attributes="value" /> | ||||
|   </form> | ||||
| </metal:block> | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -158,4 +158,5 @@ class TargetLayoutInstance(NodeLayoutInstance): | |||
|         target = self.viewAnnotations.get('target') | ||||
|         if target is None: | ||||
|             target = adapted(self.context.target) | ||||
|             #self.viewAnnotations['target'] = target    # TODO: has to be tested! | ||||
|         return target | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| # | ||||
| #  Copyright (c) 2009 Helmut Merz helmutm@cy55.de | ||||
| #  Copyright (c) 2013 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 | ||||
|  | @ -18,8 +18,6 @@ | |||
| 
 | ||||
| """ | ||||
| Base classes for layout-based views. | ||||
| 
 | ||||
| $Id$ | ||||
| """ | ||||
| 
 | ||||
| from zope.app.security.interfaces import IUnauthenticatedPrincipal | ||||
|  | @ -29,6 +27,7 @@ from zope.proxy import removeAllProxies | |||
| from zope.security.proxy import removeSecurityProxy | ||||
| from zope.traversing.browser import absoluteURL | ||||
| 
 | ||||
| from cybertools.meta.interfaces import IOptions | ||||
| from cybertools.util import format | ||||
| from loops.common import adapted | ||||
| from loops.i18n.browser import LanguageInfo | ||||
|  | @ -170,3 +169,7 @@ class BaseView(object): | |||
|     def getMetaDescription(self): | ||||
|         return self.context.title | ||||
| 
 | ||||
|     @Lazy | ||||
|     def globalOptions(self): | ||||
|         return IOptions(self.loopsRoot) | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| # | ||||
| #  Copyright (c) 2008 Helmut Merz helmutm@cy55.de | ||||
| #  Copyright (c) 2013 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 | ||||
|  | @ -18,8 +18,6 @@ | |||
| 
 | ||||
| """ | ||||
| Layout node views. | ||||
| 
 | ||||
| $Id$ | ||||
| """ | ||||
| 
 | ||||
| from zope.app.security.interfaces import IUnauthenticatedPrincipal | ||||
|  | @ -66,6 +64,9 @@ class LayoutNodeView(Page, BaseView): | |||
|         if self.target is not None: | ||||
|             targetView = component.getMultiAdapter((self.target, self.request), | ||||
|                                                    name='layout') | ||||
|             return ' - '.join((self.context.title, targetView.title)) | ||||
|             parts = [self.context.title, targetView.title] | ||||
|         else: | ||||
|             return self.context.title | ||||
|             parts = [self.context.title] | ||||
|         if self.globalOptions('reverseHeadTitle'): | ||||
|             parts.reverse() | ||||
|         return ' - '.join(parts) | ||||
|  |  | |||
										
											Binary file not shown.
										
									
								
							|  | @ -3,7 +3,7 @@ msgstr "" | |||
| 
 | ||||
| "Project-Id-Version: 0.13.0\n" | ||||
| "POT-Creation-Date: 2007-05-22 12:00 CET\n" | ||||
| "PO-Revision-Date: 2013-03-07 12:00 CET\n" | ||||
| "PO-Revision-Date: 2013-07-15 12:00 CET\n" | ||||
| "Last-Translator: Helmut Merz <helmutm@cy55.de>\n" | ||||
| "Language-Team: loops developers <helmutm@cy55.de>\n" | ||||
| "MIME-Version: 1.0\n" | ||||
|  | @ -86,6 +86,9 @@ msgstr "Thema bearbeiten..." | |||
| msgid "Modify topic." | ||||
| msgstr "Thema ändern" | ||||
| 
 | ||||
| msgid "Please correct the indicated errors." | ||||
| msgstr "Bitte berichtigen Sie die angezeigten Fehler." | ||||
| 
 | ||||
| # blog | ||||
| 
 | ||||
| msgid "Edit Blog Post..." | ||||
|  | @ -175,11 +178,29 @@ msgstr "Glossareintrag anlegen." | |||
| msgid "Answer Range" | ||||
| msgstr "Abstufung Bewertungen" | ||||
| 
 | ||||
| msgid "Feedback Footer" | ||||
| msgstr "Auswertungs-Hinweis" | ||||
| 
 | ||||
| msgid "Text that will appear at the end of the feedback page." | ||||
| msgstr "Text, der am Ende der Auswertungsseite erscheinen soll." | ||||
| 
 | ||||
| msgid "Number of items (answer options) to select from." | ||||
| msgstr "Anzahl der Abstufungen, aus denen bei der Antwort gewählt werden kann." | ||||
| 
 | ||||
| msgid "Negativ" | ||||
| msgstr "Negativbewertung" | ||||
| msgid "Minimum Number of Answers" | ||||
| msgstr "Mindestanzahl an Antworten" | ||||
| 
 | ||||
| msgid "Minumum number of questions that have to be answered. Empty means all questions have to be answered." | ||||
| msgstr "Anzahl der Fragen, die mindestens beantwortet werden müssen. Keine Angabe: Es müssen alle Fragen beantwortet werden." | ||||
| 
 | ||||
| msgid "Required" | ||||
| msgstr "Pflichtfrage" | ||||
| 
 | ||||
| msgid "Question must be answered." | ||||
| msgstr "Frage muss unbedingt beantwortet werden." | ||||
| 
 | ||||
| msgid "Negative" | ||||
| msgstr "Negative Polarität" | ||||
| 
 | ||||
| msgid "Value inversion: High selection means low value." | ||||
| msgstr "Invertierung der Bewertung: Hohe gewählte Stufe bedeutet niedriger Wert." | ||||
|  | @ -196,27 +217,54 @@ msgstr "Kategorie" | |||
| msgid "Response" | ||||
| msgstr "Beurteilung" | ||||
| 
 | ||||
| msgid "No answer" | ||||
| msgstr "Keine Antwort" | ||||
| 
 | ||||
| msgid "Does not apply" | ||||
| msgstr "Trifft nicht zu" | ||||
| 
 | ||||
| msgid "Fully applies" | ||||
| msgstr "Trifft voll zu" | ||||
| 
 | ||||
| msgid "survey_value_none" | ||||
| msgstr "Keine Antwort" | ||||
| 
 | ||||
| msgid "survey_value_0" | ||||
| msgstr "trifft für unser Unternehmen überhaupt nicht zu" | ||||
| msgstr "Trifft für unser Unternehmen überhaupt nicht zu" | ||||
| 
 | ||||
| msgid "survey_value_1" | ||||
| msgstr "trifft eher nicht zu" | ||||
| msgstr "Trifft eher nicht zu" | ||||
| 
 | ||||
| msgid "survey_value_2" | ||||
| msgstr "trifft eher zu" | ||||
| msgstr "Trifft eher zu" | ||||
| 
 | ||||
| msgid "survey_value_3" | ||||
| msgstr "trifft für unser Unternehmen voll und ganz zu" | ||||
| msgstr "Trifft für unser Unternehmen voll und ganz zu" | ||||
| 
 | ||||
| msgid "Evaluate Questionnaire" | ||||
| msgstr "Fragebogen auswerten" | ||||
| 
 | ||||
| msgid "Reset Responses Entered" | ||||
| msgstr "Eingaben zurücksetzen" | ||||
| 
 | ||||
| msgid "Back to Questionnaire" | ||||
| msgstr "Zurück zum Fragebogen" | ||||
| 
 | ||||
| msgid "Please answer at least $minAnswers questions." | ||||
| msgstr "Bitte beantworten Sie mindestens $minAnswers Fragen." | ||||
| 
 | ||||
| msgid "Please answer all questions." | ||||
| msgstr "Bitte beantworten Sie alle Fragen." | ||||
| 
 | ||||
| msgid "Please answer the obligatory questions." | ||||
| msgstr "Bitte beantworten Sie die Pflichtfragen." | ||||
| 
 | ||||
| msgid "Please answer the minimum number of questions." | ||||
| msgstr "Bitte beantworten Sie die angegebene Mindestanzahl an Fragen je Fragengruppe." | ||||
| 
 | ||||
| msgid "Obligatory question, must be answered" | ||||
| msgstr "Pflichtfrage, muss beantwortet werden" | ||||
| 
 | ||||
| # competence (qualification) | ||||
| 
 | ||||
| msgid "Validity Period (Months)" | ||||
|  | @ -495,6 +543,9 @@ msgstr "Unterbegriffe" | |||
| msgid "Resources" | ||||
| msgstr "Ressourcen" | ||||
| 
 | ||||
| msgid "Text Elements" | ||||
| msgstr "Texte" | ||||
| 
 | ||||
| msgid "Title" | ||||
| msgstr "Titel" | ||||
| 
 | ||||
|  | @ -660,6 +711,9 @@ msgstr "Zugeordnete Begriffe" | |||
| msgid "more..." | ||||
| msgstr "Mehr..." | ||||
| 
 | ||||
| msgid "More..." | ||||
| msgstr "Mehr..." | ||||
| 
 | ||||
| msgid "Versioning" | ||||
| msgstr "Versionierung" | ||||
| 
 | ||||
|  | @ -708,12 +762,27 @@ msgstr "Teilnehmerregistrierung" | |||
| msgid "Register" | ||||
| msgstr "Benutzer registrieren" | ||||
| 
 | ||||
| msgid "Register new member" | ||||
| msgstr "Neu registrieren" | ||||
| 
 | ||||
| msgid "Login name already taken." | ||||
| msgstr "Die von Ihnen eingegebene Benutzerkennung ist schon vergeben." | ||||
| 
 | ||||
| msgid "Your old password was not entered correctly." | ||||
| msgstr "Sie haben Ihr altes Passwort nicht korrekt eingegeben." | ||||
| 
 | ||||
| msgid "Password and password confirmation do not match." | ||||
| msgstr "Die Passwort-Wiederholung stimmt nicht mit dem eingegebenen Passwort überein." | ||||
| 
 | ||||
| msgid "confirmation_mail_subject" | ||||
| msgstr "Benutzer-Registrierung" | ||||
| 
 | ||||
| msgid "confirmation_mail_text" | ||||
| msgstr "Bitte clicken Sie auf den folgenden Link, um die Anmeldung abzuschließen." | ||||
| 
 | ||||
| msgid "The user account has been created." | ||||
| msgstr "Ihr Benutzerkonto wurde eingerichtet." | ||||
| 
 | ||||
| msgid "Your password has been changed." | ||||
| msgstr "Ihr Passwort wurde geändert." | ||||
| 
 | ||||
|  | @ -893,6 +962,9 @@ msgstr "Kalender" | |||
| msgid "Work Items" | ||||
| msgstr "Aktivitäten" | ||||
| 
 | ||||
| msgid "Work Items for $title" | ||||
| msgstr "Aktivitäten für $title" | ||||
| 
 | ||||
| msgid "Day" | ||||
| msgstr "Tag" | ||||
| 
 | ||||
|  | @ -956,14 +1028,26 @@ msgid "Restrict to objects with certain states" | |||
| msgstr "Auf Objekte mit bestimmtem Status beschränken" | ||||
| 
 | ||||
| msgid "Workflow" | ||||
| msgstr "Statusdefinition/Workflow" | ||||
| msgstr "Workflow" | ||||
| 
 | ||||
| msgid "States" | ||||
| msgstr "Statuswerte" | ||||
| 
 | ||||
| msgid "States Definition" | ||||
| msgstr "Workflowdefinition" | ||||
| 
 | ||||
| msgid "State Transition" | ||||
| msgstr "Workflow-Statusänderung" | ||||
| 
 | ||||
| msgid "Transition" | ||||
| msgstr "Aktion" | ||||
| 
 | ||||
| msgid "State information for $definition: $title" | ||||
| msgstr "Status ($definition): $title" | ||||
| 
 | ||||
| msgid "Available Transitions" | ||||
| msgstr "Übergänge" | ||||
| 
 | ||||
| msgid "classification_quality" | ||||
| msgstr "Klassifizierung" | ||||
| 
 | ||||
|  | @ -976,6 +1060,12 @@ msgstr "Aufgabe" | |||
| msgid "publishable_task" | ||||
| msgstr "Aufgabe/Zugriff" | ||||
| 
 | ||||
| msgid "label_transition_comments" | ||||
| msgstr "Bemerkung" | ||||
| 
 | ||||
| msgid "desc_transition_comments" | ||||
| msgstr "Notizen zum Statusübergang." | ||||
| 
 | ||||
| # state names | ||||
| 
 | ||||
| msgid "accepted" | ||||
|  |  | |||
|  | @ -185,7 +185,7 @@ sure that a principal object can be served by a corresponding factory): | |||
|   ...         'lastName': u'Sawyer', | ||||
|   ...         'firstName': u'Tom', | ||||
|   ...         'email': u'tommy@sawyer.com', | ||||
|   ...         'action': 'update',} | ||||
|   ...         'form.action': 'update',} | ||||
| 
 | ||||
| and register it. | ||||
| 
 | ||||
|  |  | |||
|  | @ -27,6 +27,18 @@ | |||
|       class="loops.organize.browser.member.MemberRegistration" | ||||
|       permission="zope.View" /> | ||||
| 
 | ||||
|   <browser:page | ||||
|       for="loops.interfaces.INode" | ||||
|       name="selfservice_registration.html" | ||||
|       class="loops.organize.browser.member.SecureMemberRegistration" | ||||
|       permission="zope.View" /> | ||||
| 
 | ||||
|   <browser:page | ||||
|       for="loops.interfaces.INode" | ||||
|       name="selfservice_confirmation.html" | ||||
|       class="loops.organize.browser.member.ConfirmMemberRegistration" | ||||
|       permission="zope.View" /> | ||||
| 
 | ||||
|   <browser:page | ||||
|       for="loops.interfaces.INode" | ||||
|       name="change_password.html" | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| # | ||||
| #  Copyright (c) 2008 Helmut Merz helmutm@cy55.de | ||||
| #  Copyright (c) 2013 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 | ||||
|  | @ -19,10 +19,10 @@ | |||
| """ | ||||
| Definition of view classes and other browser related stuff for | ||||
| members (persons). | ||||
| 
 | ||||
| $Id$ | ||||
| """ | ||||
| 
 | ||||
| from datetime import datetime | ||||
| from email.MIMEText import MIMEText | ||||
| from zope import interface, component | ||||
| from zope.app.authentication.principalfolder import InternalPrincipal | ||||
| from zope.app.form.browser.textwidgets import PasswordWidget as BasePasswordWidget | ||||
|  | @ -31,6 +31,8 @@ from zope.app.principalannotation import annotations | |||
| from zope.cachedescriptors.property import Lazy | ||||
| from zope.i18nmessageid import MessageFactory | ||||
| from zope.security import checkPermission | ||||
| from zope.sendmail.interfaces import IMailDelivery | ||||
| from zope.traversing.browser import absoluteURL | ||||
| 
 | ||||
| from cybertools.composer.interfaces import IInstance | ||||
| from cybertools.composer.schema.browser.common import schema_macros | ||||
|  | @ -38,7 +40,8 @@ from cybertools.composer.schema.browser.form import Form, CreateForm | |||
| from cybertools.composer.schema.schema import FormState, FormError | ||||
| from cybertools.meta.interfaces import IOptions | ||||
| from cybertools.typology.interfaces import IType | ||||
| from loops.browser.common import concept_macros | ||||
| from cybertools.util.randomname import generateName | ||||
| from loops.browser.common import concept_macros, form_macros | ||||
| from loops.browser.concept import ConceptView, ConceptRelationView | ||||
| from loops.browser.node import NodeView | ||||
| from loops.common import adapted | ||||
|  | @ -46,7 +49,7 @@ from loops.concept import Concept | |||
| from loops.organize.interfaces import ANNOTATION_KEY, IMemberRegistrationManager | ||||
| from loops.organize.interfaces import IMemberRegistration, IPasswordChange | ||||
| from loops.organize.party import getPersonForUser, Person | ||||
| from loops.organize.util import getInternalPrincipal | ||||
| from loops.organize.util import getInternalPrincipal, getPrincipalForUserId | ||||
| import loops.browser.util | ||||
| from loops.util import _ | ||||
| 
 | ||||
|  | @ -76,10 +79,11 @@ class PersonalInfo(ConceptView): | |||
|         return self | ||||
| 
 | ||||
| 
 | ||||
| class MemberRegistration(NodeView, CreateForm): | ||||
| class BaseMemberRegistration(NodeView): | ||||
| 
 | ||||
|     interface = IMemberRegistration | ||||
|     interface = IMemberRegistration     # TODO: add company, create institution | ||||
|     message = _(u'The user account has been created.') | ||||
|     template = form_macros | ||||
| 
 | ||||
|     formErrors = dict( | ||||
|         confirm_nomatch=FormError(_(u'Password and password confirmation do not match.')), | ||||
|  | @ -88,10 +92,23 @@ class MemberRegistration(NodeView, CreateForm): | |||
| 
 | ||||
|     label = _(u'Member Registration') | ||||
|     label_submit = _(u'Register') | ||||
|     title = _('Member Registration') | ||||
| 
 | ||||
|     permissions_key = u'registration.permissions' | ||||
|     roles_key = u'registration.roles' | ||||
|     registration_adapter_key = u'registration.adapter' | ||||
|     text_names_prefix = 'organize.member.registration' | ||||
|     # texts: reg_info, reg_feedback, conf_mail, conf_info, conf_feedback | ||||
|     info_key = 'reg_info' | ||||
|     feedback_key = 'reg_feedback' | ||||
| 
 | ||||
|     isInnerHtml = False | ||||
|     showAssignments = False | ||||
|     form_action = 'register' | ||||
|     versionInfo = None | ||||
| 
 | ||||
|     def closeAction(self, submit=True): | ||||
|         return u'' | ||||
| 
 | ||||
|     @Lazy | ||||
|     def macro(self): | ||||
|  | @ -99,7 +116,7 @@ class MemberRegistration(NodeView, CreateForm): | |||
| 
 | ||||
|     def checkPermissions(self): | ||||
|         personType = adapted(self.conceptManager['person']) | ||||
|         perms = IOptions(personType)('registration.permission') | ||||
|         perms = IOptions(personType)(self.permissions_key) | ||||
|         if perms: | ||||
|             return checkPermission(perms[0], self.context) | ||||
|         return checkPermission('loops.ManageSite', self.context) | ||||
|  | @ -108,12 +125,38 @@ class MemberRegistration(NodeView, CreateForm): | |||
|     def item(self): | ||||
|         return self | ||||
| 
 | ||||
|     @Lazy | ||||
|     def data(self): | ||||
|         return self.request.form | ||||
| 
 | ||||
|     def getPrincipalAnnotation(self, principal): | ||||
|         return annotations(principal).get(ANNOTATION_KEY, None) | ||||
| 
 | ||||
|     @Lazy | ||||
|     def infoText(self): | ||||
|         name = '.'.join((self.text_names_prefix, self.info_key)) | ||||
|         text = self.resourceManager.get(name) | ||||
|         if text: | ||||
|             return self.renderText(text.data) | ||||
|         return u'' | ||||
| 
 | ||||
|     @Lazy | ||||
|     def feedbackUrl(self): | ||||
|         name = '.'.join((self.text_names_prefix, self.feedback_key)) | ||||
|         text = self.resourceManager.get(name) | ||||
|         if text: | ||||
|             return self.getUrlForTarget(text) | ||||
| 
 | ||||
| 
 | ||||
| class MemberRegistration(BaseMemberRegistration, CreateForm): | ||||
| 
 | ||||
|     @Lazy | ||||
|     def schema(self): | ||||
|         schema = super(MemberRegistration, self).schema | ||||
|         schema.fields.remove('birthDate') | ||||
|         schema.fields.reorder(-2, 'loginName') | ||||
|         return schema | ||||
|     # TODO: add company, create institution | ||||
| 
 | ||||
|     @Lazy | ||||
|     def object(self): | ||||
|  | @ -121,7 +164,7 @@ class MemberRegistration(NodeView, CreateForm): | |||
| 
 | ||||
|     def update(self): | ||||
|         form = self.request.form | ||||
|         if not form.get('action'): | ||||
|         if not form.get('form.action'): | ||||
|             return True | ||||
|         instance = component.getAdapter(self.object, IInstance, name='editor') | ||||
|         instance.template = self.schema | ||||
|  | @ -157,6 +200,165 @@ class MemberRegistration(NodeView, CreateForm): | |||
|         return False | ||||
| 
 | ||||
| 
 | ||||
| class SecureMemberRegistration(BaseMemberRegistration, CreateForm): | ||||
| 
 | ||||
|     permissions_key = u'secure_registration.permissions' | ||||
|     roles_key = u'secure_registration.roles' | ||||
|     email_key = 'reg_email' | ||||
| 
 | ||||
|     @Lazy | ||||
|     def schema(self): | ||||
|         schema = super(SecureMemberRegistration, self).schema | ||||
|         schema.fields.remove('birthDate') | ||||
|         schema.fields.remove('password') | ||||
|         schema.fields.remove('passwordConfirm') | ||||
|         schema.fields.remove('phoneNumbers') | ||||
|         #schema.fields.reorder(-2, 'loginName') | ||||
|         return schema | ||||
| 
 | ||||
|     @Lazy | ||||
|     def macro(self): | ||||
|         return organize_macros.macros['register'] | ||||
| 
 | ||||
|     @Lazy | ||||
|     def object(self): | ||||
|         return Person(Concept()) | ||||
| 
 | ||||
|     def update(self): | ||||
|         form = self.request.form | ||||
|         if not form.get('form.action'): | ||||
|             return True | ||||
|         instance = component.getAdapter(self.object, IInstance, name='editor') | ||||
|         instance.template = self.schema | ||||
|         self.formState = formState = instance.applyTemplate(data=form, | ||||
|                                             fieldHandlers=self.fieldHandlers) | ||||
|         if formState.severity > 0: | ||||
|             # show form again | ||||
|             return True | ||||
|         login = form.get('loginName') | ||||
|         regMan = IMemberRegistrationManager(self.context.getLoopsRoot()) | ||||
|         pw = generateName() | ||||
|         email = form.get('email') | ||||
|         try:     | ||||
|             result = regMan.register(login, pw, | ||||
|                                      form.get('lastName'), form.get('firstName'), | ||||
|                                      email=email,) | ||||
|         except ValueError, e: | ||||
|             fi = formState.fieldInstances['loginName'] | ||||
|             fi.setError('duplicate_loginname', self.formErrors) | ||||
|             formState.severity = max(formState.severity, fi.severity) | ||||
|             return True | ||||
|         self.object = result | ||||
|         person = result.context | ||||
|         pa = self.getPrincipalAnnotation( | ||||
|                     getPrincipalForUserId(adapted(person).getUserId())) | ||||
|         pa['id'] = generateName() | ||||
|         pa['timestamp'] = datetime.utcnow() | ||||
|         self.notifyEmail(login, email, pa['id']) | ||||
|         if self.feedbackUrl: | ||||
|             self.request.response.redirect(self.feedbackUrl) | ||||
|         else: | ||||
|             msg = self.message | ||||
|             self.request.response.redirect('%s?loops.message=%s' % (self.url, msg)) | ||||
|         return False | ||||
| 
 | ||||
|     def notifyEmail(self, userid, recipient, id): | ||||
|         baseUrl = absoluteURL(self.context.getMenu(), self.request) | ||||
|         url = u'%s/selfservice_confirmation.html?login=%s&id=%s' % ( | ||||
|                                     baseUrl, userid, id,) | ||||
|         recipients = [recipient] | ||||
|         subject = _(u'confirmation_mail_subject') | ||||
|         name = '.'.join((self.text_names_prefix, self.email_key)) | ||||
|         text = self.resourceManager.get(name) | ||||
|         if text: | ||||
|             message = (text.data % url).encode('UTF-8') | ||||
|             subject = text.description or subject | ||||
|         else: | ||||
|             message = _(u'confirmation_mail_text') + u':\n\n' | ||||
|             message = (message + url).encode('UTF-8') | ||||
|         senderInfo = self.globalOptions('email.sender') | ||||
|         sender = senderInfo and senderInfo[0] or 'info@loops.cy55.de' | ||||
|         sender = sender.encode('UTF-8') | ||||
|         msg = MIMEText(message, 'plain', 'utf-8') | ||||
|         msg['Subject'] = subject.encode('UTF-8') | ||||
|         msg['From'] = sender | ||||
|         msg['To'] = ', '.join(recipients) | ||||
|         mailhost = component.getUtility(IMailDelivery, 'Mail') | ||||
|         mailhost.send(sender, recipients, msg.as_string()) | ||||
| 
 | ||||
| 
 | ||||
| class ConfirmMemberRegistration(BaseMemberRegistration, Form): | ||||
| 
 | ||||
|     permissions_key = u'secure_registration.permissions' | ||||
|     roles_key = u'secure_registration.roles' | ||||
|     info_key = 'confirm_info' | ||||
|     feedback_key = 'confirm_feedback' | ||||
|     email_key = 'confirm_email' | ||||
| 
 | ||||
|     form_action = 'confirm_registration' | ||||
| 
 | ||||
|     @Lazy | ||||
|     def macro(self): | ||||
|         return organize_macros.macros['confirm'] | ||||
| 
 | ||||
|     @Lazy | ||||
|     def data(self): | ||||
|         form = self.request.form | ||||
|         return dict(loginName=form.get('login'), id=form.get('id')) | ||||
| 
 | ||||
|     @Lazy | ||||
|     def schema(self): | ||||
|         schema = super(ConfirmMemberRegistration, self).schema | ||||
|         schema.fields.remove('salutation') | ||||
|         schema.fields.remove('academicTitle') | ||||
|         schema.fields.remove('birthDate') | ||||
|         schema.fields.remove('phoneNumbers') | ||||
|         schema.fields.remove('loginName') | ||||
|         schema.fields.remove('firstName') | ||||
|         schema.fields.remove('lastName') | ||||
|         schema.fields.remove('email') | ||||
|         return schema | ||||
| 
 | ||||
|     def update(self): | ||||
|         form = self.request.form | ||||
|         if form.get('form.action') != 'confirm_registration': | ||||
|             return True | ||||
|         if not form.get('login'): | ||||
|             return True | ||||
|         regMan = IMemberRegistrationManager(self.context.getLoopsRoot()) | ||||
|         prefix = regMan.getPrincipalFolderFromOption().prefix | ||||
|         userId = prefix + form['login'] | ||||
|         principal = getPrincipalForUserId(userId) | ||||
|         pa = self.getPrincipalAnnotation(principal) | ||||
|         id = form.get('id') | ||||
|         if not id or id != pa.get('id'): | ||||
|             return True | ||||
|         instance = component.getAdapter(self.object, IInstance, name='editor') | ||||
|         instance.template = self.schema | ||||
|         self.formState = formState = instance.applyTemplate(data=form, | ||||
|                                             fieldHandlers=self.fieldHandlers) | ||||
|         #formState = self.formState = self.validate(form) | ||||
|         if formState.severity > 0: | ||||
|             return True | ||||
|         pw = form.get('password') | ||||
|         pwConfirm = form.get('passwordConfirm') | ||||
|         if pw != pwConfirm: | ||||
|             fi = formState.fieldInstances['password'] | ||||
|             fi.setError('confirm_nomatch', self.formErrors) | ||||
|             formState.severity = max(formState.severity, fi.severity) | ||||
|             return True | ||||
|         del pa['id'] | ||||
|         del pa['timestamp'] | ||||
|         ip = getInternalPrincipal(userId) | ||||
|         ip.setPassword(pw) | ||||
|         if self.feedbackUrl: | ||||
|             self.request.response.redirect(self.feedbackUrl) | ||||
|         else: | ||||
|             url = '%s?loops.message=%s' % (self.url, self.message) | ||||
|             self.request.response.redirect(url) | ||||
|         return False | ||||
| 
 | ||||
| 
 | ||||
| class PasswordChange(NodeView, Form): | ||||
| 
 | ||||
|     interface = IPasswordChange | ||||
|  |  | |||
|  | @ -1,5 +1,42 @@ | |||
| <html i18n:domain="loops"> | ||||
| 
 | ||||
| 
 | ||||
| <metal:block define-macro="register"> | ||||
|   <metal:data use-macro="view/form_macros/edit"> | ||||
|     <metal:custom fill-slot="custom_header"> | ||||
|       <tbody> | ||||
|         <tr><td colspan="5"> | ||||
|           <tal:info content="structure item/infoText" /> | ||||
|         </td></tr> | ||||
|       </tbody> | ||||
|     </metal:custom> | ||||
|   </metal:data> | ||||
| </metal:block> | ||||
| 
 | ||||
| 
 | ||||
| <metal:block define-macro="confirm"> | ||||
|   <metal:data use-macro="view/form_macros/edit"> | ||||
|     <metal:custom fill-slot="custom_header"> | ||||
|       <tbody> | ||||
|         <tr><td colspan="5"> | ||||
|           <tal:info content="structure item/infoText" /> | ||||
|         </td></tr> | ||||
|         <tr><td colspan="5"> | ||||
|           <input type="hidden" name="login" | ||||
|                  tal:attributes="value item/data/loginName" /> | ||||
|           <input type="hidden" name="id" | ||||
|                  tal:attributes="value item/data/id" /> | ||||
|           <table><tr> | ||||
|             <td i18n:translate="">Login Name</td> | ||||
|             <td tal:content="item/data/loginName" /> | ||||
|           </tr></table> | ||||
|         </td></tr> | ||||
|       </tbody> | ||||
|     </metal:custom> | ||||
|   </metal:data> | ||||
| </metal:block> | ||||
| 
 | ||||
| 
 | ||||
| <metal:task define-macro="task"> | ||||
|   <metal:data use-macro="view/concept_macros/conceptdata"> | ||||
|   </metal:data> | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| # | ||||
| #  Copyright (c) 2012 Helmut Merz helmutm@cy55.de | ||||
| #  Copyright (c) 2013 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 | ||||
|  | @ -62,19 +62,29 @@ class MemberRegistrationManager(object): | |||
|     def __init__(self, context): | ||||
|         self.context = context | ||||
| 
 | ||||
|     @Lazy | ||||
|     def personType(self): | ||||
|         concepts = self.context.getConceptManager() | ||||
|         return adapted(concepts[self.person_typeName]) | ||||
| 
 | ||||
|     def getPrincipalFolderFromOption(self): | ||||
|         options = IOptions(self.personType) | ||||
|         pfName = options(self.principalfolder_key, | ||||
|                          (self.default_principalfolder,))[0] | ||||
|         return getPrincipalFolder(self.context, pfName) | ||||
| 
 | ||||
|     def register(self, userId, password, lastName, firstName=u'', | ||||
|                  groups=[], useExisting=False, pfName=None, **kw): | ||||
|         concepts = self.context.getConceptManager() | ||||
|         personType = adapted(concepts[self.person_typeName]) | ||||
|         options = IOptions(personType) | ||||
|         options = IOptions(self.personType) | ||||
|         if pfName is None: | ||||
|             pfName = options(self.principalfolder_key, | ||||
|                              (self.default_principalfolder,))[0] | ||||
|         self.createPrincipal(pfName, userId, password, lastName, firstName, useExisting=useExisting) | ||||
|         if len(groups)==0: | ||||
|         self.createPrincipal(pfName, userId, password, lastName, firstName,  | ||||
|                              useExisting=useExisting) | ||||
|         if not groups: | ||||
|             groups = options(self.groups_key, ()) | ||||
|         self.setGroupsForPrincipal(pfName, userId,  groups=groups) | ||||
|         self.createPersonForPrincipal(pfName, userId, lastName, firstName, | ||||
|         return self.createPersonForPrincipal(pfName, userId, lastName, firstName, | ||||
|                                       useExisting, **kw) | ||||
| 
 | ||||
|     def createPrincipal(self, pfName, userId, password, lastName, | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| # | ||||
| #  Copyright (c) 2008 Helmut Merz helmutm@cy55.de | ||||
| #  Copyright (c) 2013 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 | ||||
|  | @ -18,8 +18,6 @@ | |||
| 
 | ||||
| """ | ||||
| Basic implementations for stateful objects and adapters. | ||||
| 
 | ||||
| $Id$ | ||||
| """ | ||||
| 
 | ||||
| from zope.app.catalog.interfaces import ICatalog | ||||
|  | @ -27,6 +25,7 @@ from zope.cachedescriptors.property import Lazy | |||
| from zope import component | ||||
| from zope.component import adapts, adapter | ||||
| 
 | ||||
| from cybertools.composer.schema.field import Field | ||||
| from cybertools.meta.interfaces import IOptions | ||||
| from cybertools.stateful.base import Stateful as BaseStateful | ||||
| from cybertools.stateful.base import StatefulAdapter, IndexInfo | ||||
|  | @ -34,6 +33,7 @@ from cybertools.stateful.interfaces import IStatesDefinition, ITransitionEvent | |||
| from loops.common import adapted | ||||
| from loops.interfaces import ILoopsObject, IConcept, IResource | ||||
| from loops import util | ||||
| from loops.util import _ | ||||
| 
 | ||||
| 
 | ||||
| class Stateful(BaseStateful): | ||||
|  | @ -93,3 +93,10 @@ def handleTransition(obj, event): | |||
|     if next != previous: | ||||
|         cat = component.getUtility(ICatalog) | ||||
|         cat.index_doc(int(util.getUidForObject(obj)), obj) | ||||
| 
 | ||||
| 
 | ||||
| # predefined fields for transition forms | ||||
| 
 | ||||
| commentsField = Field('comments', _(u'label_transition_comments'), 'textarea',  | ||||
|                       description=_(u'desc_transition_comments'),  | ||||
|                       nostore=True) | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| # | ||||
| #  Copyright (c) 2012 Helmut Merz helmutm@cy55.de | ||||
| #  Copyright (c) 2013 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 | ||||
|  | @ -23,12 +23,16 @@ Views and actions for states management. | |||
| from zope import component | ||||
| from zope.app.pagetemplate import ViewPageTemplateFile | ||||
| from zope.cachedescriptors.property import Lazy | ||||
| from zope.event import notify | ||||
| from zope.i18n import translate | ||||
| from zope.lifecycleevent import ObjectModifiedEvent, Attributes | ||||
| 
 | ||||
| from cybertools.browser.action import Action, actions | ||||
| from cybertools.composer.schema.schema import Schema | ||||
| from cybertools.stateful.interfaces import IStateful, IStatesDefinition | ||||
| from loops.browser.common import BaseView | ||||
| from loops.browser.concept import ConceptView | ||||
| from loops.browser.form import ObjectForm, EditObject | ||||
| from loops.expert.query import And, Or, State, Type, getObjects | ||||
| from loops.expert.browser.search import search_template | ||||
| from loops.security.common import checkPermission | ||||
|  | @ -43,6 +47,16 @@ statefulActions = ('classification_quality', | |||
|                    'publishable_task',) | ||||
| 
 | ||||
| 
 | ||||
| def registerStatesPortlet(controller, view, statesDefs, | ||||
|                           region='portlet_right', priority=98): | ||||
|     cm = controller.macros | ||||
|     stfs = [component.getAdapter(view.context, IStateful, name=std)  | ||||
|                 for std in statesDefs] | ||||
|     cm.register(region, 'states', title=_(u'Workflow'), | ||||
|                 subMacro=template.macros['portlet_states'], | ||||
|                 priority=priority, info=view, stfs=stfs) | ||||
| 
 | ||||
| 
 | ||||
| class StateAction(Action): | ||||
| 
 | ||||
|     url = None | ||||
|  | @ -67,22 +81,92 @@ class StateAction(Action): | |||
| 
 | ||||
|     @Lazy | ||||
|     def icon(self): | ||||
|         icon = self.stateObject.icon or 'led%s.png' % self.stateObject.color | ||||
|         return 'cybertools.icons/' + icon | ||||
|         return self.stateObject.stateIcon | ||||
|         #icon = self.stateObject.icon or 'led%s.png' % self.stateObject.color | ||||
|         #return 'cybertools.icons/' + icon | ||||
| 
 | ||||
| 
 | ||||
| for std in statefulActions: | ||||
|     actions.register('state.' + std, 'object', StateAction, | ||||
|             definition = std, | ||||
|             definition=std, | ||||
|             cssClass='icon-action', | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| class ChangeStateBase(object): | ||||
| 
 | ||||
|     @Lazy | ||||
|     def stateful(self): | ||||
|         return component.getAdapter(self.view.virtualTargetObject, IStateful, | ||||
|                                     name=self.definition) | ||||
| 
 | ||||
|     @Lazy | ||||
|     def definition(self): | ||||
|         return self.request.form.get('stdef') or u'' | ||||
| 
 | ||||
|     @Lazy | ||||
|     def action(self): | ||||
|         return self.request.form.get('action') or u'' | ||||
| 
 | ||||
|     @Lazy | ||||
|     def transition(self): | ||||
|         return self.stateful.getStatesDefinition().transitions[self.action] | ||||
| 
 | ||||
|     @Lazy | ||||
|     def stateObject(self): | ||||
|         return self.stateful.getStateObject() | ||||
| 
 | ||||
|     @Lazy | ||||
|     def schema(self): | ||||
|         schema = self.transition.schema | ||||
|         if schema is None: | ||||
|             return Schema() | ||||
|         else: | ||||
|             schema.manager = self | ||||
|             schema.request = self.request | ||||
|             return schema | ||||
| 
 | ||||
| 
 | ||||
| class ChangeStateForm(ChangeStateBase, ObjectForm): | ||||
| 
 | ||||
|     form_action = 'change_state_action' | ||||
|     data = {} | ||||
| 
 | ||||
|     @Lazy | ||||
|     def macro(self): | ||||
|         return template.macros['change_state'] | ||||
| 
 | ||||
|     @Lazy | ||||
|     def title(self): | ||||
|         return self.virtualTargetObject.title | ||||
| 
 | ||||
| 
 | ||||
| class ChangeState(ChangeStateBase, EditObject): | ||||
| 
 | ||||
|     def update(self): | ||||
|         formData = self.request.form | ||||
|         # store data in target object (unless field.nostore) | ||||
|         self.object = self.target | ||||
|         formState = self.instance.applyTemplate(data=formData) | ||||
|         # TODO: check formState | ||||
|         # track all fields | ||||
|         trackData = dict(transition=self.action) | ||||
|         for f in self.fields: | ||||
|             if f.readonly: | ||||
|                 continue | ||||
|             name = f.name | ||||
|             fi = formState.fieldInstances[name] | ||||
|             rawValue = fi.getRawValue(formData, name, u'') | ||||
|             trackData[name] = fi.unmarshall(rawValue) | ||||
|         self.stateful.doTransition(self.action) | ||||
|         notify(ObjectModifiedEvent(self.view.virtualTargetObject, trackData)) | ||||
|         return True | ||||
| 
 | ||||
| 
 | ||||
| #class StateQuery(ConceptView): | ||||
| class StateQuery(BaseView): | ||||
| 
 | ||||
|     template = template | ||||
| 
 | ||||
|     form_action = 'execute_search_action' | ||||
| 
 | ||||
|     @Lazy | ||||
|  |  | |||
|  | @ -77,7 +77,7 @@ | |||
|              set_schema="cybertools.stateful.interfaces.IStateful" /> | ||||
|   </zope:class> | ||||
| 
 | ||||
|   <!-- views --> | ||||
|   <!-- views and form controllers --> | ||||
| 
 | ||||
|   <browser:page | ||||
|         for="loops.interfaces.IConcept" | ||||
|  | @ -91,6 +91,19 @@ | |||
|         class="loops.organize.stateful.browser.FilterAllStates" | ||||
|         permission="zope.View" /> | ||||
| 
 | ||||
|   <browser:page | ||||
|       name="change_state.html" | ||||
|       for="loops.interfaces.INode" | ||||
|       class="loops.organize.stateful.browser.ChangeStateForm" | ||||
|       permission="zope.ManageContent" /> | ||||
| 
 | ||||
|   <zope:adapter | ||||
|       name="change_state" | ||||
|       for="loops.browser.node.NodeView | ||||
|            zope.publisher.interfaces.browser.IBrowserRequest" | ||||
|       factory="loops.organize.stateful.browser.ChangeState" | ||||
|       permission="zope.ManageContent" /> | ||||
| 
 | ||||
|   <!-- event handlers --> | ||||
| 
 | ||||
|   <zope:subscriber handler="loops.organize.stateful.base.handleTransition" /> | ||||
|  |  | |||
|  | @ -26,12 +26,15 @@ from zope.component import adapter | |||
| from zope.interface import implementer | ||||
| from zope.traversing.api import getName | ||||
| 
 | ||||
| from cybertools.composer.schema.schema import Schema | ||||
| from cybertools.stateful.definition import StatesDefinition | ||||
| from cybertools.stateful.definition import State, Transition | ||||
| from cybertools.stateful.interfaces import IStatesDefinition, IStateful | ||||
| from loops.common import adapted | ||||
| from loops.organize.stateful.base import commentsField | ||||
| from loops.organize.stateful.base import StatefulLoopsObject | ||||
| from loops.security.interfaces import ISecuritySetter | ||||
| from loops.util import _ | ||||
| 
 | ||||
| 
 | ||||
| def setPermissionsForRoles(settings): | ||||
|  | @ -42,6 +45,10 @@ def setPermissionsForRoles(settings): | |||
|     return setSecurity | ||||
| 
 | ||||
| 
 | ||||
| defaultSchema = Schema(commentsField,  | ||||
|                        name='change_state') | ||||
| 
 | ||||
| 
 | ||||
| @implementer(IStatesDefinition) | ||||
| def taskStates(): | ||||
|     return StatesDefinition('task_states', | ||||
|  | @ -55,10 +62,11 @@ def taskStates(): | |||
|               color='x'), | ||||
|         State('archived', 'archived', ('reopen',), | ||||
|               color='grey'), | ||||
|         Transition('release', 'release', 'active'), | ||||
|         Transition('finish', 'finish', 'finished'), | ||||
|         Transition('cancel', 'cancel', 'cancelled'), | ||||
|         Transition('reopen', 're-open', 'draft'), | ||||
|         Transition('release', 'release', 'active', schema=defaultSchema), | ||||
|         Transition('finish', 'finish', 'finished', schema=defaultSchema), | ||||
|         Transition('cancel', 'cancel', 'cancelled', schema=defaultSchema), | ||||
|         Transition('reopen', 're-open', 'draft', schema=defaultSchema), | ||||
|         Transition('archive', 'archive', 'archived', schema=defaultSchema), | ||||
|         initialState='draft') | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -68,4 +68,82 @@ | |||
| </metal:query> | ||||
| 
 | ||||
| 
 | ||||
| <metal:actions define-macro="portlet_states"> | ||||
|     <div tal:repeat="stf macro/stfs"> | ||||
|       <div tal:condition="python:len(macro.stfs) > 1"> | ||||
|         <span i18n:translate="">States Definition</span> | ||||
|         <span i18n:translate="" | ||||
|               tal:content="stf/statesDefinition" /> | ||||
|       </div> | ||||
|       <div> | ||||
|         <b i18n:translate="">State</b>: | ||||
|         <span i18n:translate="" | ||||
|               tal:content="stf/state" /> | ||||
|         <img style="margin-bottom: -3px" | ||||
|              tal:define="stateObject stf/getStateObject" | ||||
|              tal:attributes="src string:$resourceBase/${stateObject/stateIcon}" /> | ||||
|       </div> | ||||
|       <div><b i18n:translate="">Available Transitions</b>: | ||||
|         <ul> | ||||
|           <li tal:repeat="action stf/getAvailableTransitionsForUser"> | ||||
|             <a i18n:translate="" | ||||
|                tal:define="baseUrl view/virtualTargetUrl; | ||||
|                            url string:$baseUrl/change_state.html?action=${action/name}&stdef=${stf/statesDefinition}" | ||||
|                tal:attributes="href url; | ||||
|                                onClick string:objectDialog('change_state', '$url');; | ||||
|                                               return false;" | ||||
|                tal:content="action/title" /> | ||||
|           </li> | ||||
|         </ul> | ||||
|       </div> | ||||
|     </div> | ||||
| </metal:actions> | ||||
| 
 | ||||
| 
 | ||||
| <metal:dialog define-macro="change_state"> | ||||
|   <form name="stateful_changeState" method="post"> | ||||
|     <div dojoType="dijit.layout.BorderContainer" | ||||
|          style="width: 70em; height: 600px"> | ||||
|       <div dojoType="dijit.layout.ContentPane" region="top"> | ||||
|         <h1><span i18n:translate="">State Transition</span> - | ||||
|           <span tal:content="view/title" /> | ||||
|         </h1> | ||||
|         <div> | ||||
|           <span i18n:translate="">State</span>: | ||||
|           <span i18n:translate="" | ||||
|                 tal:define="stateObject view/stateful/getStateObject" | ||||
|                 tal:content="stateObject/title" /> - | ||||
|           <span i18n:translate="">Transition</span>: | ||||
|           <span i18n:translate="" | ||||
|                 tal:content="view/transition/title" /> | ||||
|         </div> | ||||
|         <input type="hidden" name="form.action" value="change_state"> | ||||
|         <input type="hidden" name="stdef" | ||||
|                tal:attributes="value request/form/stdef|nothing"> | ||||
|         <input type="hidden" name="action" | ||||
|                tal:attributes="value request/form/action|nothing"> | ||||
|       </div> | ||||
|       <div dojoType="dijit.layout.ContentPane" region="center"> | ||||
|         <table cellpadding="3" class="form"> | ||||
|           <tbody><tr><td colspan="5" style="padding-right: 15px"> | ||||
|                 <div id="form.fields"> | ||||
|                   <metal:fields use-macro="view/fieldRenderers/fields" /> | ||||
|                 </div> | ||||
|           </td></tr></tbody> | ||||
|         </table> | ||||
|       </div> | ||||
|       <div dojoType="dijit.layout.ContentPane" region="bottom"> | ||||
|         <metal:buttons define-slot="buttons"> | ||||
|           <input value="Save" type="submit"  | ||||
|                  onClick="submit();; return false" | ||||
|                  i18n:attributes="value"> | ||||
|           <input type="button" value="Cancel" onClick="dialog.hide();" | ||||
|                  i18n:attributes="value"> | ||||
|         </metal:buttons> | ||||
|       </div> | ||||
|     </div> | ||||
|   </form> | ||||
| </metal:dialog> | ||||
| 
 | ||||
| 
 | ||||
| </html> | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| # | ||||
| #  Copyright (c) 2008 Helmut Merz helmutm@cy55.de | ||||
| #  Copyright (c) 2013 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 | ||||
|  | @ -18,10 +18,9 @@ | |||
| 
 | ||||
| """ | ||||
| Base class(es) for track/record managers. | ||||
| 
 | ||||
| $Id$ | ||||
| """ | ||||
| 
 | ||||
| from zope.app.security.interfaces import IUnauthenticatedPrincipal | ||||
| from zope.cachedescriptors.property import Lazy | ||||
| 
 | ||||
| from cybertools.meta.interfaces import IOptions | ||||
|  | @ -46,6 +45,10 @@ class BaseRecordManager(object): | |||
|     def loopsRoot(self): | ||||
|         return self.context.getLoopsRoot() | ||||
| 
 | ||||
|     @Lazy | ||||
|     def uid(self): | ||||
|         return util.getUidForObject(self.context) | ||||
| 
 | ||||
|     @Lazy | ||||
|     def storage(self): | ||||
|         records = self.loopsRoot.getRecordManager() | ||||
|  | @ -63,6 +66,8 @@ class BaseRecordManager(object): | |||
|         else: | ||||
|             principal = getPrincipalForUserId(userId, context=self.context) | ||||
|         if principal is not None: | ||||
|             if IUnauthenticatedPrincipal.providedBy(principal): | ||||
|                 return None | ||||
|             person = getPersonForUser(self.context, principal=principal) | ||||
|             if person is None: | ||||
|                 return principal.id | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| # | ||||
| #  Copyright (c) 2008 Helmut Merz helmutm@cy55.de | ||||
| #  Copyright (c) 2013 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 | ||||
|  | @ -18,8 +18,6 @@ | |||
| 
 | ||||
| """ | ||||
| Recording changes to loops objects. | ||||
| 
 | ||||
| $Id$ | ||||
| """ | ||||
| 
 | ||||
| from zope.app.container.interfaces import IObjectAddedEvent, IObjectRemovedEvent | ||||
|  | @ -53,6 +51,9 @@ class ChangeManager(BaseRecordManager): | |||
| 
 | ||||
|     @Lazy | ||||
|     def valid(self): | ||||
|         req = util.getRequest() | ||||
|         if req and req.form.get('organize.suppress_tracking'): | ||||
|             return False | ||||
|         return (not (self.context is None or | ||||
|                     self.storage is None or | ||||
|                     self.personId is None) | ||||
|  | @ -70,6 +71,12 @@ class ChangeManager(BaseRecordManager): | |||
|         if relation is not None: | ||||
|             data['predicate'] = util.getUidForObject(relation.predicate) | ||||
|             data['second'] = util.getUidForObject(relation.second) | ||||
|         event = kw.get('event') | ||||
|         if event is not None: | ||||
|             desc = getattr(event, 'descriptions', ()) | ||||
|             for item in desc: | ||||
|                 if isinstance(item, dict): | ||||
|                     data.update(item) | ||||
|         if update: | ||||
|             self.storage.updateTrack(last, data) | ||||
|         else: | ||||
|  | @ -90,16 +97,18 @@ class ChangeRecord(Track): | |||
| 
 | ||||
| @adapter(ILoopsObject, IObjectModifiedEvent) | ||||
| def recordModification(obj, event): | ||||
|     ChangeManager(obj).recordModification() | ||||
|     ChangeManager(obj).recordModification(event=event) | ||||
| 
 | ||||
| @adapter(ILoopsObject, IObjectAddedEvent) | ||||
| def recordAdding(obj, event): | ||||
|     ChangeManager(obj).recordModification('add') | ||||
|     ChangeManager(obj).recordModification('add', event=event) | ||||
| 
 | ||||
| @adapter(ILoopsObject, IAssignmentEvent) | ||||
| def recordAssignment(obj, event): | ||||
|     ChangeManager(obj).recordModification('assign', relation=event.relation) | ||||
|     ChangeManager(obj).recordModification('assign',  | ||||
|                             event=event, relation=event.relation) | ||||
| 
 | ||||
| @adapter(ILoopsObject, IDeassignmentEvent) | ||||
| def recordDeassignment(obj, event): | ||||
|     ChangeManager(obj).recordModification('deassign', relation=event.relation) | ||||
|     ChangeManager(obj).recordModification('deassign',  | ||||
|                             event=event, relation=event.relation) | ||||
|  |  | |||
|  | @ -43,13 +43,14 @@ from loops.browser.concept import ConceptView | |||
| from loops.browser.form import ObjectForm, EditObject | ||||
| from loops.browser.node import NodeView | ||||
| from loops.common import adapted | ||||
| from loops.organize.interfaces import IPerson | ||||
| from loops.organize.party import getPersonForUser | ||||
| from loops.organize.stateful.browser import StateAction | ||||
| from loops.organize.tracking.browser import BaseTrackView | ||||
| from loops.organize.tracking.report import TrackDetails | ||||
| from loops.organize.work.base import WorkItem | ||||
| from loops.security.common import canAccessObject, canListObject, canWriteObject | ||||
| from loops.security.common import checkPermission | ||||
| from loops.security.common import canAccessRestricted, checkPermission | ||||
| from loops import util | ||||
| from loops.util import _ | ||||
| 
 | ||||
|  | @ -228,6 +229,10 @@ class BaseWorkItemsView(object): | |||
|     def macro(self): | ||||
|         return self.work_macros['workitems_query'] | ||||
| 
 | ||||
|     @Lazy | ||||
|     def title(self): | ||||
|         return _(u'Work Items for $title', mapping=dict(title=self.context.title)) | ||||
| 
 | ||||
|     @Lazy | ||||
|     def workItems(self): | ||||
|         rm = self.loopsRoot.getRecordManager() | ||||
|  | @ -312,19 +317,25 @@ class RelatedTaskWorkItems(AllWorkItems): | |||
| 
 | ||||
| 
 | ||||
| class PersonWorkItems(BaseWorkItemsView, ConceptView): | ||||
|     """ A query view showing work items for a person, the query's parent. | ||||
|     """ A view showing work items for a person or the context object's parents. | ||||
|     """ | ||||
| 
 | ||||
|     columns = set(['Task', 'Title', 'Day', 'Start', 'End', 'Duration', 'Info']) | ||||
| 
 | ||||
|     def checkPermissions(self): | ||||
|         return canAccessRestricted(self.context) | ||||
| 
 | ||||
|     def getCriteria(self): | ||||
|         return self.baseCriteria | ||||
| 
 | ||||
|     def listWorkItems(self): | ||||
|         criteria = self.getCriteria() | ||||
|         for target in self.context.getParents([self.defaultPredicate]): | ||||
|             un = criteria.setdefault('userName', []) | ||||
|             un.append(util.getUidForObject(target)) | ||||
|         un = criteria.setdefault('userName', []) | ||||
|         if IPerson.providedBy(self.adapted): | ||||
|             un.append(util.getUidForObject(self.context)) | ||||
|         else: | ||||
|             for target in self.context.getParents([self.defaultPredicate]): | ||||
|                 un.append(util.getUidForObject(target)) | ||||
|         return sorted(self.query(**criteria), key=lambda x: x.track.timeStamp) | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										14
									
								
								query.py
									
										
									
									
									
								
							
							
						
						
									
										14
									
								
								query.py
									
										
									
									
									
								
							|  | @ -1,5 +1,5 @@ | |||
| # | ||||
| #  Copyright (c) 2008 Helmut Merz helmutm@cy55.de | ||||
| #  Copyright (c) 2013 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 | ||||
|  | @ -18,8 +18,6 @@ | |||
| 
 | ||||
| """ | ||||
| Query management stuff. | ||||
| 
 | ||||
| $Id$ | ||||
| """ | ||||
| 
 | ||||
| from BTrees.IOBTree import IOBTree | ||||
|  | @ -33,6 +31,7 @@ from zope.cachedescriptors.property import Lazy | |||
| from cybertools.typology.interfaces import IType | ||||
| from loops.common import AdapterBase | ||||
| from loops.interfaces import IConcept, IConceptSchema, ILoopsAdapter | ||||
| from loops.interfaces import IOptions | ||||
| from loops.security.common import canListObject | ||||
| from loops.type import TypeInterfaceSourceList | ||||
| from loops.versioning.util import getVersion | ||||
|  | @ -182,7 +181,7 @@ class ConceptQuery(BaseQuery): | |||
| 
 | ||||
| # QueryConcept: concept objects that allow querying the database. | ||||
| 
 | ||||
| class IQueryConcept(IConceptSchema, ILoopsAdapter): | ||||
| class IQueryConcept(IConceptSchema, ILoopsAdapter, IOptions): | ||||
|     """ The schema for the query type. | ||||
|     """ | ||||
| 
 | ||||
|  | @ -194,13 +193,6 @@ class IQueryConcept(IConceptSchema, ILoopsAdapter): | |||
|         default=u'', | ||||
|         required=False) | ||||
| 
 | ||||
|     options = schema.List( | ||||
|         title=_(u'Options'), | ||||
|         description=_(u'Additional settings.'), | ||||
|         value_type=schema.TextLine(), | ||||
|         default=[], | ||||
|         required=False) | ||||
| 
 | ||||
| 
 | ||||
| class QueryConcept(AdapterBase): | ||||
| 
 | ||||
|  |  | |||
|  | @ -74,6 +74,9 @@ def canListObject(obj, noCheck=False): | |||
|         return True | ||||
|     return canAccess(obj, 'title') | ||||
| 
 | ||||
| def canAccessRestricted(obj): | ||||
|     return checkPermission('loops.ViewRestricted', obj) | ||||
| 
 | ||||
| def canWriteObject(obj): | ||||
|     return canWrite(obj, 'title') or canAssignAsParent(obj)  | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										23
									
								
								type.py
									
										
									
									
									
								
							
							
						
						
									
										23
									
								
								type.py
									
										
									
									
									
								
							|  | @ -1,5 +1,5 @@ | |||
| # | ||||
| #  Copyright (c) 2006 Helmut Merz helmutm@cy55.de | ||||
| #  Copyright (c) 2013 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 | ||||
|  | @ -18,8 +18,6 @@ | |||
| 
 | ||||
| """ | ||||
| Type management stuff. | ||||
| 
 | ||||
| $Id$ | ||||
| """ | ||||
| 
 | ||||
| from zope import component, schema | ||||
|  | @ -28,12 +26,13 @@ from zope.interface import implements | |||
| from zope.cachedescriptors.property import Lazy | ||||
| from zope.dottedname.resolve import resolve | ||||
| from zope.security.proxy import removeSecurityProxy | ||||
| from zope.traversing.api import getName | ||||
| from zope.traversing.api import getName, getPath | ||||
| 
 | ||||
| from cybertools.typology.type import BaseType, TypeManager | ||||
| from cybertools.typology.interfaces import ITypeManager | ||||
| from loops.interfaces import ILoopsObject, IConcept, IResource | ||||
| from loops.interfaces import ITypeConcept | ||||
| from loops.interfaces import IOptions | ||||
| from loops.interfaces import IResourceAdapter, IFile, IExternalFile, IImage | ||||
| from loops.interfaces import ITextDocument, INote | ||||
| from loops.concept import Concept | ||||
|  | @ -49,10 +48,15 @@ class LoopsType(BaseType): | |||
|                           #document=Document) | ||||
|     containerMapping = dict(concept='concepts', resource='resources') | ||||
| 
 | ||||
|     isForeignReference = False | ||||
| 
 | ||||
|     @Lazy | ||||
|     def title(self): | ||||
|         tp = self.typeProvider | ||||
|         return tp is None and u'Unknown Type' or tp.title | ||||
|         title = tp is None and u'Unknown Type' or tp.title | ||||
|         if self.isForeignReference: | ||||
|             title += (' (Site: %s)' % getName(self.root)) | ||||
|         return title | ||||
| 
 | ||||
|     @Lazy | ||||
|     def token(self): | ||||
|  | @ -63,7 +67,11 @@ class LoopsType(BaseType): | |||
|     def tokenForSearch(self): | ||||
|         tp = self.typeProvider | ||||
|         typeName = tp is None and 'unknown' or str(getName(tp)) | ||||
|         return ':'.join(('loops', self.qualifiers[0], typeName,)) | ||||
|         if self.isForeignReference: | ||||
|             root = getPath(self.root) | ||||
|         else: | ||||
|             root = 'loops' | ||||
|         return ':'.join((root, self.qualifiers[0], typeName,)) | ||||
| 
 | ||||
|     @Lazy | ||||
|     def typeInterface(self): | ||||
|  | @ -272,7 +280,8 @@ class TypeInterfaceSourceList(object): | |||
| 
 | ||||
|     implements(schema.interfaces.IIterableSource) | ||||
| 
 | ||||
|     typeInterfaces = (ITypeConcept, IFile, IExternalFile, ITextDocument, INote) | ||||
|     typeInterfaces = (ITypeConcept, IFile, IExternalFile, ITextDocument, INote, | ||||
|                       IOptions) | ||||
| 
 | ||||
|     def __init__(self, context): | ||||
|         self.context = context | ||||
|  |  | |||
							
								
								
									
										5
									
								
								util.py
									
										
									
									
									
								
							
							
						
						
									
										5
									
								
								util.py
									
										
									
									
									
								
							|  | @ -141,4 +141,7 @@ def saveRequest(request): | |||
|     local_data.request = request | ||||
| 
 | ||||
| def getRequest(): | ||||
|     return local_data.request | ||||
|     try: | ||||
|         return local_data.request | ||||
|     except AttributeError: | ||||
|         return None | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 hplattner
						hplattner