appypod-rattail/gen/utils.py

318 lines
14 KiB
Python

# ------------------------------------------------------------------------------
import re, os, os.path, time
import appy.pod
from appy.shared.utils import getOsTempFolder, normalizeString, executeCommand
sequenceTypes = (list, tuple)
# Classes used by edit/view templates for accessing information ----------------
class Descr:
'''Abstract class for description classes.'''
def get(self): return self.__dict__
class GroupDescr(Descr):
def __init__(self, group, page, metaType):
'''Creates the data structure manipulated in ZPTs from p_group, the
Group instance used in the field definition.'''
self.type = 'group'
# All p_group attributes become self attributes.
for name, value in group.__dict__.iteritems():
if not name.startswith('_'):
setattr(self, name, value)
self.columnsWidths = [col.width for col in group.columns]
self.columnsAligns = [col.align for col in group.columns]
# Names of i18n labels
labelName = self.name
prefix = metaType
if group.label:
if isinstance(group.label, basestring): prefix = group.label
else: # It is a tuple (metaType, name)
if group.label[1]: labelName = group.label[1]
if group.label[0]: prefix = group.label[0]
self.labelId = '%s_group_%s' % (prefix, labelName)
self.descrId = self.labelId + '_descr'
self.helpId = self.labelId + '_help'
# The name of the page where the group lies
self.page = page.name
# The widgets belonging to the group that the current user may see.
# They will be stored by m_addWidget below as a list of lists because
# they will be rendered as a table.
self.widgets = [[]]
@staticmethod
def addWidget(groupDict, newWidget):
'''Adds p_newWidget into p_groupDict['widgets']. We try first to add
p_newWidget into the last widget row. If it is not possible, we
create a new row.
This method is a static method taking p_groupDict as first param
instead of being an instance method because at this time the object
has already been converted to a dict (for being maniputated within
ZPTs).'''
# Get the last row
widgetRow = groupDict['widgets'][-1]
numberOfColumns = len(groupDict['columnsWidths'])
# Computes the number of columns already filled by widgetRow
rowColumns = 0
for widget in widgetRow: rowColumns += widget['colspan']
freeColumns = numberOfColumns - rowColumns
if freeColumns >= newWidget['colspan']:
# We can add the widget in the last row.
widgetRow.append(newWidget)
else:
if freeColumns:
# Terminate the current row by appending empty cells
for i in range(freeColumns): widgetRow.append('')
# Create a new row
newRow = [newWidget]
groupDict['widgets'].append(newRow)
class PhaseDescr(Descr):
def __init__(self, name, states, obj):
self.name = name
self.states = states
self.obj = obj
self.phaseStatus = None
# The list of names of pages in this phase
self.pages = []
# The list of hidden pages in this phase
self.hiddenPages = []
# The dict below stores infor about every page listed in self.pages.
self.pagesInfo = {}
self.totalNbOfPhases = None
# The following attributes allows to browse, from a given page, to the
# last page of the previous phase and to the first page of the following
# phase if allowed by phase state.
self.previousPhase = None
self.nextPhase = None
def addPage(self, appyType, obj, layoutType):
'''Adds page-related information in the phase.'''
# If the page is already there, we have nothing more to do.
if (appyType.page.name in self.pages) or \
(appyType.page.name in self.hiddenPages): return
# Add the page only if it must be shown.
isShowableOnView = appyType.page.isShowable(obj, 'view')
isShowableOnEdit = appyType.page.isShowable(obj, 'edit')
if isShowableOnView or isShowableOnEdit:
# The page must be added.
self.pages.append(appyType.page.name)
# Create the dict about page information and add it in self.pageInfo
pageInfo = {'page': appyType.page,
'showOnView': isShowableOnView,
'showOnEdit': isShowableOnEdit}
pageInfo.update(appyType.page.getInfo(obj, layoutType))
self.pagesInfo[appyType.page.name] = pageInfo
else:
self.hiddenPages.append(appyType.page.name)
def computeStatus(self, allPhases):
'''Compute status of whole phase based on individual status of states
in this phase. If this phase includes no state, the concept of phase
is simply used as a tab, and its status depends on the page currently
shown. This method also fills fields "previousPhase" and "nextPhase"
if relevant, based on list of p_allPhases.'''
res = 'Current'
if self.states:
# Compute status base on states
res = self.states[0]['stateStatus']
if len(self.states) > 1:
for state in self.states[1:]:
if res != state['stateStatus']:
res = 'Current'
break
else:
# Compute status based on current page
page = self.obj.REQUEST.get('page', 'main')
if page in self.pages:
res = 'Current'
else:
res = 'Deselected'
# Identify previous and next phases
for phaseInfo in allPhases:
if phaseInfo['name'] == self.name:
i = allPhases.index(phaseInfo)
if i > 0:
self.previousPhase = allPhases[i-1]
if i < (len(allPhases)-1):
self.nextPhase = allPhases[i+1]
self.phaseStatus = res
class StateDescr(Descr):
def __init__(self, name, stateStatus):
self.name = name
self.stateStatus = stateStatus.capitalize()
# ------------------------------------------------------------------------------
upperLetter = re.compile('[A-Z]')
def produceNiceMessage(msg):
'''Transforms p_msg into a nice msg.'''
res = ''
if msg:
res = msg[0].upper()
for c in msg[1:]:
if c == '_':
res += ' '
elif upperLetter.match(c):
res += ' ' + c.lower()
else:
res += c
return res
# ------------------------------------------------------------------------------
class AppyObject: pass
# ------------------------------------------------------------------------------
class SomeObjects:
'''Represents a bunch of objects retrieved from a reference or a query in
portal_catalog.'''
def __init__(self, objects=None, batchSize=None, startNumber=0,
noSecurity=False):
self.objects = objects or [] # The objects
self.totalNumber = len(self.objects) # self.objects may only represent a
# part of all available objects.
self.batchSize = batchSize or self.totalNumber # The max length of
# self.objects.
self.startNumber = startNumber # The index of first object in
# self.objects in the whole list.
self.noSecurity = noSecurity
def brainsToObjects(self):
'''self.objects has been populated from brains from the portal_catalog,
not from True objects. This method turns them (or some of them
depending on batchSize and startNumber) into real objects.
If self.noSecurity is True, it gets the objects even if the logged
user does not have the right to get them.'''
start = self.startNumber
brains = self.objects[start:start + self.batchSize]
if self.noSecurity: getMethod = '_unrestrictedGetObject'
else: getMethod = 'getObject'
self.objects = [getattr(b, getMethod)() for b in brains]
# ------------------------------------------------------------------------------
class Keywords:
'''This class allows to handle keywords that a user enters and that will be
used as basis for performing requests in a Zope ZCTextIndex.'''
toRemove = '?-+*()'
def __init__(self, keywords, operator='AND'):
# Clean the p_keywords that the user has entered.
words = keywords.strip()
if words == '*': words = ''
for c in self.toRemove: words = words.replace(c, ' ')
self.keywords = words.split()
# Store the operator to apply to the keywords (AND or OR)
self.operator = operator
def merge(self, other, append=False):
'''Merges our keywords with those from p_other. If p_append is True,
p_other keywords are appended at the end; else, keywords are appended
at the begin.'''
for word in other.keywords:
if word not in self.keywords:
if append:
self.keywords.append(word)
else:
self.keywords.insert(0, word)
def get(self):
'''Returns the keywords as needed by the ZCTextIndex.'''
if self.keywords:
op = ' %s ' % self.operator
return op.join(self.keywords)+'*'
return ''
# ------------------------------------------------------------------------------
CONVERSION_ERROR = 'An error occurred while executing command "%s". %s'
class FileWrapper:
'''When you get, from an appy object, the value of a File attribute, you
get an instance of this class.'''
def __init__(self, atFile):
'''This constructor is only used by Appy to create a nice File instance
from a Plone/Zope corresponding instance (p_atFile). If you need to
create a new file and assign it to a File attribute, use the
attribute setter, do not create yourself an instance of this
class.'''
d = self.__dict__
d['_atFile'] = atFile # Not for you!
d['name'] = atFile.filename
d['content'] = atFile.data
d['mimeType'] = atFile.content_type
d['size'] = atFile.size # In bytes
def __setattr__(self, name, v):
d = self.__dict__
if name == 'name':
self._atFile.filename = v
d['name'] = v
elif name == 'content':
self._atFile.update_data(v, self.mimeType, len(v))
d['content'] = v
d['size'] = len(v)
elif name == 'mimeType':
self._atFile.content_type = self.mimeType = v
else:
raise 'Impossible to set attribute %s. "Settable" attributes ' \
'are "name", "content" and "mimeType".' % name
def dump(self, filePath=None, format=None, tool=None):
'''Writes the file on disk. If p_filePath is specified, it is the
path name where the file will be dumped; folders mentioned in it
must exist. If not, the file will be dumped in the OS temp folder.
The absolute path name of the dumped file is returned.
If an error occurs, the method returns None. If p_format is
specified, OpenOffice will be called for converting the dumped file
to the desired format. In this case, p_tool, a Appy tool, must be
provided. Indeed, any Appy tool contains parameters for contacting
OpenOffice in server mode.'''
if not filePath:
filePath = '%s/file%f.%s' % (getOsTempFolder(), time.time(),
normalizeString(self.name))
f = file(filePath, 'w')
if self.content.__class__.__name__ == 'Pdata':
# The file content is splitted in several chunks.
f.write(self.content.data)
nextPart = self.content.next
while nextPart:
f.write(nextPart.data)
nextPart = nextPart.next
else:
# Only one chunk
f.write(self.content)
f.close()
if format:
if not tool: return
# Convert the dumped file using OpenOffice
errorMessage = tool.convert(filePath, format)
# Even if we have an "error" message, it could be a simple warning.
# So we will continue here and, as a subsequent check for knowing if
# an error occurred or not, we will test the existence of the
# converted file (see below).
os.remove(filePath)
# Return the name of the converted file.
baseName, ext = os.path.splitext(filePath)
if (ext == '.%s' % format):
filePath = '%s.res.%s' % (baseName, format)
else:
filePath = '%s.%s' % (baseName, format)
if not os.path.exists(filePath):
tool.log(CONVERSION_ERROR % (cmd, errorMessage), type='error')
return
return filePath
# ------------------------------------------------------------------------------
def getClassName(klass, appName=None):
'''Generates, from appy-class p_klass, the name of the corresponding
Archetypes class. For some classes, name p_appName is required: it is
part of the class name.'''
moduleName = klass.__module__
if (moduleName == 'appy.gen.plone25.model') or \
moduleName.endswith('.appyWrappers'):
# This is a model (generation time or run time)
res = appName + klass.__name__
elif klass.__bases__ and (klass.__bases__[-1].__module__ == 'appy.gen'):
# This is a customized class (inherits from appy.gen.Tool, User,...)
res = appName + klass.__bases__[-1].__name__
else: # This is a standard class
res = klass.__module__.replace('.', '_') + '_' + klass.__name__
return res
# ------------------------------------------------------------------------------