1532 lines
68 KiB
Python
1532 lines
68 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, sys, types, mimetypes, urllib, cgi
|
|
from appy import Object
|
|
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.utils import sequenceTypes
|
|
|
|
# ------------------------------------------------------------------------------
|
|
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 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()
|
|
id = tool.generateUid(obj.portal_type)
|
|
if not initiator:
|
|
folder = tool.getPath('/data')
|
|
else:
|
|
if initiator.isPrincipiaFolderish:
|
|
folder = initiator
|
|
else:
|
|
folder = initiator.getParentNode()
|
|
obj = createObject(folder, id, obj.portal_type, tool.getAppName())
|
|
previousData = None
|
|
if not created: previousData = obj.rememberPreviousData()
|
|
# Perform the change on the object
|
|
if rq:
|
|
# Store in the database the new value coming from the form
|
|
for appyType in self.getAppyTypes('edit', rq.get('page')):
|
|
value = getattr(values, appyType.name, None)
|
|
appyType.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, obj)
|
|
|
|
# Manage "add" permissions and reindex the object
|
|
obj._appy_managePermissions()
|
|
|
|
# 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)
|
|
obj.reindex()
|
|
return obj, msg
|
|
|
|
def delete(self):
|
|
'''This methods 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 object
|
|
self.getParentNode().manage_delObjects([self.id])
|
|
|
|
def onDelete(self):
|
|
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('delete_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':''}
|
|
if rq.get('nav', None):
|
|
# 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)
|
|
# 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 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 appyType in self.getAppyTypes('edit', rq.form.get('page')):
|
|
if not appyType.validable: continue
|
|
value = appyType.getRequestValue(rq)
|
|
message = appyType.validate(self, value)
|
|
if message:
|
|
setattr(errors, appyType.name, message)
|
|
else:
|
|
setattr(values, appyType.name, appyType.getStorableValue(value))
|
|
# Validate sub-fields within Lists
|
|
if appyType.type != 'List': continue
|
|
i = -1
|
|
for row in value:
|
|
i += 1
|
|
for name, field in appyType.fields:
|
|
message = field.validate(self, getattr(row,name,None))
|
|
if message:
|
|
setattr(errors, '%s*%d' % (field.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 = 'Please correct the indicated errors.' # XXX Translate
|
|
isNew = rq.get('is_new') == 'True'
|
|
# If this object is created from an initiator, get info about him.
|
|
initiator = None
|
|
initiatorPage = None
|
|
initiatorField = None
|
|
if rq.get('nav', '').startswith('ref.'):
|
|
splitted = rq['nav'].split('.')
|
|
initiator = tool.getObject(splitted[1])
|
|
initiatorField, initiatorPage = splitted[2].split(':')
|
|
# If the user clicked on 'Cancel', go back to the previous page.
|
|
if rq.get('buttonCancel.x', None):
|
|
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('Changes canceled.') # XXX Translate
|
|
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__:
|
|
rq.set('errors', errors.__dict__)
|
|
self.say(errorMessage)
|
|
return self.gotoEdit()
|
|
|
|
# Trigger inter-field validation
|
|
msg = self.interFieldValidation(errors, values)
|
|
if not msg: msg = errorMessage
|
|
if errors.__dict__:
|
|
rq.set('errors', errors.__dict__)
|
|
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 = 'Changes saved.' # XXX Translate
|
|
# 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):
|
|
obj.unindex()
|
|
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('View'):
|
|
return self.goto(tool.getSiteUrl(), msg)
|
|
if rq.get('buttonOk.x', None) or saveConfirmed:
|
|
obj.say(msg)
|
|
if isNew and initiator:
|
|
return self.goto(initiator.getUrl(page=initiatorPage, nav=''))
|
|
else:
|
|
return self.goto(obj.getUrl())
|
|
if rq.get('buttonPrevious.x', None):
|
|
# 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.
|
|
phaseInfo = self.getAppyPhases(currentOnly=True, layoutType='edit')
|
|
pageName, pageInfo = self.getPreviousPage(phaseInfo, rq['page'])
|
|
if pageName:
|
|
# Return to the edit or view page?
|
|
if pageInfo['showOnEdit']:
|
|
rq.set('page', pageName)
|
|
return obj.gotoEdit()
|
|
else:
|
|
return self.goto(obj.getUrl(page=pageName))
|
|
else:
|
|
obj.say(msg)
|
|
return self.goto(obj.getUrl())
|
|
if rq.get('buttonNext.x', None):
|
|
# 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']
|
|
phaseInfo = self.getAppyPhases(currentOnly=True, layoutType='edit')
|
|
pageName, pageInfo = self.getNextPage(phaseInfo, pageName)
|
|
if pageName:
|
|
# Return to the edit or view page?
|
|
if pageInfo['showOnEdit']:
|
|
rq.set('page', pageName)
|
|
return obj.gotoEdit()
|
|
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:
|
|
catalog.catalog_object(self, path)
|
|
|
|
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):
|
|
'''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 appyType in self.getAllAppyTypes():
|
|
if appyType.historized:
|
|
res[appyType.name] = appyType.getValue(self)
|
|
return res
|
|
|
|
def addHistoryEvent(self, action, **kw):
|
|
'''Adds an event in the object history.'''
|
|
userId = self.getUser().getId()
|
|
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 macro "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.ui.edit(self)
|
|
|
|
def showField(self, name, layoutType='view'):
|
|
'''Must I show field named p_name on this p_layoutType ?'''
|
|
if name == 'state': return False
|
|
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 getFieldValue(self, name, onlyIfSync=False, layoutType=None,
|
|
outerValue=None):
|
|
'''Returns the database value of field named p_name for p_self.
|
|
If p_onlyIfSync is True, it returns the value only if appyType can be
|
|
retrieved in synchronous mode.'''
|
|
appyType = self.getAppyType(name)
|
|
if not onlyIfSync or (onlyIfSync and appyType.sync[layoutType]):
|
|
# We must really get the field value.
|
|
if '*' not in name: return appyType.getValue(self)
|
|
# The field is an inner field from a List.
|
|
listName, name, i = name.split('*')
|
|
listType = self.getAppyType(listName)
|
|
return listType.getInnerValue(outerValue, name, int(i))
|
|
|
|
def getFormattedFieldValue(self, name, value):
|
|
'''Gets a nice, string representation of p_value which is a value from
|
|
field named p_name.'''
|
|
return self.getAppyType(name).getFormattedValue(self, value)
|
|
|
|
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 getFileInfo(self, fileObject):
|
|
'''Returns filename and size of p_fileObject.'''
|
|
if not fileObject: return {'filename': '', 'size': 0}
|
|
return {'filename': fileObject.filename, 'size': fileObject.size}
|
|
|
|
def getAppyRefs(self, name, startNumber=None):
|
|
'''Gets the objects linked to me through Ref field named p_name.
|
|
If p_startNumber is None, this method returns all referred objects.
|
|
If p_startNumber is a number, this method will return
|
|
appyType.maxPerPage objects, starting at p_startNumber.'''
|
|
field = self.getAppyType(name)
|
|
return field.getValue(self, type='zobjects', someObjects=True,
|
|
startNumber=startNumber).__dict__
|
|
|
|
def getSelectableAppyRefs(self, name):
|
|
'''p_name is the name of a Ref field. This method returns the list of
|
|
all objects that can be selected to be linked as references to p_self
|
|
through field p_name.'''
|
|
appyType = self.getAppyType(name)
|
|
if not appyType.select:
|
|
# No select method has been defined: we must retrieve all objects
|
|
# of the referred type that the user is allowed to access.
|
|
return self.appy().search(appyType.klass)
|
|
else:
|
|
return appyType.select(self.appy())
|
|
|
|
xhtmlToText = re.compile('<.*?>', re.S)
|
|
def getReferenceLabel(self, name, refObject):
|
|
'''p_name is the name of a Ref field with link=True. I need to display,
|
|
on an edit view, the p_refObject in the listbox that will allow
|
|
the user to choose which object(s) to link through the Ref.
|
|
The information to display may only be the object title or more if
|
|
field.shownInfo is used.'''
|
|
appyType = self.getAppyType(name)
|
|
res = refObject.title
|
|
if 'title' in appyType.shownInfo:
|
|
# We may place it at another place
|
|
res = ''
|
|
for fieldName in appyType.shownInfo:
|
|
refType = refObject.o.getAppyType(fieldName)
|
|
value = getattr(refObject, fieldName)
|
|
value = refType.getFormattedValue(refObject.o, value)
|
|
if refType.type == 'String':
|
|
if refType.format == 2:
|
|
value = self.xhtmlToText.sub(' ', value)
|
|
elif type(value) in sequenceTypes:
|
|
value = ', '.join(value)
|
|
prefix = ''
|
|
if res:
|
|
prefix = ' | '
|
|
res += prefix + value
|
|
maxWidth = appyType.width or 30
|
|
if len(res) > maxWidth:
|
|
res = res[:maxWidth-2] + '...'
|
|
return res
|
|
|
|
def getReferenceUid(self, refObject):
|
|
'''Returns the UID of referred object p_refObject.'''
|
|
return refObject.o.UID()
|
|
|
|
def getAppyRefIndex(self, fieldName, obj):
|
|
'''Gets the position of p_obj within Ref field named p_fieldName.'''
|
|
refs = getattr(self.aq_base, fieldName, None)
|
|
if not refs: raise IndexError()
|
|
return refs.index(obj.UID())
|
|
|
|
def mayAddReference(self, name, folder):
|
|
'''May the user add references via Ref field named p_name in
|
|
p_folder?'''
|
|
return self.getAppyType(name).mayAdd(self, folder)
|
|
|
|
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, asDict=False, 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)
|
|
if res and asDict: return res.__dict__
|
|
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 getGroupedAppyTypes(self, layoutType, pageName, cssJs=None):
|
|
'''Returns the fields sorted by group. For every field, the appyType
|
|
(dict version) is given.'''
|
|
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 appyType.
|
|
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 appyType in self.getAllAppyTypes():
|
|
if refresh: appyType = appyType.reload(klass, self)
|
|
if appyType.page.name != pageName: continue
|
|
if not appyType.isShowable(self, layoutType): continue
|
|
if collectCssJs:
|
|
if css == None: css = []
|
|
appyType.getCss(layoutType, css)
|
|
if js == None: js = []
|
|
appyType.getJs(layoutType, js)
|
|
if not appyType.group:
|
|
res.append(appyType.__dict__)
|
|
else:
|
|
# Insert the GroupDescr instance corresponding to
|
|
# appyType.group at the right place
|
|
groupDescr = appyType.group.insertInto(res, groups,
|
|
appyType.page, self.meta_type)
|
|
GroupDescr.addWidget(groupDescr, appyType.__dict__)
|
|
if collectCssJs:
|
|
cssJs['css'] = css
|
|
cssJs['js'] = js
|
|
return res
|
|
|
|
def getAppyTypes(self, layoutType, pageName):
|
|
'''Returns the list of appyTypes that belong to a given p_page, for a
|
|
given p_layoutType.'''
|
|
res = []
|
|
for appyType in self.getAllAppyTypes():
|
|
if appyType.page.name != pageName: continue
|
|
if not appyType.isShowable(self, layoutType): continue
|
|
res.append(appyType)
|
|
return res
|
|
|
|
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 getAppyTypesFromNames(self, fieldNames, asDict=True):
|
|
'''Gets the Appy types named p_fieldNames.'''
|
|
res = []
|
|
for name in fieldNames:
|
|
if name == 'state':
|
|
# We do not return a appyType if the attribute is not a *real*
|
|
# attribute, but the workfow state.
|
|
res.append({'name': name, 'labelId': 'workflow_state',
|
|
'filterable': False})
|
|
else:
|
|
appyType = self.getAppyType(name, asDict)
|
|
if appyType: res.append(appyType)
|
|
else:
|
|
self.log('Field "%s", used as shownInfo in a Ref, ' \
|
|
'was not found.' % name, type='warning')
|
|
return res
|
|
|
|
def getAppyStates(self, phase, currentOnly=False):
|
|
'''Returns information about the states that are related to p_phase.
|
|
If p_currentOnly is True, we return the current state, even if not
|
|
related to p_phase.'''
|
|
currentState = self.State()
|
|
if currentOnly:
|
|
return [StateDescr(currentState, 'current').get()]
|
|
res = []
|
|
workflow = self.getWorkflow()
|
|
stateStatus = 'done'
|
|
for stateName in dir(workflow):
|
|
if getattr(workflow, stateName).__class__.__name__ != 'State':
|
|
continue
|
|
if stateName == currentState:
|
|
stateStatus = 'current'
|
|
elif stateStatus != 'done':
|
|
stateStatus = 'future'
|
|
state = getattr(workflow, stateName)
|
|
if (state.phase == phase) and \
|
|
(self._appy_showState(workflow, state.show)):
|
|
res.append(StateDescr(stateName, stateStatus).get())
|
|
return res
|
|
|
|
def getAppyTransitions(self, includeFake=True, includeNotShowable=False):
|
|
'''This method returns info about transitions 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.
|
|
'''
|
|
res = []
|
|
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
|
|
# Add transition-info to the result.
|
|
label = self.getWorkflowLabel(name)
|
|
tInfo = {'name': name, 'title': self.translate(label),
|
|
'confirm': '', 'may_trigger': True}
|
|
if transition.confirm:
|
|
cLabel = '%s_confirm' % label
|
|
tInfo['confirm'] = self.translate(cLabel, format='js')
|
|
if not mayTrigger:
|
|
tInfo['may_trigger'] = False
|
|
tInfo['reason'] = mayTrigger.msg
|
|
res.append(tInfo)
|
|
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 appyType in self.getAllAppyTypes():
|
|
typePhase = appyType.page.phase
|
|
if typePhase not in phases:
|
|
states = self.getAppyStates(typePhase)
|
|
phase = PhaseDescr(typePhase, states, self)
|
|
res.append(phase.__dict__)
|
|
phases[typePhase] = phase
|
|
else:
|
|
phase = phases[typePhase]
|
|
phase.addPage(appyType, self, layoutType)
|
|
if (appyType.type == 'Ref') and appyType.navigable:
|
|
phase.addPageLinks(appyType, 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]
|
|
# Then, compute status of phases
|
|
for ph in phases.itervalues():
|
|
ph.computeStatus(res)
|
|
ph.totalNbOfPhases = len(res)
|
|
# Restrict the result to the current phase if required
|
|
if currentOnly:
|
|
rq = self.REQUEST
|
|
page = rq.get('page', 'main')
|
|
for phaseInfo in res:
|
|
if page in phaseInfo['pages']:
|
|
return phaseInfo
|
|
# If I am here, it means that the page as defined in the request,
|
|
# or 'main' by default, 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 phase['pagesInfo'][page][viewAttr]:
|
|
rq.set('page', page)
|
|
pageFound = True
|
|
break
|
|
return phase
|
|
else:
|
|
return res
|
|
|
|
def getPreviousPage(self, phase, page):
|
|
'''Returns the page that precedes p_page which is in p_phase.'''
|
|
try:
|
|
pageIndex = phase['pages'].index(page)
|
|
except ValueError:
|
|
# The current page is probably not visible anymore. Return the
|
|
# first available page in current phase.
|
|
res = phase['pages'][0]
|
|
return res, phase['pagesInfo'][res]
|
|
if pageIndex > 0:
|
|
# We stay on the same phase, previous page
|
|
res = phase['pages'][pageIndex-1]
|
|
resInfo = phase['pagesInfo'][res]
|
|
return res, resInfo
|
|
else:
|
|
if phase['previousPhase']:
|
|
# We go to the last page of previous phase
|
|
previousPhase = phase['previousPhase']
|
|
res = previousPhase['pages'][-1]
|
|
resInfo = previousPhase['pagesInfo'][res]
|
|
return res, resInfo
|
|
else:
|
|
return None, None
|
|
|
|
def getNextPage(self, phase, page):
|
|
'''Returns the page that follows p_page which is in p_phase.'''
|
|
try:
|
|
pageIndex = phase['pages'].index(page)
|
|
except ValueError:
|
|
# The current page is probably not visible anymore. Return the
|
|
# first available page in current phase.
|
|
res = phase['pages'][0]
|
|
return res, phase['pagesInfo'][res]
|
|
if pageIndex < len(phase['pages'])-1:
|
|
# We stay on the same phase, next page
|
|
res = phase['pages'][pageIndex+1]
|
|
resInfo = phase['pagesInfo'][res]
|
|
return res, resInfo
|
|
else:
|
|
if phase['nextPhase']:
|
|
# We go to the first page of next phase
|
|
nextPhase = phase['nextPhase']
|
|
res = nextPhase['pages'][0]
|
|
resInfo = nextPhase['pagesInfo'][res]
|
|
return res, resInfo
|
|
else:
|
|
return None, None
|
|
|
|
def changeRefOrder(self, fieldName, objectUid, newIndex, isDelta):
|
|
'''This method changes the position of object with uid p_objectUid in
|
|
reference field p_fieldName to p_newIndex i p_isDelta is False, or
|
|
to actualIndex+p_newIndex if p_isDelta is True.'''
|
|
refs = getattr(self.aq_base, fieldName, None)
|
|
oldIndex = refs.index(objectUid)
|
|
refs.remove(objectUid)
|
|
if isDelta:
|
|
newIndex = oldIndex + newIndex
|
|
else:
|
|
pass # To implement later on
|
|
refs.insert(newIndex, objectUid)
|
|
|
|
def onChangeRefOrder(self):
|
|
'''This method is called when the user wants to change order of an
|
|
item in a reference field.'''
|
|
rq = self.REQUEST
|
|
# Move the item up (-1), down (+1) ?
|
|
move = -1 # Move up
|
|
if rq['move'] == 'down':
|
|
move = 1 # Down
|
|
isDelta = True
|
|
self.changeRefOrder(rq['fieldName'], rq['refObjectUid'], move, isDelta)
|
|
|
|
def onSortReference(self):
|
|
'''This method is called when the user wants to sort the content of a
|
|
reference field.'''
|
|
rq = self.REQUEST
|
|
fieldName = rq.get('fieldName')
|
|
sortKey = rq.get('sortKey')
|
|
reverse = rq.get('reverse') == 'True'
|
|
self.appy().sort(fieldName, sortKey=sortKey, reverse=reverse)
|
|
|
|
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:
|
|
appyClass = self.wrapperClass.__bases__[-1]
|
|
else:
|
|
appyClass = self.getTool().getAppyClass(className)
|
|
if hasattr(appyClass, 'workflow'): wf = appyClass.workflow
|
|
else:
|
|
wf = gen.WorkflowAnonymous
|
|
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 refreshSecurity(self):
|
|
'''Refresh security info on this object. Returns True if the info has
|
|
effectively been updated.'''
|
|
wf = self.getWorkflow()
|
|
try:
|
|
# Get the state definition of the object's current state.
|
|
state = getattr(wf, self.State())
|
|
except AttributeError:
|
|
# The workflow information for this object does not correspond to
|
|
# its current workflow attribution. Add a new fake event
|
|
# representing passage of this object to the initial state of his
|
|
# currently attributed workflow.
|
|
stateName = self.State(name=True, initial=True)
|
|
self.addHistoryEvent(None, review_state=stateName)
|
|
state = self.State(name=False, initial=True)
|
|
self.log('Wrong workflow info for a "%s"; is not in state "%s".' % \
|
|
(self.meta_type, stateName))
|
|
# Update permission attributes on the object if required
|
|
updated = state.updatePermissions(wf, self)
|
|
if updated:
|
|
# Reindex the object because security-related info is indexed.
|
|
self.reindex()
|
|
return updated
|
|
|
|
def hasHistory(self):
|
|
'''Has this object an history?'''
|
|
if hasattr(self.aq_base, 'workflow_history') and self.workflow_history:
|
|
key = self.workflow_history.keys()[0]
|
|
for event in self.workflow_history[key]:
|
|
if event['action'] and (event['comments'] != '_invisible_'):
|
|
return True
|
|
return False
|
|
|
|
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.'''
|
|
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()
|
|
return {'events': history[startNumber:startNumber+batchSize],
|
|
'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 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? This condition
|
|
comes as an addition/refinement to the corresponding workflow
|
|
permission.'''
|
|
appyObj = self.appy()
|
|
if hasattr(appyObj, 'mayDelete'): return appyObj.mayDelete()
|
|
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',mimetypes.guess_type(msg.name)[0])
|
|
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):
|
|
'''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):
|
|
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 fieldValueSelected(self, fieldName, vocabValue, dbValue):
|
|
'''When displaying a selection box (ie a String with a validator being a
|
|
list), must the _vocabValue appear as selected?'''
|
|
rq = self.REQUEST
|
|
# Get the value we must compare (from request or from database)
|
|
if rq.has_key(fieldName):
|
|
compValue = rq.get(fieldName)
|
|
else:
|
|
compValue = dbValue
|
|
# Compare the value
|
|
if type(compValue) in sequenceTypes:
|
|
if vocabValue in compValue: return True
|
|
else:
|
|
if vocabValue == compValue: return True
|
|
|
|
def checkboxChecked(self, fieldName, dbValue):
|
|
'''When displaying a checkbox, must it be checked or not?'''
|
|
rq = self.REQUEST
|
|
# Get the value we must compare (from request or from database)
|
|
if rq.has_key(fieldName):
|
|
compValue = rq.get(fieldName)
|
|
compValue = compValue in ('True', 1, '1')
|
|
else:
|
|
compValue = dbValue
|
|
# Compare the value
|
|
return compValue
|
|
|
|
def dateValueSelected(self, fieldName, fieldPart, dateValue, dbValue):
|
|
'''When displaying a date field, must the particular p_dateValue be
|
|
selected in the field corresponding to the date part?'''
|
|
# Get the value we must compare (from request or from database)
|
|
rq = self.REQUEST
|
|
partName = '%s_%s' % (fieldName, fieldPart)
|
|
if rq.has_key(partName):
|
|
compValue = rq.get(partName)
|
|
if compValue.isdigit():
|
|
compValue = int(compValue)
|
|
else:
|
|
compValue = dbValue
|
|
if compValue:
|
|
compValue = getattr(compValue, fieldPart)()
|
|
# Compare the value
|
|
return compValue == dateValue
|
|
|
|
def getSelectableYears(self, name):
|
|
'''Gets the list of selectable years for Date field named p_name.'''
|
|
return self.getAppyType(name).getSelectableYears()
|
|
|
|
def getPossibleValues(self, name, withTranslations, withBlankValue,
|
|
className=None):
|
|
'''Gets the possible values for field named p_name. This field must be a
|
|
String with isSelection()=True. If p_withTranslations is True,
|
|
instead of returning a list of string values, the result is a list
|
|
of tuples (s_value, s_translation). If p_withBlankValue is True, a
|
|
blank value is prepended to the list. If no p_className is defined,
|
|
the field is supposed to belong to self's class.'''
|
|
appyType = self.getAppyType(name, className=className)
|
|
if className:
|
|
# We need an instance of className, but self can be an instance of
|
|
# another class. So here we will search such an instance.
|
|
brains = self.executeQuery(className, maxResults=1, brainsOnly=True)
|
|
if brains:
|
|
obj = brains[0].getObject()
|
|
else:
|
|
obj = self
|
|
else:
|
|
obj = self
|
|
return appyType.getPossibleValues(obj, withTranslations, withBlankValue)
|
|
|
|
def getCaptchaChallenge(self, name):
|
|
return self.getAppyType(name).getCaptchaChallenge(self.REQUEST.SESSION)
|
|
|
|
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
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Standard 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 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 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.'''
|
|
res = set()
|
|
# Get, from the workflow, roles having permission 'View'.
|
|
for role in self.getProductConfig().rolesForPermissionOn('View', self):
|
|
res.add(role)
|
|
# Add users having, locally, this role on this object.
|
|
localRoles = getattr(self, '__ac_local_roles__', None)
|
|
if not localRoles: return list(res)
|
|
for id, roles in localRoles.iteritems():
|
|
for role in roles:
|
|
if role in res:
|
|
res.add('user:%s' % id)
|
|
return list(res)
|
|
|
|
def _appy_showState(self, workflow, stateShow):
|
|
'''Must I show a state whose "show value" is p_stateShow?'''
|
|
if callable(stateShow):
|
|
return stateShow(workflow, self.appy())
|
|
else: return stateShow
|
|
|
|
def _appy_managePermissions(self):
|
|
'''When an object is created or updated, we must update "add"
|
|
permissions accordingly: if the object is a folder, we must set on
|
|
it permissions that will allow to create, inside it, objects through
|
|
Ref fields; if it is not a folder, we must update permissions on its
|
|
parent folder instead.'''
|
|
# Determine on which folder we need to set "add" permissions
|
|
folder = self
|
|
if not self.isPrincipiaFolderish:
|
|
folder = self.getParentNode()
|
|
# On this folder, set "add" permissions for every content type that will
|
|
# be created through reference fields
|
|
allCreators = {} # One key for every add permission
|
|
addPermissions = self.getProductConfig().ADD_CONTENT_PERMISSIONS
|
|
for appyType in self.getAllAppyTypes():
|
|
if appyType.type != 'Ref': continue
|
|
if appyType.isBack or appyType.link: continue
|
|
# Indeed, no possibility to create objects with such Refs
|
|
refType = self.getTool().getPortalType(appyType.klass)
|
|
if refType not in addPermissions: continue
|
|
# Get roles that may add this content type
|
|
creators = getattr(appyType.klass, 'creators', None)
|
|
if not creators:
|
|
creators = self.getProductConfig().defaultAddRoles
|
|
# Add those creators to the list of creators for this meta_type
|
|
addPermission = addPermissions[refType]
|
|
if addPermission in allCreators:
|
|
allCreators[addPermission] = allCreators[\
|
|
addPermission].union(creators)
|
|
else:
|
|
allCreators[addPermission] = set(creators)
|
|
# Update the permissions
|
|
for permission, creators in allCreators.iteritems():
|
|
updateRolesForPermission(permission, tuple(creators), folder)
|
|
|
|
def _appy_getPortalType(self, request):
|
|
'''Guess the portal_type of p_self from info about p_self and
|
|
p_request.'''
|
|
res = None
|
|
# If the object is being created, self.portal_type is not correctly
|
|
# initialized yet.
|
|
if request.has_key('__factory__info__'):
|
|
factoryInfo = request['__factory__info__']
|
|
if factoryInfo.has_key('stack'):
|
|
res = factoryInfo['stack'][0]
|
|
if not res:
|
|
res = self.portal_type
|
|
return res
|
|
|
|
getUrlDefaults = {'page':True, 'nav':True}
|
|
def getUrl(self, base=None, mode='view', **kwargs):
|
|
'''Returns a Appy URL.
|
|
* If p_base is None, it will be the base URL for this object
|
|
(ie, 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 = '/ui/%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'):
|
|
suffix = 'ui/%s' % mode
|
|
if base.endswith(suffix):
|
|
base = base[:-len(suffix)].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 getUser(self):
|
|
'''Gets the Zope object representing the authenticated user.'''
|
|
from AccessControl import getSecurityManager
|
|
user = getSecurityManager().getUser()
|
|
if not user:
|
|
from AccessControl.User import nobody
|
|
return nobody
|
|
return user
|
|
|
|
def getTool(self):
|
|
'''Returns the application tool.'''
|
|
return self.getPhysicalRoot().config
|
|
|
|
def getProductConfig(self):
|
|
'''Returns a reference to the config module.'''
|
|
return self.__class__.config
|
|
|
|
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.getUser().has_role('Manager'):
|
|
return False
|
|
if parent.meta_type != 'Folder': return parent
|
|
|
|
def index_html(self):
|
|
'''Redirects to /ui.'''
|
|
return self.REQUEST.RESPONSE.redirect(self.getUrl())
|
|
|
|
def userIsAnon(self):
|
|
'''Is the currently logged user anonymous ?'''
|
|
return self.getUser().getUserName() == 'Anonymous User'
|
|
|
|
def getUserLanguage(self):
|
|
'''Gets the language (code) of the current user.'''
|
|
if not hasattr(self, 'REQUEST'): return 'en'
|
|
# Try first the "LANGUAGE" key from the request
|
|
res = self.REQUEST.get('LANGUAGE', None)
|
|
if res: return res
|
|
# Try then 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 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 == 'js':
|
|
res = text.replace('\r\n', '').replace('\n', '')
|
|
res = res.replace("'", "\\'")
|
|
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:
|
|
# p_field is the dict version of a appy type or group
|
|
if field['type'] != 'group':
|
|
fieldMapping = field['mapping'][label]
|
|
if fieldMapping:
|
|
if callable(fieldMapping):
|
|
appyField = self.getAppyType(field['name'])
|
|
fieldMapping=appyField.callMethod(self,fieldMapping)
|
|
mapping.update(fieldMapping)
|
|
label = 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.get()
|
|
|
|
def getPageTemplate(self, ui, templateName):
|
|
'''Returns, in the ui folder, the page template corresponding to
|
|
p_templateName.'''
|
|
res = ui
|
|
for name in templateName.split('/'): res = getattr(res, name)
|
|
return res
|
|
|
|
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'):
|
|
from zExceptions import NotFound
|
|
raise NotFound()
|
|
theFile = getattr(self.aq_base, name, None)
|
|
if theFile:
|
|
response = self.REQUEST.RESPONSE
|
|
response.setHeader('Content-Disposition', 'inline;filename="%s"' % \
|
|
theFile.filename)
|
|
# Define content type
|
|
if theFile.content_type:
|
|
response.setHeader('Content-Type', theFile.content_type)
|
|
response.setHeader('Cachecontrol', 'no-cache')
|
|
response.setHeader('Expires', 'Thu, 11 Dec 1975 12:05:05 GMT')
|
|
return theFile.index_html(self.REQUEST, self.REQUEST.RESPONSE)
|
|
|
|
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 ?'''
|
|
hasPermission = self.getUser().has_permission(permission, self)
|
|
if not hasPermission and raiseError:
|
|
from AccessControl import Unauthorized
|
|
raise Unauthorized
|
|
return hasPermission
|
|
|
|
def getEditorInit(self, name):
|
|
'''Gets the Javascript init code for displaying a rich editor for
|
|
field named p_name.'''
|
|
# Define the attributes that will initialize the ckeditor instance for
|
|
# this field.
|
|
field = self.getAppyType(name)
|
|
ckAttrs = {'toolbar': field.richText and 'AppyRich' or 'Appy',
|
|
'format_tags': '%s' % ';'.join(field.styles)}
|
|
if field.allowImageUpload:
|
|
ckAttrs['filebrowserUploadUrl'] = '%s/upload' % self.absolute_url()
|
|
ck = ''
|
|
for k, v in ckAttrs.iteritems():
|
|
ck += "%s: '%s'," % (k, v)
|
|
res = "CKEDITOR.replace('%s', {%s})" % (name, ck)
|
|
return res
|
|
|
|
def getCalendarInit(self, name, years):
|
|
'''Gets the Javascript init code for displaying a calendar popup for
|
|
field named p_name.'''
|
|
return 'Calendar.setup({inputField: "%s", button: "%s_img", ' \
|
|
'onSelect: onSelectDate, range:[%d,%d]});' % \
|
|
(name, name, years[0], years[-1])
|
|
|
|
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'
|
|
# ------------------------------------------------------------------------------
|