appypod-rattail/gen/mixins/__init__.py

1796 lines
82 KiB
Python
Raw Normal View History

2009-06-29 07:06:01 -05:00
'''This package contains mixin classes that are mixed in with generated classes:
- mixins/BaseMixin is mixed in with standard Zope classes;
- mixins/ToolMixin is mixed in with the generated application Tool class.'''
2009-06-29 07:06:01 -05:00
# ------------------------------------------------------------------------------
import os, os.path, re, sys, types, urllib, cgi
from appy import Object
from appy.px import Px
2013-09-24 05:26:31 -05:00
from appy.fields.workflow import UiTransition
import appy.gen as gen
from appy.gen.utils import *
from appy.gen.layout import Table
from appy.gen.descriptors import WorkflowDescriptor, ClassDescriptor
from appy.shared import utils as sutils
from appy.shared.data import rtlLanguages
2014-08-07 07:17:21 -05:00
from appy.shared.xml_parser import XmlMarshaller, XmlUnmarshaller
from appy.shared.diff import HtmlDiff
2009-06-29 07:06:01 -05:00
# ------------------------------------------------------------------------------
NUMBERED_ID = re.compile('.+\d{4}$')
2009-06-29 07:06:01 -05:00
# ------------------------------------------------------------------------------
class BaseMixin:
2011-11-25 11:01:20 -06:00
'''Every Zope class generated by appy.gen inherits from this class or a
subclass of it.'''
_appy_meta_type = 'Class'
2009-06-29 07:06:01 -05:00
def get_o(self):
'''In some cases, we want the Zope object, we don't know if the current
object is a Zope or Appy object. By defining this property,
"someObject.o" produces always the Zope object, be someObject an Appy
or Zope object.'''
return self
o = property(get_o)
def getInitiatorInfo(self, appy=False):
'''Gets information about a potential initiator object from the request.
Returns a 2-tuple (initiator, field):
* initiator is the initiator (Zope or Appy) object;
* field is the Ref instance.
'''
rq = self.REQUEST
if not rq.get('nav', '').startswith('ref.'): return None, None
splitted = rq['nav'].split('.')
initiator = self.getTool().getObject(splitted[1])
field = initiator.getAppyType(splitted[2])
if appy: initiator = initiator.appy()
return initiator, field
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.
2011-11-25 11:01:20 -06:00
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
2011-11-25 11:01:20 -06:00
if created and rq:
# Create the final object and put it at the right place.
tool = self.getTool()
# The app may define a method klass.generateUid for producing an UID
# for instance of this class. If no such method is found, we use the
# standard Appy method to produce an UID.
id = None
klass = tool.getAppyClass(obj.portal_type)
if hasattr(klass, 'generateUid'):
id = klass.generateUid(obj.REQUEST)
if not id:
id = tool.generateUid(obj.portal_type)
2011-11-25 11:01:20 -06:00
if not initiator:
folder = tool.getPath('/data')
else:
folder = initiator.getCreateFolder()
# Check that the user can add objects through this Ref.
initiatorField.checkAdd(initiator)
2011-11-25 11:01:20 -06:00
obj = createObject(folder, id, obj.portal_type, tool.getAppName())
# Get the fields on the current page
fields = None
if rq: fields = self.getAppyTypes('edit', rq.get('page'))
# Remember the previous values of fields, for potential historization
previousData = None
if not created and fields:
previousData = obj.rememberPreviousData(fields)
2011-11-25 11:01:20 -06:00
# Perform the change on the object
if fields:
# Store in the database the new value coming from the form
for field in fields:
value = getattr(values, field.name, None)
field.store(obj, value)
if previousData:
# Keep in history potential changes on historized fields
2011-11-25 11:01:20 -06:00
obj.historizeData(previousData)
# Call the custom "onEditEarly" if available. This method is called
# *before* potentially linking the object to its initiator.
appyObject = obj.appy()
if created and hasattr(appyObject, 'onEditEarly'):
appyObject.onEditEarly()
# Manage potential link with an initiator object
if created and initiator:
initiator.appy().link(initiatorField.name, appyObject)
# Call the custom "onEdit" if available
msg = None # The message to display to the user. It can be set by onEdit
if hasattr(appyObject, 'onEdit'): msg = appyObject.onEdit(created)
# Update last modification date
if not created:
from DateTime import DateTime
obj.modified = DateTime()
# Unlock the currently saved page on the object
if rq: self.removeLock(rq['page'])
2011-11-25 11:01:20 -06:00
obj.reindex()
return obj, msg
def updateField(self, name, value):
'''Updates a single field p_name with new p_value.'''
field = self.getAppyType(name)
# Remember previous value if the field is historized.
previousData = self.rememberPreviousData([field])
# Store the new value into the database
field.store(self, value)
# Update the object history when relevant
if previousData: self.historizeData(previousData)
# Update last modification date
from DateTime import DateTime
self.modified = DateTime()
def delete(self):
'''This method is self's suicide.'''
# Call a custom "onDelete" if it exists
appyObj = self.appy()
if hasattr(appyObj, 'onDelete'): appyObj.onDelete()
# Any people referencing me must forget me now
for field in self.getAllAppyTypes():
if field.type != 'Ref': continue
for obj in field.getValue(self):
field.back.unlinkObject(obj, appyObj, back=True)
2011-11-28 15:50:01 -06:00
# Uncatalog the object
self.reindex(unindex=True)
# Delete the filesystem folder corresponding to this object
folder = os.path.join(*self.getFsFolder())
if os.path.exists(folder):
sutils.FolderDeleter.delete(folder)
sutils.FolderDeleter.deleteEmpty(os.path.dirname(folder))
# Delete the object
self.getParentNode().manage_delObjects([self.id])
def onDelete(self):
'''Called when an object deletion is triggered from the ui'''
rq = self.REQUEST
tool = self.getTool()
obj = tool.getObject(rq['uid'])
obj.delete()
msg = obj.translate('action_done')
# If we are called from an Ajax request, simply return msg
if hasattr(rq, 'pxContext') and rq.pxContext['ajax']: return msg
if obj.getUrl(rq['HTTP_REFERER'], mode='raw') == obj.getUrl(mode='raw'):
# We were consulting the object that has been deleted. Go back to
# the main page.
urlBack = tool.getSiteUrl()
else:
urlBack = obj.getUrl(rq['HTTP_REFERER'])
obj.say(msg)
obj.goto(urlBack)
def onDeleteEvent(self):
'''Called when an event (from object history) deletion is triggered
from the ui.'''
rq = self.REQUEST
# Re-create object history, but without the event corresponding to
# rq['eventTime']
history = []
from DateTime import DateTime
eventToDelete = DateTime(rq['eventTime'])
for event in self.workflow_history['appy']:
if (event['action'] != '_datachange_') or \
(event['time'] != eventToDelete):
history.append(event)
self.workflow_history['appy'] = tuple(history)
appy = self.appy()
self.log('data change event deleted for %s.' % appy.uid)
self.goto(self.getUrl(rq['HTTP_REFERER']))
def onLink(self):
'''Called when object (un)linking is triggered from the ui.'''
rq = self.REQUEST
tool = self.getTool()
sourceObject = tool.getObject(rq['sourceUid'])
field = sourceObject.getAppyType(rq['fieldName'])
return field.onUiRequest(sourceObject, rq)
def onCreate(self):
'''This method is called when a user wants to create a root object in
2011-11-25 11:01:20 -06:00
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
2011-11-25 11:01:20 -06:00
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':'',
'inPopup':rq.get('popup') == '1'}
initiator, initiatorField = self.getInitiatorInfo()
if initiator:
# The object to create will be linked to an initiator object through
# a Ref field. We create here a new navigation string with one more
# item, that will be the currently created item.
splitted = rq.get('nav').split('.')
splitted[-1] = splitted[-2] = str(int(splitted[-1])+1)
urlParams['nav'] = '.'.join(splitted)
# Check that the user can add objects through this Ref field
initiatorField.checkAdd(initiator)
2011-11-25 11:01:20 -06:00
# Create a temp object in /temp_folder
tool = self.getTool()
id = tool.generateUid(className)
appName = tool.getAppName()
obj = createObject(tool.getPath('/temp_folder'), id, className, appName)
return self.goto(obj.getUrl(**urlParams))
def getDbFolder(self):
'''Gets the folder, on the filesystem, where the database (Data.fs and
sub-folders) lies.'''
return os.path.dirname(self.getTool().getApp()._p_jar.db().getName())
def getFsFolder(self, create=False):
'''Gets the folder where binary files tied to this object will be stored
on the filesystem. If p_create is True and the folder does not exist,
it is created (together with potentially missing parent folders).
This folder is returned as a tuple (s_baseDbFolder, s_subPath).'''
objId = self.id
# Get the root folder where Data.fs lies.
dbFolder = self.getDbFolder()
# Build the list of path elements within this db folder.
path = []
inConfig = False
for elem in self.getPhysicalPath():
if not elem: continue
if elem == 'data': continue
if elem == 'config': inConfig = True
if not path or ((len(path) == 1) and inConfig):
# This object is at the root of the filesystem.
if NUMBERED_ID.match(elem):
path.append(elem[-4:])
path.append(elem)
# We are done if elem corresponds to the object id.
if elem == objId: break
path = os.sep.join(path)
if create:
fullPath = os.path.join(dbFolder, path)
if not os.path.exists(fullPath): os.makedirs(fullPath)
return dbFolder, path
def view(self):
'''Returns the view PX.'''
2013-08-21 05:35:30 -05:00
obj = self.appy()
return obj.pxView({'obj': obj, 'tool': obj.tool})
def edit(self):
'''Returns the edit PX.'''
2013-08-21 05:35:30 -05:00
obj = self.appy()
return obj.pxEdit({'obj': obj, 'tool': obj.tool})
def ajax(self):
'''Called via an Ajax request to render some PX whose name is in the
request.'''
obj = self.appy()
return obj.pxAjax({'obj': obj, 'tool': obj.tool})
def setLock(self, user, page):
'''A p_user edits a given p_page on this object: we will set a lock, to
prevent other users to edit this page at the same time.'''
if not hasattr(self.aq_base, 'locks'):
# Create the persistent mapping that will store the lock
2013-01-18 04:26:01 -06:00
# ~{s_page: (s_userId, DateTime_lockDate)}~
from persistent.mapping import PersistentMapping
self.locks = PersistentMapping()
# Raise an error is the page is already locked by someone else. If the
# page is already locked by the same user, we don't mind: he could have
# used back/forward buttons of its browser...
2013-08-21 05:35:30 -05:00
userId = user.login
if (page in self.locks) and (userId != self.locks[page][0]):
2014-05-03 15:45:51 -05:00
self.raiseUnauthorized('This page is locked.')
# Set the lock
2013-01-18 04:26:01 -06:00
from DateTime import DateTime
self.locks[page] = (userId, DateTime())
def isLocked(self, user, page):
'''Is this page locked? If the page is locked by the same user, we don't
mind and consider the page as unlocked. If the page is locked, this
2013-01-18 04:26:01 -06:00
method returns the tuple (userId, lockDate).'''
if hasattr(self.aq_base, 'locks') and (page in self.locks):
2013-08-21 05:35:30 -05:00
if (user.login != self.locks[page][0]): return self.locks[page]
def removeLock(self, page, force=False):
'''Removes the lock on the current page. This happens:
- after the page has been saved: the lock must be released;
- or when an admin wants to force the deletion of a lock that was
left on p_page for too long (p_force=True).
'''
if page not in self.locks: return
# Raise an error if the user that saves changes is not the one that
# has locked the page (excepted if p_force is True)
if not force:
2013-08-21 05:35:30 -05:00
userId = self.getTool().getUser().login
if self.locks[page][0] != userId:
2014-05-03 15:45:51 -05:00
self.raiseUnauthorized('This page was locked by someone else.')
# Remove the lock
del self.locks[page]
def removeMyLock(self, user, page):
'''If p_user has set a lock on p_page, this method removes it. This
method is called when the user that locked a page consults
2013-08-21 15:25:27 -05:00
pxView for this page. In this case, we consider that the user has
left the edit page in an unexpected way and we remove the lock.'''
if hasattr(self.aq_base, 'locks') and (page in self.locks) and \
2013-08-21 05:35:30 -05:00
(user.login == self.locks[page][0]):
del self.locks[page]
def onUnlock(self):
'''Called when an admin wants to remove a lock that was left for too
long by some user.'''
rq = self.REQUEST
tool = self.getTool()
obj = tool.getObject(rq['objectUid'])
obj.removeLock(rq['pageName'], force=True)
urlBack = self.getUrl(rq['HTTP_REFERER'])
self.say(self.translate('action_done'))
self.goto(urlBack)
def intraFieldValidation(self, errors, values):
'''This method performs field-specific validation for every field from
the page that is being created or edited. For every field whose
validation generates an error, we add an entry in p_errors. For every
field, we add in p_values an entry with the "ready-to-store" field
value.'''
rq = self.REQUEST
for field in self.getAppyTypes('edit', rq.form.get('page')):
if not field.validable or not field.isClientVisible(self): continue
value = field.getRequestValue(self)
message = field.validate(self, value)
if message:
setattr(errors, field.name, message)
else:
setattr(values, field.name, field.getStorableValue(self, value))
# Validate sub-fields within Lists
if field.type != 'List': continue
i = -1
for row in value:
i += 1
for name, subField in field.fields:
message = subField.validate(self, getattr(row,name,None))
if message:
setattr(errors, '%s*%d' % (subField.name, i), message)
def interFieldValidation(self, errors, values):
'''This method is called when individual validation of all fields
succeed (when editing or creating an object). Then, this method
performs inter-field validation. This way, the user must first
correct individual fields before being confronted to potential
inter-field validation errors.'''
obj = self.appy()
if not hasattr(obj, 'validate'): return
msg = obj.validate(values, errors)
# Those custom validation methods may have added fields in the given
# p_errors object. Within this object, for every error message that is
# not a string, we replace it with the standard validation error for the
# corresponding field.
for key, value in errors.__dict__.iteritems():
resValue = value
if not isinstance(resValue, basestring):
resValue = self.translate('field_invalid')
setattr(errors, key, resValue)
return msg
def onUpdate(self):
'''This method is executed when a user wants to update an object.
2011-11-25 11:01:20 -06:00
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()
2012-06-03 11:34:56 -05:00
errorMessage = self.translate('validation_error')
2012-12-18 15:49:26 -06:00
isNew = self.isTemporary()
inPopup = rq.get('popup') == '1'
# If this object is created from an initiator, get info about him
initiator, initiatorField = self.getInitiatorInfo()
initiatorPage = initiatorField and initiatorField.pageName or None
# If the user clicked on 'Cancel', go back to the previous page
buttonClicked = rq.get('button')
if buttonClicked == 'cancel':
if inPopup: back = tool.backFromPopup()
elif initiator: # Go back to the initiator page
urlBack = initiator.getUrl(page=initiatorPage, nav='')
else:
if isNew: urlBack = tool.getHomePage() # Go back to home page
else:
# Return to the same page, excepted if unshowable on view
phaseObj = self.getAppyPhases(True, 'view')
pageInfo = phaseObj.getPageInfo(rq['page'], 'view')
if not pageInfo: urlBack = tool.getHomePage()
else: urlBack = self.getUrl(page=pageInfo.page.name)
self.removeLock(rq['page'])
2015-02-05 07:05:29 -06:00
self.say(self.translate('object_canceled'))
if inPopup: return back
return self.goto(urlBack)
# Object for storing validation errors
errors = Object()
# Object for storing the (converted) values from the request
values = Object()
# Trigger field-specific validation
self.intraFieldValidation(errors, values)
if errors.__dict__:
for k,v in errors.__dict__.iteritems(): rq.set('%s_error' % k, v)
self.say(errorMessage)
return self.gotoEdit()
# Trigger inter-field validation
msg = self.interFieldValidation(errors, values)
if not msg: msg = errorMessage
if errors.__dict__:
for k,v in errors.__dict__.iteritems(): rq.set('%s_error' % k, v)
self.say(msg)
return self.gotoEdit()
# Before saving data, must we ask a confirmation by the user ?
appyObj = self.appy()
saveConfirmed = rq.get('confirmed') == 'True'
if hasattr(appyObj, 'confirm') and not saveConfirmed:
msg = appyObj.confirm(values)
if msg:
rq.set('confirmMsg', msg.replace("'", "\\'"))
return self.gotoEdit()
# Create or update the object in the database
obj, msg = self.createOrUpdate(isNew, values, initiator, initiatorField)
# Redirect the user to the appropriate page
2012-06-03 11:34:56 -05:00
if not msg: msg = self.translate('object_saved')
# If the object has already been deleted (ie, it is a kind of transient
# object like a one-shot form and has already been deleted in method
# onEdit) or if the user can't access the object anymore, redirect him
# to the user's home page.
if not getattr(obj.getParentNode().aq_base, obj.id, None) or \
not obj.mayView():
if inPopup: return tool.backFromPopup()
return self.goto(tool.getHomePage(), msg)
if (buttonClicked == 'save') or saveConfirmed:
obj.say(msg)
if inPopup: return tool.backFromPopup()
if isNew and initiator:
return self.goto(initiator.getUrl(page=initiatorPage, nav=''))
# Return to the same page, if showable on view
phaseObj = self.getAppyPhases(True, 'view')
pageInfo = phaseObj.getPageInfo(rq['page'], 'view')
if not pageInfo: return self.goto(tool.getHomePage(), msg)
return self.goto(obj.getUrl(page=pageInfo.page.name))
# Get the current page name. We keep it in "pageName" because rq['page']
# can be changed by m_getAppyPhases called below.
pageName = rq['page']
if buttonClicked in ('previous', 'next'):
# Go to the previous or next page for this object. We recompute the
# list of phases and pages because things may have changed since the
# object has been updated (ie, additional pages may be shown or
# hidden now, so the next and previous pages may have changed).
# Moreover, previous and next pages may not be available in "edit"
# mode, so we return the edit or view pages depending on page.show.
phaseObj = self.getAppyPhases(True, 'edit')
methodName = 'get%sPage' % buttonClicked.capitalize()
pageName, pageInfo = getattr(phaseObj, methodName)(pageName)
if pageName:
# Return to the edit or view page?
2013-08-21 05:35:30 -05:00
if pageInfo.showOnEdit:
2012-12-18 15:49:26 -06:00
# I do not use gotoEdit here because I really need to
# redirect the user to the edit page. Indeed, the object
# edit URL may have moved from temp_folder to another place.
return self.goto(obj.getUrl(mode='edit', page=pageName,
inPopup=inPopup))
else:
return self.goto(obj.getUrl(page=pageName, inPopup=inPopup))
else:
obj.say(msg)
return self.goto(obj.getUrl(inPopup=inPopup))
return obj.gotoEdit()
2011-11-25 11:01:20 -06:00
def reindex(self, indexes=None, unindex=False):
'''Reindexes this object the catalog. If names of indexes are specified
in p_indexes, recataloging is limited to those indexes. If p_unindex
is True, instead of cataloguing the object, it uncatalogs it.'''
path = '/'.join(self.getPhysicalPath())
2011-11-25 11:01:20 -06:00
catalog = self.getPhysicalRoot().catalog
if unindex:
catalog.uncatalog_object(path)
2011-11-25 11:01:20 -06:00
else:
2011-11-28 15:50:01 -06:00
if indexes:
catalog.catalog_object(self, path, idxs=indexes)
2011-11-28 15:50:01 -06:00
else:
# Get the list of indexes that apply on this object. Else, Zope
# will reindex all indexes defined in the catalog, and through
# acquisition, wrong methods can be called on wrong objects.
iNames = self.wrapperClass.getIndexes().keys()
catalog.catalog_object(self, path, idxs=iNames)
2011-11-25 11:01:20 -06:00
def xml(self, action=None):
'''If no p_action is defined, this method returns the XML version of
this object. Else, it calls method named p_action on the
corresponding Appy wrapper and returns, as XML, its result.'''
self.REQUEST.RESPONSE.setHeader('Content-Type','text/xml;charset=utf-8')
# Check if the user is allowed to consult this object
2014-05-03 15:45:51 -05:00
if not self.mayView(): return XmlMarshaller().marshall('Unauthorized')
if not action:
marshaller = XmlMarshaller(rootTag=self.getClass().__name__,
dumpUnicode=True)
res = marshaller.marshall(self, objectType='appy')
else:
appyObj = self.appy()
try:
methodRes = getattr(appyObj, action)()
if isinstance(methodRes, Px):
res = methodRes({'self': self.appy()})
elif isinstance(methodRes, file):
res = methodRes.read()
methodRes.close()
elif isinstance(methodRes, basestring) and \
methodRes.startswith('<?xml'): # Already XML
return methodRes
else:
marshaller = XmlMarshaller()
oType = isinstance(methodRes, Object) and 'popo' or 'appy'
res = marshaller.marshall(methodRes, objectType=oType)
except Exception, e:
tb = sutils.Traceback.get()
res = XmlMarshaller(rootTag='exception').marshall(tb)
self.log(tb, type='error')
import transaction
transaction.abort()
return res
def say(self, msg, type='info'):
'''Prints a p_msg in the user interface. p_logLevel may be "info",
"warning" or "error".'''
2011-11-25 11:01:20 -06:00
rq = self.REQUEST
2011-12-05 11:15:45 -06:00
if 'messages' not in rq.SESSION.keys():
plist = self.getProductConfig().PersistentList
messages = rq.SESSION['messages'] = plist()
2011-11-25 11:01:20 -06:00
else:
2011-12-05 11:15:45 -06:00
messages = rq.SESSION['messages']
messages.append( (type, msg) )
def log(self, msg, type='info'):
'''Logs a p_msg in the log file. p_logLevel may be "info", "warning"
or "error".'''
logger = self.getProductConfig().logger
if type == 'warning': logMethod = logger.warn
elif type == 'error': logMethod = logger.error
else: logMethod = logger.info
user = self.getTool().getUser()
# There could be not user at all (even not "anon") if we are trying to
# authenticate an inexistent user for example.
login = user and user.login or 'anon'
logMethod('%s: %s' % (login, msg))
2011-11-25 11:01:20 -06:00
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:
2011-11-25 11:01:20 -06:00
obj = self
2012-05-09 02:45:15 -05:00
if rq.get('appy', None) == '1': obj = obj.appy()
return getattr(obj, 'on'+action)()
2014-05-03 15:45:51 -05:00
def raiseUnauthorized(self, msg=None):
'''Raise an error "Unauthorized access".'''
from AccessControl import Unauthorized
if msg: raise Unauthorized(msg)
raise Unauthorized()
def rememberPreviousData(self, fields):
'''This method is called before updating an object and remembers, for
every historized field, the previous value. Result is a dict
~{s_fieldName: previousFieldValue}~'''
res = {}
for field in fields:
if not field.getAttribute(self, 'historized'): continue
res[field.name] = field.getValue(self)
return res
def addHistoryEvent(self, action, **kw):
'''Adds an event in the object history.'''
2013-08-21 05:35:30 -05:00
user = self.getTool().getUser()
userId = user and user.login or 'system'
from DateTime import DateTime
event = {'action': action, 'actor': userId, 'time': DateTime(),
'comments': ''}
event.update(kw)
2011-11-25 11:01:20 -06:00
if 'review_state' not in event: event['review_state'] = self.State()
# Add the event to the history
self.workflow_history['appy'] += (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.
For a multilingual string field, p_changes can contain a key for
every language, of the form <field name>-<language>.'''
# Add to the p_changes dict the field labels
for name in changes.keys():
# "name" can contain the language for multilingual fields.
if '-' in name:
fieldName, lg = name.split('-')
else:
fieldName = name
lg = None
field = self.getAppyType(fieldName)
if notForPreviouslyEmptyValues:
# Check if the previous field value was empty
if lg:
isEmpty = not changes[name] or not changes[name].get(lg)
else:
isEmpty = field.isEmptyValue(self, changes[name])
if isEmpty:
del changes[name]
else:
changes[name] = (changes[name], field.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 name in previousData.keys():
field = self.getAppyType(name)
prev = previousData[name]
curr = field.getValue(self)
try:
if (prev == curr) or ((prev == None) and (curr == '')) or \
((prev == '') and (curr == None)):
del previousData[name]
continue
except UnicodeDecodeError, ude:
# The string comparisons above may imply silent encoding-related
# conversions that may produce this exception.
continue
# In some cases the old value must be formatted.
if field.type == 'Ref':
previousData[name] = [r.o.getShownValue('title') \
for r in previousData[name]]
elif field.type == 'String':
languages = field.getAttribute(self, 'languages')
if len(languages) > 1:
# Consider every language-specific value as a first-class
# value.
del previousData[name]
for lg in languages:
lgPrev = prev and prev.get(lg) or None
lgCurr = curr and curr.get(lg) or None
if lgPrev == lgCurr: continue
previousData['%s-%s' % (name, lg)] = lgPrev
if previousData:
self.addDataChange(previousData)
def goto(self, url, msg=None):
'''Brings the user to some p_url after an action has been executed.'''
2011-12-05 11:15:45 -06:00
if msg: self.say(msg)
return self.REQUEST.RESPONSE.redirect(url)
def gotoEdit(self):
'''Brings the user to the edit page for this object. This method takes
care of not carrying any password value. Unlike m_goto above, there
2013-08-21 05:35:30 -05:00
is no HTTP redirect here: we execute directly PX "edit" and we
return the result.'''
page = self.REQUEST.get('page', 'main')
for field in self.getAppyTypes('edit', page):
if (field.type == 'String') and (field.format in (3,4)):
self.REQUEST.set(field.name, '')
2013-08-21 05:35:30 -05:00
return self.edit()
def gotoTied(self):
'''Redirects the user to an object tied to this one.'''
return self.getAppyType(self.REQUEST['field']).onGotoTied(self)
def getCreateFolder(self):
'''When an object must be created from this one through a Ref field, we
must know where to put the newly create object: within this one if it
is folderish, besides this one in its parent else.
'''
if self.isPrincipiaFolderish: return self
return self.getParentNode()
def getFieldValue(self, name, layoutType=None, outerValue=None):
'''Returns the database value of field named p_name for p_self.'''
if layoutType == 'search': return # No object in search screens
field = self.getAppyType(name)
if field.type == 'Pod': return
if '*' not in name: return field.getValue(self)
# The field is an inner field from a List.
listName, name, i = name.split('*')
listType = self.getAppyType(listName)
return listType.getInnerValue(self, outerValue, name, int(i))
def getRequestFieldValue(self, name):
'''Gets the value of field p_name as may be present in the request.'''
# Return the request value for standard fields.
if '*' not in name:
return self.getAppyType(name).getRequestValue(self)
# For sub-fields within Lists, the corresponding request values have
# already been computed in the request key corresponding to the whole
# List.
listName, name, rowIndex = name.split('*')
rowIndex = int(rowIndex)
if rowIndex == -1: return ''
allValues = self.REQUEST.get(listName)
if not allValues: return ''
return getattr(allValues[rowIndex], name, '')
def isDebug(self):
'''Are we in debug mode ?'''
for arg in sys.argv:
if arg == 'debug-mode=on': return True
def getClass(self, reloaded=False):
'''Returns the Appy class that dictates self's behaviour.'''
if not reloaded:
return self.getTool().getAppyClass(self.__class__.__name__)
else:
klass = self.appy().klass
moduleName = klass.__module__
exec 'import %s' % moduleName
exec 'reload(%s)' % moduleName
exec 'res = %s.%s' % (moduleName, klass.__name__)
# More manipulations may have occurred in m_update
if hasattr(res, 'update'):
parentName= res.__bases__[-1].__name__
moduleName= 'Products.%s.wrappers' % self.getTool().getAppName()
exec 'import %s' % moduleName
exec 'parent = %s.%s' % (moduleName, parentName)
res.update(parent)
return res
2013-08-21 05:35:30 -05:00
def getAppyType(self, name, className=None):
'''Returns the Appy type named p_name. If no p_className is defined, the
field is supposed to belong to self's class.'''
isInnerType = '*' in name # An inner type lies within a List type.
subName = None
2012-03-01 10:35:23 -06:00
if isInnerType:
elems = name.split('*')
if len(elems) == 2: name, subName = elems
else: name, subName, i = elems
if not className:
klass = self.__class__.wrapperClass
else:
klass = self.getTool().getAppyClass(className, wrapper=True)
res = getattr(klass, name, None)
if res and isInnerType: res = res.getField(subName)
return res
def getAllAppyTypes(self, className=None):
'''Returns the ordered list of all Appy types for self's class if
p_className is not specified, or for p_className else.'''
if not className:
klass = self.__class__.wrapperClass
else:
klass = self.getTool().getAppyClass(className, wrapper=True)
return klass.__fields__
2013-08-21 05:35:30 -05:00
def getGroupedFields(self, layoutType, pageName, cssJs=None):
'''Returns the fields sorted by group. If a dict is given in p_cssJs,
we will add it in the css and js files required by the fields.'''
2009-06-29 07:06:01 -05:00
res = []
groups = {} # The already encountered groups
# If a dict is given in p_cssJs, we must fill it with the CSS and JS
2013-08-21 05:35:30 -05:00
# files required for every returned field.
collectCssJs = isinstance(cssJs, dict)
css = js = None
config = self.getProductConfig(True)
# If param "refresh" is there, we must reload the Python class
refresh = ('refresh' in self.REQUEST)
if refresh: klass = self.getClass(reloaded=True)
2013-08-21 05:35:30 -05:00
for field in self.getAllAppyTypes():
if refresh: field = field.reload(klass, self)
if field.page.name != pageName: continue
if not field.isShowable(self, layoutType): continue
if collectCssJs:
if css == None: css = []
field.getCss(layoutType, css, config)
if js == None: js = []
field.getJs(layoutType, js, config)
2013-08-21 05:35:30 -05:00
if not field.group:
res.append(field)
2009-06-29 07:06:01 -05:00
else:
# Insert the UiGroup instance corresponding to field.group
2013-08-21 05:35:30 -05:00
uiGroup = field.group.insertInto(res, groups, field.page,
self.meta_type)
2013-09-24 05:26:31 -05:00
uiGroup.addElement(field)
if collectCssJs:
2013-08-21 05:35:30 -05:00
cssJs['css'] = css or ()
cssJs['js'] = js or ()
2009-06-29 07:06:01 -05:00
return res
def getAppyTypes(self, layoutType, pageName, type=None):
'''Returns the list of fields that belong to a given page (p_pageName)
for a given p_layoutType. If p_pageName is None, fields of all pages
are returned. If p_type is defined, only fields of this p_type are
returned. '''
2009-06-29 07:06:01 -05:00
res = []
for field in self.getAllAppyTypes():
if pageName and (field.page.name != pageName): continue
if type and (field.type != type): continue
if not field.isRenderable(layoutType): continue
if not field.isShowable(self, layoutType): continue
res.append(field)
2009-06-29 07:06:01 -05:00
return res
def getSlavesRequestInfo(self, pageName):
'''When slave fields must be updated via Ajax requests, we must carry
some information from the global request object to the ajax requests:
- the selected values in slave fields;
- validation errors.'''
requestValues = {}
errors = {}
req = self.REQUEST
for field in self.getAllAppyTypes():
if field.page.name != pageName: continue
if field.masterValue and callable(field.masterValue):
# We have a slave field that is updated via ajax requests.
name = field.name
# Remember the request value for this field if present.
if req.has_key(name) and req[name]:
requestValues[name] = req[name]
# Remember the validation error for this field if present.
errorKey = '%s_error' % name
if req.has_key(errorKey):
errors[name] = req[errorKey]
return sutils.getStringDict(requestValues), sutils.getStringDict(errors)
def getCssJs(self, fields, layoutType, res):
'''Gets, in p_res ~{'css':[s_css], 'js':[s_js]}~ the lists of
Javascript and CSS files required by Appy types p_fields when shown
on p_layoutType.'''
2012-03-01 10:35:23 -06:00
# 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 = []
config = self.getProductConfig(True)
for field in fields:
field.getCss(layoutType, css, config)
field.getJs(layoutType, js, config)
res['css'] = css
res['js'] = js
def getCssFor(self, elem):
'''Gets the name of the CSS class to use for styling some p_elem. If
self's class does not define a dict "styles", the defaut CSS class
to use will be named p_elem.'''
klass = self.getClass()
if hasattr(klass, 'styles') and (elem in klass.styles):
return klass.styles[elem]
return elem
def getAppyPhases(self, currentOnly=False, layoutType='view'):
2009-06-29 07:06:01 -05:00
'''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.'''
2009-06-29 07:06:01 -05:00
# Get the list of phases
res = [] # Ordered list of phases
phases = {} # Dict of phases
2013-08-21 05:35:30 -05:00
for field in self.getAllAppyTypes():
fieldPhase = field.page.phase
if fieldPhase not in phases:
phase = gen.Phase(fieldPhase, self)
res.append(phase)
phases[fieldPhase] = phase
2009-06-29 07:06:01 -05:00
else:
2013-08-21 05:35:30 -05:00
phase = phases[fieldPhase]
phase.addPage(field, self, layoutType)
if (field.type == 'Ref') and field.navigable:
phase.addPageLinks(field, self)
2009-06-29 07:06:01 -05:00
# Remove phases that have no visible page
for i in range(len(res)-1, -1, -1):
2013-08-21 05:35:30 -05:00
if not res[i].pages:
del phases[res[i].name]
2009-06-29 07:06:01 -05:00
del res[i]
# Compute next/previous phases of every phase
2009-06-29 07:06:01 -05:00
for ph in phases.itervalues():
ph.computeNextPrevious(res)
2009-06-29 07:06:01 -05:00
ph.totalNbOfPhases = len(res)
# Restrict the result to the current phase if required
2009-06-29 07:06:01 -05:00
if currentOnly:
rq = self.REQUEST
page = rq.get('page', None)
if not page:
if layoutType == 'edit': page = self.getDefaultEditPage()
else: page = self.getDefaultViewPage()
2013-08-21 05:35:30 -05:00
for phase in res:
if page in phase.pages:
return phase
# If I am here, it means that the page as defined in the request,
# or the default page, is not existing nor visible in any phase.
# In this case I find the first visible page among all phases.
viewAttr = 'showOn%s' % layoutType.capitalize()
for phase in res:
2013-08-21 05:35:30 -05:00
for page in phase.pages:
if getattr(phase.pagesInfo[page], viewAttr):
rq.set('page', page)
pageFound = True
break
return phase
2009-06-29 07:06:01 -05:00
else:
# Return an empty list if we have a single, link-free page within
# a single phase.
2013-08-21 05:35:30 -05:00
if (len(res) == 1) and (len(res[0].pages) == 1) and \
not res[0].pagesInfo[res[0].pages[0]].links:
return
2009-06-29 07:06:01 -05:00
return res
def highlight(self, text):
'''This method highlights parts of p_value if we are in the context of
a query whose keywords must be highlighted.'''
# Must we highlight something?
criteria = self.REQUEST.SESSION.get('searchCriteria')
if not criteria or ('SearchableText' not in criteria): return text
# Highlight every variant of every keyword
for word in criteria['SearchableText'].strip().split():
sWord = word.strip(' *').lower()
for variant in (sWord, sWord.capitalize(), sWord.upper()):
text = text.replace(variant, '***+%s-***' % variant)
# If I replace immediately with the opening and ending "span"
# tag (see below), I will have problems if the next keyword is
# included in the tag, ie "la" (in 'class', or "spa" (in
# 'span').
text = text.replace('***+', '<span class="highlight">')
return text.replace('-***', '</span>')
2013-04-11 09:01:52 -05:00
def getSupTitle(self, navInfo=''):
'''Gets the html code (icons,...) that can be shown besides the title
of an object.'''
obj = self.appy()
if hasattr(obj, 'getSupTitle'): return obj.getSupTitle(navInfo)
return ''
def getSubTitle(self):
'''Gets the content that must appear below the title of an object.'''
obj = self.appy()
if hasattr(obj, 'getSubTitle'): return obj.getSubTitle()
return ''
def getSupBreadCrumb(self):
'''Gets the html code that can be shown besides the title of an object
in the breadcrumb.'''
obj = self.appy()
if hasattr(obj, 'getSupBreadCrumb'): return obj.getSupBreadCrumb()
return ''
def getSubBreadCrumb(self):
'''Gets the content that must appear below the title of an object in the
breadcrumb.'''
obj = self.appy()
if hasattr(obj, 'getSubBreadCrumb'): return obj.getSubBreadCrumb()
return ''
def getListTitle(self, mode='link', nav='', target=None, page='main',
inPopup=False, selectJs=None, highlight=False):
'''Gets the title as it must appear in lists of objects (ie in lists of
tied objects in a Ref, in query results...).
In most cases, a title must appear as a link that leads to the object
view layout. In this case (p_mode == "link"):
* p_nav is the navigation parameter allowing navigation between
this object and others;
* p_target specifies if the link must be opened in the popup or not;
* p_page specifies which page to show on the target object view;
* p_inPopup indicates if we are already in the popup or not.
Another p_mode is "select". In this case, we are in a popup for
selecting objects: every title must not be a link, but clicking on it
must trigger Javascript code (in p_selectJs) that will select this
object.
The last p_mode is "text". In this case, we simply show the object
title but with no tied action (link, select).
If p_highlight is True, keywords will be highlighted if we are in the
context of a query with keywords.
'''
# Compute common parts
cssClass = self.getCssFor('title')
# Get the title, with highlighted parts when relevant
title = self.getShownValue('title')
if highlight: title = self.highlight(title)
if mode == 'link':
inPopup = inPopup or (target.target != '_self')
url = self.getUrl(page=page, nav=nav, inPopup=inPopup)
2015-02-10 10:20:50 -06:00
onClick = target.openPopup or 'clickOn(this)'
res = '<a href="%s" class="%s" onclick="%s" target="%s">%s</a>' % \
(url, cssClass, onClick, target.target, title)
elif mode == 'select':
res = '<span class="%s clickable" onclick="%s">%s</span>' % \
(cssClass, selectJs, title)
elif mode == 'text':
res = '<span class="%s">%s</span>' % (cssClass, title)
return res
# Workflow methods ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def initializeWorkflow(self):
'''Called when an object is created, be it temp or not, for initializing
workflow-related data on the object.'''
wf = self.getWorkflow()
# Get the initial workflow state
2011-11-25 11:01:20 -06:00
initialState = self.State(name=False)
# Create a Transition instance representing the initial transition
initialTransition = gen.Transition((initialState, initialState))
initialTransition.trigger('_init_', self, wf, '', doSay=False)
def getWorkflow(self, name=False, className=None):
'''Returns the workflow applicable for p_self (or for any instance of
p_className if given), or its name, if p_name is True.'''
if not className:
wrapperClass = self.wrapperClass
else:
wrapperClass = self.getTool().getAppyClass(className, wrapper=True)
wf = wrapperClass.getWorkflow()
if not name: return wf
return WorkflowDescriptor.getWorkflowName(wf)
2009-06-29 07:06:01 -05:00
def getWorkflowLabel(self, name=None):
'''Gets the i18n label for p_name (which can denote a state or a
transition), or for the current object state if p_name is None.'''
name = name or self.State()
if name == 'create_from_predecessor': return name
return '%s_%s' % (self.getWorkflow(name=True), name)
2009-06-29 07:06:01 -05:00
def getTransitions(self, includeFake=True, includeNotShowable=False,
grouped=True):
'''This method returns info about transitions (as UiTransition
instances) that one can trigger from the user interface.
* if p_includeFake is True, it retrieves transitions that the user
can't trigger, but for which he needs to know for what reason he
can't trigger it;
* if p_includeNotShowable is True, it includes transitions for which
show=False. Indeed, because "showability" is only a UI concern,
and not a security concern, in some cases it has sense to set
includeNotShowable=True, because those transitions are triggerable
from a security point of view.
* If p_grouped is True, transitions are grouped according to their
"group" attribute, in a similar way to fields or searches.
'''
res = []
groups = {} # The already encountered groups of transitions.
wfPage = gen.Page('workflow')
wf = self.getWorkflow()
currentState = self.State(name=False)
# Loop on every transition
for name in dir(wf):
transition = getattr(wf, name)
if (transition.__class__.__name__ != 'Transition'): continue
# Filter transitions that do not have currentState as start state
if not transition.hasState(currentState, True): continue
# Check if the transition can be triggered
mayTrigger = transition.isTriggerable(self, wf)
# Compute the condition that will lead to including or not this
# transition
if not includeFake:
includeIt = mayTrigger
else:
includeIt = mayTrigger or isinstance(mayTrigger, gen.No)
if not includeNotShowable:
includeIt = includeIt and transition.isShowable(wf, self)
if not includeIt: continue
# Create the UiTransition instance.
info = UiTransition(name, transition, self, mayTrigger)
# Add the transition into the result.
if not transition.group or not grouped:
res.append(info)
else:
# Insert the UiGroup instance corresponding to transition.group.
uiGroup = transition.group.insertInto(res, groups, wfPage,
self.__class__.__name__, content='transitions')
uiGroup.addElement(info)
return res
def applyUserIdChange(self, oldId, newId):
'''A user whose ID was p_oldId has now p_newId. If the old ID was
mentioned in self's local roles, update it to the new ID. This
method returns 1 if a change occurred, 0 else.'''
if oldId in self.__ac_local_roles__:
localRoles = self.__ac_local_roles__.copy()
localRoles[newId] = localRoles[oldId]
del localRoles[oldId]
self.__ac_local_roles__ = localRoles
self.reindex()
return 1
return 0
def findNewValue(self, field, language, history, stopIndex):
'''This function tries to find a more recent version of value of p_field
on p_self. In the case of a multilingual field, p_language is
specified. The method first tries to find it in
history[:stopIndex+1]. If it does not find it there, it returns the
current value on p_obj.'''
i = stopIndex + 1
name = language and ('%s-%s' % (field.name, language)) or field.name
while (i-1) >= 0:
i -= 1
if history[i]['action'] != '_datachange_': continue
if name not in history[i]['changes']: continue
# We have found it!
return history[i]['changes'][name][0] or ''
# A most recent version was not found in the history: return the current
# field value.
val = field.getValue(self)
if not language: return val or ''
return val and val.get(language) or ''
def getHistoryTexts(self, event):
'''Returns a tuple (insertText, deleteText) containing texts to show on,
respectively, inserted and deleted chunks of text in a XHTML diff.'''
tool = self.getTool()
mapping = {'userName': tool.getUserName(event['actor'])}
res = []
for type in ('insert', 'delete'):
msg = self.translate('history_%s' % type, mapping=mapping)
date = tool.formatDate(event['time'], withHour=True)
msg = '%s: %s' % (date, msg)
res.append(msg)
return res
def hasHistory(self, name=None):
'''Has this object an history? If p_name is specified, the question
becomes: has this object an history for field p_name?'''
if not hasattr(self.aq_base, 'workflow_history') or \
not self.workflow_history: return
# Return False if the user can't consult the object history
klass = self.getClass()
if hasattr(klass, 'showHistory'):
show = klass.showHistory
if callable(show): show = klass.showHistory(self.appy())
if not show: return
# Get the object history
history = self.workflow_history['appy']
if not name:
for event in history:
if event['action'] and (event['comments'] != '_invisible_'):
return True
else:
field = self.getAppyType(name)
# Is this field a multilingual field ?
languages = None
if field.type == 'String':
languages = field.getAttribute(self, 'languages')
multilingual = len(languages) > 1
for event in history:
if event['action'] != '_datachange_': continue
# Is there a value present for this field in this data change?
if not multilingual:
if (name in event['changes']) and \
(event['changes'][name][0]):
return True
else:
# At least one language-specific value must be present
for lg in languages:
lgName = '%s-%s' % (field.name, lg)
if (lgName in event['changes']) and \
event['changes'][lgName][0]:
return True
def getHistory(self, startNumber=0, reverse=True, includeInvisible=False,
batchSize=5):
'''Returns a copy of the history for this object, sorted in p_reverse
order if specified (most recent change first), whose invisible events
have been removed if p_includeInvisible is True.'''
history = list(self.workflow_history['appy'][1:])
if not includeInvisible:
history = [e for e in history if e['comments'] != '_invisible_']
if reverse: history.reverse()
2015-02-23 05:38:49 -06:00
# Keep only events which are within the batch
res = []
stopIndex = startNumber + batchSize - 1
i = -1
while (i+1) < len(history):
i += 1
# Ignore events outside range startNumber:startNumber+batchSize
if i < startNumber: continue
if i > stopIndex: break
if history[i]['action'] == '_datachange_':
# Take a copy of the event: we will modify it and replace
# fields' old values by their formatted counterparts.
event = history[i].copy()
event['changes'] = {}
for name, oldValue in history[i]['changes'].iteritems():
# "name" can specify a language-specific part in a
# multilingual field. "oldValue" is a tuple
# (value, fieldName).
if '-' in name:
fieldName, lg = name.split('-')
else:
fieldName = name
lg = None
field = self.getAppyType(fieldName)
# Field 'name' may not exist, if the history has been
# transferred from another site. In this case we can't show
# this data change.
if not field: continue
if (field.type == 'String') and \
(field.format == gen.String.XHTML):
# For rich text fields, instead of simply showing the
# previous value, we propose a diff with the next
2013-01-10 04:47:39 -06:00
# version, excepted if the previous value is empty.
if lg: isEmpty = not oldValue[0]
else: isEmpty = field.isEmptyValue(self, oldValue[0])
if isEmpty:
val = '-'
else:
newValue= self.findNewValue(field, lg, history, i-1)
# Compute the diff between oldValue and newValue
iMsg, dMsg = self.getHistoryTexts(event)
comparator= HtmlDiff(oldValue[0],newValue,iMsg,dMsg)
val = comparator.get()
else:
fmt = lg and 'getUnilingualFormattedValue' or \
'getFormattedValue'
val = getattr(field, fmt)(self, oldValue[0]) or '-'
if isinstance(val, list) or isinstance(val, tuple):
val = '<ul>%s</ul>' % \
''.join(['<li>%s</li>' % v for v in val])
event['changes'][name] = (val, oldValue[1])
else:
event = history[i]
res.append(event)
2013-08-21 15:25:27 -05:00
return Object(events=res, totalNumber=len(history))
def getHistoryCollapse(self):
'''Gets a Collapsible instance for showing a collapse or expanded
history in this object.'''
return Collapsible('objectHistory', self.REQUEST)
def getHistoryAjaxData(self, hook, startNumber, batchSize):
'''Gets data allowing to ajax-ask paginated history data.'''
params = {'startNumber': startNumber, 'maxPerPage': batchSize}
# Convert params into a JS dict
params = sutils.getStringDict(params)
return "getAjaxHook('%s',true)['ajax']=new AjaxData('%s','pxHistory', "\
"%s, null, '%s')" % (hook, hook, params, self.absolute_url())
def mayNavigate(self):
'''May the currently logged user see the navigation panel linked to
this object?'''
appyObj = self.appy()
if hasattr(appyObj, 'mayNavigate'): return appyObj.mayNavigate()
return True
def getDefaultViewPage(self):
'''Which view page must be shown by default?'''
appyObj = self.appy()
if hasattr(appyObj, 'getDefaultViewPage'):
return appyObj.getDefaultViewPage()
return 'main'
def getDefaultEditPage(self):
'''Which edit page must be shown by default?'''
appyObj = self.appy()
if hasattr(appyObj, 'getDefaultEditPage'):
return appyObj.getDefaultEditPage()
return 'main'
def mayAct(self):
2014-05-03 15:45:51 -05:00
'''m_mayAct allows to hide the whole set of actions for an object.
Indeed, beyond workflow security, it can be useful to hide controls
like "edit" icons/buttons. For example, if a user may only edit some
Ref fields with add=True on an object, when clicking on "edit", he
will see an empty edit form.'''
appyObj = self.appy()
if hasattr(appyObj, 'mayAct'): return appyObj.mayAct()
return True
def mayDelete(self):
'''May the currently logged user delete this object?'''
res = self.allows('delete')
if not res: return
# An additional, user-defined condition, may refine the base permission.
appyObj = self.appy()
if hasattr(appyObj, 'mayDelete'): return appyObj.mayDelete()
return True
2014-05-03 15:45:51 -05:00
def mayEdit(self, permission='write', permOnly=False, raiseError=False):
'''May the currently logged user edit this object? p_permission can be a
field-specific permission. If p_permOnly is True, the specific
user-defined condition is not evaluated. If p_raiseError is True, if
the user may not edit p_self, an error is raised.'''
res = self.allows(permission, raiseError=raiseError)
if not res: return
if permOnly: return res
# An additional, user-defined condition, may refine the base permission.
appyObj = self.appy()
if hasattr(appyObj, 'mayEdit'):
res = appyObj.mayEdit()
if not res and raiseError: self.raiseUnauthorized()
return res
return True
def mayView(self, permission='read', raiseError=False):
'''May the currently logged user view this object? p_permission can be a
field-specific permission. If p_raiseError is True, if the user may
not view p_self, an error is raised.'''
res = self.allows(permission, raiseError=raiseError)
if not res: return
# An additional, user-defined condition, may refine the base permission.
appyObj = self.appy()
2014-05-03 15:45:51 -05:00
if hasattr(appyObj, 'mayView'):
res = appyObj.mayView()
if not res and raiseError: self.raiseUnauthorized()
return res
return True
def onExecuteAction(self):
'''Called when a user wants to execute an Appy action on an object.'''
rq = self.REQUEST
return self.getAppyType(rq['fieldName']).onUiRequest(self, rq)
2011-11-25 11:01:20 -06:00
def onTrigger(self):
'''Called when a user wants to trigger a transition on an object.'''
rq = self.REQUEST
wf = self.getWorkflow()
# Get the transition
name = rq['transition']
transition = getattr(wf, name, None)
if not transition or (transition.__class__.__name__ != 'Transition'):
raise Exception('Transition "%s" not found.' % name)
return transition.onUiRequest(self, wf, name, rq)
def getRolesFor(self, permission):
'''Gets, according to the workflow, the roles that are currently granted
p_permission on this object.'''
state = self.State(name=False)
if permission not in state.permissions:
wf = self.getWorkflow().__name__
raise Exception('Permission "%s" not in permissions dict for ' \
'state %s.%s' % \
(permission, wf, self.State(name=True)))
roles = state.permissions[permission]
if roles: return [role.name for role in roles]
return ()
def appy(self):
'''Returns a wrapper object allowing to manipulate p_self the Appy
way.'''
# Create the dict for storing Appy wrapper on the REQUEST if needed.
2011-11-25 11:01:20 -06:00
rq = getattr(self, 'REQUEST', None)
2011-12-05 11:15:45 -06:00
if not rq:
# We are in test mode or Zope is starting. Use static variable
# config.fakeRequest instead.
rq = self.getProductConfig().fakeRequest
if not hasattr(rq, 'wrappers'): rq.wrappers = {}
2011-12-05 11:15:45 -06:00
# Return the Appy wrapper if already present in the cache
uid = self.id
if uid in rq.wrappers: return rq.wrappers[uid]
# Create the Appy wrapper, cache it in rq.wrappers and return it
wrapper = self.wrapperClass(self)
rq.wrappers[uid] = wrapper
return wrapper
2011-11-25 11:01:20 -06:00
# --------------------------------------------------------------------------
# Methods for computing values of standard Appy indexes
2011-11-25 11:01:20 -06:00
# --------------------------------------------------------------------------
def UID(self):
'''Returns the unique identifier for this object.'''
return self.id
2011-11-25 11:01:20 -06:00
def Title(self):
'''Returns the title for this object.'''
title = self.getAppyType('title')
if title: return title.getIndexValue(self)
2011-11-25 11:01:20 -06:00
return self.id
def SortableTitle(self):
'''Returns the title as must be stored in index "SortableTitle".'''
return sutils.normalizeText(self.Title())
2011-11-25 11:01:20 -06:00
def SearchableText(self):
'''This method concatenates the content of every field with
searchable=True for indexing purposes.'''
res = []
for field in self.getAllAppyTypes():
if not field.searchable: continue
res.append(field.getIndexValue(self, forSearch=True))
return res
def Creator(self):
'''Who create this object?'''
return self.creator
def Created(self):
'''When was this object created ?'''
return self.created
def Modified(self):
'''When was this object last modified ?'''
if hasattr(self.aq_base, 'modified'): return self.modified
return self.created
2011-11-25 11:01:20 -06:00
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
res = self.workflow_history['appy'][-1]['review_state']
2011-11-25 11:01:20 -06:00
# 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
2011-11-28 15:50:01 -06:00
def Allowed(self):
'''Returns the list of roles and users that are allowed to view this
object. This index value will be used within catalog queries for
filtering objects the user is allowed to see.'''
# Get, from the workflow, roles having permission 'read'.
res = self.getRolesFor('read')
# Add users or groups having, locally, this role on this object.
localRoles = getattr(self.aq_base, '__ac_local_roles__', None)
if not localRoles: return res
2011-11-28 15:50:01 -06:00
for id, roles in localRoles.iteritems():
for role in roles:
if role in res:
usr = 'user:%s' % id
if usr not in res: res.append(usr)
return res
2011-11-28 15:50:01 -06:00
def showState(self):
'''Must I show self's current state ?'''
stateShow = self.State(name=False).show
2009-06-29 07:06:01 -05:00
if callable(stateShow):
return stateShow(self.getWorkflow(), self.appy())
return stateShow
2009-06-29 07:06:01 -05:00
def showTransitions(self, layoutType):
'''Must we show the buttons/icons for triggering transitions on
p_layoutType?'''
# Never show transitions on edit pages.
if layoutType == 'edit': return
# Use the default value if self's class does not specify it.
klass = self.getClass()
if not hasattr(klass, 'showTransitions'): return (layoutType=='view')
showValue = klass.showTransitions
# This value can be a single value or a tuple/list of values.
if isinstance(showValue, basestring): return layoutType == showValue
return layoutType in showValue
getUrlDefaults = {'page':True, 'nav':True}
def getUrl(self, base=None, mode='view', inPopup=False, relative=False,
**kwargs):
2013-08-21 05:35:30 -05:00
'''Returns an URL for this object.
* If p_base is None, it will be the base URL for this object
(ie, Zope self.absolute_url() or an URL this is relative to the
root site if p_relative is True).
* p_mode can be "edit", "view" or "raw" (a non-param, base URL)
* If p_inPopup is True, the link will be opened in the Appy iframe.
An additional param "popup=1" will be added to URL params, in order
to tell Appy that the link target will be shown in a popup, in a
minimalistic way (no portlet...).
* 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 = ''
2013-08-21 05:35:30 -05:00
if mode != 'raw': suffix = '/%s' % mode
# Define the base URL if omitted
if not base:
base = relative and self.absolute_url_path() or self.absolute_url()
base += suffix
existingParams = ''
else:
existingParams = urllib.splitquery(base)[1]
# If a raw URL is asked, remove any param and suffix
if mode == 'raw':
if '?' in base: base = base[:base.index('?')]
base = base.rstrip('/')
for mode in ('view', 'edit'):
2013-08-21 05:35:30 -05:00
if base.endswith(mode):
base = base[:-len(mode)].rstrip('/')
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]))
# Manage inPopup
if inPopup and ('popup=' not in existingParams):
params.append('popup=1')
if params:
params = '&'.join(params)
if base.find('?') != -1: params = '&' + params
else: params = '?' + params
else:
params = ''
return '%s%s' % (base, params)
2011-11-28 15:50:01 -06:00
def getTool(self):
'''Returns the application tool.'''
return self.getPhysicalRoot().config
def getProductConfig(self, app=False):
'''Returns a reference to the config module. If p_app is True, it
returns the application config.'''
res = self.__class__.config
if app: res = res.appConfig
return res
2011-11-28 15:50:01 -06:00
def getParent(self):
'''If this object is stored within another one, this method returns it.
Else (if the object is stored directly within the tool or the root
data folder) it returns None.'''
parent = self.getParentNode()
# Not-Managers can't navigate back to the tool
2013-08-21 05:35:30 -05:00
if (parent.id == 'config') and \
not self.getTool().getUser().has_role('Manager'):
return False
if parent.meta_type not in ('Folder', 'Temporary Folder'): return parent
2014-12-15 12:36:00 -06:00
def getShownValue(self, name='title', language=None):
'''Call field.getShownValue on field named p_name.'''
2014-12-15 12:36:00 -06:00
field = self.getAppyType(name)
return field.getShownValue(self, field.getValue(self),
language=language)
def getBreadCrumb(self, inPopup=False):
'''Gets breadcrumb info about this object and its parents (if it must
be shown).'''
# Return an empty breadcrumb if it must not be shown.
klass = self.getClass()
if hasattr(klass, 'breadcrumb') and not klass.breadcrumb: return ()
# Compute the breadcrumb
title = self.getAppyType('title')
res = [Object(url=self.getUrl(inPopup=inPopup),
title=title.getShownValue(self, title.getValue(self)))]
# In a popup: limit the breadcrumb to the current object.
if inPopup: return res
parent = self.getParent()
if parent: res = parent.getBreadCrumb() + res
return res
2011-11-28 15:50:01 -06:00
def index_html(self):
'''Base method called when hitting this object.
- The standard behaviour is to redirect to /view.
- If a parameter named "do" is present in the request, it is supposed
to contain the name of a method to call on this object. In this
case, we call this method and return its result as XML.
- If method is POST, we consider the request to be XML data, that we
marshall to Python, and we call the method in param "do" with, as
arg, this marshalled Python object. While this could sound strange
to expect a query string containing a param "do" in a HTTP POST,
the HTTP spec does not prevent to do it.'''
rq = self.REQUEST
if (rq.REQUEST_METHOD == 'POST') and rq.QUERY_STRING:
# A POST method containing XML data.
rq.args = XmlUnmarshaller().parse(rq.stdin.getvalue())
# Find the name of the method to call.
methodName = rq.QUERY_STRING.split('=')[1]
return self.xml(action=methodName)
elif rq.has_key('do'):
# The user wants to call a method on this object and get its result
# as XML.
return self.xml(action=rq['do'])
else:
# The user wants to consult the view page for this object
return rq.RESPONSE.redirect(self.getUrl())
2011-11-28 15:50:01 -06:00
def getUserLanguage(self):
'''Gets the language (code) of the current user.'''
if not hasattr(self, 'REQUEST'):
return self.getProductConfig().appConfig.languages[0]
# Return the cached value on the request object if present
rq = self.REQUEST
if hasattr(rq, 'userLanguage'): return rq.userLanguage
# Try the value which comes from the cookie. Indeed, if such a cookie is
# present, it means that the user has explicitly chosen this language
# via the language selector.
if '_ZopeLg' in rq.cookies:
res = rq.cookies['_ZopeLg']
else:
# Try the LANGUAGE key from the request: it corresponds to the
# language as configured in the user's browser.
res = rq.get('LANGUAGE', None)
if not res:
# Try the HTTP_ACCEPT_LANGUAGE key from the request, which
# stores language preferences as defined in the user's browser.
# Several languages can be listed, from most to less wanted.
res = rq.get('HTTP_ACCEPT_LANGUAGE', None)
if res:
if ',' in res: res = res[:res.find(',')]
if '-' in res: res = res[:res.find('-')]
else:
res = self.getProductConfig().appConfig.languages[0]
# Cache this result
rq.userLanguage = res
return res
def getLanguageDirection(self, lang):
'''Determines if p_lang is a LTR or RTL language.'''
if lang in rtlLanguages: return 'rtl'
return 'ltr'
def formatText(self, text, format='html'):
'''Produces a representation of p_text into the desired p_format, which
is "html" by default.'''
if 'html' in format:
if format == 'html_from_text': text = cgi.escape(text)
res = text.replace('\r\n', '<br/>').replace('\n', '<br/>')
elif format == 'text':
res = text.replace('<br/>', '\n')
else:
res = text
return res
def translate(self, label, mapping={}, domain=None, default=None,
language=None, format='html', field=None):
'''Translates a given p_label into p_domain with p_mapping.
If p_field is given, p_label does not correspond to a full label
name, but to a label type linked to p_field: "label", "descr"
or "help". Indeed, in this case, a specific i18n mapping may be
available on the field, so we must merge this mapping into
p_mapping.'''
cfg = self.getProductConfig()
if not domain: domain = cfg.PROJECTNAME
# Get the label name, and the field-specific mapping if any.
if field:
2013-08-21 05:35:30 -05:00
if field.type != 'group':
fieldMapping = field.mapping[label]
if fieldMapping:
if callable(fieldMapping):
2013-08-21 05:35:30 -05:00
fieldMapping = field.callMethod(self, fieldMapping)
mapping.update(fieldMapping)
2013-08-21 05:35:30 -05:00
label = getattr(field, '%sId' % label)
# We will get the translation from a Translation object.
# In what language must we get the translation?
if not language: language = self.getUserLanguage()
tool = self.getTool()
try:
translation = getattr(tool, language).appy()
except AttributeError:
# We have no translation for this language. Fallback to 'en'.
translation = getattr(tool, 'en').appy()
res = getattr(translation, label, '')
if not res:
# Fallback to 'en'.
translation = getattr(tool, 'en').appy()
res = getattr(translation, label, '')
# If still no result, put a nice name derived from the label instead of
# a translated message.
if not res: res = produceNiceMessage(label.rsplit('_', 1)[-1])
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.'''
res = self.wrapperClass.getPageLayouts()[layoutType]
if isinstance(res, basestring): res = Table(res)
return res
def download(self, name=None):
'''Downloads the content of the file that is in the File field whose
name is in the request. This name can also represent an attribute
2012-04-19 02:20:15 -05:00
storing an image within a rich text field. If p_name is not given, it
is retrieved from the request.'''
rq = self.REQUEST
name = rq.get('name')
if not name: return
2014-05-03 15:45:51 -05:00
# Security check
if '_img_' not in name:
2014-05-03 15:45:51 -05:00
field = self.getAppyType(name)
else:
2014-05-03 15:45:51 -05:00
field = self.getAppyType(name.split('_img_')[0])
self.mayView(field.readPermission, raiseError=True)
# Write the file in the HTTP response
info = getattr(self.aq_base, name, None)
if info:
# Content disposition may be given in the request
disposition = rq.get('disposition', 'attachment')
if disposition not in ('inline', 'attachment'):
disposition = 'attachment'
info.writeResponse(rq.RESPONSE, self.getDbFolder(), disposition)
2010-11-30 10:41:18 -06:00
def upload(self):
'''Receives an image uploaded by the user via ckeditor and stores it in
a special field on this object.'''
# Get the name of the rich text field for which an image must be stored.
params = self.REQUEST['QUERY_STRING'].split('&')
fieldName = params[0].split('=')[1]
ckNum = params[1].split('=')[1]
# We will store the image in a field named [fieldName]_img_[nb].
i = 1
attrName = '%s_img_%d' % (fieldName, i)
while True:
if not hasattr(self.aq_base, attrName):
break
else:
i += 1
attrName = '%s_img_%d' % (fieldName, i)
# Store the image. Create a fake File instance for doing the job.
fakeFile = gen.File(isImage=True)
fakeFile.name = attrName
fakeFile.store(self, self.REQUEST['upload'])
# Return the URL of the image.
url = '%s/download?name=%s' % (self.absolute_url(), attrName)
response = self.REQUEST.RESPONSE
response.setHeader('Content-Type', 'text/html')
resp = "<script type='text/javascript'>window.parent.CKEDITOR.tools" \
".callFunction(%s, '%s');</script>" % (ckNum, url)
response.write(resp)
def allows(self, permission, raiseError=False):
'''Has the logged user p_permission on p_self ?'''
2013-08-21 05:35:30 -05:00
res = self.getTool().getUser().has_permission(permission, self)
2014-05-03 15:45:51 -05:00
if not res and raiseError: self.raiseUnauthorized()
2013-08-21 05:35:30 -05:00
return res
2011-11-25 11:01:20 -06:00
def isTemporary(self):
'''Is this object temporary ?'''
parent = self.getParentNode()
2014-05-03 15:45:51 -05:00
if not parent: return # Is probably being created through code
2011-11-25 11:01:20 -06:00
return parent.getId() == 'temp_folder'
def onProcess(self):
'''This method is a general hook for transfering processing of a request
to a given field, whose name must be in the request.'''
return self.getAppyType(self.REQUEST['name']).process(self)
def onCall(self):
'''Calls a specific method on the corresponding wrapper.'''
2014-05-03 15:45:51 -05:00
self.mayView(raiseError=True)
method = self.REQUEST['method']
obj = self.appy()
return getattr(obj, method)()
def onReindex(self):
'''Called for reindexing an index or all indexes on the currently shown
object.'''
if not self.getTool().getUser().has_role('Manager'):
self.raiseUnauthorized()
rq = self.REQUEST
indexName = rq['indexName']
if indexName == '_all_':
self.reindex()
else:
self.reindex(indexes=(indexName,))
self.say(self.translate('action_done'))
self.goto(self.getUrl(rq['HTTP_REFERER']))
2009-06-29 07:06:01 -05:00
# ------------------------------------------------------------------------------