1633 lines
74 KiB
Python
1633 lines
74 KiB
Python
'''This package contains mixin classes that are mixed in with generated classes:
|
|
- mixins/BaseMixin is mixed in with standard Zope classes;
|
|
- mixins/ToolMixin is mixed in with the generated application Tool class.'''
|
|
|
|
# ------------------------------------------------------------------------------
|
|
import os, os.path, re, sys, types, urllib, cgi
|
|
from appy import Object
|
|
from appy.px import Px
|
|
from appy.fields.workflow import UiTransition
|
|
import appy.gen as gen
|
|
from appy.gen.utils import *
|
|
from appy.gen.layout import Table, defaultPageLayouts
|
|
from appy.gen.descriptors import WorkflowDescriptor, ClassDescriptor
|
|
from appy.shared import utils as sutils
|
|
from appy.shared.data import rtlLanguages
|
|
from appy.shared.xml_parser import XmlMarshaller
|
|
from appy.shared.diff import HtmlDiff
|
|
|
|
# ------------------------------------------------------------------------------
|
|
NUMBERED_ID = re.compile('.+\d{4}$')
|
|
|
|
# ------------------------------------------------------------------------------
|
|
class BaseMixin:
|
|
'''Every Zope class generated by appy.gen inherits from this class or a
|
|
subclass of it.'''
|
|
_appy_meta_type = 'Class'
|
|
|
|
def get_o(self):
|
|
'''In some cases, we want the Zope object, we don't know if the current
|
|
object is a Zope or Appy object. By defining this property,
|
|
"someObject.o" produces always the Zope object, be someObject an Appy
|
|
or Zope object.'''
|
|
return self
|
|
o = property(get_o)
|
|
|
|
def getInitiatorInfo(self):
|
|
'''Gets information about a potential initiator object from the request.
|
|
Returns a 3-tuple (initiator, pageName, field):
|
|
* initiator is the initiator (Zope) object;
|
|
* pageName is the page on the initiator where the origin of the Ref
|
|
field lies;
|
|
* field is the Ref instance.
|
|
'''
|
|
rq = self.REQUEST
|
|
if not rq.get('nav', '').startswith('ref.'): return None, None, None
|
|
splitted = rq['nav'].split('.')
|
|
initiator = self.getTool().getObject(splitted[1])
|
|
fieldName, page = splitted[2].split(':')
|
|
return initiator, page, initiator.getAppyType(fieldName)
|
|
|
|
def createOrUpdate(self, created, values,
|
|
initiator=None, initiatorField=None):
|
|
'''This method creates (if p_created is True) or updates an object.
|
|
p_values are manipulated versions of those from the HTTP request.
|
|
In the case of an object creation from the web (p_created is True
|
|
and a REQUEST object is present), p_self is a temporary object
|
|
created in /temp_folder, and this method moves it at its "final"
|
|
place. In the case of an update, this method simply updates fields
|
|
of p_self.'''
|
|
rq = getattr(self, 'REQUEST', None)
|
|
obj = self
|
|
if created and rq:
|
|
# Create the final object and put it at the right place.
|
|
tool = self.getTool()
|
|
# The app may define a method klass.generateUid for producing an UID
|
|
# for instance of this class. If no such method is found, we use the
|
|
# standard Appy method to produce an UID.
|
|
id = None
|
|
klass = tool.getAppyClass(obj.portal_type)
|
|
if hasattr(klass, 'generateUid'):
|
|
id = klass.generateUid(obj.REQUEST)
|
|
if not id:
|
|
id = tool.generateUid(obj.portal_type)
|
|
if not initiator:
|
|
folder = tool.getPath('/data')
|
|
else:
|
|
folder = initiator.getCreateFolder()
|
|
# Check that the user can add objects through this Ref.
|
|
initiatorField.checkAdd(initiator)
|
|
obj = createObject(folder, id, obj.portal_type, tool.getAppName())
|
|
# Get the fields on the current page
|
|
fields = None
|
|
if rq: fields = self.getAppyTypes('edit', rq.get('page'))
|
|
# Remember the previous values of fields, for potential historization
|
|
previousData = None
|
|
if not created and fields:
|
|
previousData = obj.rememberPreviousData(fields)
|
|
# Perform the change on the object
|
|
if fields:
|
|
# Store in the database the new value coming from the form
|
|
for field in fields:
|
|
value = getattr(values, field.name, None)
|
|
field.store(obj, value)
|
|
if previousData:
|
|
# Keep in history potential changes on historized fields
|
|
obj.historizeData(previousData)
|
|
|
|
# Manage potential link with an initiator object
|
|
if created and initiator: initiator.appy().link(initiatorField.name,obj)
|
|
|
|
# Call the custom "onEdit" if available
|
|
msg = None # The message to display to the user. It can be set by onEdit
|
|
if obj.wrapperClass:
|
|
appyObject = obj.appy()
|
|
if hasattr(appyObject, 'onEdit'):
|
|
msg = appyObject.onEdit(created)
|
|
# Update last modification date
|
|
if not created:
|
|
from DateTime import DateTime
|
|
obj.modified = DateTime()
|
|
# Unlock the currently saved page on the object
|
|
if rq: self.removeLock(rq['page'])
|
|
obj.reindex()
|
|
return obj, msg
|
|
|
|
def updateField(self, name, value):
|
|
'''Updates a single field p_name with new p_value.'''
|
|
field = self.getAppyType(name)
|
|
# Remember previous value if the field is historized.
|
|
previousData = self.rememberPreviousData([field])
|
|
# Store the new value into the database
|
|
field.store(self, value)
|
|
# Update the object history when relevant
|
|
if previousData: self.historizeData(previousData)
|
|
# Update last modification date
|
|
from DateTime import DateTime
|
|
self.modified = DateTime()
|
|
|
|
def delete(self):
|
|
'''This method is self's suicide.'''
|
|
# Call a custom "onDelete" if it exists
|
|
appyObj = self.appy()
|
|
if hasattr(appyObj, 'onDelete'): appyObj.onDelete()
|
|
# Any people referencing me must forget me now
|
|
for field in self.getAllAppyTypes():
|
|
if field.type != 'Ref': continue
|
|
for obj in field.getValue(self):
|
|
field.back.unlinkObject(obj, self, back=True)
|
|
# Uncatalog the object
|
|
self.reindex(unindex=True)
|
|
# Delete the filesystem folder corresponding to this object
|
|
folder = os.path.join(*self.getFsFolder())
|
|
if os.path.exists(folder):
|
|
sutils.FolderDeleter.delete(folder)
|
|
sutils.FolderDeleter.deleteEmpty(os.path.dirname(folder))
|
|
# Delete the object
|
|
self.getParentNode().manage_delObjects([self.id])
|
|
|
|
def onDelete(self):
|
|
'''Called when an object deletion is triggered from the ui.'''
|
|
rq = self.REQUEST
|
|
self.delete()
|
|
if self.getUrl(rq['HTTP_REFERER'],mode='raw') ==self.getUrl(mode='raw'):
|
|
# We were consulting the object that has been deleted. Go back to
|
|
# the main page.
|
|
urlBack = self.getTool().getSiteUrl()
|
|
else:
|
|
urlBack = self.getUrl(rq['HTTP_REFERER'])
|
|
self.say(self.translate('action_done'))
|
|
self.goto(urlBack)
|
|
|
|
def onDeleteEvent(self):
|
|
'''Called when an event (from object history) deletion is triggered
|
|
from the ui.'''
|
|
rq = self.REQUEST
|
|
# Re-create object history, but without the event corresponding to
|
|
# rq['eventTime']
|
|
history = []
|
|
from DateTime import DateTime
|
|
eventToDelete = DateTime(rq['eventTime'])
|
|
key = self.workflow_history.keys()[0]
|
|
for event in self.workflow_history[key]:
|
|
if (event['action'] != '_datachange_') or \
|
|
(event['time'] != eventToDelete):
|
|
history.append(event)
|
|
self.workflow_history[key] = tuple(history)
|
|
appy = self.appy()
|
|
self.log('Data change event deleted by %s for %s (UID=%s).' % \
|
|
(appy.user.login, appy.klass.__name__, appy.uid))
|
|
self.goto(self.getUrl(rq['HTTP_REFERER']))
|
|
|
|
def onUnlink(self):
|
|
'''Called when an object unlinking is triggered from the ui.'''
|
|
rq = self.REQUEST
|
|
tool = self.getTool()
|
|
sourceObject = tool.getObject(rq['sourceUid'])
|
|
targetObject = tool.getObject(rq['targetUid'])
|
|
field = sourceObject.getAppyType(rq['fieldName'])
|
|
field.unlinkObject(sourceObject, targetObject)
|
|
urlBack = self.getUrl(rq['HTTP_REFERER'])
|
|
self.say(self.translate('action_done'))
|
|
self.goto(urlBack)
|
|
|
|
def onCreate(self):
|
|
'''This method is called when a user wants to create a root object in
|
|
the "data" folder or an object through a reference field. A temporary
|
|
object is created in /temp_folder and the edit page to it is
|
|
returned.'''
|
|
rq = self.REQUEST
|
|
className = rq.get('className')
|
|
# Create the params to add to the URL we will redirect the user to
|
|
# create the object.
|
|
urlParams = {'mode':'edit', 'page':'main', 'nav':''}
|
|
initiator, initiatorPage, initiatorField = self.getInitiatorInfo()
|
|
if initiator:
|
|
# The object to create will be linked to an initiator object through
|
|
# a Ref field. We create here a new navigation string with one more
|
|
# item, that will be the currently created item.
|
|
splitted = rq.get('nav').split('.')
|
|
splitted[-1] = splitted[-2] = str(int(splitted[-1])+1)
|
|
urlParams['nav'] = '.'.join(splitted)
|
|
# Check that the user can add objects through this Ref field
|
|
initiatorField.checkAdd(initiator)
|
|
# Create a temp object in /temp_folder
|
|
tool = self.getTool()
|
|
id = tool.generateUid(className)
|
|
appName = tool.getAppName()
|
|
obj = createObject(tool.getPath('/temp_folder'), id, className, appName)
|
|
return self.goto(obj.getUrl(**urlParams))
|
|
|
|
def getDbFolder(self):
|
|
'''Gets the folder, on the filesystem, where the database (Data.fs and
|
|
sub-folders) lies.'''
|
|
return os.path.dirname(self.getTool().getApp()._p_jar.db().getName())
|
|
|
|
def getFsFolder(self, create=False):
|
|
'''Gets the folder where binary files tied to this object will be stored
|
|
on the filesystem. If p_create is True and the folder does not exist,
|
|
it is created (together with potentially missing parent folders).
|
|
This folder is returned as a tuple (s_baseDbFolder, s_subPath).'''
|
|
objId = self.id
|
|
# Get the root folder where Data.fs lies.
|
|
dbFolder = self.getDbFolder()
|
|
# Build the list of path elements within this db folder.
|
|
path = []
|
|
inConfig = False
|
|
for elem in self.getPhysicalPath():
|
|
if not elem: continue
|
|
if elem == 'data': continue
|
|
if elem == 'config': inConfig = True
|
|
if not path or ((len(path) == 1) and inConfig):
|
|
# This object is at the root of the filesystem.
|
|
if NUMBERED_ID.match(elem):
|
|
path.append(elem[-4:])
|
|
path.append(elem)
|
|
# We are done if elem corresponds to the object id.
|
|
if elem == objId: break
|
|
path = os.sep.join(path)
|
|
if create:
|
|
fullPath = os.path.join(dbFolder, path)
|
|
if not os.path.exists(fullPath): os.makedirs(fullPath)
|
|
return dbFolder, path
|
|
|
|
def view(self):
|
|
'''Returns the view PX.'''
|
|
obj = self.appy()
|
|
return obj.pxView({'obj': obj, 'tool': obj.tool})
|
|
|
|
def edit(self):
|
|
'''Returns the edit PX.'''
|
|
obj = self.appy()
|
|
return obj.pxEdit({'obj': obj, 'tool': obj.tool})
|
|
|
|
def ajax(self):
|
|
'''Called via an Ajax request to render some PX whose name is in the
|
|
request.'''
|
|
obj = self.appy()
|
|
return obj.pxAjax({'obj': obj, 'tool': obj.tool})
|
|
|
|
def setLock(self, user, page):
|
|
'''A p_user edits a given p_page on this object: we will set a lock, to
|
|
prevent other users to edit this page at the same time.'''
|
|
if not hasattr(self.aq_base, 'locks'):
|
|
# Create the persistent mapping that will store the lock
|
|
# ~{s_page: (s_userId, DateTime_lockDate)}~
|
|
from persistent.mapping import PersistentMapping
|
|
self.locks = PersistentMapping()
|
|
# Raise an error is the page is already locked by someone else. If the
|
|
# page is already locked by the same user, we don't mind: he could have
|
|
# used back/forward buttons of its browser...
|
|
userId = user.login
|
|
if (page in self.locks) and (userId != self.locks[page][0]):
|
|
from AccessControl import Unauthorized
|
|
raise Unauthorized('This page is locked.')
|
|
# Set the lock
|
|
from DateTime import DateTime
|
|
self.locks[page] = (userId, DateTime())
|
|
|
|
def isLocked(self, user, page):
|
|
'''Is this page locked? If the page is locked by the same user, we don't
|
|
mind and consider the page as unlocked. If the page is locked, this
|
|
method returns the tuple (userId, lockDate).'''
|
|
if hasattr(self.aq_base, 'locks') and (page in self.locks):
|
|
if (user.login != self.locks[page][0]): return self.locks[page]
|
|
|
|
def removeLock(self, page, force=False):
|
|
'''Removes the lock on the current page. This happens:
|
|
- after the page has been saved: the lock must be released;
|
|
- or when an admin wants to force the deletion of a lock that was
|
|
left on p_page for too long (p_force=True).
|
|
'''
|
|
if page not in self.locks: return
|
|
# Raise an error if the user that saves changes is not the one that
|
|
# has locked the page (excepted if p_force is True)
|
|
if not force:
|
|
userId = self.getTool().getUser().login
|
|
if self.locks[page][0] != userId:
|
|
from AccessControl import Unauthorized
|
|
raise Unauthorized('This page was locked by someone else.')
|
|
# Remove the lock
|
|
del self.locks[page]
|
|
|
|
def removeMyLock(self, user, page):
|
|
'''If p_user has set a lock on p_page, this method removes it. This
|
|
method is called when the user that locked a page consults
|
|
pxView for this page. In this case, we consider that the user has
|
|
left the edit page in an unexpected way and we remove the lock.'''
|
|
if hasattr(self.aq_base, 'locks') and (page in self.locks) and \
|
|
(user.login == self.locks[page][0]):
|
|
del self.locks[page]
|
|
|
|
def onUnlock(self):
|
|
'''Called when an admin wants to remove a lock that was left for too
|
|
long by some user.'''
|
|
rq = self.REQUEST
|
|
tool = self.getTool()
|
|
obj = tool.getObject(rq['objectUid'])
|
|
obj.removeLock(rq['pageName'], force=True)
|
|
urlBack = self.getUrl(rq['HTTP_REFERER'])
|
|
self.say(self.translate('action_done'))
|
|
self.goto(urlBack)
|
|
|
|
def onCreateWithoutForm(self):
|
|
'''This method is called when a user wants to create a object from a
|
|
reference field, automatically (without displaying a form).'''
|
|
rq = self.REQUEST
|
|
self.appy().create(rq['fieldName'])
|
|
|
|
def intraFieldValidation(self, errors, values):
|
|
'''This method performs field-specific validation for every field from
|
|
the page that is being created or edited. For every field whose
|
|
validation generates an error, we add an entry in p_errors. For every
|
|
field, we add in p_values an entry with the "ready-to-store" field
|
|
value.'''
|
|
rq = self.REQUEST
|
|
for field in self.getAppyTypes('edit', rq.form.get('page')):
|
|
if not field.validable: continue
|
|
value = field.getRequestValue(rq)
|
|
message = field.validate(self, value)
|
|
if message:
|
|
setattr(errors, field.name, message)
|
|
else:
|
|
setattr(values, field.name, field.getStorableValue(value))
|
|
# Validate sub-fields within Lists
|
|
if field.type != 'List': continue
|
|
i = -1
|
|
for row in value:
|
|
i += 1
|
|
for name, subField in field.fields:
|
|
message = subField.validate(self, getattr(row,name,None))
|
|
if message:
|
|
setattr(errors, '%s*%d' % (subField.name, i), message)
|
|
|
|
def interFieldValidation(self, errors, values):
|
|
'''This method is called when individual validation of all fields
|
|
succeed (when editing or creating an object). Then, this method
|
|
performs inter-field validation. This way, the user must first
|
|
correct individual fields before being confronted to potential
|
|
inter-field validation errors.'''
|
|
obj = self.appy()
|
|
if not hasattr(obj, 'validate'): return
|
|
msg = obj.validate(values, errors)
|
|
# Those custom validation methods may have added fields in the given
|
|
# p_errors object. Within this object, for every error message that is
|
|
# not a string, we replace it with the standard validation error for the
|
|
# corresponding field.
|
|
for key, value in errors.__dict__.iteritems():
|
|
resValue = value
|
|
if not isinstance(resValue, basestring):
|
|
resValue = self.translate('field_invalid')
|
|
setattr(errors, key, resValue)
|
|
return msg
|
|
|
|
def onUpdate(self):
|
|
'''This method is executed when a user wants to update an object.
|
|
The object may be a temporary object created in /temp_folder.
|
|
In this case, the update consists in moving it to its "final" place.
|
|
If the object is not a temporary one, this method updates its
|
|
fields in the database.'''
|
|
rq = self.REQUEST
|
|
tool = self.getTool()
|
|
errorMessage = self.translate('validation_error')
|
|
isNew = self.isTemporary()
|
|
# If this object is created from an initiator, get info about him.
|
|
initiator, initiatorPage, initiatorField = self.getInitiatorInfo()
|
|
# If the user clicked on 'Cancel', go back to the previous page.
|
|
buttonClicked = rq.get('button')
|
|
if buttonClicked == 'cancel':
|
|
if initiator:
|
|
# Go back to the initiator page.
|
|
urlBack = initiator.getUrl(page=initiatorPage, nav='')
|
|
else:
|
|
if isNew:
|
|
# Go back to the root of the site.
|
|
urlBack = tool.getSiteUrl()
|
|
else:
|
|
urlBack = self.getUrl()
|
|
self.say(self.translate('object_canceled'))
|
|
self.removeLock(rq['page'])
|
|
return self.goto(urlBack)
|
|
|
|
# Object for storing validation errors
|
|
errors = Object()
|
|
# Object for storing the (converted) values from the request
|
|
values = Object()
|
|
|
|
# Trigger field-specific validation
|
|
self.intraFieldValidation(errors, values)
|
|
if errors.__dict__:
|
|
for k,v in errors.__dict__.iteritems(): rq.set('%s_error' % k, v)
|
|
self.say(errorMessage)
|
|
return self.gotoEdit()
|
|
|
|
# Trigger inter-field validation
|
|
msg = self.interFieldValidation(errors, values)
|
|
if not msg: msg = errorMessage
|
|
if errors.__dict__:
|
|
for k,v in errors.__dict__.iteritems(): rq.set('%s_error' % k, v)
|
|
self.say(msg)
|
|
return self.gotoEdit()
|
|
|
|
# Before saving data, must we ask a confirmation by the user ?
|
|
appyObj = self.appy()
|
|
saveConfirmed = rq.get('confirmed') == 'True'
|
|
if hasattr(appyObj, 'confirm') and not saveConfirmed:
|
|
msg = appyObj.confirm(values)
|
|
if msg:
|
|
rq.set('confirmMsg', msg.replace("'", "\\'"))
|
|
return self.gotoEdit()
|
|
|
|
# Create or update the object in the database
|
|
obj, msg = self.createOrUpdate(isNew, values, initiator, initiatorField)
|
|
|
|
# Redirect the user to the appropriate page
|
|
if not msg: msg = self.translate('object_saved')
|
|
# If the object has already been deleted (ie, it is a kind of transient
|
|
# object like a one-shot form and has already been deleted in method
|
|
# onEdit), redirect to the main site page.
|
|
if not getattr(obj.getParentNode().aq_base, obj.id, None):
|
|
return self.goto(tool.getSiteUrl(), msg)
|
|
# If the user can't access the object anymore, redirect him to the
|
|
# main site page.
|
|
if not obj.allows('read'):
|
|
return self.goto(tool.getSiteUrl(), msg)
|
|
if (buttonClicked == 'save') or saveConfirmed:
|
|
obj.say(msg)
|
|
if isNew and initiator:
|
|
return self.goto(initiator.getUrl(page=initiatorPage, nav=''))
|
|
else:
|
|
return self.goto(obj.getUrl())
|
|
if buttonClicked == 'previous':
|
|
# Go to the previous page for this object.
|
|
# We recompute the list of phases and pages because things
|
|
# may have changed since the object has been updated (ie,
|
|
# additional pages may be shown or hidden now, so the next and
|
|
# previous pages may have changed). Moreover, previous and next
|
|
# pages may not be available in "edit" mode, so we return the edit
|
|
# or view pages depending on page.show.
|
|
phaseObj = self.getAppyPhases(currentOnly=True, layoutType='edit')
|
|
pageName, pageInfo = phaseObj.getPreviousPage(rq['page'])
|
|
if pageName:
|
|
# Return to the edit or view page?
|
|
if pageInfo.showOnEdit:
|
|
rq.set('page', pageName)
|
|
# I do not use gotoEdit here because I really need to
|
|
# redirect the user to the edit page. Indeed, the object
|
|
# edit URL may have moved from temp_folder to another place.
|
|
return self.goto(obj.getUrl(mode='edit', page=pageName))
|
|
else:
|
|
return self.goto(obj.getUrl(page=pageName))
|
|
else:
|
|
obj.say(msg)
|
|
return self.goto(obj.getUrl())
|
|
if buttonClicked == 'next':
|
|
# Go to the next page for this object.
|
|
# We remember page name, because the next method may set a new
|
|
# current page if the current one is not visible anymore.
|
|
pageName = rq['page']
|
|
phaseObj = self.getAppyPhases(currentOnly=True, layoutType='edit')
|
|
pageName, pageInfo = phaseObj.getNextPage(pageName)
|
|
if pageName:
|
|
# Return to the edit or view page?
|
|
if pageInfo.showOnEdit:
|
|
# Same remark as above (click on "previous").
|
|
return self.goto(obj.getUrl(mode='edit', page=pageName))
|
|
else:
|
|
return self.goto(obj.getUrl(page=pageName))
|
|
else:
|
|
obj.say(msg)
|
|
return self.goto(obj.getUrl())
|
|
return obj.gotoEdit()
|
|
|
|
def reindex(self, indexes=None, unindex=False):
|
|
'''Reindexes this object the catalog. If names of indexes are specified
|
|
in p_indexes, recataloging is limited to those indexes. If p_unindex
|
|
is True, instead of cataloguing the object, it uncatalogs it.'''
|
|
path = '/'.join(self.getPhysicalPath())
|
|
catalog = self.getPhysicalRoot().catalog
|
|
if unindex:
|
|
catalog.uncatalog_object(path)
|
|
else:
|
|
if indexes:
|
|
catalog.catalog_object(self, path, idxs=indexes)
|
|
else:
|
|
# Get the list of indexes that apply on this object. Else, Zope
|
|
# will reindex all indexes defined in the catalog, and through
|
|
# acquisition, wrong methods can be called on wrong objects.
|
|
iNames = self.wrapperClass.getIndexes().keys()
|
|
catalog.catalog_object(self, path, idxs=iNames)
|
|
|
|
def xml(self, action=None):
|
|
'''If no p_action is defined, this method returns the XML version of
|
|
this object. Else, it calls method named p_action on the
|
|
corresponding Appy wrapper and returns, as XML, its result.'''
|
|
self.REQUEST.RESPONSE.setHeader('Content-Type','text/xml;charset=utf-8')
|
|
# Check if the user is allowed to consult this object
|
|
if not self.allows('read'):
|
|
return XmlMarshaller().marshall('Unauthorized')
|
|
if not action:
|
|
marshaller = XmlMarshaller(rootTag=self.getClass().__name__,
|
|
dumpUnicode=True)
|
|
res = marshaller.marshall(self, objectType='appy')
|
|
else:
|
|
appyObj = self.appy()
|
|
try:
|
|
methodRes = getattr(appyObj, action)()
|
|
if isinstance(methodRes, Px):
|
|
res = methodRes({'self': self.appy()})
|
|
elif isinstance(methodRes, file):
|
|
res = methodRes.read()
|
|
methodRes.close()
|
|
elif isinstance(methodRes, basestring) and \
|
|
methodRes.startswith('<?xml'): # Already XML
|
|
return methodRes
|
|
else:
|
|
res = XmlMarshaller().marshall(methodRes, objectType='appy')
|
|
except Exception, e:
|
|
tb = sutils.Traceback.get()
|
|
res = XmlMarshaller().marshall(tb, objectType='appy')
|
|
return res
|
|
|
|
def say(self, msg, type='info'):
|
|
'''Prints a p_msg in the user interface. p_logLevel may be "info",
|
|
"warning" or "error".'''
|
|
rq = self.REQUEST
|
|
if 'messages' not in rq.SESSION.keys():
|
|
plist = self.getProductConfig().PersistentList
|
|
messages = rq.SESSION['messages'] = plist()
|
|
else:
|
|
messages = rq.SESSION['messages']
|
|
messages.append( (type, msg) )
|
|
|
|
def log(self, msg, type='info'):
|
|
'''Logs a p_msg in the log file. p_logLevel may be "info", "warning"
|
|
or "error".'''
|
|
logger = self.getProductConfig().logger
|
|
if type == 'warning': logMethod = logger.warn
|
|
elif type == 'error': logMethod = logger.error
|
|
else: logMethod = logger.info
|
|
logMethod(msg)
|
|
|
|
def do(self):
|
|
'''Performs some action from the user interface.'''
|
|
rq = self.REQUEST
|
|
action = rq['action']
|
|
if rq.get('objectUid', None):
|
|
obj = self.getTool().getObject(rq['objectUid'])
|
|
else:
|
|
obj = self
|
|
if rq.get('appy', None) == '1': obj = obj.appy()
|
|
return getattr(obj, 'on'+action)()
|
|
|
|
def rememberPreviousData(self, fields):
|
|
'''This method is called before updating an object and remembers, for
|
|
every historized field, the previous value. Result is a dict
|
|
~{s_fieldName: previousFieldValue}~'''
|
|
res = {}
|
|
for field in fields:
|
|
if not field.historized: continue
|
|
# appyType.historized can be a method or a boolean.
|
|
if callable(field.historized):
|
|
historized = field.callMethod(self, field.historized)
|
|
else:
|
|
historized = field.historized
|
|
if historized:
|
|
res[field.name] = field.getValue(self)
|
|
return res
|
|
|
|
def addHistoryEvent(self, action, **kw):
|
|
'''Adds an event in the object history.'''
|
|
user = self.getTool().getUser()
|
|
userId = user and user.login or 'system'
|
|
from DateTime import DateTime
|
|
event = {'action': action, 'actor': userId, 'time': DateTime(),
|
|
'comments': ''}
|
|
event.update(kw)
|
|
if 'review_state' not in event: event['review_state'] = self.State()
|
|
# Add the event to the history
|
|
histKey = self.workflow_history.keys()[0]
|
|
self.workflow_history[histKey] += (event,)
|
|
|
|
def addDataChange(self, changes, notForPreviouslyEmptyValues=False):
|
|
'''This method allows to add "manually" a data change into the objet's
|
|
history. Indeed, data changes are "automatically" recorded only when
|
|
a HTTP form is uploaded, not if, in the code, a setter is called on
|
|
a field. The method is also called by m_historizeData below, that
|
|
performs "automatic" recording when a HTTP form is uploaded. Field
|
|
changes for which the previous value was empty are not recorded into
|
|
the history if p_notForPreviouslyEmptyValues is True.'''
|
|
# Add to the p_changes dict the field labels
|
|
for fieldName in changes.keys():
|
|
appyType = self.getAppyType(fieldName)
|
|
if notForPreviouslyEmptyValues and \
|
|
appyType.isEmptyValue(changes[fieldName], self):
|
|
del changes[fieldName]
|
|
else:
|
|
changes[fieldName] = (changes[fieldName], appyType.labelId)
|
|
# Add an event in the history
|
|
self.addHistoryEvent('_datachange_', changes=changes)
|
|
|
|
def historizeData(self, previousData):
|
|
'''Records in the object history potential changes on historized fields.
|
|
p_previousData contains the values, before an update, of the
|
|
historized fields, while p_self already contains the (potentially)
|
|
modified values.'''
|
|
# Remove from previousData all values that were not changed
|
|
for field in previousData.keys():
|
|
prev = previousData[field]
|
|
appyType = self.getAppyType(field)
|
|
curr = appyType.getValue(self)
|
|
try:
|
|
if (prev == curr) or ((prev == None) and (curr == '')) or \
|
|
((prev == '') and (curr == None)):
|
|
del previousData[field]
|
|
except UnicodeDecodeError, ude:
|
|
# The string comparisons above may imply silent encoding-related
|
|
# conversions that may produce this exception.
|
|
pass
|
|
if (appyType.type == 'Ref') and (field in previousData):
|
|
previousData[field] = [r.title for r in previousData[field]]
|
|
if previousData:
|
|
self.addDataChange(previousData)
|
|
|
|
def goto(self, url, msg=None):
|
|
'''Brings the user to some p_url after an action has been executed.'''
|
|
if msg: self.say(msg)
|
|
return self.REQUEST.RESPONSE.redirect(url)
|
|
|
|
def gotoEdit(self):
|
|
'''Brings the user to the edit page for this object. This method takes
|
|
care of not carrying any password value. Unlike m_goto above, there
|
|
is no HTTP redirect here: we execute directly PX "edit" and we
|
|
return the result.'''
|
|
page = self.REQUEST.get('page', 'main')
|
|
for field in self.getAppyTypes('edit', page):
|
|
if (field.type == 'String') and (field.format in (3,4)):
|
|
self.REQUEST.set(field.name, '')
|
|
return self.edit()
|
|
|
|
def showField(self, name, layoutType='view'):
|
|
'''Must I show field named p_name on this p_layoutType ?'''
|
|
return self.getAppyType(name).isShowable(self, layoutType)
|
|
|
|
def getMethod(self, methodName):
|
|
'''Returns the method named p_methodName.'''
|
|
# If I write "self.aq_base" instead of self, acquisition will be
|
|
# broken on returned object.
|
|
return getattr(self, methodName, None)
|
|
|
|
def getCreateFolder(self):
|
|
'''When an object must be created from this one through a Ref field, we
|
|
must know where to put the newly create object: within this one if it
|
|
is folderish, besides this one in its parent else.
|
|
'''
|
|
if self.isPrincipiaFolderish: return self
|
|
return self.getParentNode()
|
|
|
|
def getFieldValue(self, name, layoutType=None, outerValue=None):
|
|
'''Returns the database value of field named p_name for p_self.'''
|
|
if layoutType == 'search': return # No object in search screens.
|
|
field = self.getAppyType(name)
|
|
if field.type == 'Pod': return
|
|
if '*' not in name: return field.getValue(self)
|
|
# The field is an inner field from a List.
|
|
listName, name, i = name.split('*')
|
|
listType = self.getAppyType(listName)
|
|
return listType.getInnerValue(self, outerValue, name, int(i))
|
|
|
|
def getRequestFieldValue(self, name):
|
|
'''Gets the value of field p_name as may be present in the request.'''
|
|
# Return the request value for standard fields.
|
|
if '*' not in name:
|
|
return self.getAppyType(name).getRequestValue(self.REQUEST)
|
|
# For sub-fields within Lists, the corresponding request values have
|
|
# already been computed in the request key corresponding to the whole
|
|
# List.
|
|
listName, name, rowIndex = name.split('*')
|
|
rowIndex = int(rowIndex)
|
|
if rowIndex == -1: return ''
|
|
allValues = self.REQUEST.get(listName)
|
|
if not allValues: return ''
|
|
return getattr(allValues[rowIndex], name, '')
|
|
|
|
def isDebug(self):
|
|
'''Are we in debug mode ?'''
|
|
for arg in sys.argv:
|
|
if arg == 'debug-mode=on': return True
|
|
|
|
def getClass(self, reloaded=False):
|
|
'''Returns the Appy class that dictates self's behaviour.'''
|
|
if not reloaded:
|
|
return self.getTool().getAppyClass(self.__class__.__name__)
|
|
else:
|
|
klass = self.appy().klass
|
|
moduleName = klass.__module__
|
|
exec 'import %s' % moduleName
|
|
exec 'reload(%s)' % moduleName
|
|
exec 'res = %s.%s' % (moduleName, klass.__name__)
|
|
# More manipulations may have occurred in m_update
|
|
if hasattr(res, 'update'):
|
|
parentName= res.__bases__[-1].__name__
|
|
moduleName= 'Products.%s.wrappers' % self.getTool().getAppName()
|
|
exec 'import %s' % moduleName
|
|
exec 'parent = %s.%s' % (moduleName, parentName)
|
|
res.update(parent)
|
|
return res
|
|
|
|
def getAppyType(self, name, className=None):
|
|
'''Returns the Appy type named p_name. If no p_className is defined, the
|
|
field is supposed to belong to self's class.'''
|
|
isInnerType = '*' in name # An inner type lies within a List type.
|
|
subName = None
|
|
if isInnerType:
|
|
elems = name.split('*')
|
|
if len(elems) == 2: name, subName = elems
|
|
else: name, subName, i = elems
|
|
if not className:
|
|
klass = self.__class__.wrapperClass
|
|
else:
|
|
klass = self.getTool().getAppyClass(className, wrapper=True)
|
|
res = getattr(klass, name, None)
|
|
if res and isInnerType: res = res.getField(subName)
|
|
return res
|
|
|
|
def getAllAppyTypes(self, className=None):
|
|
'''Returns the ordered list of all Appy types for self's class if
|
|
p_className is not specified, or for p_className else.'''
|
|
if not className:
|
|
klass = self.__class__.wrapperClass
|
|
else:
|
|
klass = self.getTool().getAppyClass(className, wrapper=True)
|
|
return klass.__fields__
|
|
|
|
def getGroupedFields(self, layoutType, pageName, cssJs=None):
|
|
'''Returns the fields sorted by group. If a dict is given in p_cssJs,
|
|
we will add it in the css and js files required by the fields.'''
|
|
res = []
|
|
groups = {} # The already encountered groups.
|
|
# If a dict is given in p_cssJs, we must fill it with the CSS and JS
|
|
# files required for every returned field.
|
|
collectCssJs = isinstance(cssJs, dict)
|
|
css = js = None
|
|
# If param "refresh" is there, we must reload the Python class
|
|
refresh = ('refresh' in self.REQUEST)
|
|
if refresh:
|
|
klass = self.getClass(reloaded=True)
|
|
for field in self.getAllAppyTypes():
|
|
if refresh: field = field.reload(klass, self)
|
|
if field.page.name != pageName: continue
|
|
if not field.isShowable(self, layoutType): continue
|
|
if collectCssJs:
|
|
if css == None: css = []
|
|
field.getCss(layoutType, css)
|
|
if js == None: js = []
|
|
field.getJs(layoutType, js)
|
|
if not field.group:
|
|
res.append(field)
|
|
else:
|
|
# Insert the UiGroup instance corresponding to field.group.
|
|
uiGroup = field.group.insertInto(res, groups, field.page,
|
|
self.meta_type)
|
|
uiGroup.addElement(field)
|
|
if collectCssJs:
|
|
cssJs['css'] = css or ()
|
|
cssJs['js'] = js or ()
|
|
return res
|
|
|
|
def getAppyTypes(self, layoutType, pageName):
|
|
'''Returns the list of fields that belong to a given page (p_pageName)
|
|
for a given p_layoutType. If p_pageName is None, fields of all pages
|
|
are returned.'''
|
|
res = []
|
|
for field in self.getAllAppyTypes():
|
|
if pageName and (field.page.name != pageName): continue
|
|
if not field.isShowable(self, layoutType): continue
|
|
res.append(field)
|
|
return res
|
|
|
|
def getSlavesRequestInfo(self, pageName):
|
|
'''When slave fields must be updated via Ajax requests, we must carry
|
|
some information from the global request object to the ajax requests:
|
|
- the selected values in slave fields;
|
|
- validation errors.'''
|
|
requestValues = {}
|
|
errors = {}
|
|
req = self.REQUEST
|
|
for field in self.getAllAppyTypes():
|
|
if field.page.name != pageName: continue
|
|
if field.masterValue and callable(field.masterValue):
|
|
# We have a slave field that is updated via ajax requests.
|
|
name = field.name
|
|
# Remember the request value for this field if present.
|
|
if req.has_key(name) and req[name]:
|
|
requestValues[name] = req[name]
|
|
# Remember the validation error for this field if present.
|
|
errorKey = '%s_error' % name
|
|
if req.has_key(errorKey):
|
|
errors[name] = req[errorKey]
|
|
return sutils.getStringDict(requestValues), sutils.getStringDict(errors)
|
|
|
|
def getCssJs(self, fields, layoutType, res):
|
|
'''Gets, in p_res ~{'css':[s_css], 'js':[s_js]}~ the lists of
|
|
Javascript and CSS files required by Appy types p_fields when shown
|
|
on p_layoutType.'''
|
|
# Lists css and js below are not sets, because order of Javascript
|
|
# inclusion can be important, and this could be losed by using sets.
|
|
css = []
|
|
js = []
|
|
for field in fields:
|
|
field.getCss(layoutType, css)
|
|
field.getJs(layoutType, js)
|
|
res['css'] = css
|
|
res['js'] = js
|
|
|
|
def getCssFor(self, elem):
|
|
'''Gets the name of the CSS class to use for styling some p_elem. If
|
|
self's class does not define a dict "styles", the defaut CSS class
|
|
to use will be named p_elem.'''
|
|
klass = self.getClass()
|
|
if hasattr(klass, 'styles') and (elem in klass.styles):
|
|
return klass.styles[elem]
|
|
return elem
|
|
|
|
def getTransitions(self, includeFake=True, includeNotShowable=False,
|
|
grouped=True):
|
|
'''This method returns info about transitions (as UiTransition
|
|
instances) that one can trigger from the user interface.
|
|
* if p_includeFake is True, it retrieves transitions that the user
|
|
can't trigger, but for which he needs to know for what reason he
|
|
can't trigger it;
|
|
* if p_includeNotShowable is True, it includes transitions for which
|
|
show=False. Indeed, because "showability" is only a GUI concern,
|
|
and not a security concern, in some cases it has sense to set
|
|
includeNotShowable=True, because those transitions are triggerable
|
|
from a security point of view.
|
|
* If p_grouped is True, transitions are grouped according to their
|
|
"group" attribute, in a similar way to fields or searches.
|
|
'''
|
|
res = []
|
|
groups = {} # The already encountered groups of transitions.
|
|
wfPage = gen.Page('workflow')
|
|
wf = self.getWorkflow()
|
|
currentState = self.State(name=False)
|
|
# Loop on every transition
|
|
for name in dir(wf):
|
|
transition = getattr(wf, name)
|
|
if (transition.__class__.__name__ != 'Transition'): continue
|
|
# Filter transitions that do not have currentState as start state
|
|
if not transition.hasState(currentState, True): continue
|
|
# Check if the transition can be triggered
|
|
mayTrigger = transition.isTriggerable(self, wf)
|
|
# Compute the condition that will lead to including or not this
|
|
# transition
|
|
if not includeFake:
|
|
includeIt = mayTrigger
|
|
else:
|
|
includeIt = mayTrigger or isinstance(mayTrigger, gen.No)
|
|
if not includeNotShowable:
|
|
includeIt = includeIt and transition.isShowable(wf, self)
|
|
if not includeIt: continue
|
|
# Create the UiTransition instance.
|
|
info = UiTransition(name, transition, self, mayTrigger)
|
|
# Add the transition into the result.
|
|
if not transition.group or not grouped:
|
|
res.append(info)
|
|
else:
|
|
# Insert the UiGroup instance corresponding to transition.group.
|
|
uiGroup = transition.group.insertInto(res, groups, wfPage,
|
|
self.__class__.__name__, content='transitions')
|
|
uiGroup.addElement(info)
|
|
return res
|
|
|
|
def getAppyPhases(self, currentOnly=False, layoutType='view'):
|
|
'''Gets the list of phases that are defined for this content type. If
|
|
p_currentOnly is True, the search is limited to the phase where the
|
|
current page (as defined in the request) lies.'''
|
|
# Get the list of phases
|
|
res = [] # Ordered list of phases
|
|
phases = {} # Dict of phases
|
|
for field in self.getAllAppyTypes():
|
|
fieldPhase = field.page.phase
|
|
if fieldPhase not in phases:
|
|
phase = gen.Phase(fieldPhase, self)
|
|
res.append(phase)
|
|
phases[fieldPhase] = phase
|
|
else:
|
|
phase = phases[fieldPhase]
|
|
phase.addPage(field, self, layoutType)
|
|
if (field.type == 'Ref') and field.navigable:
|
|
phase.addPageLinks(field, self)
|
|
# Remove phases that have no visible page
|
|
for i in range(len(res)-1, -1, -1):
|
|
if not res[i].pages:
|
|
del phases[res[i].name]
|
|
del res[i]
|
|
# Compute next/previous phases of every phase
|
|
for ph in phases.itervalues():
|
|
ph.computeNextPrevious(res)
|
|
ph.totalNbOfPhases = len(res)
|
|
# Restrict the result to the current phase if required
|
|
if currentOnly:
|
|
rq = self.REQUEST
|
|
page = rq.get('page', None)
|
|
if not page:
|
|
if layoutType == 'edit': page = self.getDefaultEditPage()
|
|
else: page = self.getDefaultViewPage()
|
|
for phase in res:
|
|
if page in phase.pages:
|
|
return phase
|
|
# If I am here, it means that the page as defined in the request,
|
|
# or the default page, is not existing nor visible in any phase.
|
|
# In this case I find the first visible page among all phases.
|
|
viewAttr = 'showOn%s' % layoutType.capitalize()
|
|
for phase in res:
|
|
for page in phase.pages:
|
|
if getattr(phase.pagesInfo[page], viewAttr):
|
|
rq.set('page', page)
|
|
pageFound = True
|
|
break
|
|
return phase
|
|
else:
|
|
# Return an empty list if we have a single, link-free page within
|
|
# a single phase.
|
|
if (len(res) == 1) and (len(res[0].pages) == 1) and \
|
|
not res[0].pagesInfo[res[0].pages[0]].links:
|
|
return
|
|
return res
|
|
|
|
def getSupTitle(self, navInfo=''):
|
|
'''Gets the html code (icons,...) that can be shown besides the title
|
|
of an object.'''
|
|
appyObj = self.appy()
|
|
if hasattr(appyObj, 'getSupTitle'): return appyObj.getSupTitle(navInfo)
|
|
return ''
|
|
|
|
def getSubTitle(self):
|
|
'''Gets the content that must appear below the title of an object.'''
|
|
appyObj = self.appy()
|
|
if hasattr(appyObj, 'getSubTitle'): return appyObj.getSubTitle()
|
|
return ''
|
|
|
|
def notifyWorkflowCreated(self):
|
|
'''This method is called every time an object is created, be it temp or
|
|
not. The objective here is to initialise workflow-related data on
|
|
the object.'''
|
|
wf = self.getWorkflow()
|
|
# Get the initial workflow state
|
|
initialState = self.State(name=False)
|
|
# Create a Transition instance representing the initial transition.
|
|
initialTransition = gen.Transition((initialState, initialState))
|
|
initialTransition.trigger('_init_', self, wf, '')
|
|
|
|
def getWorkflow(self, name=False, className=None):
|
|
'''Returns the workflow applicable for p_self (or for any instance of
|
|
p_className if given), or its name, if p_name is True.'''
|
|
if not className:
|
|
wrapperClass = self.wrapperClass
|
|
else:
|
|
wrapperClass = self.getTool().getAppyClass(className, wrapper=True)
|
|
wf = wrapperClass.getWorkflow()
|
|
if not name: return wf
|
|
return WorkflowDescriptor.getWorkflowName(wf)
|
|
|
|
def getWorkflowLabel(self, stateName=None):
|
|
'''Gets the i18n label for p_stateName, or for the current object state
|
|
if p_stateName is not given. Note that if p_stateName is given, it
|
|
can also represent the name of a transition.'''
|
|
stateName = stateName or self.State()
|
|
return '%s_%s' % (self.getWorkflow(name=True), stateName)
|
|
|
|
def applyUserIdChange(self, oldId, newId):
|
|
'''A user whose ID was p_oldId has now p_newId. If the old ID was
|
|
mentioned in self's local roles, update it to the new ID. This
|
|
method returns 1 if a change occurred, 0 else.'''
|
|
if oldId in self.__ac_local_roles__:
|
|
localRoles = self.__ac_local_roles__.copy()
|
|
localRoles[newId] = localRoles[oldId]
|
|
del localRoles[oldId]
|
|
self.__ac_local_roles__ = localRoles
|
|
self.reindex()
|
|
return 1
|
|
return 0
|
|
|
|
def findNewValue(self, field, history, stopIndex):
|
|
'''This function tries to find a more recent version of value of p_field
|
|
on p_self. It first tries to find it in history[:stopIndex+1]. If
|
|
it does not find it there, it returns the current value on p_obj.'''
|
|
i = stopIndex + 1
|
|
while (i-1) >= 0:
|
|
i -= 1
|
|
if history[i]['action'] != '_datachange_': continue
|
|
if field.name not in history[i]['changes']: continue
|
|
# We have found it!
|
|
return history[i]['changes'][field.name][0] or ''
|
|
return field.getValue(self) or ''
|
|
|
|
def getHistoryTexts(self, event):
|
|
'''Returns a tuple (insertText, deleteText) containing texts to show on,
|
|
respectively, inserted and deleted chunks of text in a XHTML diff.'''
|
|
tool = self.getTool()
|
|
userName = tool.getUserName(event['actor'])
|
|
mapping = {'userName': userName.decode('utf-8')}
|
|
res = []
|
|
for type in ('insert', 'delete'):
|
|
msg = self.translate('history_%s' % type, mapping=mapping)
|
|
date = tool.formatDate(event['time'], withHour=True)
|
|
msg = '%s: %s' % (date, msg)
|
|
res.append(msg.encode('utf-8'))
|
|
return res
|
|
|
|
def hasHistory(self, fieldName=None):
|
|
'''Has this object an history? If p_fieldName is specified, the question
|
|
becomes: has this object an history for field p_fieldName?'''
|
|
if hasattr(self.aq_base, 'workflow_history') and self.workflow_history:
|
|
history = self.workflow_history.values()[0]
|
|
if not fieldName:
|
|
for event in history:
|
|
if event['action'] and (event['comments'] != '_invisible_'):
|
|
return True
|
|
else:
|
|
for event in history:
|
|
if (event['action'] == '_datachange_') and \
|
|
(fieldName in event['changes']) and \
|
|
event['changes'][fieldName][0]: return True
|
|
|
|
def getHistory(self, startNumber=0, reverse=True, includeInvisible=False,
|
|
batchSize=5):
|
|
'''Returns the history for this object, sorted in reverse order (most
|
|
recent change first) if p_reverse is True.'''
|
|
# Get a copy of the history, reversed if needed, whose invisible events
|
|
# have been removed if needed.
|
|
key = self.workflow_history.keys()[0]
|
|
history = list(self.workflow_history[key][1:])
|
|
if not includeInvisible:
|
|
history = [e for e in history if e['comments'] != '_invisible_']
|
|
if reverse: history.reverse()
|
|
# Keep only events which are within the batch.
|
|
res = []
|
|
stopIndex = startNumber + batchSize - 1
|
|
i = -1
|
|
while (i+1) < len(history):
|
|
i += 1
|
|
# Ignore events outside range startNumber:startNumber+batchSize
|
|
if i < startNumber: continue
|
|
if i > stopIndex: break
|
|
if history[i]['action'] == '_datachange_':
|
|
# Take a copy of the event: we will modify it and replace
|
|
# fields' old values by their formatted counterparts.
|
|
event = history[i].copy()
|
|
event['changes'] = {}
|
|
for name, oldValue in history[i]['changes'].iteritems():
|
|
# oldValue is a tuple (value, fieldName).
|
|
field = self.getAppyType(name)
|
|
# Field 'name' may not exist, if the history has been
|
|
# transferred from another site. In this case we can't show
|
|
# this data change.
|
|
if not field: continue
|
|
if (field.type == 'String') and \
|
|
(field.format == gen.String.XHTML):
|
|
# For rich text fields, instead of simply showing the
|
|
# previous value, we propose a diff with the next
|
|
# version, excepted if the previous value is empty.
|
|
if field.isEmptyValue(oldValue[0]):
|
|
val = '-'
|
|
else:
|
|
newValue = self.findNewValue(field, history, i-1)
|
|
# Compute the diff between oldValue and newValue
|
|
iMsg, dMsg = self.getHistoryTexts(event)
|
|
comparator= HtmlDiff(oldValue[0],newValue,iMsg,dMsg)
|
|
val = comparator.get()
|
|
event['changes'][name] = (val, oldValue[1])
|
|
else:
|
|
val = field.getFormattedValue(self, oldValue[0]) or '-'
|
|
if isinstance(val, list) or isinstance(val, tuple):
|
|
val = '<ul>%s</ul>' % \
|
|
''.join(['<li>%s</li>' % v for v in val])
|
|
event['changes'][name] = (val, oldValue[1])
|
|
else:
|
|
event = history[i]
|
|
res.append(event)
|
|
return Object(events=res, totalNumber=len(history))
|
|
|
|
def mayNavigate(self):
|
|
'''May the currently logged user see the navigation panel linked to
|
|
this object?'''
|
|
appyObj = self.appy()
|
|
if hasattr(appyObj, 'mayNavigate'): return appyObj.mayNavigate()
|
|
return True
|
|
|
|
def getDefaultViewPage(self):
|
|
'''Which view page must be shown by default?'''
|
|
appyObj = self.appy()
|
|
if hasattr(appyObj, 'getDefaultViewPage'):
|
|
return appyObj.getDefaultViewPage()
|
|
return 'main'
|
|
|
|
def getDefaultEditPage(self):
|
|
'''Which edit page must be shown by default?'''
|
|
appyObj = self.appy()
|
|
if hasattr(appyObj, 'getDefaultEditPage'):
|
|
return appyObj.getDefaultEditPage()
|
|
return 'main'
|
|
|
|
def mayAct(self):
|
|
'''May the currently logged user see column "actions" for this
|
|
object? This can be used for hiding the "edit" icon, for example:
|
|
when a user may edit only a restricted set of fields on an object,
|
|
we may avoid showing him the global "edit" icon.'''
|
|
appyObj = self.appy()
|
|
if hasattr(appyObj, 'mayAct'): return appyObj.mayAct()
|
|
return True
|
|
|
|
def mayDelete(self):
|
|
'''May the currently logged user delete this object?'''
|
|
res = self.allows('delete')
|
|
if not res: return
|
|
# An additional, user-defined condition, may refine the base permission.
|
|
appyObj = self.appy()
|
|
if hasattr(appyObj, 'mayDelete'): return appyObj.mayDelete()
|
|
return True
|
|
|
|
def mayEdit(self, permission='write'):
|
|
'''May the currently logged user edit this object? p_perm can be a
|
|
field-specific permission.'''
|
|
res = self.allows(permission)
|
|
if not res: return
|
|
# An additional, user-defined condition, may refine the base permission.
|
|
appyObj = self.appy()
|
|
if hasattr(appyObj, 'mayEdit'): return appyObj.mayEdit()
|
|
return True
|
|
|
|
def executeAppyAction(self, actionName, reindex=True):
|
|
'''Executes action with p_fieldName on this object.'''
|
|
appyType = self.getAppyType(actionName)
|
|
actionRes = appyType(self.appy())
|
|
parent = self.getParentNode()
|
|
parentAq = getattr(parent, 'aq_base', parent)
|
|
if not hasattr(parentAq, self.id):
|
|
# Else, it means that the action has led to self's deletion.
|
|
self.reindex()
|
|
return appyType.result, actionRes
|
|
|
|
def onExecuteAppyAction(self):
|
|
'''This method is called every time a user wants to execute an Appy
|
|
action on an object.'''
|
|
rq = self.REQUEST
|
|
resultType, actionResult = self.executeAppyAction(rq['fieldName'])
|
|
successfull, msg = actionResult
|
|
if not msg:
|
|
# Use the default i18n messages
|
|
suffix = 'ko'
|
|
if successfull: suffix = 'ok'
|
|
msg = self.translate('action_%s' % suffix)
|
|
if (resultType == 'computation') or not successfull:
|
|
self.say(msg)
|
|
return self.goto(self.getUrl(rq['HTTP_REFERER']))
|
|
elif resultType.startswith('file'):
|
|
# msg does not contain a message, but a file instance.
|
|
response = self.REQUEST.RESPONSE
|
|
response.setHeader('Content-Type', sutils.getMimeType(msg.name))
|
|
response.setHeader('Content-Disposition', 'inline;filename="%s"' %\
|
|
os.path.basename(msg.name))
|
|
response.write(msg.read())
|
|
msg.close()
|
|
if resultType == 'filetmp':
|
|
# p_msg is a temp file. We need to delete it.
|
|
try:
|
|
os.remove(msg.name)
|
|
self.log('Temp file "%s" was deleted.' % msg.name)
|
|
except IOError, err:
|
|
self.log('Could not remove temp "%s" (%s).' % \
|
|
(msg.name, str(err)), type='warning')
|
|
except OSError, err:
|
|
self.log('Could not remove temp "%s" (%s).' % \
|
|
(msg.name, str(err)), type='warning')
|
|
elif resultType == 'redirect':
|
|
# msg does not contain a message, but the URL where to redirect
|
|
# the user.
|
|
return self.goto(msg)
|
|
|
|
def trigger(self, transitionName, comment='', doAction=True, doNotify=True,
|
|
doHistory=True, doSay=True, noSecurity=False):
|
|
'''Triggers transition named p_transitionName.'''
|
|
# Check that this transition exists.
|
|
wf = self.getWorkflow()
|
|
if not hasattr(wf, transitionName) or \
|
|
getattr(wf, transitionName).__class__.__name__ != 'Transition':
|
|
raise 'Transition "%s" was not found.' % transitionName
|
|
# Is this transition triggerable?
|
|
transition = getattr(wf, transitionName)
|
|
if not transition.isTriggerable(self, wf, noSecurity=noSecurity):
|
|
raise 'Transition "%s" can\'t be triggered.' % transitionName
|
|
# Trigger the transition
|
|
transition.trigger(transitionName, self, wf, comment, doAction=doAction,
|
|
doNotify=doNotify, doHistory=doHistory, doSay=doSay)
|
|
|
|
def onTrigger(self):
|
|
'''This method is called whenever a user wants to trigger a workflow
|
|
transition on an object.'''
|
|
rq = self.REQUEST
|
|
self.trigger(rq['workflow_action'], comment=rq.get('comment', ''))
|
|
self.reindex()
|
|
return self.goto(self.getUrl(rq['HTTP_REFERER']))
|
|
|
|
def getRolesFor(self, permission):
|
|
'''Gets, according to the workflow, the roles that are currently granted
|
|
p_permission on this object.'''
|
|
state = self.State(name=False)
|
|
roles = state.permissions[permission]
|
|
if roles: return [role.name for role in roles]
|
|
return ()
|
|
|
|
def appy(self):
|
|
'''Returns a wrapper object allowing to manipulate p_self the Appy
|
|
way.'''
|
|
# Create the dict for storing Appy wrapper on the REQUEST if needed.
|
|
rq = getattr(self, 'REQUEST', None)
|
|
if not rq:
|
|
# We are in test mode or Zope is starting. Use static variable
|
|
# config.fakeRequest instead.
|
|
rq = self.getProductConfig().fakeRequest
|
|
if not hasattr(rq, 'wrappers'): rq.wrappers = {}
|
|
# Return the Appy wrapper if already present in the cache
|
|
uid = self.UID()
|
|
if uid in rq.wrappers: return rq.wrappers[uid]
|
|
# Create the Appy wrapper, cache it in rq.wrappers and return it
|
|
wrapper = self.wrapperClass(self)
|
|
rq.wrappers[uid] = wrapper
|
|
return wrapper
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Methods for computing values of standard Appy indexes
|
|
# --------------------------------------------------------------------------
|
|
def UID(self):
|
|
'''Returns the unique identifier for this object.'''
|
|
return self._at_uid
|
|
|
|
def Title(self):
|
|
'''Returns the title for this object.'''
|
|
title = self.getAppyType('title')
|
|
if title: return title.getValue(self)
|
|
return self.id
|
|
|
|
def SortableTitle(self):
|
|
'''Returns the title as must be stored in index "SortableTitle".'''
|
|
return sutils.normalizeText(self.Title())
|
|
|
|
def SearchableText(self):
|
|
'''This method concatenates the content of every field with
|
|
searchable=True for indexing purposes.'''
|
|
res = []
|
|
for field in self.getAllAppyTypes():
|
|
if not field.searchable: continue
|
|
res.append(field.getIndexValue(self, forSearch=True))
|
|
return res
|
|
|
|
def Creator(self):
|
|
'''Who create this object?'''
|
|
return self.creator
|
|
|
|
def Created(self):
|
|
'''When was this object created ?'''
|
|
return self.created
|
|
|
|
def Modified(self):
|
|
'''When was this object last modified ?'''
|
|
if hasattr(self.aq_base, 'modified'): return self.modified
|
|
return self.created
|
|
|
|
def State(self, name=True, initial=False):
|
|
'''Returns information about the current object state. If p_name is
|
|
True, the returned info is the state name. Else, it is the State
|
|
instance. If p_initial is True, instead of returning info about the
|
|
current state, it returns info about the workflow initial state.'''
|
|
wf = self.getWorkflow()
|
|
if initial or not hasattr(self.aq_base, 'workflow_history'):
|
|
# No workflow information is available (yet) on this object, or
|
|
# initial state is asked. In both cases, return info about this
|
|
# initial state.
|
|
res = 'active'
|
|
for elem in dir(wf):
|
|
attr = getattr(wf, elem)
|
|
if (attr.__class__.__name__ == 'State') and attr.initial:
|
|
res = elem
|
|
break
|
|
else:
|
|
# Return info about the current object state
|
|
key = self.workflow_history.keys()[0]
|
|
res = self.workflow_history[key][-1]['review_state']
|
|
# Return state name or state definition?
|
|
if name: return res
|
|
else: return getattr(wf, res)
|
|
|
|
def ClassName(self):
|
|
'''Returns the name of the (Zope) class for self.'''
|
|
return self.portal_type
|
|
|
|
def Allowed(self):
|
|
'''Returns the list of roles and users that are allowed to view this
|
|
object. This index value will be used within catalog queries for
|
|
filtering objects the user is allowed to see.'''
|
|
# Get, from the workflow, roles having permission 'read'.
|
|
res = self.getRolesFor('read')
|
|
# Add users or groups having, locally, this role on this object.
|
|
localRoles = getattr(self.aq_base, '__ac_local_roles__', None)
|
|
if not localRoles: return res
|
|
for id, roles in localRoles.iteritems():
|
|
for role in roles:
|
|
if role in res:
|
|
usr = 'user:%s' % id
|
|
if usr not in res: res.append(usr)
|
|
return res
|
|
|
|
def showState(self):
|
|
'''Must I show self's current state ?'''
|
|
stateShow = self.State(name=False).show
|
|
if callable(stateShow):
|
|
return stateShow(self.getWorkflow(), self.appy())
|
|
return stateShow
|
|
|
|
def showTransitions(self, layoutType):
|
|
'''Must we show the buttons/icons for triggering transitions on
|
|
p_layoutType?'''
|
|
# Never show transitions on edit pages.
|
|
if layoutType == 'edit': return
|
|
# Use the default value if self's class does not specify it.
|
|
klass = self.getClass()
|
|
if not hasattr(klass, 'showTransitions'): return (layoutType=='view')
|
|
showValue = klass.showTransitions
|
|
# This value can be a single value or a tuple/list of values.
|
|
if isinstance(showValue, basestring): return layoutType == showValue
|
|
return layoutType in showValue
|
|
|
|
getUrlDefaults = {'page':True, 'nav':True}
|
|
def getUrl(self, base=None, mode='view', **kwargs):
|
|
'''Returns an URL for this object.
|
|
* If p_base is None, it will be the base URL for this object
|
|
(ie, Zope self.absolute_url()).
|
|
* p_mode can be "edit", "view" or "raw" (a non-param, base URL)
|
|
* p_kwargs can store additional parameters to add to the URL.
|
|
In this dict, every value that is a string will be added to the
|
|
URL as-is. Every value that is True will be replaced by the value
|
|
in the request for the corresponding key (if existing; else, the
|
|
param will not be included in the URL at all).'''
|
|
# Define the URL suffix
|
|
suffix = ''
|
|
if mode != 'raw': suffix = '/%s' % mode
|
|
# Define base URL if omitted
|
|
if not base:
|
|
base = self.absolute_url() + suffix
|
|
# If a raw URL is asked, remove any param and suffix.
|
|
if mode == 'raw':
|
|
if '?' in base: base = base[:base.index('?')]
|
|
base = base.strip('/')
|
|
for mode in ('view', 'edit'):
|
|
if base.endswith(mode):
|
|
base = base[:-len(mode)].strip('/')
|
|
break
|
|
return base
|
|
# Manage default args
|
|
if not kwargs: kwargs = self.getUrlDefaults
|
|
if 'page' not in kwargs: kwargs['page'] = True
|
|
if 'nav' not in kwargs: kwargs['nav'] = True
|
|
# Create URL parameters from kwargs
|
|
params = []
|
|
for name, value in kwargs.iteritems():
|
|
if isinstance(value, basestring):
|
|
params.append('%s=%s' % (name, value))
|
|
elif self.REQUEST.get(name, ''):
|
|
params.append('%s=%s' % (name, self.REQUEST[name]))
|
|
if params:
|
|
params = '&'.join(params)
|
|
if base.find('?') != -1: params = '&' + params
|
|
else: params = '?' + params
|
|
else:
|
|
params = ''
|
|
return '%s%s' % (base, params)
|
|
|
|
def getTool(self):
|
|
'''Returns the application tool.'''
|
|
return self.getPhysicalRoot().config
|
|
|
|
def getProductConfig(self, app=False):
|
|
'''Returns a reference to the config module. If p_app is True, it
|
|
returns the application config.'''
|
|
res = self.__class__.config
|
|
if app: res = res.appConfig
|
|
return res
|
|
|
|
def getParent(self):
|
|
'''If this object is stored within another one, this method returns it.
|
|
Else (if the object is stored directly within the tool or the root
|
|
data folder) it returns None.'''
|
|
parent = self.getParentNode()
|
|
# Not-Managers can't navigate back to the tool
|
|
if (parent.id == 'config') and \
|
|
not self.getTool().getUser().has_role('Manager'):
|
|
return False
|
|
if parent.meta_type not in ('Folder', 'Temporary Folder'): return parent
|
|
|
|
def getBreadCrumb(self):
|
|
'''Gets breadcrumb info about this object and its parents (if it must
|
|
be shown).'''
|
|
# Return an empty breadcrumb if it must not be shown.
|
|
klass = self.getClass()
|
|
if hasattr(klass, 'breadcrumb') and not klass.breadcrumb: return ()
|
|
# Compute the breadcrumb
|
|
res = [Object(url=self.absolute_url(),
|
|
title=self.getFieldValue('title', layoutType='view'))]
|
|
parent = self.getParent()
|
|
if parent:
|
|
res = parent.getBreadCrumb() + res
|
|
return res
|
|
|
|
def index_html(self):
|
|
'''Base method called when hitting this object.
|
|
- The standard behaviour is to redirect to /view.
|
|
- If a parameter named "do" is present in the request, it is supposed
|
|
to contain the name of a method to call on this object. In this
|
|
case, we call this method and return its result as XML.
|
|
- If method is POST, we consider the request to be XML data, that we
|
|
marshall to Python, and we call the method in param "do" with, as
|
|
arg, this marshalled Python object. While this could sound strange
|
|
to expect a query string containing a param "do" in a HTTP POST,
|
|
the HTTP spec does not prevent to do it.'''
|
|
rq = self.REQUEST
|
|
if (rq.REQUEST_METHOD == 'POST') and rq.QUERY_STRING:
|
|
# A POST method containing XML data.
|
|
rq.args = XmlUnmarshaller().parse(rq.stdin.getvalue())
|
|
# Find the name of the method to call.
|
|
methodName = rq.QUERY_STRING.split('=')[1]
|
|
return self.xml(action=methodName)
|
|
elif rq.has_key('do'):
|
|
# The user wants to call a method on this object and get its result
|
|
# as XML.
|
|
return self.xml(action=rq['do'])
|
|
else:
|
|
# The user wants to consult the view page for this object
|
|
return rq.RESPONSE.redirect(self.getUrl())
|
|
|
|
def getUserLanguage(self):
|
|
'''Gets the language (code) of the current user.'''
|
|
if not hasattr(self, 'REQUEST'): return 'en'
|
|
# Try the value which comes from the cookie. Indeed, if such a cookie is
|
|
# present, it means that the user has explicitly chosen this language
|
|
# via the language selector.
|
|
rq = self.REQUEST
|
|
if '_ZopeLg' in rq.cookies: return rq.cookies['_ZopeLg']
|
|
# Try the LANGUAGE key from the request: it corresponds to the language
|
|
# as configured in the user's browser.
|
|
res = self.REQUEST.get('LANGUAGE', None)
|
|
if res: return res
|
|
# Try the HTTP_ACCEPT_LANGUAGE key from the request, which stores
|
|
# language preferences as defined in the user's browser. Several
|
|
# languages can be listed, from most to less wanted.
|
|
res = self.REQUEST.get('HTTP_ACCEPT_LANGUAGE', None)
|
|
if not res: return 'en'
|
|
if ',' in res: res = res[:res.find(',')]
|
|
if '-' in res: res = res[:res.find('-')]
|
|
return res
|
|
|
|
def getLanguageDirection(self, lang):
|
|
'''Determines if p_lang is a LTR or RTL language.'''
|
|
if lang in rtlLanguages: return 'rtl'
|
|
return 'ltr'
|
|
|
|
def formatText(self, text, format='html'):
|
|
'''Produces a representation of p_text into the desired p_format, which
|
|
is "html" by default.'''
|
|
if 'html' in format:
|
|
if format == 'html_from_text': text = cgi.escape(text)
|
|
res = text.replace('\r\n', '<br/>').replace('\n', '<br/>')
|
|
elif format == 'text':
|
|
res = text.replace('<br/>', '\n')
|
|
else:
|
|
res = text
|
|
return res
|
|
|
|
def translate(self, label, mapping={}, domain=None, default=None,
|
|
language=None, format='html', field=None):
|
|
'''Translates a given p_label into p_domain with p_mapping.
|
|
|
|
If p_field is given, p_label does not correspond to a full label
|
|
name, but to a label type linked to p_field: "label", "descr"
|
|
or "help". Indeed, in this case, a specific i18n mapping may be
|
|
available on the field, so we must merge this mapping into
|
|
p_mapping.'''
|
|
cfg = self.getProductConfig()
|
|
if not domain: domain = cfg.PROJECTNAME
|
|
# Get the label name, and the field-specific mapping if any.
|
|
if field:
|
|
if field.type != 'group':
|
|
fieldMapping = field.mapping[label]
|
|
if fieldMapping:
|
|
if callable(fieldMapping):
|
|
fieldMapping = field.callMethod(self, fieldMapping)
|
|
mapping.update(fieldMapping)
|
|
label = getattr(field, '%sId' % label)
|
|
# We will get the translation from a Translation object.
|
|
# In what language must we get the translation?
|
|
if not language: language = self.getUserLanguage()
|
|
tool = self.getTool()
|
|
try:
|
|
translation = getattr(tool, language).appy()
|
|
except AttributeError:
|
|
# We have no translation for this language. Fallback to 'en'.
|
|
translation = getattr(tool, 'en').appy()
|
|
res = getattr(translation, label, '')
|
|
if not res:
|
|
# Fallback to 'en'.
|
|
translation = getattr(tool, 'en').appy()
|
|
res = getattr(translation, label, '')
|
|
# If still no result, put the label instead of a translated message
|
|
if not res: res = label
|
|
else:
|
|
# Perform replacements, according to p_format.
|
|
res = self.formatText(res, format)
|
|
# Perform variable replacements
|
|
for name, repl in mapping.iteritems():
|
|
if not isinstance(repl, basestring): repl = str(repl)
|
|
res = res.replace('${%s}' % name, repl)
|
|
return res
|
|
|
|
def getPageLayout(self, layoutType):
|
|
'''Returns the layout corresponding to p_layoutType for p_self.'''
|
|
appyClass = self.wrapperClass.__bases__[-1]
|
|
if hasattr(appyClass, 'layouts'):
|
|
layout = appyClass.layouts[layoutType]
|
|
if isinstance(layout, basestring):
|
|
layout = Table(layout)
|
|
else:
|
|
layout = defaultPageLayouts[layoutType]
|
|
return layout
|
|
|
|
def download(self, name=None):
|
|
'''Downloads the content of the file that is in the File field whose
|
|
name is in the request. This name can also represent an attribute
|
|
storing an image within a rich text field. If p_name is not given, it
|
|
is retrieved from the request.'''
|
|
name = self.REQUEST.get('name')
|
|
if not name: return
|
|
if '_img_' not in name:
|
|
appyType = self.getAppyType(name)
|
|
else:
|
|
appyType = self.getAppyType(name.split('_img_')[0])
|
|
if (not appyType.isShowable(self, 'view')) and \
|
|
(not appyType.isShowable(self, 'result')):
|
|
from zExceptions import NotFound
|
|
raise NotFound()
|
|
info = getattr(self.aq_base, name, None)
|
|
if info:
|
|
# Write the file in the HTTP response.
|
|
info.writeResponse(self.REQUEST.RESPONSE, self.getDbFolder())
|
|
|
|
def upload(self):
|
|
'''Receives an image uploaded by the user via ckeditor and stores it in
|
|
a special field on this object.'''
|
|
# Get the name of the rich text field for which an image must be stored.
|
|
params = self.REQUEST['QUERY_STRING'].split('&')
|
|
fieldName = params[0].split('=')[1]
|
|
ckNum = params[1].split('=')[1]
|
|
# We will store the image in a field named [fieldName]_img_[nb].
|
|
i = 1
|
|
attrName = '%s_img_%d' % (fieldName, i)
|
|
while True:
|
|
if not hasattr(self.aq_base, attrName):
|
|
break
|
|
else:
|
|
i += 1
|
|
attrName = '%s_img_%d' % (fieldName, i)
|
|
# Store the image. Create a fake File instance for doing the job.
|
|
fakeFile = gen.File(isImage=True)
|
|
fakeFile.name = attrName
|
|
fakeFile.store(self, self.REQUEST['upload'])
|
|
# Return the URL of the image.
|
|
url = '%s/download?name=%s' % (self.absolute_url(), attrName)
|
|
response = self.REQUEST.RESPONSE
|
|
response.setHeader('Content-Type', 'text/html')
|
|
resp = "<script type='text/javascript'>window.parent.CKEDITOR.tools" \
|
|
".callFunction(%s, '%s');</script>" % (ckNum, url)
|
|
response.write(resp)
|
|
|
|
def allows(self, permission, raiseError=False):
|
|
'''Has the logged user p_permission on p_self ?'''
|
|
res = self.getTool().getUser().has_permission(permission, self)
|
|
if not res and raiseError:
|
|
from AccessControl import Unauthorized
|
|
raise Unauthorized
|
|
return res
|
|
|
|
def isTemporary(self):
|
|
'''Is this object temporary ?'''
|
|
parent = self.getParentNode()
|
|
if not parent: # Is propably being created through code
|
|
return False
|
|
return parent.getId() == 'temp_folder'
|
|
|
|
def onProcess(self):
|
|
'''This method is a general hook for transfering processing of a request
|
|
to a given field, whose name must be in the request.'''
|
|
return self.getAppyType(self.REQUEST['name']).process(self)
|
|
# ------------------------------------------------------------------------------
|