1451 lines
64 KiB
Python
1451 lines
64 KiB
Python
'''This package contains mixin classes that are mixed in with generated classes:
|
|
- mixins/BaseMixin is mixed in with Standard Archetypes 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
|
|
from appy.gen import Type, String, Selection, Role, No, WorkflowAnonymous, \
|
|
Transition, Permission
|
|
from appy.gen.utils import *
|
|
from appy.gen.layout import Table, defaultPageLayouts
|
|
from appy.gen.descriptors import WorkflowDescriptor
|
|
from appy.gen.plone25.descriptors import ClassDescriptor
|
|
|
|
# ------------------------------------------------------------------------------
|
|
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 = self.translate(
|
|
'Please correct the indicated errors.', domain='plone')
|
|
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(self.translate('Changes canceled.', domain='plone'))
|
|
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 = obj.translate('Changes saved.', domain='plone')
|
|
# 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.'''
|
|
url = self.absolute_url_path()
|
|
catalog = self.getPhysicalRoot().catalog
|
|
if unindex:
|
|
catalog.uncatalog_object(url)
|
|
else:
|
|
if indexes:
|
|
catalog.catalog_object(self, url, idxs=indexes)
|
|
else:
|
|
catalog.catalog_object(self, url)
|
|
|
|
def say(self, msg, type='info'):
|
|
'''Prints a p_msg in the user interface. p_logLevel may be "info",
|
|
"warning" or "error".'''
|
|
mType = type
|
|
rq = self.REQUEST
|
|
if not hasattr(rq, 'messages'):
|
|
messages = rq.messages = []
|
|
else:
|
|
messages = rq.messages
|
|
if mType == 'warning': mType = 'warn'
|
|
elif mType == 'error': mType = 'stop'
|
|
messages.append( (mType, 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
|
|
return obj.getMethod('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:
|
|
# Remove previous message if any
|
|
if 'portal_status_message=' in url:
|
|
url = url[:url.find('portal_status_message=')-1]
|
|
if '?' in url: op = '&'
|
|
else: op = '?'
|
|
url += op + urllib.urlencode([('portal_status_message',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 == 3):
|
|
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 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.Extensions.appyWrappers' % \
|
|
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: name, subName, i = name.split('*')
|
|
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):
|
|
'''Returns the fields sorted by group. For every field, the appyType
|
|
(dict version) is given.'''
|
|
res = []
|
|
groups = {} # The already encountered groups
|
|
# 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 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__)
|
|
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 getCssAndJs(self, fields, layoutType):
|
|
'''Gets the list 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:
|
|
fieldCss = field.getCss(layoutType)
|
|
if fieldCss:
|
|
for fcss in fieldCss:
|
|
if fcss not in css: css.append(fcss)
|
|
fieldJs = field.getJs(layoutType)
|
|
if fieldJs:
|
|
for fjs in fieldJs:
|
|
if fjs not in js: js.append(fjs)
|
|
return {'css':css, '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, 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)
|
|
# 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 by Zope/CMF 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 = 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 = 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
|
|
return state.updatePermissions(wf, self)
|
|
|
|
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 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 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: rq = Object()
|
|
if not hasattr(rq, 'appyWrappers'): rq.appyWrappers = {}
|
|
# Return the Appy wrapper from rq.appyWrappers if already there
|
|
uid = self.UID()
|
|
if uid in rq.appyWrappers: return rq.appyWrappers[uid]
|
|
# Create the Appy wrapper, cache it in rq.appyWrappers and return it
|
|
wrapper = self.wrapperClass(self)
|
|
rq.appyWrappers[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 index_html(self):
|
|
"""Redirects to /ui. Transfers the status message if any."""
|
|
rq = self.REQUEST
|
|
msg = rq.get('portal_status_message', '')
|
|
if msg:
|
|
url = self.getUrl(portal_status_message=msg)
|
|
else:
|
|
url = self.getUrl()
|
|
return rq.RESPONSE.redirect(url)
|
|
|
|
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.'''
|
|
# 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("'", "\\'")
|
|
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
|
|
if domain != cfg.PROJECTNAME:
|
|
# We need to translate something that is in a standard Zope catalog
|
|
try:
|
|
res = self.Control_Panel.TranslationService.utranslate(
|
|
domain, label, mapping, self, default=default,
|
|
target_language=language)
|
|
except AttributeError:
|
|
# When run in test mode, Zope does not create the
|
|
# TranslationService
|
|
res = label
|
|
else:
|
|
# 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):
|
|
'''Downloads the content of the file that is in the File field named
|
|
p_name.'''
|
|
name = self.REQUEST.get('name')
|
|
if not name: return
|
|
appyType = self.getAppyType(name)
|
|
if (not appyType.type =='File') or not appyType.isShowable(self,'view'):
|
|
return
|
|
theFile = getattr(self.aq_base, name, None)
|
|
if theFile:
|
|
response = self.REQUEST.RESPONSE
|
|
response.setHeader('Content-Disposition', 'inline;filename="%s"' % \
|
|
theFile.filename)
|
|
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 allows(self, permission):
|
|
'''Has the logged user p_permission on p_self ?'''
|
|
return self.getUser().has_permission(permission, self)
|
|
|
|
def getEditorInit(self, name):
|
|
'''Gets the Javascrit init code for displaying a rich editor for
|
|
field p_name.'''
|
|
return 'tinyMCE.init({\nmode : "textareas",\ntheme : "simple",\n' \
|
|
'elements : "%s",\neditor_selector : "rich_%s"\n});'% (name,name)
|
|
|
|
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'
|
|
# ------------------------------------------------------------------------------
|