def __init__(self, fields, validator=None, multiplicity=(0, 1), default=None, show=True, page='main', group=None, layouts=None, move=0, indexed=False, searchable=False, specificReadPermission=False, specificWritePermission=False, width='', height=None, maxChars=None, colspan=1, master=None, masterValue=None, focus=False, historized=False, mapping=None, label=None, subLayouts=Table('frv', width=None), widths=None): Field.__init__(self, validator, multiplicity, default, show, page, group, layouts, move, indexed, False, specificReadPermission, specificWritePermission, width, height, None, colspan, master, masterValue, focus, historized, mapping, label, None, None, None, None, True) self.validable = True # Tuples of (names, Field instances) determining the format of every # element in the list. self.fields = fields # Force some layouting for sub-fields, if subLayouts are given. So the # one who wants freedom on tuning layouts at the field level must # specify subLayouts=None. if subLayouts: for name, field in self.fields: field.layouts = field.formatLayouts(subLayouts) # One may specify the width of every column in the list. Indeed, using # widths and layouts of sub-fields may not be sufficient. if not widths: self.widths = [''] * len(self.fields) else: self.widths = widths
def getDefaultLayouts(self): '''Returns the default layouts for this type. Default layouts can vary acccording to format, multiplicity or history.''' if self.format == String.TEXT: return {'view': 'l-f', 'edit': 'lrv-d-f'} elif self.format == String.XHTML: if self.historized: # self.historized can be a method or a boolean. If it is a # method, it means that under some condition, historization will # be enabled. So we come here also in this case. view = 'lc-f' else: view = 'l-f' return {'view': Table(view, width='100%'), 'edit': 'lrv-d-f'} elif self.isMultiValued(): return {'view': 'l-f', 'edit': 'lrv-f'}
def __init__(self, fields, validator=None, multiplicity=(0, 1), default=None, show=True, page='main', group=None, layouts=None, move=0, indexed=False, searchable=False, specificReadPermission=False, specificWritePermission=False, width=None, height=None, maxChars=None, colspan=1, master=None, masterValue=None, focus=False, historized=False, mapping=None, label=None, subLayouts=Table('fv', width=None)): Field.__init__(self, validator, multiplicity, default, show, page, group, layouts, move, indexed, False, specificReadPermission, specificWritePermission, width, height, None, colspan, master, masterValue, focus, historized, True, mapping, label, None, None, None, None) self.validable = True # Tuples of (names, Field instances) determining the format of every # element in the list. self.fields = fields self.fieldsd = [(n, f.__dict__) for (n, f) in self.fields] # Force some layouting for sub-fields, if subLayouts are given. So the # one who wants freedom on tuning layouts at the field level must # specify subLayouts=None. if subLayouts: for name, field in self.fields: field.layouts = field.formatLayouts(subLayouts)
def getDefaultLayouts(self): return { 'view': 'lf', 'edit': Table('f;lrv;=', width=None), 'search': 'l-f' }
class Boolean(Field): '''Field for storing boolean values.''' pxView = pxCell = Px('''<x>:value</x> <input type="hidden" if="masterCss" class=":masterCss" value=":rawValue" name=":name" id=":name"/>''') pxEdit = Px(''' <x var="isChecked=field.isChecked(zobj, rawValue)"> <input type="checkbox" name=":name + '_visible'" id=":name" class=":masterCss" checked=":isChecked" onclick=":'toggleCheckbox(%s, %s); %s' % (q(name), \ q('%s_hidden' % name), \ field.getOnChange(zobj, layoutType))"/> <input type="hidden" name=":name" id=":'%s_hidden' % name" value=":isChecked and 'True' or 'False'"/> </x>''') pxSearch = Px('''<x var="typedWidget='%s*bool' % widgetName"> <x var="valueId='%s_yes' % name"> <input type="radio" value="True" name=":typedWidget" id=":valueId"/> <label lfor=":valueId">:_('yes')</label> </x> <x var="valueId='%s_no' % name"> <input type="radio" value="False" name=":typedWidget" id=":valueId"/> <label lfor=":valueId">:_('no')</label> </x> <x var="valueId='%s_whatever' % name"> <input type="radio" value="" name=":typedWidget" id=":valueId" checked="checked"/> <label lfor=":valueId">:_('whatever')</label> </x><br/></x>''') def __init__(self, validator=None, multiplicity=(0, 1), default=None, show=True, page='main', group=None, layouts=None, move=0, indexed=False, searchable=False, specificReadPermission=False, specificWritePermission=False, width=None, height=None, maxChars=None, colspan=1, master=None, masterValue=None, focus=False, historized=False, mapping=None, label=None, sdefault=False, scolspan=1, swidth=None, sheight=None, persist=True): Field.__init__(self, validator, multiplicity, default, show, page, group, layouts, move, indexed, searchable, specificReadPermission, specificWritePermission, width, height, None, colspan, master, masterValue, focus, historized, mapping, label, sdefault, scolspan, swidth, sheight, persist) self.pythonType = bool # Layout including a description dLayouts = {'view': 'lf', 'edit': Table('flrv;=d', width=None)} # Centered layout, no description cLayouts = {'view': 'lf|', 'edit': 'flrv|'} def getDefaultLayouts(self): return { 'view': 'lf', 'edit': Table('f;lrv;=', width=None), 'search': 'l-f' } def getValue(self, obj): '''Never returns "None". Returns always "True" or "False", even if "None" is stored in the DB.''' value = Field.getValue(self, obj) if value == None: return False return value def getFormattedValue(self, obj, value, showChanges=False): if value: res = obj.translate('yes') else: res = obj.translate('no') return res def getStorableValue(self, value): if not self.isEmptyValue(value): exec 'res = %s' % value return res def isChecked(self, obj, dbValue): '''When rendering this field as a checkbox, must it be checked or not?''' rq = obj.REQUEST # Get the value we must compare (from request or from database) if rq.has_key(self.name): return rq.get(self.name) in ('True', 1, '1') return dbValue
class Pod(Field): '''A pod is a field allowing to produce a (PDF, ODT, Word, RTF...) document from data contained in Appy class and linked objects or anything you want to put in it. It is the way gen uses pod.''' # Some right-aligned layouts, convenient for pod fields exporting query # results or multi-template pod fields. rLayouts = {'view': Table('fl!', css_class='podTable')} # "r"ight # "r"ight "m"ulti-template (where the global field label is not used rmLayouts = {'view': Table('f!', css_class='podTable')} allFormats = {'.odt': ('pdf', 'doc', 'odt'), '.ods': ('xls', 'ods')} POD_ERROR = 'An error occurred while generating the document. Please ' \ 'contact the system administrator.' NO_TEMPLATE = 'Please specify a pod template in field "template".' UNAUTHORIZED = 'You are not allow to perform this action.' TEMPLATE_NOT_FOUND = 'Template not found at %s.' FREEZE_ERROR = 'Error while trying to freeze a "%s" file in pod field ' \ '"%s" (%s).' FREEZE_FATAL_ERROR = 'Server error. Please contact the administrator.' # Icon allowing to generate a given template in a given format. pxIcon = Px(''' <img var="iconSuffix=frozen and 'Frozen' or ''; gc=field.getChecked and q(field.getChecked) or 'null'" src=":url(fmt + iconSuffix)" class="clickable" title=":field.getIconTitle(obj, fmt, frozen)" onclick=":'generatePod(%s,%s,%s,%s,%s,null,%s)' % (q(uid), q(name), \ q(info.template), q(fmt), q(ztool.getQueryInfo()), gc)"/>''' ) pxView = pxCell = Px(''' <x var="uid=obj.uid" for="info in field.getVisibleTemplates(obj)" var2="mailings=field.getVisibleMailings(obj, info.template); lineBreak=((loop.info.nb + 1) % field.maxPerRow) == 0"> <x for="fmt in info.formats" var2="freezeAllowed=(fmt in info.freezeFormats) and \ (field.show != 'result'); hasMailings=mailings and (fmt in mailings); dropdownEnabled=freezeAllowed or hasMailings; frozen=field.isFrozen(obj, info.template, fmt)"> <!-- A clickable icon if no freeze action is allowed and no mailing is available for this format --> <x if="not dropdownEnabled">:field.pxIcon</x> <!-- A clickable icon and a dropdown menu else. --> <span if="dropdownEnabled" class="dropdownMenu" var2="dropdownId='%s_%s' % (uid, \ field.getFreezeName(info.template, fmt, sep='_'))" onmouseover=":'toggleDropdown(%s)' % q(dropdownId)" onmouseout=":'toggleDropdown(%s,%s)' % (q(dropdownId), q('none'))"> <x>:field.pxIcon</x> <!-- The dropdown menu containing freeze actions --> <table id=":dropdownId" class="dropdown" width="100px"> <!-- Unfreeze --> <tr if="freezeAllowed and frozen" valign="top"> <td width="85px"> <a onclick=":'freezePod(%s,%s,%s,%s,%s)' % (q(uid), q(name), \ q(info.template), q(fmt), q('unfreeze'))" class="smaller">:_('unfreezeField')</a> </td> <td width="15px"><img src=":url('unfreeze')"/></td> </tr> <!-- (Re-)freeze --> <tr if="freezeAllowed" valign="top"> <td width="85px"> <a onclick=":'freezePod(%s,%s,%s,%s,%s)' % (q(uid), q(name), \ q(info.template), q(fmt), q('freeze'))" class="smaller">:_('freezeField')</a> </td> <td width="15px"><img src=":url('freeze')"/></td> </tr> <!-- (Re-)upload --> <tr if="freezeAllowed" valign="top"> <td width="85px"> <a onclick=":'uploadPod(%s,%s,%s,%s)' % (q(uid), q(name), \ q(info.template), q(fmt))" class="smaller">:_('uploadField')</a> </td> <td width="15px"><img src=":url('upload')"/></td> </tr> <!-- Mailing lists --> <x if="hasMailings" var2="sendLabel=_('email_send')"> <tr for="mailing in mailings[fmt]" valign="top" var2="mailingName=field.getMailingName(obj, mailing)"> <td colspan="2"> <a var="js='generatePod(%s,%s,%s,%s,%s,null,null,%s)' % \ (q(uid), q(name), q(info.template), q(fmt), \ q(ztool.getQueryInfo()), q(mailing))" onclick=":'askConfirm(%s,%s)' % (q('script'), q(js, False))" title=":sendLabel"> <img src=":url('email')" align="left" style="margin-right: 2px"/> <x>:mailingName</x></a> </td> </tr> </x> </table> </span> </x> <!-- Show the specific template name only if there is more than one template. For a single template, the field label already does the job. --> <span if="len(field.template) > 1" class=":(not loop.info.last and not lineBreak) and 'pod smaller' \ or 'smaller'">:field.getTemplateName(obj, info.template)</span> <br if="lineBreak"/> </x>''') pxEdit = pxSearch = '' def __init__(self, validator=None, default=None, show=('view', 'result'), page='main', group=None, layouts=None, move=0, indexed=False, searchable=False, specificReadPermission=False, specificWritePermission=False, width=None, height=None, maxChars=None, colspan=1, master=None, masterValue=None, focus=False, historized=False, mapping=None, label=None, template=None, templateName=None, showTemplate=None, freezeTemplate=None, maxPerRow=5, context=None, stylesMapping={}, formats=None, getChecked=None, mailing=None, mailingName=None, showMailing=None, mailingInfo=None): # Param "template" stores the path to the pod template(s). If there is # a single template, a string is expected. Else, a list or tuple of # strings is expected. Every such path must be relative to your # application. A pod template name Test.odt that is stored at the root # of your app will be referred as "Test.odt" in self.template. If it is # stored within sub-folder "pod", it will be referred as "pod/Test.odt". if not template: raise Exception(Pod.NO_TEMPLATE) if isinstance(template, basestring): self.template = [template] elif isinstance(template, tuple): self.template = list(template) else: self.template = template # Param "templateName", if specified, is a method that will be called # with the current template (from self.template) as single arg and must # return the name of this template. If self.template stores a single # template, you have no need to use param "templateName". Simply use the # field label to name the template. But if you have a multi-pod field # (with several templates specified as a list or tuple in param # "template"), you will probably choose to hide the field label and use # param "templateName" to give a specific name to every template. If # "template" contains several templates and "templateName" is None, Appy # will produce names from template filenames. self.templateName = templateName # "showTemplate" determines if the current user may generate documents # based on this pod field. More precisely, "showTemplate", if specified, # must be a method that will be called with the current template as # single arg (one among self.template) and that must return the list or # tuple of formats that the current user may use as output formats for # generating a document. If the current user is not allowed at all to # generate documents based on the current template, "showTemplate" must # return an empty tuple/list. If "showTemplate" is not specified, the # user will be able to generate documents based on the current template, # in any format from self.formats (see below). # "showTemplate" comes in addition to self.show. self.show dictates the # visibility of the whole field (ie, all templates from self.template) # while "showTemplate" dictates the visiblity of a specific template # within self.template. self.showTemplate = showTemplate # "freezeTemplate" determines if the current user may freeze documents # normally generated dynamically from this pod field. More precisely, # "freezeTemplate", if specified, must be a method that will be called # with the current template as single arg and must return the (possibly # empty) list or tuple of formats the current user may freeze. The # "freezing-related actions" that are granted by "freezeTemplate" are # the following. When no document is frozen yet for a given # template/format, the user may: # - freeze the document: pod will be called to produce a document from # the current database content and will store it in the database. # Subsequent user requests for this pod field will return the frozen # doc instead of generating on-the-fly documents; # - upload a document: the user will be able to upload a document that # will be stored in the database. Subsequent user requests for this # pod field will return this doc instead of generating on-the-fly # documents. # When a document is already frozen or uploaded for a given # template/format, the user may: # - unfreeze the document: the frozen or uploaded document will be # deleted from the database and subsequent user requests for the pod # field will again generate on-the-fly documents; # - re-freeze the document: the frozen or uploaded document will be # deleted, a new document will be generated from the current database # content and will be frozen as a replacement to the deleted one; # - upload a document: the frozen or uploaded document will be replaced # by a new document uploaded by the current user. self.freezeTemplate = freezeTemplate # If p_template contains more than 1 template, "maxPerRow" tells how # much templates must appear side by side. self.maxPerRow = maxPerRow # The context is a dict containing a specific pod context, or a method # that returns such a dict. self.context = context # A global styles mapping that would apply to the whole template self.stylesMapping = stylesMapping # What are the output formats when generating documents from this pod ? self.formats = formats if not formats: # Compute default ones self.formats = self.getAllFormats(self.template[0]) # Parameter "getChecked" can specify the name of a Ref field belonging # to the same gen class. If it is the case, the context of the pod # template will contain an additional object, name "_checked", and # "_checked.<name of the Ref field>" will contain the list of the # objects linked via the Ref field that are currently selected in the # user interface. self.getChecked = getChecked # Mailing lists can be defined for this pod field. For every visible # mailing list, a menu item will be available in the user interface and # will allow to send the pod result as attachment to the mailing list # recipients. Attribute p_mailing stores a mailing list's id # (as a string) or a list of ids. self.mailing = mailing if isinstance(mailing, basestring): self.mailing = [mailing] elif isinstance(mailing, tuple): self.mailing = list(mailing) # "mailingName" returns the name of the mailing as will be shown in the # user interface. It must be a method accepting the mailing list id # (from self.mailing) as single arg and returning the mailing list's # name. self.mailingName = mailingName # "showMailing" below determines when the mailing list(s) must be shown. # It may store a method accepting a mailing list's id (among # self.mailing) and a template (among self.template) and returning the # list or tuple of formats for which the pod result can be sent to the # mailing list. If no such method is defined, the mailing list will be # available for all visible templates and formats. self.showMailing = showMailing # When it it time to send an email, "mailingInfo" gives all the # necessary information for this email: recipients, subject, body. It # must be a method whose single arg is the mailing id (from # self.mailing) and that returns an instance of class Mailing (above). self.mailingInfo = mailingInfo Field.__init__(self, None, (0, 1), default, show, page, group, layouts, move, indexed, searchable, specificReadPermission, specificWritePermission, width, height, None, colspan, master, masterValue, focus, historized, mapping, label, None, None, None, None, True) # Param "persist" is set to True but actually, persistence for a pod # field is determined by freezing. self.validable = False def getExtension(self, template): '''Gets a p_template's extension (".odt" or ".ods"). Because a template can simply be a pointer to another template (ie, "Item.odt.variant"), the logic for getting the extension is a bit more tricky.''' elems = os.path.splitext(template) if elems[1] in Pod.allFormats: return elems[1] # p_template must be a pointer to another template and has one more # extension. return os.path.splitext(elems[0])[1] def getAllFormats(self, template): '''Gets all the output formats that are available for a given p_template.''' return Pod.allFormats[self.getExtension(template)] def setTemplateFolder(self, folder): '''This methods adds a prefix to every template name in self.template. This can be useful if a plug-in module needs to replace an application template by its own templates. Here is an example: imagine a base application has a pod field with: self.templates = ["Item.odt", "Decision.odt"] The plug-in module, named "PlugInApp", wants to replace it with its own templates Item.odt, Decision.odt and Other.odt, stored in its sub-folder "pod". Suppose the base pod field is in <podField>. The plug-in will write: <podField>.templates = ["Item.odt", "Decision.odt", "Other.odt"] <podField>.setTemplateFolder('../PlugInApp/pod') The following code is equivalent, will work, but is precisely the kind of things we want to avoid. <podField>.templates = ["../PlugInApp/pod/Item.odt", "../PlugInApp/pod/Decision.odt", "../PlugInApp/pod/Other.odt"] ''' for i in range(len(self.template)): self.template[i] = os.path.join(folder, self.template[i]) def getTemplateName(self, obj, fileName): '''Gets the name of a template given its p_fileName.''' res = None if self.templateName: # Use the method specified in self.templateName res = self.templateName(obj, fileName) # Else, deduce a nice name from p_fileName if not res: name = os.path.splitext(os.path.basename(fileName))[0] res = gutils.produceNiceMessage(name) return res def getTemplatePath(self, diskFolder, template): '''Return the absolute path to some pod p_template, by prefixing it with the application path. p_template can be a pointer to another template.''' res = sutils.resolvePath(os.path.join(diskFolder, template)) if not os.path.isfile(res): raise Exception(self.TEMPLATE_NOT_FOUND % templatePath) # Unwrap the path if the file is simply a pointer to another one. elems = os.path.splitext(res) if elems[1] not in Pod.allFormats: res = self.getTemplatePath(diskFolder, elems[0]) return res def getDownloadName(self, obj, template, format, queryRelated): '''Gets the name of the pod result as will be seen by the user that will download it. Ensure the returned name is not too long for the OS that will store the downloaded file with this name.''' norm = obj.tool.normalize fileName = norm(self.getTemplateName(obj, template))[:100] if not queryRelated: # This is a POD for a single object: personalize the file name with # the object title. fileName = '%s-%s' % (norm(obj.title)[:140], fileName) return fileName + '.' + format def getVisibleTemplates(self, obj): '''Returns, among self.template, the template(s) that can be shown.''' res = [] if not self.showTemplate: # Show them all in the formats spoecified in self.formats. for template in self.template: res.append( Object(template=template, formats=self.formats, freezeFormats=self.getFreezeFormats(obj, template))) else: isManager = obj.user.has_role('Manager') for template in self.template: formats = self.showTemplate(obj, template) if not formats: continue if isManager: formats = self.getAllFormats(template) elif isinstance(formats, bool): formats = self.formats elif isinstance(formats, basestring): formats = (formats, ) res.append( Object(template=template, formats=formats, freezeFormats=self.getFreezeFormats(obj, template))) return res def getVisibleMailings(self, obj, template): '''Gets, among self.mailing, the mailing(s) that can be shown for p_template, as a dict ~{s_format:[s_id]}~.''' if not self.mailing: return res = {} for mailing in self.mailing: # Is this mailing visible ? In which format(s) ? if not self.showMailing: # By default, the mailing is available in any format formats = True else: formats = self.showMailing(obj, mailing, template) if not formats: continue if isinstance(formats, bool): formats = self.formats elif isinstance(formats, basestring): formats = (formats, ) # Add this mailing to the result for fmt in formats: if fmt in res: res[fmt].append(mailing) else: res[fmt] = [mailing] return res def getMailingName(self, obj, mailing): '''Gets the name of a particular p_mailing.''' res = None if self.mailingName: # Use the method specified in self.mailingName res = self.mailingName(obj, mailing) if not res: # Deduce a nice name from p_mailing res = gutils.produceNiceMessage(mailing) return res def getMailingInfo(self, obj, template, mailing): '''Gets the necessary information for sending an email to p_mailing list.''' res = self.mailingInfo(obj, mailing) subject = res.subject if not subject: # Give a predefined subject mapping = { 'site': obj.tool.o.getSiteUrl(), 'title': obj.o.getShownValue('title'), 'template': self.getTemplateName(obj, template) } subject = obj.translate('podmail_subject', mapping=mapping) body = res.body if not body: # Give a predefined body mapping = {'site': obj.tool.o.getSiteUrl()} body = obj.translate('podmail_body', mapping=mapping) return res.logins, subject, body def sendMailing(self, obj, template, mailing, attachment): '''Sends the emails for m_mailing.''' logins, subject, body = self.getMailingInfo(obj, template, mailing) if not logins: obj.log('mailing %s contains no recipient.' % mailing) return 'action_ko' tool = obj.tool # Collect logins corresponding to inexistent users and recipients missing = [] recipients = [] for login in logins: user = tool.search1('User', noSecurity=True, login=login) if not user: missing.append(login) continue else: recipient = user.getMailRecipient() if not recipient: missing.append(login) else: recipients.append(recipient) if missing: obj.log('mailing %s: inexistent user or no email for %s.' % \ (mailing, str(missing))) if not recipients: obj.log('mailing %s contains no recipient (after removing wrong ' \ 'entries, see above).' % mailing) msg = 'action_ko' else: tool.sendMail(recipients, subject, body, [attachment]) msg = 'action_done' return msg def getValue(self, obj, template=None, format=None, result=None, queryData=None, customContext=None, noSecurity=False): '''For a pod field, getting its value means computing a pod document or returning a frozen one. A pod field differs from other field types because there can be several ways to produce the field value (ie: self.template can hold various templates; output file format can be odt, pdf,.... We get those precisions about the way to produce the file, either from params, or from default values. * p_template is the specific template, among self.template, that must be used as base for generating the document; * p_format is the output format of the resulting document; * p_result, if given, must be the absolute path of the document that will be computed by pod. If not given, pod will produce a doc in the OS temp folder; * if the pod document is related to a query, the query parameters needed to re-trigger the query are given in p_queryData; * dict p_customContext may be specified and will override any other value available in the context, including values from the field-specific context. ''' obj = obj.appy() template = template or self.template[0] format = format or 'odt' # Security check. if not noSecurity and not queryData: if self.showTemplate and not self.showTemplate(obj, template): raise Exception(self.UNAUTHORIZED) # Return the possibly frozen document (not applicable for query-related # pods). if not queryData: frozen = self.isFrozen(obj, template, format) if frozen: fileName = self.getDownloadName(obj, template, format, False) return FileInfo(frozen, inDb=False, uploadName=fileName) # We must call pod to compute a pod document from "template". tool = obj.tool diskFolder = tool.getDiskFolder() # Get the path to the pod template. templatePath = self.getTemplatePath(diskFolder, template) # Get or compute the specific POD context specificContext = None if callable(self.context): specificContext = self.callMethod(obj, self.context) else: specificContext = self.context # Compute the name of the result file. if not result: result = '%s/%s_%f.%s' % (sutils.getOsTempFolder(), obj.uid, time.time(), format) # Define parameters to give to the appy.pod renderer podContext = { 'tool': tool, 'user': obj.user, 'self': obj, 'field': self, 'now': obj.o.getProductConfig().DateTime(), '_': obj.translate, 'projectFolder': diskFolder, 'template': template, 'request': tool.request } # If the pod document is related to a query, re-trigger it and put the # result in the pod context. if queryData: # Retrieve query params cmd = ', '.join(tool.o.queryParamNames) cmd += " = queryData.split(';')" exec cmd # (re-)execute the query, but without any limit on the number of # results; return Appy objects. objs = tool.o.executeQuery(obj.o.portal_type, searchName=search, sortBy=sortKey, sortOrder=sortOrder, filterKey=filterKey, filterValue=filterValue, maxResults='NO_LIMIT') podContext['objects'] = [o.appy() for o in objs.objects] podContext['queryData'] = queryData.split(';') # Add the field-specific and custom contexts if present. if specificContext: podContext.update(specificContext) if customContext: podContext.update(customContext) # Variable "_checked" can be expected by a template but absent (ie, # when generating frozen documents). if '_checked' not in podContext: podContext['_checked'] = Object() # Define a potential global styles mapping if callable(self.stylesMapping): stylesMapping = self.callMethod(obj, self.stylesMapping) else: stylesMapping = self.stylesMapping rendererParams = { 'template': templatePath, 'context': podContext, 'result': result, 'stylesMapping': stylesMapping, 'imageResolver': tool.o.getApp(), 'overwriteExisting': True } if tool.unoEnabledPython: rendererParams['pythonWithUnoPath'] = tool.unoEnabledPython if tool.openOfficePort: rendererParams['ooPort'] = tool.openOfficePort # Launch the renderer try: renderer = Renderer(**rendererParams) renderer.run() except PodError, pe: if not os.path.exists(result): # In some (most?) cases, when OO returns an error, the result is # nevertheless generated. obj.log(str(pe).strip(), type='error') return Pod.POD_ERROR # Give a friendly name for this file fileName = self.getDownloadName(obj, template, format, queryData) # Get a FileInfo instance to manipulate the file on the filesystem. return FileInfo(result, inDb=False, uploadName=fileName)
class Boolean(Field): '''Field for storing boolean values.''' yesNo = {'true': 'yes', 'false': 'no', True: 'yes', False: 'no'} trueFalse = {True: 'true', False: 'false'} # Default layout (render = "checkbox") ("b" stands for "base"). bLayouts = { 'view': 'lf', 'edit': Table('f;lrv;=', width=None), 'search': 'l-f' } # Layout including a description. dLayouts = {'view': 'lf', 'edit': Table('flrv;=d', width=None)} # Centered layout, no description. cLayouts = {'view': 'lf|', 'edit': 'flrv|'} # Layout for radio buttons (render = "radios") rLayouts = {'edit': 'f', 'view': 'f', 'search': 'l-f'} rlLayouts = {'edit': 'l-f', 'view': 'lf', 'search': 'l-f'} pxView = pxCell = Px('''<x>:value</x> <input type="hidden" if="masterCss" class=":masterCss" value=":rawValue" name=":name" id=":name"/>''') pxEdit = Px('''<x var="isTrue=field.isTrue(zobj, rawValue)"> <x if="field.render == 'checkbox'"> <input type="checkbox" name=":name + '_visible'" id=":name" class=":masterCss" checked=":isTrue" onclick=":'toggleCheckbox(%s, %s); %s' % (q(name), \ q('%s_hidden' % name), \ field.getOnChange(zobj, layoutType))"/> <input type="hidden" name=":name" id=":'%s_hidden' % name" value=":isTrue and 'True' or 'False'"/> </x> <x if="field.render == 'radios'" var2="falseId='%s_false' % name; trueId='%s_true' % name"> <input type="radio" name=":name" id=":falseId" class=":masterCss" value="False" checked=":not isTrue"/> <label lfor=":falseId">:_(field.labelId + '_false')</label><br/> <input type="radio" name=":name" id=":trueId" class=":masterCss" value="True" checked=":isTrue"/> <label lfor=":trueId">:_(field.labelId + '_true')</label> </x></x>''') pxSearch = Px('''<x var="typedWidget='%s*bool' % widgetName"> <x var="valueId='%s_yes' % name"> <input type="radio" value="True" name=":typedWidget" id=":valueId"/> <label lfor=":valueId">:_(field.getValueLabel(True))</label> </x> <x var="valueId='%s_no' % name"> <input type="radio" value="False" name=":typedWidget" id=":valueId"/> <label lfor=":valueId">:_(field.getValueLabel(False))</label> </x> <x var="valueId='%s_whatever' % name"> <input type="radio" value="" name=":typedWidget" id=":valueId" checked="checked"/> <label lfor=":valueId">:_('whatever')</label> </x><br/></x>''') def __init__(self, validator=None, multiplicity=(0, 1), default=None, show=True, page='main', group=None, layouts=None, move=0, indexed=False, searchable=False, specificReadPermission=False, specificWritePermission=False, width=None, height=None, maxChars=None, colspan=1, master=None, masterValue=None, focus=False, historized=False, mapping=None, label=None, sdefault=False, scolspan=1, swidth=None, sheight=None, persist=True, render='checkbox'): # By default, a boolean is edited via a checkbox. It can also be edited # via 2 radio buttons (p_render="radios"). self.render = render Field.__init__(self, validator, multiplicity, default, show, page, group, layouts, move, indexed, searchable, specificReadPermission, specificWritePermission, width, height, None, colspan, master, masterValue, focus, historized, mapping, label, sdefault, scolspan, swidth, sheight, persist) self.pythonType = bool def getDefaultLayouts(self): return (self.render == 'radios') and self.rLayouts or self.bLayouts def getValue(self, obj): '''Never returns "None". Returns always "True" or "False", even if "None" is stored in the DB.''' value = Field.getValue(self, obj) if value == None: return False return value def getValueLabel(self, value): '''Returns the label for p_value (True or False): if self.render is "checkbox", the label is simply the translated version of "yes" or "no"; if self.render is "radios", there are specific labels.''' if self.render == 'radios': return '%s_%s' % (self.labelId, self.trueFalse[value]) return self.yesNo[value] def getFormattedValue(self, obj, value, showChanges=False, language=None): return obj.translate(self.getValueLabel(value), language=language) def getStorableValue(self, obj, value): if not self.isEmptyValue(obj, value): exec 'res = %s' % value return res def isTrue(self, obj, dbValue): '''When rendering this field as a checkbox, must it be checked or not?''' rq = obj.REQUEST # Get the value we must compare (from request or from database) if rq.has_key(self.name): return rq.get(self.name) in ('True', 1, '1') return dbValue
def getDefaultLayouts(self): return {'view': Table('l-f', width='100%'), 'edit': 'lrv-f'}
class Ref(Field): # Some default layouts. "w" stands for "wide": those layouts produce tables # of Ref objects whose width is 100%. wLayouts = Table('lrv-f', width='100%') # "d" stands for "description": a description label is added. wdLayouts = {'view': Table('l-d-f', width='100%')} # This PX displays the title of a referenced object, with a link on it to # reach the consult view for this object. If we are on a back reference, the # link allows to reach the correct page where the forward reference is # defined. If we are on a forward reference, the "nav" parameter is added to # the URL for allowing to navigate from one object to the next/previous on # ui/view. pxObjectTitle = Px(''' <x var="navInfo='ref.%s.%s:%s.%d.%d' % (zobj.UID(), field.name, \ field.pageName, loop.ztied.nb + startNumber, totalNumber); navInfo=not field.isBack and navInfo or ''; cssClass=ztied.getCssFor('title')"> <x>::ztied.getSupTitle(navInfo)</x> <a var="pageName=field.isBack and field.back.pageName or 'main'; fullUrl=ztied.getUrl(page=pageName, nav=navInfo)" href=":fullUrl" class=":cssClass">:(not includeShownInfo) and \ ztied.Title() or field.getReferenceLabel(ztied.appy()) </a><span name="subTitle" style=":showSubTitles and 'display:inline' or \ 'display:none'">::ztied.getSubTitle()"</span> </x>''') # This PX displays icons for triggering actions on a given referenced object # (edit, delete, etc). pxObjectActions = Px(''' <table class="noStyle" var="isBack=field.isBack"> <tr> <!-- Arrows for moving objects up or down --> <td if="not isBack and (len(zobjects)>1) and changeOrder and canWrite" var2="objectIndex=field.getIndexOf(zobj, ztied); ajaxBaseCall=navBaseCall.replace('**v**','%s,%s,{%s:%s,%s:%s}'%\ (q(startNumber), q('ChangeRefOrder'), q('refObjectUid'), q(ztied.UID()), q('move'), q('**v**')))"> <img if="objectIndex > 0" class="clickable" src=":url('arrowUp')" title=":_('move_up')" onclick=":ajaxBaseCall.replace('**v**', 'up')"/> <img if="objectIndex < (totalNumber-1)" class="clickable" src=":url('arrowDown')" title=":_('move_down')" onclick=":ajaxBaseCall.replace('**v**', 'down')"/> </td> <!-- Workflow transitions --> <td if="ztied.showTransitions('result')" var2="targetObj=ztied">:targetObj.appy().pxTransitions</td> <!-- Edit --> <td if="not field.noForm and ztied.mayEdit() and field.delete"> <a var="navInfo='ref.%s.%s:%s.%d.%d' % (zobj.UID(), field.name, \ field.pageName, loop.ztied.nb+startNumber, totalNumber)" href=":ztied.getUrl(mode='edit', page='main', nav=navInfo)"> <img src=":url('edit')" title=":_('object_edit')"/></a> </td> <!-- Delete --> <td if="not isBack and field.delete and canWrite and ztied.mayDelete()"> <img class="clickable" title=":_('object_delete')" src=":url('delete')" onclick=":'onDeleteObject(%s)' % q(ztied.UID())"/> </td> <!-- Unlink --> <td if="not isBack and field.unlink and canWrite"> <img class="clickable" title=":_('object_unlink')" src=":url('unlink')" onclick=":'onUnlinkObject(%s,%s,%s)' % (q(zobj.UID()), \ q(field.name), q(ztied.UID()))"/> </td> </tr> </table>''') # Displays the button allowing to add a new object through a Ref field, if # it has been declared as addable and if multiplicities allow it. pxAdd = Px(''' <input if="showPlusIcon" type="button" class="button" var2="navInfo='ref.%s.%s:%s.%d.%d' % (zobj.UID(), \ field.name, field.pageName, 0, totalNumber); formCall='goto(%s)' % \ q('%s/do?action=Create&className=%s&nav=%s' % \ (folder.absolute_url(), linkedPortalType, navInfo)); formCall=not field.addConfirm and formCall or \ 'askConfirm(%s,%s,%s)' % (q('script'), q(formCall), \ q(addConfirmMsg)); noFormCall=navBaseCall.replace('**v**', \ '%d,%s' % (startNumber, q('CreateWithoutForm'))); noFormCall=not field.addConfirm and noFormCall or \ 'askConfirm(%s, %s, %s)' % (q('script'), q(noFormCall), \ q(addConfirmMsg))" style=":url('buttonAdd', bg=True)" value=":_('add_ref')" onclick=":field.noForm and noFormCall or formCall"/>''') # This PX displays, in a cell header from a ref table, icons for sorting the # ref field according to the field that corresponds to this column. pxSortIcons = Px(''' <x if="changeOrder and canWrite and ztool.isSortable(field.name, \ zobjects[0].meta_type, 'ref')" var2="ajaxBaseCall=navBaseCall.replace('**v**', '%s,%s,{%s:%s,%s:%s}'% \ (q(startNumber), q('SortReference'), q('sortKey'), \ q(field.name), q('reverse'), q('**v**')))"> <img class="clickable" src=":url('sortAsc')" onclick=":ajaxBaseCall.replace('**v**', 'False')"/> <img class="clickable" src=":url('sortDesc')" onclick=":ajaxBaseCall.replace('**v**', 'True')"/> </x>''') # This PX is called by a XmlHttpRequest (or directly by pxView) for # displaying the referred objects of a reference field. pxViewContent = Px(''' <div var="field=zobj.getAppyType(req['fieldName']); innerRef=req.get('innerRef', False) == 'True'; ajaxHookId=zobj.UID() + field.name; startNumber=int(req.get('%s_startNumber' % ajaxHookId, 0)); info=field.getLinkedObjects(zobj, startNumber); zobjects=info.objects; totalNumber=info.totalNumber; batchSize=info.batchSize; batchNumber=len(zobjects); folder=zobj.getCreateFolder(); linkedPortalType=ztool.getPortalType(field.klass); canWrite=not field.isBack and zobj.allows(field.writePermission); showPlusIcon=zobj.mayAddReference(field.name); atMostOneRef=(field.multiplicity[1] == 1) and \ (len(zobjects)<=1); addConfirmMsg=field.addConfirm and \ _('%s_addConfirm' % field.labelId) or ''; navBaseCall='askRefField(%s,%s,%s,%s,**v**)' % \ (q(ajaxHookId), q(zobj.absolute_url()), \ q(field.name), q(innerRef)); changeOrder=field.changeOrderEnabled(zobj); showSubTitles=req.get('showSubTitles', 'true') == 'true'" id=":ajaxHookId"> <!-- The definition of "atMostOneRef" above may sound strange: we shouldn't check the actual number of referenced objects. But for back references people often forget to specify multiplicities. So concretely, multiplicities (0,None) are coded as (0,1). --> <!-- Display a simplified widget if at most 1 referenced object. --> <table if="atMostOneRef"> <tr valign="top"> <!-- If there is no object --> <x if="not zobjects"> <td class="discreet">:_('no_ref')</td> <td>:field.pxAdd</td> </x> <!-- If there is an object --> <x if="zobjects"> <td for="ztied in zobjects" var2="includeShownInfo=True">:field.pxObjectTitle</td> </x> </tr> </table> <!-- Display a table in all other cases --> <x if="not atMostOneRef"> <div if="not innerRef or showPlusIcon" style="margin-bottom: 4px"> (<x>:totalNumber</x>) <x>:field.pxAdd</x> <!-- The search button if field is queryable --> <input if="zobjects and field.queryable" type="button" class="button" style=":url('buttonSearch', bg=True)" value=":_('search_title')" onclick=":'goto(%s)' % \ q('%s/ui/search?className=%s&ref=%s:%s' % \ (ztool.absolute_url(), linkedPortalType, zobj.UID(), \ field.name))"/> </div> <!-- Appy (top) navigation --> <x>:obj.pxAppyNavigate</x> <!-- No object is present --> <p class="discreet" if="not zobjects">:_('no_ref')</p> <table if="zobjects" class=":innerRef and 'innerAppyTable' or ''" width="100%"> <tr valign="bottom"> <td> <!-- Show forward or backward reference(s) --> <table class="not innerRef and 'list' or ''" width=":innerRef and '100%' or field.layouts['view']['width']" var="columns=zobjects[0].getColumnsSpecifiers(\ field.shownInfo, dir)"> <tr if="field.showHeaders"> <th for="column in columns" width=":column['width']" align="column['align']" var2="field=column['field']"> <span>:_(field.labelId)</span> <x>:field.pxSortIcons</x> <x var="className=linkedPortalType">:obj.pxShowDetails</x> </th> </tr> <tr for="ztied in zobjects" valign="top" class=":loop.ztied.odd and 'even' or 'odd'"> <td for="column in columns" width=":column['width']" align=":column['align']" var2="field=column['field']"> <!-- The "title" field --> <x if="python: field.name == 'title'"> <x>:field.pxObjectTitle</x> <div if="ztied.mayAct()">:field.pxObjectActions</div> </x> <!-- Any other field --> <x if="field.name != 'title'"> <x var="zobj=ztied; obj=ztied.appy(); layoutType='cell'; innerRef=True" if="zobj.showField(field.name, \ layoutType='result')">:field.pxView</x> </x> </td> </tr> </table> </td> </tr> </table> <!-- Appy (bottom) navigation --> <x>:obj.pxAppyNavigate</x> </x> </div>''') pxView = pxCell = Px(''' <x var="x=req.set('fieldName', field.name)">:field.pxViewContent</x>''') pxEdit = Px(''' <select if="field.link" var2="requestValue=req.get(name, []); inRequest=req.has_key(name); zobjects=field.getSelectableObjects(obj); uids=[o.UID() for o in \ field.getLinkedObjects(zobj).objects]; isBeingCreated=zobj.isTemporary()" name=":name" size="isMultiple and field.height or ''" multiple="isMultiple and 'multiple' or ''"> <option value="" if="not isMultiple">:_('choose_a_value')"></option> <option for="ztied in zobjects" var2="uid=ztied.o.UID()" selected=":inRequest and (uid in requestValue) or \ (uid in uids)" value=":uid">:field.getReferenceLabel(ztied)</option> </select>''') pxSearch = Px('''<x> <label lfor=":widgetName">:_(field.labelId)"></label><br/> <!-- The "and" / "or" radio buttons --> <x if="field.multiplicity[1] != 1" var2="operName='o_%s' % name; orName='%s_or' % operName; andName='%s_and' % operName"> <input type="radio" name=":operName" id=":orName" checked="checked" value="or"/> <label lfor=":orName">:_('search_or')"></label> <input type="radio" name=":operName" id=":andName" value="and"/> <label lfor=":andName">:_('search_and')"></label><br/> </x> <!-- The list of values --> <select name=":widgetName" size=":field.sheight" multiple="multiple"> <option for="v in ztool.getSearchValues(name, className)" var2="uid=v[0]; title=field.getReferenceLabel(v[1])" value=":uid" title=":title">:ztool.truncateValue(title,field.swidth)"></option> </select> </x>''') def __init__(self, klass=None, attribute=None, validator=None, multiplicity=(0, 1), default=None, add=False, addConfirm=False, delete=None, noForm=False, link=True, unlink=None, back=None, show=True, page='main', group=None, layouts=None, showHeaders=False, shownInfo=(), select=None, maxPerPage=30, move=0, indexed=False, searchable=False, specificReadPermission=False, specificWritePermission=False, width=None, height=5, maxChars=None, colspan=1, master=None, masterValue=None, focus=False, historized=False, mapping=None, label=None, queryable=False, queryFields=None, queryNbCols=1, navigable=False, searchSelect=None, changeOrder=True, sdefault='', scolspan=1, swidth=None, sheight=None): self.klass = klass self.attribute = attribute # May the user add new objects through this ref ? self.add = add # When the user adds a new object, must a confirmation popup be shown? self.addConfirm = addConfirm # May the user delete objects via this Ref? self.delete = delete if delete == None: # By default, one may delete objects via a Ref for which one can # add objects. self.delete = bool(self.add) # If noForm is True, when clicking to create an object through this ref, # the object will be created automatically, and no creation form will # be presented to the user. self.noForm = noForm # May the user link existing objects through this ref? self.link = link # May the user unlink existing objects? self.unlink = unlink if unlink == None: # By default, one may unlink objects via a Ref for which one can # link objects. self.unlink = bool(self.link) self.back = None if back: # It is a forward reference self.isBack = False # Initialise the backward reference self.back = back self.backd = back.__dict__ back.isBack = True back.back = self back.backd = self.__dict__ # klass may be None in the case we are defining an auto-Ref to the # same class as the class where this field is defined. In this case, # when defining the field within the class, write # myField = Ref(None, ...) # and, at the end of the class definition (name it K), write: # K.myField.klass = K # setattr(K, K.myField.back.attribute, K.myField.back) if klass: setattr(klass, back.attribute, back) # When displaying a tabular list of referenced objects, must we show # the table headers? self.showHeaders = showHeaders # When displaying referenced object(s), we will display its title + all # other fields whose names are listed in the following attribute. self.shownInfo = list(shownInfo) if not self.shownInfo: self.shownInfo.append('title') # If a method is defined in this field "select", it will be used to # filter the list of available tied objects. self.select = select # Maximum number of referenced objects shown at once. self.maxPerPage = maxPerPage # Specifies sync sync = {'view': False, 'edit': True} # If param p_queryable is True, the user will be able to perform queries # from the UI within referenced objects. self.queryable = queryable # Here is the list of fields that will appear on the search screen. # If None is specified, by default we take every indexed field # defined on referenced objects' class. self.queryFields = queryFields # The search screen will have this number of columns self.queryNbCols = queryNbCols # Within the portlet, will referred elements appear ? self.navigable = navigable # The search select method is used if self.indexed is True. In this # case, we need to know among which values we can search on this field, # in the search screen. Those values are returned by self.searchSelect, # which must be a static method accepting the tool as single arg. self.searchSelect = searchSelect # If changeOrder is False, it even if the user has the right to modify # the field, it will not be possible to move objects or sort them. self.changeOrder = changeOrder Field.__init__(self, validator, multiplicity, default, show, page, group, layouts, move, indexed, False, specificReadPermission, specificWritePermission, width, height, None, colspan, master, masterValue, focus, historized, sync, mapping, label, sdefault, scolspan, swidth, sheight) self.validable = self.link def getDefaultLayouts(self): return {'view': Table('l-f', width='100%'), 'edit': 'lrv-f'} def isShowable(self, obj, layoutType): res = Field.isShowable(self, obj, layoutType) if not res: return res # We add here specific Ref rules for preventing to show the field under # some inappropriate circumstances. if (layoutType == 'edit') and \ (self.mayAdd(obj) or not self.link): return False if self.isBack: if layoutType == 'edit': return False else: return getattr(obj.aq_base, self.name, None) return res def getValue(self, obj, type='objects', noListIfSingleObj=False, startNumber=None, someObjects=False): '''Returns the objects linked to p_obj through this Ref field. - If p_type is "objects", it returns the Appy wrappers; - If p_type is "zobjects", it returns the Zope objects; - If p_type is "uids", it returns UIDs of objects (= strings). * If p_startNumber is None, it returns all referred objects. * If p_startNumber is a number, it returns self.maxPerPage objects, starting at p_startNumber. If p_noListIfSingleObj is True, it returns the single reference as an object and not as a list. If p_someObjects is True, it returns an instance of SomeObjects instead of returning a list of references.''' uids = getattr(obj.aq_base, self.name, []) if not uids: # Maybe is there a default value? defValue = Field.getValue(self, obj) if defValue: # I must prefix call to function "type" with "__builtins__" # because this name was overridden by a method parameter. if __builtins__['type'](defValue) in sutils.sequenceTypes: uids = [o.o.UID() for o in defValue] else: uids = [defValue.o.UID()] # Prepare the result: an instance of SomeObjects, that will be unwrapped # if not required. res = gutils.SomeObjects() res.totalNumber = res.batchSize = len(uids) batchNeeded = startNumber != None if batchNeeded: res.batchSize = self.maxPerPage if startNumber != None: res.startNumber = startNumber # Get the objects given their uids i = res.startNumber while i < (res.startNumber + res.batchSize): if i >= res.totalNumber: break # Retrieve every reference in the correct format according to p_type if type == 'uids': ref = uids[i] else: ref = obj.getTool().getObject(uids[i]) if type == 'objects': ref = ref.appy() res.objects.append(ref) i += 1 # Manage parameter p_noListIfSingleObj if res.objects and noListIfSingleObj: if self.multiplicity[1] == 1: res.objects = res.objects[0] if someObjects: return res return res.objects def getLinkedObjects(self, obj, startNumber=None): '''Gets the objects linked to p_obj via this Ref field. If p_startNumber is None, all linked objects are returned. If p_startNumber is a number, self.maxPerPage objects will be returned, starting at p_startNumber.''' return self.getValue(obj, type='zobjects', someObjects=True, startNumber=startNumber) def getFormattedValue(self, obj, value, showChanges=False): return value def getIndexType(self): return 'ListIndex' def getIndexValue(self, obj, forSearch=False): '''Value for indexing is the list of UIDs of linked objects. If p_forSearch is True, it will return a list of the linked objects' titles instead.''' if not forSearch: res = getattr(obj.aq_base, self.name, []) if res: # The index does not like persistent lists. res = list(res) else: # Ugly catalog: if I return an empty list, the previous value # is kept. res.append('') return res else: # For the global search: return linked objects' titles. res = [o.title for o in self.getValue(type='objects')] if not res: res.append('') return res def validateValue(self, obj, value): if not self.link: return None # We only check "link" Refs because in edit views, "add" Refs are # not visible. So if we check "add" Refs, on an "edit" view we will # believe that that there is no referred object even if there is. # If the field is a reference, we must ensure itself that multiplicities # are enforced. if not value: nbOfRefs = 0 elif isinstance(value, basestring): nbOfRefs = 1 else: nbOfRefs = len(value) minRef = self.multiplicity[0] maxRef = self.multiplicity[1] if maxRef == None: maxRef = sys.maxint if nbOfRefs < minRef: return obj.translate('min_ref_violated') elif nbOfRefs > maxRef: return obj.translate('max_ref_violated') def linkObject(self, obj, value, back=False): '''This method links p_value (which can be a list of objects) to p_obj through this Ref field.''' # p_value can be a list of objects if type(value) in sutils.sequenceTypes: for v in value: self.linkObject(obj, v, back=back) return # Gets the list of referred objects (=list of uids), or create it. obj = obj.o refs = getattr(obj.aq_base, self.name, None) if refs == None: refs = obj.getProductConfig().PersistentList() setattr(obj, self.name, refs) # Insert p_value into it. uid = value.o.UID() if uid not in refs: # Where must we insert the object? At the start? At the end? if callable(self.add): add = self.callMethod(obj, self.add) else: add = self.add if add == 'start': refs.insert(0, uid) else: refs.append(uid) # Update the back reference if not back: self.back.linkObject(value, obj, back=True) def unlinkObject(self, obj, value, back=False): '''This method unlinks p_value (which can be a list of objects) from p_obj through this Ref field.''' # p_value can be a list of objects if type(value) in sutils.sequenceTypes: for v in value: self.unlinkObject(obj, v, back=back) return obj = obj.o refs = getattr(obj.aq_base, self.name, None) if not refs: return # Unlink p_value uid = value.o.UID() if uid in refs: refs.remove(uid) # Update the back reference if not back: self.back.unlinkObject(value, obj, back=True) def store(self, obj, value): '''Stores on p_obj, the p_value, which can be: * None; * an object UID (=string); * a list of object UIDs (=list of strings). Generally, UIDs or lists of UIDs come from Ref fields with link:True edited through the web; * a Zope object; * a Appy object; * a list of Appy or Zope objects.''' # Standardize p_value into a list of Zope objects objects = value if not objects: objects = [] if type(objects) not in sutils.sequenceTypes: objects = [objects] tool = obj.getTool() for i in range(len(objects)): if isinstance(objects[i], basestring): # We have a UID here objects[i] = tool.getObject(objects[i]) else: # Be sure to have a Zope object objects[i] = objects[i].o uids = [o.UID() for o in objects] # Unlink objects that are not referred anymore refs = getattr(obj.aq_base, self.name, None) if refs: i = len(refs) - 1 while i >= 0: if refs[i] not in uids: # Object having this UID must unlink p_obj self.back.unlinkObject(tool.getObject(refs[i]), obj) i -= 1 # Link new objects if objects: self.linkObject(obj, objects) def mayAdd(self, obj): '''May the user create a new referred object from p_obj via this Ref?''' # We can't (yet) do that on back references. if self.isBack: return No('is_back') # Check if this Ref is addable if callable(self.add): add = self.callMethod(obj, self.add) else: add = self.add if not add: return No('no_add') # Have we reached the maximum number of referred elements? if self.multiplicity[1] != None: refCount = len(getattr(obj, self.name, ())) if refCount >= self.multiplicity[1]: return No('max_reached') # May the user edit this Ref field? if not obj.allows(self.writePermission): return No('no_write_perm') # Have the user the correct add permission? tool = obj.getTool() addPermission = '%s: Add %s' % (tool.getAppName(), tool.getPortalType(self.klass)) folder = obj.getCreateFolder() if not obj.getUser().has_permission(addPermission, folder): return No('no_add_perm') return True def checkAdd(self, obj): '''Compute m_mayAdd above, and raise an Unauthorized exception if m_mayAdd returns False.''' may = self.mayAdd(obj) if not may: from AccessControl import Unauthorized raise Unauthorized("User can't write Ref field '%s' (%s)." % \ (self.name, may.msg)) def changeOrderEnabled(self, obj): '''Is changeOrder enabled?''' if isinstance(self.changeOrder, bool): return self.changeOrder else: return self.callMethod(obj, self.changeOrder) def getSelectableObjects(self, obj): '''This method returns the list of all objects that can be selected to be linked as references to p_obj via p_self.''' if not self.select: # No select method has been defined: we must retrieve all objects # of the referred type that the user is allowed to access. return obj.search(self.klass) else: return self.select(obj) xhtmlToText = re.compile('<.*?>', re.S) def getReferenceLabel(self, refObject): '''p_self must have 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 self.shownInfo is used.''' res = '' for fieldName in self.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 = self.width or 30 if len(res) > maxWidth: res = res[:maxWidth - 2] + '...' return res def getIndexOf(self, obj, refObj): '''Gets the position of p_refObj within this field on p_obj.''' uids = getattr(obj.aq_base, self.name, None) if not uids: raise IndexError() return uids.index(refObj.UID())
def formatLayouts(self, layouts): '''Standardizes the given p_layouts. .''' # First, get the layouts as a dictionary, if p_layouts is None or # expressed as a simple string. areDefault = False if not layouts: # Get the default layouts as defined by the subclass areDefault = True layouts = self.computeDefaultLayouts() else: if isinstance(layouts, basestring): # The user specified a single layoutString (the "edit" one) layouts = {'edit': layouts} elif isinstance(layouts, Table): # Idem, but with a Table instance layouts = {'edit': Table(other=layouts)} else: # Here, we make a copy of the layouts, because every layout can # be different, even if the user decides to reuse one from one # field to another. This is because we modify every layout for # adding master/slave-related info, focus-related info, etc, # which can be different from one field to the other. layouts = copy.deepcopy(layouts) if 'edit' not in layouts: defEditLayout = self.computeDefaultLayouts() if type(defEditLayout) == dict: defEditLayout = defEditLayout['edit'] layouts['edit'] = defEditLayout # We have now a dict of layouts in p_layouts. Ensure now that a Table # instance is created for every layout (=value from the dict). Indeed, # a layout could have been expressed as a simple layout string. for layoutType in layouts.iterkeys(): if isinstance(layouts[layoutType], basestring): layouts[layoutType] = Table(layouts[layoutType]) # Derive "view", "search" and "cell" layouts from the "edit" layout # when relevant. if 'view' not in layouts: layouts['view'] = Table(other=layouts['edit'], derivedType='view') if 'search' not in layouts: layouts['search'] = Table(other=layouts['view'], derivedType='search') # Create the "cell" layout from the 'view' layout if not specified. if 'cell' not in layouts: layouts['cell'] = Table(other=layouts['view'], derivedType='cell') # Put the required CSS classes in the layouts layouts['cell'].addCssClasses('noStyle') if self.focus: # We need to make it flashy layouts['view'].addCssClasses('focus') layouts['edit'].addCssClasses('focus') # If layouts are the default ones, set width=None instead of width=100% # for the field if it is not in a group (excepted for rich texts and # refs). if areDefault and not self.group and \ not ((self.type == 'String') and (self.format == self.XHTML)) and \ not (self.type == 'Ref'): for layoutType in layouts.iterkeys(): layouts[layoutType].width = '' # Remove letters "r" from the layouts if the field is not required. if not self.required: for layoutType in layouts.iterkeys(): layouts[layoutType].removeElement('r') # Derive some boolean values from the layouts. self.hasLabel = self.hasLayoutElement('l', layouts) # "renderLabel" indicates if the existing label (if hasLabel is True) # must be rendered by pxLabel. For example, if field is an action, the # label will be rendered within the button, not by pxLabel. self.renderLabel = self.hasLabel # If field is within a group rendered like a tab, the label will already # be rendered in the corresponding tab. if self.group and (self.group.style == 'tabs'): self.renderLabel = False self.hasDescr = self.hasLayoutElement('d', layouts) self.hasHelp = self.hasLayoutElement('h', layouts) return layouts
class Field: '''Basic abstract class for defining any field.''' # Some global static variables nullValues = (None, '', []) validatorTypes = (types.FunctionType, types.UnboundMethodType, type(re.compile(''))) labelTypes = ('label', 'descr', 'help') # Those attributes can be overridden by subclasses for defining, # respectively, names of CSS and Javascript files that are required by this # field, keyed by layoutType. cssFiles = {} jsFiles = {} dLayouts = 'lrv-d-f' hLayouts = 'lhrv-f' wLayouts = Table('lrv-f') # Render a field. Optional vars: # * fieldName can be given as different as field.name for fields included # in List fields: in this case, fieldName includes the row # index. # * showChanges If True, a variant of the field showing successive changes # made to it is shown. pxRender = Px(''' <x var="showChanges=showChanges|req.get('showChanges',False); layoutType=layoutType|req.get('layoutType'); isSearch = layoutType == 'search'; layout=field.layouts[layoutType]; name=fieldName|field.name; widgetName = isSearch and ('w_%s' % name) or name; outerValue=value|None; rawValue=not isSearch and zobj.getFieldValue(name, \ layoutType=layoutType, outerValue=outerValue); value=not isSearch and \ field.getFormattedValue(zobj, rawValue, showChanges); requestValue=not isSearch and zobj.getRequestFieldValue(name); inRequest=req.has_key(name); error=req.get('%s_error' % name); isMultiple=(field.multiplicity[1] == None) or \ (field.multiplicity[1] > 1); masterCss=field.slaves and ('master_%s' % name) or ''; slaveCss=field.getSlaveCss(); tagCss=tagCss|''; tagCss=('%s %s' % (slaveCss, tagCss)).strip(); zobj=zobj or ztool; tagId='%s_%s' % (zobj.id, name); tagName=field.master and 'slave' or ''; layoutTarget=field">:tool.pxLayoutedObject</x>''') # Displays a field label. pxLabel = Px('''<label if="field.hasLabel and field.renderLabel" lfor=":field.name">::_('label', field=field)</label>''') # Displays a field description. pxDescription = Px('''<span if="field.hasDescr" class="discreet">::_('descr', field=field)</span>''') # Displays a field help. pxHelp = Px('''<acronym title=":_('help', field=field)"><img src=":url('help')"/></acronym>''') # Displays validation-error-related info about a field. pxValidation = Px('''<x><acronym if="error" title=":error"><img src=":url('warning')"/></acronym><img if="not error" src=":url('warning_no.gif')"/></x>''') # Displays the fact that a field is required. pxRequired = Px('''<img src=":url('required.gif')"/>''') # Button for showing changes to the field. pxChanges = Px('''<x if=":zobj.hasHistory(name)"><img class="clickable" if="not showChanges" src=":url('changes')" title="_('changes_show')" onclick=":'askField(%s,%s,%s,%s)' % \ (q(tagId), q(zobj.absolute_url()), q('view'), q('True'))"/><img class="clickable" if="showChanges" src=":url('changesNo')" onclick=":'askField(%s,%s,%s,%s)' % \ (q(tagId), q(zobj.absolute_url(), q('view'), q('True'))" title=":_('changes_hide')"/></x>''') def __init__(self, validator, multiplicity, default, show, page, group, layouts, move, indexed, searchable, specificReadPermission, specificWritePermission, width, height, maxChars, colspan, master, masterValue, focus, historized, mapping, label, sdefault, scolspan, swidth, sheight, persist): # The validator restricts which values may be defined. It can be an # interval (1,None), a list of string values ['choice1', 'choice2'], # a regular expression, a custom function, a Selection instance, etc. self.validator = validator # Multiplicity is a 2-tuple indicating the minimum and maximum # occurrences of values. self.multiplicity = multiplicity # Is the field required or not ? (derived from multiplicity) self.required = self.multiplicity[0] > 0 # Default value self.default = default # Must the field be visible or not? self.show = show # When displaying/editing the whole object, on what page and phase must # this field value appear? self.page = Page.get(page) self.pageName = self.page.name # Within self.page, in what group of fields must this one appear? self.group = Group.get(group) # The following attribute allows to move a field back to a previous # position (useful for moving fields above predefined ones). self.move = move # If indexed is True, a database index will be set on the field for # fast access. self.indexed = indexed # If specified "searchable", the field will be added to some global # index allowing to perform application-wide, keyword searches. self.searchable = searchable # Normally, permissions to read or write every attribute in a type are # granted if the user has the global permission to read or # edit instances of the whole type. If you want a given attribute # to be protected by specific permissions, set one or the 2 next boolean # values to "True". In this case, you will create a new "field-only" # read and/or write permission. If you need to protect several fields # with the same read/write permission, you can avoid defining one # specific permission for every field by specifying a "named" # permission (string) instead of assigning "True" to the following # arg(s). A named permission will be global to your whole Zope site, so # take care to the naming convention. Typically, a named permission is # of the form: "<yourAppName>: Write|Read ---". If, for example, I want # to define, for my application "MedicalFolder" a specific permission # for a bunch of fields that can only be modified by a doctor, I can # define a permission "MedicalFolder: Write medical information" and # assign it to the "specificWritePermission" of every impacted field. self.specificReadPermission = specificReadPermission self.specificWritePermission = specificWritePermission # Widget width and height self.width = width self.height = height # While width and height refer to widget dimensions, maxChars hereafter # represents the maximum number of chars that a given input field may # accept (corresponds to HTML "maxlength" property). "None" means # "unlimited". self.maxChars = maxChars or '' # If the widget is in a group with multiple columns, the following # attribute specifies on how many columns to span the widget. self.colspan = colspan or 1 # The list of slaves of this field, if it is a master self.slaves = [] # The behaviour of this field may depend on another, "master" field self.master = master if master: self.master.slaves.append(self) # The semantics of attribute "masterValue" below is as follows: # - if "masterValue" is anything but a method, the field will be shown # only when the master has this value, or one of it if multivalued; # - if "masterValue" is a method, the value(s) of the slave field will # be returned by this method, depending on the master value(s) that # are given to it, as its unique parameter. self.masterValue = gutils.initMasterValue(masterValue) # If a field must retain attention in a particular way, set focus=True. # It will be rendered in a special way. self.focus = focus # If we must keep track of changes performed on a field, "historized" # must be set to True. self.historized = historized # Mapping is a dict of contexts that, if specified, are given when # translating the label, descr or help related to this field. self.mapping = self.formatMapping(mapping) self.id = id(self) self.type = self.__class__.__name__ self.pythonType = None # The True corresponding Python type # Get the layouts. Consult layout.py for more info about layouts. self.layouts = self.formatLayouts(layouts) # Can we filter this field? self.filterable = False # Can this field have values that can be edited and validated? self.validable = True # The base label for translations is normally generated automatically. # It is made of 2 parts: the prefix, based on class name, and the name, # which is the field name by default. You can change this by specifying # a value for param "label". If this value is a string, it will be # understood as a new prefix. If it is a tuple, it will represent the # prefix and another name. If you want to specify a new name only, and # not a prefix, write (None, newName). self.label = label # When you specify a default value "for search" (= "sdefault"), on a # search screen, in the search field corresponding to this field, this # default value will be present. self.sdefault = sdefault # Colspan for rendering the search widget corresponding to this field. self.scolspan = scolspan or 1 # Width and height for the search widget self.swidth = swidth or width self.sheight = sheight or height # "persist" indicates if field content must be stored in the database. # For some fields it is not wanted (ie, fields used only as masters to # update slave's selectable values). self.persist = persist def init(self, name, klass, appName): '''When the application server starts, this secondary constructor is called for storing the name of the Appy field (p_name) and other attributes that are based on the name of the Appy p_klass, and the application name (p_appName).''' if hasattr(self, 'name'): return # Already initialized self.name = name # Determine prefix for this class if not klass: prefix = appName else: prefix = gutils.getClassName(klass, appName) # Recompute the ID (and derived attributes) that may have changed if # we are in debug mode (because we recreate new Field instances). self.id = id(self) # Remember master name on every slave for slave in self.slaves: slave.masterName = name # Determine ids of i18n labels for this field labelName = name trPrefix = None if self.label: if isinstance(self.label, basestring): trPrefix = self.label else: # It is a tuple (trPrefix, name) if self.label[1]: labelName = self.label[1] if self.label[0]: trPrefix = self.label[0] if not trPrefix: trPrefix = prefix # Determine name to use for i18n self.labelId = '%s_%s' % (trPrefix, labelName) self.descrId = self.labelId + '_descr' self.helpId = self.labelId + '_help' # Determine read and write permissions for this field rp = self.specificReadPermission if rp and not isinstance(rp, basestring): self.readPermission = '%s: Read %s %s' % (appName, prefix, name) elif rp and isinstance(rp, basestring): self.readPermission = rp else: self.readPermission = 'read' wp = self.specificWritePermission if wp and not isinstance(wp, basestring): self.writePermission = '%s: Write %s %s' % (appName, prefix, name) elif wp and isinstance(wp, basestring): self.writePermission = wp else: self.writePermission = 'write' if (self.type == 'Ref') and not self.isBack: # We must initialise the corresponding back reference self.back.klass = klass self.back.init(self.back.attribute, self.klass, appName) if self.type == "List": for subName, subField in self.fields: fullName = '%s_%s' % (name, subName) subField.init(fullName, klass, appName) subField.name = '%s*%s' % (name, subName) def reload(self, klass, obj): '''In debug mode, we want to reload layouts without restarting Zope. So this method will prepare a "new", reloaded version of p_self, that corresponds to p_self after a "reload" of its containing Python module has been performed.''' res = getattr(klass, self.name, None) if not res: return self if (self.type == 'Ref') and self.isBack: return self res.init(self.name, klass, obj.getProductConfig().PROJECTNAME) return res def isMultiValued(self): '''Does this type definition allow to define multiple values?''' res = False maxOccurs = self.multiplicity[1] if (maxOccurs == None) or (maxOccurs > 1): res = True return res def isSortable(self, usage): '''Can fields of this type be used for sorting purposes (when sorting search results (p_usage="search") or when sorting reference fields (p_usage="ref")?''' if usage == 'search': return self.indexed and not self.isMultiValued() and not \ ((self.type == 'String') and self.isSelection()) elif usage == 'ref': return self.type in ('Integer', 'Float', 'Boolean', 'Date') or \ ((self.type == 'String') and (self.format == 0)) def isShowable(self, obj, layoutType): '''When displaying p_obj on a given p_layoutType, must we show this field?''' # Check if the user has the permission to view or edit the field perm = (layoutType == 'edit') and self.writePermission or \ self.readPermission if not obj.allows(perm): return # Evaluate self.show if callable(self.show): res = self.callMethod(obj, self.show) else: res = self.show # Take into account possible values 'view', 'edit', 'result'... if type(res) in sutils.sequenceTypes: for r in res: if r == layoutType: return True return elif res in ('view', 'edit', 'result'): return res == layoutType return bool(res) def isClientVisible(self, obj): '''This method returns True if this field is visible according to master/slave relationships.''' masterData = self.getMasterData() if not masterData: return True else: master, masterValue = masterData if masterValue and callable(masterValue): return True reqValue = master.getRequestValue(obj.REQUEST) # reqValue can be a list or not if type(reqValue) not in sutils.sequenceTypes: return reqValue in masterValue else: for m in masterValue: for r in reqValue: if m == r: return True def formatMapping(self, mapping): '''Creates a dict of mappings, one entry by label type (label, descr, help).''' if isinstance(mapping, dict): # Is it a dict like {'label':..., 'descr':...}, or is it directly a # dict with a mapping? for k, v in mapping.iteritems(): if (k not in self.labelTypes) or isinstance(v, basestring): # It is already a mapping return { 'label': mapping, 'descr': mapping, 'help': mapping } # If we are here, we have {'label':..., 'descr':...}. Complete # it if necessary. for labelType in self.labelTypes: if labelType not in mapping: mapping[labelType] = None # No mapping for this value. return mapping else: # Mapping is a method that must be applied to any i18n message. return {'label': mapping, 'descr': mapping, 'help': mapping} def formatLayouts(self, layouts): '''Standardizes the given p_layouts. .''' # First, get the layouts as a dictionary, if p_layouts is None or # expressed as a simple string. areDefault = False if not layouts: # Get the default layouts as defined by the subclass areDefault = True layouts = self.computeDefaultLayouts() else: if isinstance(layouts, basestring): # The user specified a single layoutString (the "edit" one) layouts = {'edit': layouts} elif isinstance(layouts, Table): # Idem, but with a Table instance layouts = {'edit': Table(other=layouts)} else: # Here, we make a copy of the layouts, because every layout can # be different, even if the user decides to reuse one from one # field to another. This is because we modify every layout for # adding master/slave-related info, focus-related info, etc, # which can be different from one field to the other. layouts = copy.deepcopy(layouts) if 'edit' not in layouts: defEditLayout = self.computeDefaultLayouts() if type(defEditLayout) == dict: defEditLayout = defEditLayout['edit'] layouts['edit'] = defEditLayout # We have now a dict of layouts in p_layouts. Ensure now that a Table # instance is created for every layout (=value from the dict). Indeed, # a layout could have been expressed as a simple layout string. for layoutType in layouts.iterkeys(): if isinstance(layouts[layoutType], basestring): layouts[layoutType] = Table(layouts[layoutType]) # Derive "view", "search" and "cell" layouts from the "edit" layout # when relevant. if 'view' not in layouts: layouts['view'] = Table(other=layouts['edit'], derivedType='view') if 'search' not in layouts: layouts['search'] = Table(other=layouts['view'], derivedType='search') # Create the "cell" layout from the 'view' layout if not specified. if 'cell' not in layouts: layouts['cell'] = Table(other=layouts['view'], derivedType='cell') # Put the required CSS classes in the layouts layouts['cell'].addCssClasses('noStyle') if self.focus: # We need to make it flashy layouts['view'].addCssClasses('focus') layouts['edit'].addCssClasses('focus') # If layouts are the default ones, set width=None instead of width=100% # for the field if it is not in a group (excepted for rich texts and # refs). if areDefault and not self.group and \ not ((self.type == 'String') and (self.format == self.XHTML)) and \ not (self.type == 'Ref'): for layoutType in layouts.iterkeys(): layouts[layoutType].width = '' # Remove letters "r" from the layouts if the field is not required. if not self.required: for layoutType in layouts.iterkeys(): layouts[layoutType].removeElement('r') # Derive some boolean values from the layouts. self.hasLabel = self.hasLayoutElement('l', layouts) # "renderLabel" indicates if the existing label (if hasLabel is True) # must be rendered by pxLabel. For example, if field is an action, the # label will be rendered within the button, not by pxLabel. self.renderLabel = self.hasLabel # If field is within a group rendered like a tab, the label will already # be rendered in the corresponding tab. if self.group and (self.group.style == 'tabs'): self.renderLabel = False self.hasDescr = self.hasLayoutElement('d', layouts) self.hasHelp = self.hasLayoutElement('h', layouts) return layouts def hasLayoutElement(self, element, layouts): '''This method returns True if the given layout p_element can be found at least once among the various p_layouts defined for this field.''' for layout in layouts.itervalues(): if element in layout.layoutString: return True return False def getDefaultLayouts(self): '''Any subclass can define this for getting a specific set of default layouts. If None is returned, a global set of default layouts will be used.''' def getInputLayouts(self): '''Gets, as a string, the layouts as could have been specified as input value for the Field constructor.''' res = '{' for k, v in self.layouts.iteritems(): res += '"%s":"%s",' % (k, v.layoutString) res += '}' return res def computeDefaultLayouts(self): '''This method gets the default layouts from an Appy type, or a copy from the global default field layouts when they are not available.''' res = self.getDefaultLayouts() if not res: # Get the global default layouts res = copy.deepcopy(defaultFieldLayouts) return res def getCss(self, layoutType, res): '''This method completes the list p_res with the names of CSS files that are required for displaying widgets of self's type on a given p_layoutType. p_res is not a set because order of inclusion of CSS files may be important and may be loosed by using sets.''' if layoutType in self.cssFiles: for fileName in self.cssFiles[layoutType]: if fileName not in res: res.append(fileName) def getJs(self, layoutType, res): '''This method completes the list p_res with the names of Javascript files that are required for displaying widgets of self's type on a given p_layoutType. p_res is not a set because order of inclusion of CSS files may be important and may be loosed by using sets.''' if layoutType in self.jsFiles: for fileName in self.jsFiles[layoutType]: if fileName not in res: res.append(fileName) def getValue(self, obj): '''Gets, on_obj, the value conforming to self's type definition.''' value = getattr(obj.aq_base, self.name, None) if self.isEmptyValue(value): # If there is no value, get the default value if any: return # self.default, of self.default() if it is a method. if callable(self.default): try: return self.callMethod(obj, self.default) except (Exception, e): # Already logged. Here I do not raise the exception, # because it can be raised as the result of reindexing # the object in situations that are not foreseen by # method in self.default. return else: return self.default return value def getFormattedValue(self, obj, value, showChanges=False): '''p_value is a real p_obj(ect) value from a field from this type. This method returns a pretty, string-formatted version, for displaying purposes. Needs to be overridden by some child classes. If p_showChanges is True, the result must also include the changes that occurred on p_value across the ages.''' if self.isEmptyValue(value): return '' return value def getIndexType(self): '''Returns the name of the technical, Zope-level index type for this field.''' # Normally, self.indexed contains a Boolean. If a string value is given, # we consider it to be an index type. It allows to bypass the standard # way to decide what index type must be used. if isinstance(self.indexed, str): return self.indexed if self.name == 'title': return 'TextIndex' return 'FieldIndex' def getIndexValue(self, obj, forSearch=False): '''This method returns a version for this field value on p_obj that is ready for indexing purposes. Needs to be overridden by some child classes. If p_forSearch is True, it will return a "string" version of the index value suitable for a global search.''' value = self.getValue(obj) if forSearch and (value != None): if isinstance(value, unicode): res = value.encode('utf-8') elif type(value) in sutils.sequenceTypes: res = [] for v in value: if isinstance(v, unicode): res.append(v.encode('utf-8')) else: res.append(str(v)) res = ' '.join(res) else: res = str(value) return res return value def getRequestValue(self, request, requestName=None): '''Gets a value for this field as carried in the request object. In the simplest cases, the request value is a single value whose name in the request is the name of the field. Sometimes (ie: a Date: see the overriden method in the Date class), several request values must be combined. Sometimes (ie, a field which is a sub-field in a List), the name of the request value(s) representing the field value do not correspond to the field name (ie: the request name includes information about the container field). In this case, p_requestName must be used for searching into the request, instead of the field name (self.name).''' name = requestName or self.name return request.get(name, None) def getStorableValue(self, value): '''p_value is a valid value initially computed through calling m_getRequestValue. So, it is a valid string (or list of strings) representation of the field value coming from the request. This method computes the real (potentially converted or manipulated in some other way) value as can be stored in the database.''' if self.isEmptyValue(value): return return value def getMasterData(self): '''Gets the master of this field (and masterValue) or, recursively, of containing groups when relevant.''' if self.master: return (self.master, self.masterValue) if self.group: return self.group.getMasterData() def getSlaveCss(self): '''Gets the CSS class that must apply to this field in the web UI when this field is the slave of another field.''' if not self.master: return '' res = 'slave*%s*' % self.masterName if not callable(self.masterValue): res += '*'.join(self.masterValue) else: res += '+' return res def getOnChange(self, zobj, layoutType, className=None): '''When this field is a master, this method computes the call to the Javascript function that will be called when its value changes (in order to update slaves).''' if not self.slaves: return '' q = zobj.getTool().quote # When the field is on a search screen, we need p_className. cName = className and (',%s' % q(className)) or '' return 'updateSlaves(this,null,%s,%s,null,null%s)' % \ (q(zobj.absolute_url()), q(layoutType), cName) def isEmptyValue(self, value, obj=None): '''Returns True if the p_value must be considered as an empty value.''' return value in self.nullValues def validateValue(self, obj, value): '''This method may be overridden by child classes and will be called at the right moment by m_validate defined below for triggering type-specific validation. p_value is never empty.''' return def securityCheck(self, obj, value): '''This method performs some security checks on the p_value that represents user input.''' if not isinstance(value, basestring): return # Search Javascript code in the value (prevent XSS attacks). if '<script' in value: obj.log('Detected Javascript in user input.', type='error') raise Exception('Your behaviour is considered a security ' \ 'attack. System administrator has been warned.') def validate(self, obj, value): '''This method checks that p_value, coming from the request (p_obj is being created or edited) and formatted through a call to m_getRequestValue defined above, is valid according to this type definition. If it is the case, None is returned. Else, a translated error message is returned.''' # Check that a value is given if required. if self.isEmptyValue(value, obj): if self.required and self.isClientVisible(obj): # If the field is required, but not visible according to # master/slave relationships, we consider it not to be required. return obj.translate('field_required') else: return # Perform security checks on p_value self.securityCheck(obj, value) # Triggers the sub-class-specific validation for this value message = self.validateValue(obj, value) if message: return message # Evaluate the custom validator if one has been specified value = self.getStorableValue(value) if self.validator and (type(self.validator) in self.validatorTypes): obj = obj.appy() if type(self.validator) != self.validatorTypes[-1]: # It is a custom function. Execute it. try: validValue = self.validator(obj, value) if isinstance(validValue, basestring) and validValue: # Validation failed; and p_validValue contains an error # message. return validValue else: if not validValue: return obj.translate('field_invalid') except (Exception, e): return str(e) except: return obj.translate('field_invalid') else: # It is a regular expression if not self.validator.match(value): return obj.translate('field_invalid') def store(self, obj, value): '''Stores the p_value (produced by m_getStorableValue) that complies to p_self type definition on p_obj.''' if self.persist: setattr(obj, self.name, value) def callMethod(self, obj, method, cache=True): '''This method is used to call a p_method on p_obj. p_method is part of this type definition (ie a default method, the method of a Computed field, a method used for showing or not a field...). Normally, those methods are called without any arg. But one may need, within the method, to access the related field. This method tries to call p_method with no arg *or* with the field arg.''' obj = obj.appy() try: return gutils.callMethod(obj, method, cache=cache) except TypeError, te: # Try a version of the method that would accept self as an # additional parameter. In this case, we do not try to cache the # value (we do not call gutils.callMethod), because the value may # be different depending on the parameter. tb = sutils.Traceback.get() try: return method(obj, self) except (Exception, e): obj.log(tb, type='error') # Raise the initial error. raise te except (Exception, e): obj.log(sutils.Traceback.get(), type='error') raise e
class Pod(Field): '''A pod is a field allowing to produce a (PDF, ODT, Word, RTF...) document from data contained in Appy class and linked objects or anything you want to put in it. It is the way gen uses pod.''' # Layout for rendering a POD field for exporting query results. rLayouts = {'view': Table('fl', width=None)} POD_ERROR = 'An error occurred while generating the document. Please ' \ 'contact the system administrator.' DELETE_TEMP_DOC_ERROR = 'A temporary document could not be removed. %s.' pxView = pxCell = Px('''<x> <!-- Ask action --> <x if="field.askAction" var2="doLabel='%s_askaction' % field.labelId; chekboxId='%s_%s_cb' % (zobj.UID(), name)"> <input type="checkbox" name=":doLabel" id=":chekboxId"/> <label lfor=":chekboxId" class="discreet">:_(doLabel)"></label> </x> <img for="fmt in field.getToolInfo(obj)[1]" src=":url(fmt)" onclick=":'generatePodDocument(%s, %s, %s, %s)' % \ (q(zobj.UID()), q(name), q(fmt), q(ztool.getQueryInfo()))" title=":fmt.capitalize()" class="clickable"/> </x>''') pxEdit = pxSearch = '' def __init__(self, validator=None, default=None, show=('view', 'result'), page='main', group=None, layouts=None, move=0, indexed=False, searchable=False, specificReadPermission=False, specificWritePermission=False, width=None, height=None, maxChars=None, colspan=1, master=None, masterValue=None, focus=False, historized=False, mapping=None, label=None, template=None, context=None, action=None, askAction=False, stylesMapping={}, freezeFormat='pdf'): # The following param stores the path to a POD template self.template = template # The context is a dict containing a specific pod context, or a method # that returns such a dict. self.context = context # Next one is a method that will be triggered after the document has # been generated. self.action = action # If askAction is True, the action will be triggered only if the user # checks a checkbox, which, by default, will be unchecked. self.askAction = askAction # A global styles mapping that would apply to the whole template self.stylesMapping = stylesMapping # Freeze format is by PDF by default self.freezeFormat = freezeFormat Field.__init__(self, None, (0, 1), default, show, page, group, layouts, move, indexed, searchable, specificReadPermission, specificWritePermission, width, height, None, colspan, master, masterValue, focus, historized, False, mapping, label, None, None, None, None) self.validable = False def isFrozen(self, obj): '''Is there a frozen document for p_self on p_obj?''' value = getattr(obj.o.aq_base, self.name, None) return isinstance(value, obj.o.getProductConfig().File) def getToolInfo(self, obj): '''Gets information related to this field (p_self) that is available in the tool: the POD template and the available output formats. If this field is frozen, available output formats are not available anymore: only the format of the frozen doc is returned.''' tool = obj.tool appyClass = tool.o.getAppyClass(obj.o.meta_type) # Get the output format(s) if self.isFrozen(obj): # The only available format is the one from the frozen document fileName = getattr(obj.o.aq_base, self.name).filename formats = (os.path.splitext(fileName)[1][1:], ) else: # Available formats are those which are selected in the tool. name = tool.getAttributeName('formats', appyClass, self.name) formats = getattr(tool, name) # Get the POD template name = tool.getAttributeName('podTemplate', appyClass, self.name) template = getattr(tool, name) return (template, formats) def getValue(self, obj): '''Gets, on_obj, the value conforming to self's type definition. For a Pod field, if a file is stored in the field, it means that the field has been frozen. Else, it means that the value must be retrieved by calling pod to compute the result.''' rq = getattr(obj, 'REQUEST', None) res = getattr(obj.aq_base, self.name, None) if res and res.size: # Return the frozen file. return sutils.FileWrapper(res) # If we are here, it means that we must call pod to compute the file. # A Pod field differs from other field types because there can be # several ways to produce the field value (ie: output file format can be # odt, pdf,...; self.action can be executed or not...). We get those # precisions about the way to produce the file from the request object # and from the tool. If we don't find the request object (or if it does # not exist, ie, when Zope runs in test mode), we use default values. obj = obj.appy() tool = obj.tool # Get POD template and available formats from the tool. template, availFormats = self.getToolInfo(obj) # Get the output format defaultFormat = 'pdf' if defaultFormat not in availFormats: defaultFormat = availFormats[0] outputFormat = getattr(rq, 'podFormat', defaultFormat) # Get or compute the specific POD context specificContext = None if callable(self.context): specificContext = self.callMethod(obj, self.context) else: specificContext = self.context # Temporary file where to generate the result tempFileName = '%s/%s_%f.%s' % (sutils.getOsTempFolder(), obj.uid, time.time(), outputFormat) # Define parameters to give to the appy.pod renderer podContext = { 'tool': tool, 'user': obj.user, 'self': obj, 'field': self, 'now': obj.o.getProductConfig().DateTime(), '_': obj.translate, 'projectFolder': tool.getDiskFolder() } # If the POD document is related to a query, get it from the request, # execute it and put the result in the context. isQueryRelated = rq.get('queryData', None) if isQueryRelated: # Retrieve query params from the request cmd = ', '.join(tool.o.queryParamNames) cmd += " = rq['queryData'].split(';')" exec cmd # (re-)execute the query, but without any limit on the number of # results; return Appy objects. objs = tool.o.executeQuery(obj.o.portal_type, searchName=search, sortBy=sortKey, sortOrder=sortOrder, filterKey=filterKey, filterValue=filterValue, maxResults='NO_LIMIT') podContext['objects'] = [o.appy() for o in objs['objects']] # Add the field-specific context if present. if specificContext: podContext.update(specificContext) # If a custom param comes from the request, add it to the context. A # custom param must have format "name:value". Custom params override any # other value in the request, including values from the field-specific # context. customParams = rq.get('customParams', None) if customParams: paramsDict = eval(customParams) podContext.update(paramsDict) # Define a potential global styles mapping if callable(self.stylesMapping): stylesMapping = self.callMethod(obj, self.stylesMapping) else: stylesMapping = self.stylesMapping rendererParams = { 'template': StringIO.StringIO(template.content), 'context': podContext, 'result': tempFileName, 'stylesMapping': stylesMapping, 'imageResolver': tool.o.getApp() } if tool.unoEnabledPython: rendererParams['pythonWithUnoPath'] = tool.unoEnabledPython if tool.openOfficePort: rendererParams['ooPort'] = tool.openOfficePort # Launch the renderer try: renderer = Renderer(**rendererParams) renderer.run() except PodError, pe: if not os.path.exists(tempFileName): # In some (most?) cases, when OO returns an error, the result is # nevertheless generated. obj.log(str(pe), type='error') return Pod.POD_ERROR # Give a friendly name for this file fileName = obj.translate(self.labelId) if not isQueryRelated: # This is a POD for a single object: personalize the file name with # the object title. fileName = '%s-%s' % (obj.title, fileName) fileName = tool.normalize(fileName) + '.' + outputFormat # Get a FileWrapper instance from the temp file on the filesystem res = File.getFileObject(tempFileName, fileName) # Execute the related action if relevant doAction = getattr(rq, 'askAction', False) in ('True', True) if doAction and self.action: self.action(obj, podContext) # Returns the doc and removes the temp file try: os.remove(tempFileName) except OSError, oe: obj.log(Pod.DELETE_TEMP_DOC_ERROR % str(oe), type='warning')