1287 lines
58 KiB
Python
1287 lines
58 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
|
|
import appy.gen
|
|
from appy.gen import Type, String, Selection, Role, No
|
|
from appy.gen.utils import *
|
|
from appy.gen.layout import Table, defaultPageLayouts
|
|
from appy.gen.plone25.descriptors import ClassDescriptor
|
|
from appy.gen.plone25.utils import updateRolesForPermission,checkTransitionGuard
|
|
|
|
# ------------------------------------------------------------------------------
|
|
class BaseMixin:
|
|
'''Every Archetype 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):
|
|
'''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 (p_created is True), p_self is a
|
|
temporary object created in the request by portal_factory, and this
|
|
method creates the corresponding final object. In the case of an
|
|
update, this method simply updates fields of p_self.'''
|
|
rq = self.REQUEST
|
|
obj = self
|
|
if created:
|
|
obj = self.portal_factory.doCreate(self, self.id) # portal_factory
|
|
# creates the final object from the temp object.
|
|
previousData = None
|
|
if not created: previousData = self.rememberPreviousData()
|
|
# Perform the change on the object, unless self is a tool being created.
|
|
if (obj._appy_meta_type == 'Tool') and created:
|
|
# We do not process form data (=real update on the object) if the
|
|
# tool itself is being created.
|
|
pass
|
|
else:
|
|
# 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 created:
|
|
# Now we have a title for the object, so we derive a nice id
|
|
obj.unmarkCreationFlag()
|
|
obj._renameAfterCreation(check_auto_id=True)
|
|
if previousData:
|
|
# Keep in history potential changes on historized fields
|
|
self.historizeData(previousData)
|
|
|
|
# Manage potential link with an initiator object
|
|
if created and rq.get('nav', None):
|
|
# Get the initiator
|
|
splitted = rq['nav'].split('.')
|
|
if splitted[0] == 'search': return # Not an initiator but a search.
|
|
initiator = self.uid_catalog(UID=splitted[1])[0].getObject()
|
|
fieldName = splitted[2].split(':')[0]
|
|
initiator.appy().link(fieldName, obj)
|
|
|
|
# Call the custom "onEdit" if available
|
|
msg = None # The message to display to the user. It can be set by onEdit
|
|
if obj.wrapperClass:
|
|
appyObject = obj.appy()
|
|
if hasattr(appyObject, 'onEdit'):
|
|
msg = appyObject.onEdit(created)
|
|
# Manage "add" permissions and reindex the object
|
|
obj._appy_managePermissions()
|
|
obj.reindexObject()
|
|
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()
|
|
# 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 application folder or an object through a reference field.'''
|
|
rq = self.REQUEST
|
|
typeName = rq.get('type_name')
|
|
# 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)
|
|
# Determine base URL
|
|
baseUrl = self.absolute_url()
|
|
if (self._appy_meta_type == 'Tool') and not urlParams['nav']:
|
|
# This is the creation of a root object in the app folder
|
|
baseUrl = self.getAppFolder().absolute_url()
|
|
objId = self.generateUniqueId(typeName)
|
|
editUrl = '%s/portal_factory/%s/%s/skyn/edit' % \
|
|
(baseUrl, typeName, objId)
|
|
return self.goto(self.getUrl(editUrl, **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))
|
|
|
|
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 by portal_factory in
|
|
the request. In this case, the update consists in the creation of
|
|
the "final" object in the database. 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'
|
|
# Go back to the consult view if the user clicked on 'Cancel'
|
|
if rq.get('buttonCancel.x', None):
|
|
if isNew:
|
|
if rq.get('nav', ''):
|
|
# We can go back to the initiator page.
|
|
splitted = rq['nav'].split('.')
|
|
initiator = tool.getObject(splitted[1])
|
|
initiatorPage = splitted[2].split(':')[1]
|
|
urlBack = initiator.getUrl(page=initiatorPage, nav='')
|
|
else:
|
|
# 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 = AppyObject()
|
|
# Object for storing the (converted) values from the request
|
|
values = AppyObject()
|
|
|
|
# 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)
|
|
|
|
# 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(), obj.id, None):
|
|
obj.unindexObject()
|
|
return self.goto(tool.getSiteUrl(), msg)
|
|
# If the user can't access the object anymore, redirect him to the
|
|
# main site page.
|
|
user = self.portal_membership.getAuthenticatedMember()
|
|
if not user.has_permission('View', obj):
|
|
return self.goto(tool.getSiteUrl(), msg)
|
|
if rq.get('buttonOk.x', None) or saveConfirmed:
|
|
# Go to the consult view for this object
|
|
obj.say(msg)
|
|
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 say(self, msg, type='info'):
|
|
'''Prints a p_msg in the user interface. p_logLevel may be "info",
|
|
"warning" or "error".'''
|
|
mType = type
|
|
if mType == 'warning': mType = 'warn'
|
|
elif mType == 'error': mType = 'stop'
|
|
try:
|
|
self.plone_utils.addPortalMessage(msg, type=mType)
|
|
except UnicodeDecodeError:
|
|
self.plone_utils.addPortalMessage(msg.decode('utf-8'), type=mType)
|
|
|
|
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 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 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)
|
|
# Create the event to record in the history
|
|
DateTime = self.getProductConfig().DateTime
|
|
state = self.portal_workflow.getInfoFor(self, 'review_state')
|
|
user = self.portal_membership.getAuthenticatedMember()
|
|
event = {'action': '_datachange_', 'changes': changes,
|
|
'review_state': state, 'actor': user.id,
|
|
'time': DateTime(), 'comments': ''}
|
|
# Add the event to the history
|
|
histKey = self.workflow_history.keys()[0]
|
|
self.workflow_history[histKey] += (event,)
|
|
|
|
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.skyn.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.'''
|
|
return getattr(self, methodName, None)
|
|
|
|
def getFieldValue(self, name, onlyIfSync=False, layoutType=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]):
|
|
return appyType.getValue(self)
|
|
return None
|
|
|
|
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 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.'''
|
|
appyType = self.getAppyType(name)
|
|
return appyType.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.'''
|
|
sortedObjectsUids = self._appy_getSortedField(fieldName)
|
|
res = sortedObjectsUids.index(obj.UID())
|
|
return res
|
|
|
|
def isDebug(self):
|
|
'''Are we in debug mode ?'''
|
|
for arg in sys.argv:
|
|
if arg == 'debug-mode=on': return True
|
|
return False
|
|
|
|
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.'''
|
|
className = className or self.__class__.__name__
|
|
attrs = self.getProductConfig().attributesDict[className]
|
|
appyType = attrs.get(name, None)
|
|
if appyType and asDict: return appyType.__dict__
|
|
return appyType
|
|
|
|
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.'''
|
|
className = className or self.__class__.__name__
|
|
return self.getProductConfig().attributes[className]
|
|
|
|
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
|
|
# In debug mode, reload the module containing self's class.
|
|
debug = self.isDebug()
|
|
if debug:
|
|
klass = self.getClass(reloaded=True)
|
|
for appyType in self.getAllAppyTypes():
|
|
if debug:
|
|
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, addTitle=True):
|
|
'''Gets the Appy types named p_fieldNames. If 'title' is not among
|
|
p_fieldNames and p_addTitle is True, field 'title' is prepended to
|
|
the result.'''
|
|
res = []
|
|
for name in fieldNames:
|
|
appyType = self.getAppyType(name, asDict)
|
|
if appyType: res.append(appyType)
|
|
elif 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:
|
|
self.appy().log('Field "%s", used as shownInfo in a Ref, ' \
|
|
'was not found.' % name, type='warning')
|
|
if addTitle and ('title' not in fieldNames):
|
|
res.insert(0, self.getAppyType('title', asDict))
|
|
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.'''
|
|
res = []
|
|
dcWorkflow = self.getWorkflow(appy=False)
|
|
if not dcWorkflow: return res
|
|
currentState = self.portal_workflow.getInfoFor(self, 'review_state')
|
|
if currentOnly:
|
|
return [StateDescr(currentState,'current').get()]
|
|
workflow = self.getWorkflow(appy=True)
|
|
if workflow:
|
|
stateStatus = 'done'
|
|
for stateName in workflow._states:
|
|
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 is similar to portal_workflow.getTransitionsFor, but:
|
|
* is able (or not, depending on boolean p_includeFake) to retrieve
|
|
transitions that the user can't trigger, but for which he needs to
|
|
know for what reason he can't trigger it;
|
|
* is able (or not, depending on p_includeNotShowable) to include
|
|
transitions for which show=False at the Appy level. 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;
|
|
* the transition-info is richer: it contains fake-related info (as
|
|
described above) and confirm-related info (ie, when clicking on
|
|
the button, do we ask the user to confirm via a popup?)'''
|
|
res = []
|
|
# Get some Plone stuff from the Plone-level config.py
|
|
TRIGGER_USER_ACTION = self.getProductConfig().TRIGGER_USER_ACTION
|
|
sm = self.getProductConfig().getSecurityManager
|
|
# Get the workflow definition for p_obj.
|
|
workflow = self.getWorkflow(appy=False)
|
|
if not workflow: return res
|
|
appyWorkflow = self.getWorkflow(appy=True)
|
|
# What is the current state for this object?
|
|
currentState = workflow._getWorkflowStateOf(self)
|
|
if not currentState: return res
|
|
# Analyse all the transitions that start from this state.
|
|
for transitionId in currentState.transitions:
|
|
transition = workflow.transitions.get(transitionId, None)
|
|
appyTr = appyWorkflow._transitionsMapping[transitionId]
|
|
if not transition or (transition.trigger_type!=TRIGGER_USER_ACTION)\
|
|
or not transition.actbox_name: continue
|
|
# We have a possible candidate for a user-triggerable transition
|
|
if transition.guard is None:
|
|
mayTrigger = True
|
|
else:
|
|
mayTrigger = checkTransitionGuard(transition.guard, sm(),
|
|
workflow, self)
|
|
# 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 appyTr.isShowable(appyWorkflow, self)
|
|
if not includeIt: continue
|
|
# Add transition-info to the result.
|
|
tInfo = {'id': transition.id, 'title': transition.title,
|
|
'title_or_id': transition.title_or_id(),
|
|
'description': transition.description, 'confirm': '',
|
|
'name': transition.actbox_name, 'may_trigger': True,
|
|
'url': transition.actbox_url %
|
|
{'content_url': self.absolute_url(),
|
|
'portal_url' : '', 'folder_url' : ''}}
|
|
if appyTr.confirm:
|
|
label = '%s_confirm' % tInfo['name']
|
|
tInfo['confirm'] = self.translate(label, 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.'''
|
|
sortedObjectsUids = self._appy_getSortedField(fieldName)
|
|
oldIndex = sortedObjectsUids.index(objectUid)
|
|
sortedObjectsUids.remove(objectUid)
|
|
if isDelta:
|
|
newIndex = oldIndex + newIndex
|
|
else:
|
|
pass # To implement later on
|
|
sortedObjectsUids.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 getWorkflow(self, appy=True):
|
|
'''Returns the Appy workflow instance that is relevant for this
|
|
object. If p_appy is False, it returns the DC workflow.'''
|
|
res = None
|
|
if appy:
|
|
# Get the workflow class first
|
|
workflowClass = None
|
|
if self.wrapperClass:
|
|
appyClass = self.wrapperClass.__bases__[-1]
|
|
if hasattr(appyClass, 'workflow'):
|
|
workflowClass = appyClass.workflow
|
|
if workflowClass:
|
|
# Get the corresponding prototypical workflow instance
|
|
res = self.getProductConfig().workflowInstances[workflowClass]
|
|
else:
|
|
dcWorkflows = self.portal_workflow.getWorkflowsFor(self)
|
|
if dcWorkflows:
|
|
res = dcWorkflows[0]
|
|
return res
|
|
|
|
def getWorkflowLabel(self, stateName=None):
|
|
'''Gets the i18n label for the workflow current state. If no p_stateName
|
|
is given, workflow label is given for the current state.'''
|
|
res = ''
|
|
wf = self.getWorkflow(appy=False)
|
|
if wf:
|
|
res = stateName
|
|
if not res:
|
|
res = self.portal_workflow.getInfoFor(self, 'review_state')
|
|
appyWf = self.getWorkflow(appy=True)
|
|
if appyWf:
|
|
res = '%s_%s' % (wf.id, res)
|
|
return res
|
|
|
|
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 may(self, transitionName):
|
|
'''May the user execute transition named p_transitionName?'''
|
|
# Get the Appy workflow instance
|
|
workflow = self.getWorkflow()
|
|
res = False
|
|
if workflow:
|
|
# Get the corresponding Appy transition
|
|
transition = workflow._transitionsMapping[transitionName]
|
|
user = self.portal_membership.getAuthenticatedMember()
|
|
if isinstance(transition.condition, Role):
|
|
# It is a role. Transition may be triggered if the user has this
|
|
# role.
|
|
res = user.has_role(transition.condition.name, self)
|
|
elif type(transition.condition) == types.FunctionType:
|
|
res = transition.condition(workflow, self.appy())
|
|
elif type(transition.condition) in (tuple, list):
|
|
# It is a list of roles and or functions. Transition may be
|
|
# triggered if user has at least one of those roles and if all
|
|
# functions return True.
|
|
hasRole = None
|
|
for roleOrFunction in transition.condition:
|
|
if isinstance(roleOrFunction, basestring):
|
|
if hasRole == None:
|
|
hasRole = False
|
|
if user.has_role(roleOrFunction, self):
|
|
hasRole = True
|
|
elif type(roleOrFunction) == types.FunctionType:
|
|
if not roleOrFunction(workflow, self.appy()):
|
|
return False
|
|
if hasRole != False:
|
|
res = True
|
|
return res
|
|
|
|
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())
|
|
if self.getParentNode().get(self.id):
|
|
# Else, it means that the action has led to self's deletion.
|
|
self.reindexObject()
|
|
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'
|
|
appyType = self.getAppyType(rq['fieldName'])
|
|
label = '%s_action_%s' % (appyType.labelId, suffix)
|
|
msg = self.translate(label)
|
|
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 onTriggerTransition(self):
|
|
'''This method is called whenever a user wants to trigger a workflow
|
|
transition on an object.'''
|
|
rq = self.REQUEST
|
|
self.portal_workflow.doActionFor(self, rq['workflow_action'],
|
|
comment = rq.get('comment', ''))
|
|
self.reindexObject()
|
|
# Where to redirect the user back ?
|
|
# TODO (?): remove the "phase" param for redirecting the user to the
|
|
# next phase when relevant.
|
|
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 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 = self.REQUEST
|
|
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
|
|
|
|
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 Ref
|
|
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)
|
|
# Beyond content-type-specific "add" permissions, creators must also
|
|
# have the main permission "Add portal content".
|
|
permission = 'Add portal content'
|
|
for creators in allCreators.itervalues():
|
|
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
|
|
|
|
def _appy_getSortedField(self, fieldName):
|
|
'''Gets, for reference field p_fieldName, the Appy persistent list
|
|
that contains the sorted list of referred object UIDs. If this list
|
|
does not exist, it is created.'''
|
|
sortedFieldName = '_appy_%s' % fieldName
|
|
if not hasattr(self.aq_base, sortedFieldName):
|
|
pList = self.getProductConfig().PersistentList
|
|
exec 'self.%s = pList()' % sortedFieldName
|
|
return getattr(self, sortedFieldName)
|
|
|
|
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 = '/skyn/%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 = 'skyn/%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 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 format in ('html', 'xhtml'):
|
|
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, className=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. If p_className is not given, we consider p_self being an
|
|
instance of the class where p_field is defined.'''
|
|
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:
|
|
# Maybe we do not have the field itself, but only its name
|
|
if isinstance(field, basestring):
|
|
field = self.getAppyType(field, className=className)
|
|
# Include field-specific mapping if any.
|
|
fieldMapping = field.mapping[label]
|
|
if fieldMapping:
|
|
if callable(fieldMapping):
|
|
fieldMapping = field.callMethod(self, fieldMapping)
|
|
mapping.update(fieldMapping)
|
|
# Get the label
|
|
label = getattr(field, label+'Id')
|
|
# 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():
|
|
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, skyn, templateName):
|
|
'''Returns, in the skyn folder, the page template corresponding to
|
|
p_templateName.'''
|
|
res = skyn
|
|
for name in templateName.split('/'):
|
|
res = res.get(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, 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 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
|
|
# ------------------------------------------------------------------------------
|