Exemple #1
0
class Integer(Field):
    pxView = pxCell = Px('''
     <x>::field.getInlineEditableValue(obj, value, layoutType)</x>
     <input type="hidden" if="masterCss"
            class=":masterCss" value=":value" name=":name" id=":name"/>''')

    pxEdit = Px('''
     <input type="text" id=":name" name=":name" size=":field.width"
            maxlength=":field.maxChars"
            value=":field.getInputValue(inRequest, requestValue, value)"/>
     <script if="hostLayout">:'prepareForAjaxSave(%s,%s,%s,%s)' % \
      (q(name),q(obj.id),q(obj.url),q(hostLayout))</script>''')

    pxSearch = Px('''
     <!-- From -->
     <x var="fromName='%s*int' % widgetName">
      <label lfor=":fromName">:_('search_from')</label>
      <input type="text" name=":fromName" maxlength=":field.maxChars"
             value=":field.sdefault[0]" size=":field.swidth"/>
     </x>
     <!-- To -->
     <x var="toName='%s_to' % name">
      <label lfor=":toName">:_('search_to')</label>
      <input type="text" name=":toName" maxlength=":field.maxChars"
             value=":field.sdefault[1]" size=":field.swidth"/>
     </x><br/>''')

    def __init__(self, validator=None, multiplicity=(0,1), default=None,
      show=True, page='main', group=None, layouts=None, move=0, indexed=False,
      mustIndex=True, indexValue=None, searchable=False,
      specificReadPermission=False, specificWritePermission=False, width=5,
      height=None, maxChars=13, colspan=1, master=None, masterValue=None,
      focus=False, historized=False, mapping=None, label=None, sdefault=('',''),
      scolspan=1, swidth=None, sheight=None, persist=True, inlineEdit=False,
      view=None, cell=None, xml=None):
        Field.__init__(self, validator, multiplicity, default, show, page,
          group, layouts, move, indexed, mustIndex, indexValue, searchable,
          specificReadPermission, specificWritePermission, width, height,
          maxChars, colspan, master, masterValue, focus, historized, mapping,
          label, sdefault, scolspan, swidth, sheight, persist, inlineEdit, view,
          cell, xml)
        self.pythonType = int

    def validateValue(self, obj, value):
        try:
            value = self.pythonType(value)
        except ValueError:
            return obj.translate('bad_%s' % self.pythonType.__name__)

    def getStorableValue(self, obj, value):
        if not self.isEmptyValue(obj, value): return self.pythonType(value)

    def getFormattedValue(self, obj, value, layoutType='view',
                          showChanges=False, language=None):
        if self.isEmptyValue(obj, value): return ''
        return str(value)
Exemple #2
0
class UiSearch:
    '''Instances of this class are generated on-the-fly for manipulating a
       Search from the User Interface.'''
    # PX for rendering a search.
    pxView = Px('''
     <div class="portletSearch">
      <a href=":'%s?className=%s&amp;search=%s' % \
                 (queryUrl, className, search.name)"
         class=":(search.name == currentSearch) and 'current' or ''"
         title=":search.translatedDescr">:search.translated</a>
     </div>''')

    def __init__(self, search, className, tool):
        self.search = search
        self.name = search.name
        self.type = 'search'
        self.colspan = search.colspan
        if search.translated:
            self.translated = search.translated
            self.translatedDescr = search.translatedDescr
        else:
            # The label may be specific in some special cases.
            labelDescr = ''
            if search.name == 'allSearch':
                label = '%s_plural' % className
            elif search.name == 'customSearch':
                label = 'search_results'
            else:
                label = '%s_search_%s' % (className, search.name)
                labelDescr = label + '_descr'
            self.translated = tool.translate(label)
            if labelDescr:
                self.translatedDescr = tool.translate(labelDescr)
            else:
                self.translatedDescr = ''
Exemple #3
0
class UiTransition:
    '''Represents a widget that displays a transition'''
    pxView = Px('''
     <x var="label=transition.title;
             inButtons=layoutType == 'buttons';
             css=ztool.getButtonCss(label, inButtons)">
      <!-- Real button -->
      <input if="transition.mayTrigger" type="button" class=":css"
             var="back=transition.getBackHook(zobj, inButtons, q)"
             id=":transition.name" style=":url(transition.icon, bg=True)"
             value=":label"
             onclick=":'triggerTransition(%s,this,%s,%s)' % \
                        (q(formId), q(transition.confirm), back)"/>

      <!-- Fake button, explaining why the transition can't be triggered -->
      <input if="not transition.mayTrigger" type="button"
             class=":'fake %s' % css" style=":url('fake', bg=True)"
             value=":label" title=":transition.reason"/></x>''')

    def __init__(self, name, transition, obj, mayTrigger):
        self.name = name
        self.transition = transition
        self.type = 'transition'
        self.icon = transition.icon
        label = obj.getWorkflowLabel(name)
        self.title = obj.translate(label)
        if transition.confirm:
            msg = obj.translate('%s_confirm' % label, blankOnError=True) or \
                  obj.translate('action_confirm')
            self.confirm = msg
        else:
            self.confirm = ''
        # May this transition be triggered via the UI?
        self.mayTrigger = True
        self.reason = ''
        if not mayTrigger:
            self.mayTrigger = False
            self.reason = mayTrigger.msg
        # Required by the UiGroup
        self.colspan = 1

    def getBackHook(self, obj, inButtons, q):
        '''If, when the transition has been triggered, we must ajax-refresh some
           part of the page, this method will return the ID of the corresponding
           DOM node. Else (ie, the entire page needs to be refreshed), it
           returns None.'''
        if inButtons and (self.transition.redirect != 'page'): return q(obj.id)
        return 'null'
Exemple #4
0
class UiTransition:
    '''Represents a widget that displays a transition.'''
    pxView = Px('''<x var="buttonCss = (buttonsMode == 'small') and \
                                       'buttonSmall button' or 'button'">
      <!-- Real button -->
      <input if="transition.mayTrigger" type="button" class=":buttonCss"
             var2="label=transition.title"
             style=":'%s; %s' % (url(transition.icon, bg=True), \
                                 ztool.getButtonWidth(label))"
             value=":label"
             onclick=":'triggerTransition(%s,%s,%s)' % (q(formId), \
                        q(transition.name), q(transition.confirm))"/>

      <!-- Fake button, explaining why the transition can't be triggered -->
      <input if="not transition.mayTrigger" type="button"
             class=":'fake ' + buttonCss" var2="label=transition.title"
             style=":'%s; %s' % (url('fake', bg=True),
                                 ztool.getButtonWidth(label))"
             value=":label" title=":transition.reason"/></x>''')

    def __init__(
        self,
        name,
        transition,
        obj,
        mayTrigger,
    ):
        self.name = name
        self.transition = transition
        self.type = 'transition'
        self.icon = transition.icon
        label = obj.getWorkflowLabel(name)
        self.title = obj.translate(label)
        if transition.confirm:
            self.confirm = obj.translate('%s_confirm' % label)
        else:
            self.confirm = ''
        # May this transition be triggered via the UI?
        self.mayTrigger = True
        self.reason = ''
        if not mayTrigger:
            self.mayTrigger = False
            self.reason = mayTrigger.msg
        # Required by the UiGroup.
        self.colspan = 1
Exemple #5
0
class Collapsible:
    '''Represents a chunk of HTML code that can be collapsed/expanded via
       clickable icons.'''
    # Various sets of icons can be used. Each one has a CSS class in appy.css
    iconSets = {
        'expandCollapse': Object(expand='expand', collapse='collapse'),
        'showHide': Object(expand='show', collapse='hide'),
        'showHideInv': Object(expand='hide', collapse='show')
    }

    # Icon allowing to collapse/expand a chunk of HTML
    px = Px('''
     <img var="coll=collapse; icons=coll.icons"
          id=":'%s_img' % coll.id" align=":coll.align" class=":coll.css"
          onclick=":'toggleCookie(%s,%s,%s,%s,%s)' % (q(coll.id), \
                    q(coll.display), q(coll.default), \
                    q(icons.expand), q(icons.collapse))"
       src=":coll.expanded and url(icons.collapse) or url(icons.expand)"/>''')

    def __init__(self,
                 id,
                 request,
                 default='collapsed',
                 display='block',
                 icons='expandCollapse',
                 align='left'):
        '''p_display is the value of style attribute "display" for the XHTML
           element when it must be displayed. By default it is "block"; for a
           table it must be "table", etc.'''
        self.id = id  # The ID of the collapsible HTML element
        self.request = request  # The request object
        self.default = default
        self.display = display
        self.align = align
        # Must the element be collapsed or expanded ?
        self.expanded = request.get(id, default) == 'expanded'
        self.style = 'display:%s' % (self.expanded and self.display or 'none')
        # The name of the CSS class depends on the set of applied icons
        self.css = icons
        self.icons = self.iconSets[icons]
Exemple #6
0
class Phase:
    '''A group of pages'''

    pxView = Px('''
     <table class="phase"
            var="singlePage=len(phase.pages) == 1;
                 label='%s_phase_%s' % (zobj.meta_type, phase.name)">
      <tr valign="top">
       <!-- The page(s) within the phase -->
       <td for="aPage in phase.pages"
           var2="aPageInfo=phase.pagesInfo[aPage]"
           class=":(aPage == page) and 'currentPage' or ''">
        <!-- First line: page name and icons -->
        <span if="not (singlePhase and singlePage)">
         <x var="label=aPageInfo.page.getLabel(zobj)">
          <a if="aPageInfo.showOnView"
             href=":zobj.getUrl(page=aPage, inPopup=inPopup)">::label</a>
          <x if="not aPageInfo.showOnView">:label</x>
         </x>
         <x var="locked=zobj.isLocked(user, aPage);
                 editable=mayEdit and aPageInfo.showOnEdit and \
                          aPageInfo.showEdit">
          <a if="editable and not locked"
             href=":zobj.getUrl(mode='edit', page=aPage, inPopup=inPopup)">
           <img src=":url('edit')" title=":_('object_edit')"/></a>
          <a if="editable and locked">
           <img style="cursor: help"
                var="lockDate=ztool.formatDate(locked[1]);
                     lockMap={'user':ztool.getUserName(locked[0]), \
                              'date':lockDate};
                     lockMsg=_('page_locked', mapping=lockMap)"
                src=":url('locked')" title=":lockMsg"/></a>
          <a if="editable and locked and user.has_role('Manager')">
           <img class="clickable" title=":_('page_unlock')" src=":url('unlock')"
                onclick=":'onUnlockPage(%s,%s)' % (q(zobj.id), q(aPage))"/></a>
         </x>
        </span>
        <!-- Next lines: links -->
        <x var="links=aPageInfo.links" if="links">
         <div for="link in links" class="refLink">
          <a href=":link.url">:link.title</a></div>
        </x>
       </td>
      </tr>
     </table>''')

    # "Static" PX displaying all phases of a given object
    pxAllPhases = Px('''
     <x var="singlePhase=len(phases)==1;
             page=req.get('page', '');
             uid=zobj.id;
             mayEdit=zobj.mayEdit()">
      <x if="singlePhase" var2="phase=phases[0]">:phase.pxView</x>
      <!-- Display several phases in tabs -->
      <x if="not singlePhase">
       <table cellpadding="0" cellspacing="0">
        <!-- First row: the tabs -->
        <tr><td style="border-bottom: 1px solid #ff8040; padding-bottom: 1px">
         <table cellpadding="0" cellspacing="0" class="tabs"
                id=":'tabs_%s' % uid">
          <tr valign="middle">
           <x for="phase in phases"
              var2="suffix='%s_%s' % (uid, phase.name);
                    tabId='tab_%s' % suffix">
            <td><img src=":url('tabLeft')" id=":'%s_left' % tabId"/></td>
            <td style=":url('tabBg',bg=True)" id=":tabId" class="tab">
             <a onclick=":'showTab(%s)' % q(suffix)"
                class="clickable">:_('%s_phase_%s' % (zobj.meta_type, \
                                                      phase.name))</a>
            </td>
            <td><img id=":'%s_right' % tabId" src=":url('tabRight')"/></td>
           </x>
          </tr>
         </table>
        </td></tr>
        <!-- Other rows: the fields -->
        <tr for="phase in phases" id=":'tabcontent_%s_%s' % (uid, phase.name)"
          style=":(loop.phase.nb==0) and 'display:table-row' or 'display:none'">
         <td>:phase.pxView</td>
        </tr>
       </table>
       <script type="text/javascript">:'initTab(%s,%s)' % \
        (q('tab_%s' % uid), q('%s_%s' % (uid, phases[0].name)))
       </script>
      </x>
     </x>''')

    def __init__(self, name, obj):
        self.name = name
        self.obj = obj
        # The list of names of pages in this phase
        self.pages = []
        # The list of hidden pages in this phase
        self.hiddenPages = []
        # The dict below stores info about every page listed in self.pages.
        self.pagesInfo = {}
        self.totalNbOfPhases = None
        # The following attributes allows to browse, from a given page, to the
        # last page of the previous phase and to the first page of the following
        # phase if allowed by phase state.
        self.previousPhase = None
        self.nextPhase = None

    def addPageLinks(self, field, obj):
        '''If p_field is a navigable Ref, we must add, within self.pagesInfo,
           objects linked to p_obj through this Ref as links.'''
        if field.page.name in self.hiddenPages: return
        infos = []
        for ztied in field.getValue(obj, appy=False):
            infos.append(Object(title=ztied.title, url=ztied.absolute_url()))
        self.pagesInfo[field.page.name].links = infos

    def addPage(self, field, obj, layoutType):
        '''Adds page-related information in the phase.'''
        # If the page is already there, we have nothing more to do.
        if (field.page.name in self.pages) or \
           (field.page.name in self.hiddenPages):
            return
        # Add the page only if it must be shown.
        showOnView = field.page.isShowable(obj, 'view')
        showOnEdit = field.page.isShowable(obj, 'edit')
        if showOnView or showOnEdit:
            # The page must be added
            self.pages.append(field.page.name)
            # Create the dict about page information and add it in self.pageInfo
            pageInfo = Object(page=field.page,
                              showOnView=showOnView,
                              showOnEdit=showOnEdit,
                              links=None)
            pageInfo.update(field.page.getInfo(obj, layoutType))
            self.pagesInfo[field.page.name] = pageInfo
        else:
            self.hiddenPages.append(field.page.name)

    def computeNextPrevious(self, allPhases):
        '''This method also fills fields "previousPhase" and "nextPhase"
           if relevant, based on list of p_allPhases.'''
        # Identify previous and next phases
        for phase in allPhases:
            if phase.name == self.name:
                i = allPhases.index(phase)
                if i > 0:
                    self.previousPhase = allPhases[i - 1]
                if i < (len(allPhases) - 1):
                    self.nextPhase = allPhases[i + 1]

    def getPreviousPage(self, page):
        '''Returns the page that precedes p_page in this phase'''
        try:
            pageIndex = self.pages.index(page)
        except ValueError:
            # The current page is probably not visible anymore. Return the
            # first available page in current phase.
            if self.pages:
                res = self.pages[0]
                return res, self.pagesInfo[res]
            return None, None
        if pageIndex > 0:
            # We stay on the same phase, previous page
            res = self.pages[pageIndex - 1]
            return res, self.pagesInfo[res]
        else:
            if self.previousPhase:
                # We go to the last page of previous phase
                previousPhase = self.previousPhase
                res = previousPhase.pages[-1]
                return res, previousPhase.pagesInfo[res]
            else:
                return None, None

    def getNextPage(self, page):
        '''Returns the page that follows p_page in this phase.'''
        try:
            pageIndex = self.pages.index(page)
        except ValueError:
            # The current page is probably not visible anymore. Return the
            # first available page in current phase.
            if self.pages:
                res = self.pages[0]
                return res, self.pagesInfo[res]
            return None, None
        if pageIndex < (len(self.pages) - 1):
            # We stay on the same phase, next page
            res = self.pages[pageIndex + 1]
            return res, self.pagesInfo[res]
        else:
            if self.nextPhase:
                # We go to the first page of next phase
                nextPhase = self.nextPhase
                res = nextPhase.pages[0]
                return res, nextPhase.pagesInfo[res]
            else:
                return None, None

    def getPageInfo(self, page, layoutType):
        '''Return the page info corresponding to the given p_page. If this page
           cannot be shown on p_layoutType, this method returns page info about
           the first showable page on p_layoutType, or None if no page is
           showable at all.'''
        res = self.pagesInfo[page]
        showAttribute = 'showOn%s' % layoutType.capitalize()
        if getattr(res, showAttribute): return res
        # Find the first showable page in this phase on p_layoutType.
        for pageName in self.pages:
            if pageName == page: continue
            pageInfo = self.pagesInfo[pageName]
            if getattr(pageInfo, showAttribute): return pageInfo
Exemple #7
0
class File(Field):

    pxView = pxCell = Px('''
      <x>::field.getDownloadLink(name, zobj, layoutType)</x>''')

    pxEdit = Px('''
     <x var="fName='%s_file' % name">
      <x if="value">:field.pxView</x><br if="value"/>
      <x if="value">
       <!-- Keep the file unchanged -->
       <input type="radio" value="nochange"
              checked=":value and 'checked' or None"
              name=":'%s_delete' % name" id=":'%s_nochange' % name"
              onclick=":'document.getElementById(%s).disabled=true'% q(fName)"/>
       <label lfor=":'%s_nochange' % name">:_('keep_file')</label><br/>
       <!-- Delete the file -->
       <x if="not field.required">
        <input type="radio" value="delete"
               name=":'%s_delete' % name" id=":'%s_delete' % name"
               onclick=":'document.getElementById(%s).disabled=true'%q(fName)"/>
        <label lfor=":'%s_delete' % name">:_('delete_file')</label><br/>
       </x>
       <!-- Replace with a new file -->
       <input type="radio" value=""
              checked=":not value and 'checked' or None"
              name=":'%s_delete' % name" id=":'%s_upload' % name"
              onclick=":'document.getElementById(%s).disabled=false'%q(fName)"/>
       <label lfor=":'%s_upload' % name">:_('replace_file')</label><br/>
      </x>
      <!-- The upload field -->
      <input type="file" name=":fName" id=":fName" style=":field.getStyle()"
             onChange=":field.getJsOnChange()"/>
      <script var="isDisabled=not value and 'false' or 'true'"
             type="text/javascript">:'document.getElementById(%s).disabled=%s'%\
                                     (q(fName), isDisabled)</script></x>''')

    pxSearch = ''

    def __init__(self,
                 validator=None,
                 multiplicity=(0, 1),
                 default=None,
                 show=True,
                 page='main',
                 group=None,
                 layouts=None,
                 move=0,
                 specificReadPermission=False,
                 specificWritePermission=False,
                 width=None,
                 height=None,
                 maxChars=None,
                 colspan=1,
                 master=None,
                 masterValue=None,
                 focus=False,
                 historized=False,
                 mapping=None,
                 label=None,
                 isImage=False,
                 downloadAction=None,
                 sdefault='',
                 scolspan=1,
                 swidth=None,
                 sheight=None,
                 view=None,
                 cell=None,
                 xml=None,
                 render=None,
                 icon='paperclip',
                 nameStorer=None):
        # This boolean is True if the file is an image
        self.isImage = isImage
        # "downloadAction" can be a method called every time the file is
        # downloaded. The method gets, as single arg, the FileInfo instance
        # representing the downloaded file.
        self.downloadAction = downloadAction
        # If "render" is "icon", the file will be rendered as an icon
        # (self.icon) on "buttons" and "result" layouts.
        self.render = render
        # Icon to use when this file is rendered as an icon
        self.icon = icon
        # In "nameStorer", you can specify another field that will store the
        # file name. This field must be a String belonging to the same class as
        # p_self. As soon as, in the UI, a file is selected in p_self's widget,
        # its name will be copied into the nameStorer field, without the
        # extension, only if this latter is not empty.
        self.nameStorer = nameStorer
        # Call the base constructor
        Field.__init__(self, validator, multiplicity, default, show, page,
                       group, layouts, move, False, True, None, False,
                       specificReadPermission, specificWritePermission, width,
                       height, None, colspan, master, masterValue, focus,
                       historized, mapping, label, sdefault, scolspan, swidth,
                       sheight, True, False, view, cell, xml)

    def getRequestValue(self, obj, requestName=None):
        name = requestName or self.name
        return obj.REQUEST.get('%s_file' % name)

    def getRequestSuffix(self):
        return '_file'

    def getCopyValue(self, obj):
        '''Create a copy of the FileInfo instance stored for p_obj for this
           field. This copy will contain the absolute path to the file on the
           filesystem. This way, the file may be read independently from p_obj
           (and copied somewhere else).'''
        info = self.getValue(obj)
        if not info: return
        # Create a "not-in-DB", temporary FileInfo
        return FileInfo(info.getFilePath(obj),
                        inDb=False,
                        uploadName=info.uploadName)

    def getDefaultLayouts(self):
        return Layouts.File.b

    def isEmptyValue(self, obj, value):
        '''Must p_value be considered as empty?'''
        if value: return
        # If "nochange", the value must not be considered as empty
        if hasattr(obj, 'REQUEST'):
            return obj.REQUEST.get('%s_delete' % self.name) != 'nochange'
        return True

    def getJsOnChange(self):
        '''Gets the JS code for updaing the name storer when defined'''
        storer = self.nameStorer
        if not storer: return ''
        return 'updateFileNameStorer(this, "%s")' % storer.name

    def getStyle(self):
        '''Get the content of the "style" attribute of the "input" tag on the
           "edit" layout for this field.'''
        if self.width:
            return 'width: %s' % self.width
        return ''

    def isRenderable(self, layoutType):
        '''A file with 'icon' rendering is potentially renderable everywhere'''
        if self.render == 'icon': return True
        return layoutType != 'buttons'

    def getDownloadLink(self, name, obj, layoutType):
        '''Gets the HTML code for downloading the file p_value as stored in
           field p_name on p_obj.'''
        value = obj.getFieldValue(name)
        # Display an empty value
        if not value: return (layoutType != 'cell') and '-' or ''
        # Get the "file title", derived from the upload name and size
        size = value.getShownSize()
        title = "%s - %s" % (value.uploadName, size)
        # On "edit", simply repeat the file title
        if layoutType == 'edit': return value.uploadName
        # Build the URL for downloading or displaying the file
        url = '%s/download?name=%s' % (obj.absolute_url(), name)
        # For images, display them directly
        if self.isImage: return '<img src="%s" title="%s"/>' % (url, title)
        # For non-images, display a link for downloading it, as an icon when
        # relevant.
        if self.render == 'icon':
            content = '<img src="%s" title="%s"/>' % \
                      (obj.getTool().getIncludeUrl(self.icon), title)
            # On "view", we have place, so display "title" besides the icon
            suffix = (layoutType == 'view') and title or ''
        else:
            # Display textual information only
            content = title
            suffix = ''
        # Style the suffix
        if suffix: suffix = '<span class="refLink">%s</span>' % suffix
        return '<a href="%s">%s%s</a>' % (url, content, suffix)

    imageExts = ('.jpg', '.jpeg', '.png', '.gif')

    def validateValue(self, obj, value):
        form = obj.REQUEST.form
        action = '%s_delete' % self.name
        if (not value or not value.filename) and action in form and \
            not form[action]:
            # If this key is present but empty, it means that the user selected
            # "replace the file with a new one". So in this case he must provide
            # a new file to upload.
            return obj.translate('file_required')
        # Check that, if self.isImage, the uploaded file is really an image
        if value and value.filename and self.isImage:
            ext = os.path.splitext(value.filename)[1].lower()
            if ext not in File.imageExts:
                return obj.translate('image_required')

    defaultMimeType = 'application/octet-stream'

    def store(self, obj, value):
        '''Stores the p_value that represents some file. p_value can be:
           a. an instance of Zope class ZPublisher.HTTPRequest.FileUpload. In
              this case, it is file content coming from a HTTP POST;
           b. an instance of Zope class OFS.Image.File (legacy within-ZODB file
              object);
           c. an instance of appy.utils.path.UnmarshalledFile. In this case, the
              file comes from a peer Appy site, unmarshalled from XML content
              sent via an HTTP request;
           d. a string. In this case, the string represents the path of a file
              on disk;
           e. a 2-tuple (fileName, fileContent) where:
              - fileName is the name of the file (ie "myFile.odt")
              - fileContent is the binary or textual content of the file or an
                open file handler.
           f. a 3-tuple (fileName, fileContent, mimeType) where
              - fileName and fileContent have the same meaning than above;
              - mimeType is the MIME type of the file.
           g. a FileInfo instance, that must be "not-in-DB", ie, with an
              absolute path in attribute fsPath.
        '''
        zobj = obj.o
        if value:
            # There is a new value to store. Get the folder on disk where to
            # store the new file.
            dbFolder, folder = zobj.getFsFolder(create=True)
            # Remove the previous file if it existed
            info = getattr(obj.aq_base, self.name, None)
            if info:
                # The previous file can be a legacy File object in an old
                # database we are migrating.
                if isinstance(info, FileInfo): info.removeFile(dbFolder)
                else: delattr(obj, self.name)
            # Store the new file. As a preamble, create a FileInfo instance.
            info = FileInfo(folder)
            cfg = zobj.getProductConfig()
            if isinstance(value, cfg.FileUpload) or isinstance(
                    value, cfg.File):
                # Cases a, b
                value.filename = value.filename.replace('/', '-')
                info.writeFile(self.name, value, dbFolder)
            elif isinstance(value, putils.UnmarshalledFile):
                # Case c
                fileInfo = (value.name, value.content, value.mimeType)
                info.writeFile(self.name, fileInfo, dbFolder)
            elif isinstance(value, basestring):
                # Case d
                info.copyFile(self.name, value, dbFolder)
            elif isinstance(value, FileInfo):
                # Case g
                info.writeFile(self.name, value, dbFolder)
            else:
                # Cases e, f. Extract file name, content and MIME type.
                fileName = mimeType = None
                if len(value) == 2:
                    fileName, fileContent = value
                elif len(value) == 3:
                    fileName, fileContent, mimeType = value
                if not fileName:
                    raise Exception(WRONG_FILE_TUPLE)
                mimeType = mimeType or guessMimeType(fileName)
                info.writeFile(self.name, (fileName, fileContent, mimeType),
                               dbFolder)
            # Store the FileInfo instance in the database
            setattr(obj, self.name, info)
        else:
            # I store value "None", excepted if I find in the request the desire
            # to keep the file unchanged.
            action = None
            rq = getattr(zobj, 'REQUEST', None)
            if rq: action = rq.get('%s_delete' % self.name, None)
            if action != 'nochange':
                # Delete the file on disk
                info = getattr(zobj.aq_base, self.name, None)
                if info:
                    info.removeFile(zobj.getDbFolder(),
                                    removeEmptyFolders=True)
                # Delete the FileInfo in the DB
                setattr(zobj, self.name, None)

    def onDownload(self, obj, rq):
        '''Triggered when a file download is requested from the ui'''
        # Security check
        obj.mayView(self.readPermission, raiseError=True)
        # Write the file in the HTTP response
        info = getattr(obj.aq_base, self.name, None)
        if info:
            # Content disposition may be given in the request
            disposition = rq.get('disposition', 'attachment')
            if disposition not in ('inline', 'attachment'):
                disposition = 'attachment'
            info.writeResponse(rq.RESPONSE, obj.getDbFolder(), disposition)
            # Call the "download action" if specified
            if self.downloadAction: self.downloadAction(obj.appy(), info)
Exemple #8
0
class UiGroup:
    '''On-the-fly-generated data structure that groups all elements
       (fields, searches, transitions...) sharing the same Group instance, that
       the currently logged user can see.'''

    # PX that renders a help icon for a group.
    pxHelp = Px('''<acronym title="obj.translate('help', field=field)"><img
     src=":url('help')"/></acronym>''')

    # PX that renders the content of a group (which is referred as var "field").
    pxContent = Px('''
     <table var="cellgap=field.cellgap" width=":field.wide"
            align=":ztool.flipLanguageDirection(field.align, dir)"
            id=":tagId" name=":tagName" class=":groupCss"
            cellspacing=":field.cellspacing" cellpadding=":field.cellpadding">
      <!-- Display the title of the group if not rendered a fieldset. -->
      <tr if="(field.style != 'fieldset') and field.hasLabel">
       <td colspan=":len(field.columnsWidths)" class=":field.style"
           align=":dleft">
        <x>::_(field.labelId)</x><x if="field.hasHelp">:field.pxHelp</x>
       </td>
      </tr>
      <tr if="(field.style != 'fieldset') and field.hasDescr">
       <td colspan=":len(field.columnsWidths)"
           class="discreet">::_(field.descrId)</td>
      </tr>
      <!-- The column headers -->
      <tr>
       <th for="colNb in range(len(field.columnsWidths))"
           align=":ztool.flipLanguageDirection(field.columnsAligns[colNb], dir)"
           width=":field.columnsWidths[colNb]">::field.hasHeaders and \
            _('%s_col%d' % (field.labelId, (colNb+1))) or ''</th>
      </tr>
      <!-- The rows of widgets -->
      <tr valign=":field.valign" for="row in field.elements">
       <td for="field in row" colspan=":field.colspan"
           style=":not loop.field.last and ('padding-right:%s'% cellgap) or ''">
        <x if="field">
         <x if="field.type == 'group'">:field.pxView</x>
         <x if="field.type != 'group'">:field.pxRender</x>
        </x>
       </td>
      </tr>
     </table>''')

    # PX that renders a group of fields (the group is refered as var "field").
    pxView = Px('''
     <x var="tagCss=field.master and ('slave*%s*%s' % \
                    (field.masterName, '*'.join(field.masterValue))) or '';
             widgetCss=field.css_class;
             groupCss=tagCss and ('%s %s' % (tagCss, widgetCss)) or widgetCss;
             tagName=field.master and 'slave' or '';
             tagId='%s_%s' % (zobj.id, field.name)">

      <!-- Render the group as a fieldset if required -->
      <fieldset if="field.style == 'fieldset'">
       <legend if="field.hasLabel">
         <i>::_(field.labelId)></i><x if="field.hasHelp">:field.pxHelp</x>
       </legend>
       <div if="field.hasDescr" class="discreet">::_(field.descrId)</div>
       <x>:field.pxContent</x>
      </fieldset>

      <!-- Render the group as a section if required -->
      <x if="field.style not in ('fieldset', 'tabs')">:field.pxContent</x>

      <!-- Render the group as tabs if required -->
      <x if="field.style == 'tabs'" var2="tabsCount=len(field.elements)">
       <table width=":field.wide" class=":groupCss" id=":tagId" name=":tagName">
        <!-- First row: the tabs. -->
        <tr valign="middle"><td style="border-bottom: 1px solid #ff8040">
         <table class="tabs" cellpadding="0" cellspacing="0">
          <tr valign="middle">
           <x for="sub in field.elements"
              var2="nb = loop.sub.nb + 1;
                    suffix='%s_%d_%d' % (field.name, nb, tabsCount);
                    tabId='tab_%s' % suffix">
            <td><img src=":url('tabLeft')" id=":'%s_left' % tabId"/></td>
            <td style=":url('tabBg', bg=True)" class="tab" id=":tabId">
             <a onclick=":'showTab(%s)' % q(suffix)"
                class="clickable">:_(sub.labelId)</a>
            </td>
            <td><img id=":'%s_right' % tabId" src=":url('tabRight')"/></td>
           </x>
          </tr>
         </table>
        </td></tr>

        <!-- Other rows: the fields -->
        <tr for="sub in field.elements"
            var2="nb=loop.sub.nb + 1"
            id=":'tabcontent_%s_%d_%d' % (field.name, nb, tabsCount)"
            style=":(nb == 1) and 'display:table-row' or 'display:none'">
         <td var="field=sub">
          <x if="field.type == 'group'">:field.pxView</x>
          <x if="field.type != 'group'">:field.pxRender</x>
         </td>
        </tr>
       </table>
       <script type="text/javascript">:'initTab(%s,%s)' % \
        (q('tab_%s' % field.name), q('%s_1_%d' % (field.name, tabsCount)))
       </script>
      </x>
     </x>''')

    # PX that renders a group of searches.
    pxViewSearches = Px('''
     <x var="expanded=req.get(field.labelId, 'collapsed') == 'expanded'">
      <!-- Group name, prefixed by the expand/collapse icon -->
      <div class="portletGroup">
       <img class="clickable" style="margin-right: 3px" align=":dleft"
            id=":'%s_img' % field.labelId"
            src=":expanded and url('collapse.gif') or url('expand.gif')"
            onclick=":'toggleCookie(%s)' % q(field.labelId)"/>
       <x if="not field.translated">:_(field.labelId)</x>
       <x if="field.translated">:field.translated</x>
      </div>
      <!-- Group content -->
      <div var="display=expanded and 'display:block' or 'display:none'"
           id=":field.labelId" style=":'padding-left: 10px; %s' % display">
       <x for="searches in field.elements">
        <x for="elem in searches">
         <!-- An inner group within this group -->
         <x if="elem.type == 'group'"
            var2="field=elem">:field.pxViewSearches</x>
         <!-- A search -->
         <x if="elem.type != 'group'" var2="search=elem">:search.pxView</x>
        </x>
       </x>
      </div>
     </x>''')

    # PX that renders a group of transitions.
    pxViewTransitions = Px('''
     <!-- Render a group of transitions, as a one-column table -->
     <table>
      <x for="row in uiGroup.elements">
       <x for="transition in row"><tr><td>:transition.pxView</td></tr></x>
      </x>
     </table>''')

    # What PX to use, depending on group content?
    pxByContent = {'fields': pxView, 'searches': pxViewSearches,
                   'transitions': pxViewTransitions}

    def __init__(self, group, page, className, content='fields'):
        '''A UiGroup can group various kinds of elements: fields, searches,
           transitions..., The type of content that one may find in this group
           is given in p_content.
           * p_group      is the Group instance corresponding to this UiGroup;
           * p_page       is the Page instance where the group is rendered (for
                          transitions, it corresponds to a virtual page
                          "workflow");
           * p_className  is the name of the class that holds the elements to
                          group.'''
        self.type = 'group'
        # All p_group attributes become self attributes. This is required
        # because a UiGroup, in some PXs, must behave like a Field (ie, have
        # the same attributes, like "master".
        for name, value in group.__dict__.iteritems():
            if not name.startswith('_'):
                setattr(self, name, value)
        self.group = group
        self.columnsWidths = [col.width for col in group.columns]
        self.columnsAligns = [col.align for col in group.columns]
        # Names of i18n labels for this group.
        labelName = self.name
        prefix = className
        if group.label:
            if isinstance(group.label, basestring): prefix = group.label
            else: # It is a tuple (className, name)
                if group.label[1]: labelName = group.label[1]
                if group.label[0]: prefix = group.label[0]
        gp = (content == 'searches') and 'searchgroup' or 'group'
        self.labelId = '%s_%s_%s' % (prefix, gp, labelName)
        self.descrId = self.labelId + '_descr'
        self.helpId  = self.labelId + '_help'
        # The name of the page where the group lies
        self.page = page.name
        # The elements (fields or sub-groups) contained in the group, that the
        # current user may see. They will be inserted by m_addElement below.
        if self.style != 'tabs':
            # In most cases, "elements" will be a list of lists for rendering
            # them as a table.
            self.elements = [[]]
        else:
            # If the group is a tab, elements will be stored as a simple list.
            self.elements = []
        # PX to use for rendering this group.
        self.px = self.pxByContent[content]

    def addElement(self, element):
        '''Adds p_element into self.elements. We try first to add p_element into
           the last row. If it is not possible, we create a new row.'''
        if self.style == 'tabs':
            self.elements.append(element)
            return
        # Get the last row
        lastRow = self.elements[-1]
        numberOfColumns = len(self.columnsWidths)
        # Compute the number of columns already filled in the last row.
        filledColumns = 0
        for rowElem in lastRow: filledColumns += rowElem.colspan
        freeColumns = numberOfColumns - filledColumns
        if freeColumns >= element.colspan:
            # We can add the element in the last row.
            lastRow.append(element)
        else:
            if freeColumns:
                # Terminate the current row by appending empty cells
                for i in range(freeColumns): lastRow.append('')
            # Create a new row
            self.elements.append([element])
class ToolWrapper(AbstractWrapper):

    # --------------------------------------------------------------------------
    # Navigation-related PXs
    # --------------------------------------------------------------------------
    # Icon for hiding/showing details below the title of an object shown in a
    # list of objects.
    pxShowDetails = Px('''
     <img if="(field.name == 'title') and ztool.subTitleIsUsed(className)"
          class="clickable" src=":url('toggleDetails')"
          onclick="toggleSubTitles()"/>''')

    # Displays up/down arrows in a table header column for sorting a given
    # column. Requires variables "sortable", 'filterable' and 'field'.
    pxSortAndFilter = Px('''
     <x if="sortable">
      <img if="(sortKey != field.name) or (sortOrder == 'desc')"
           onclick=":navBaseCall.replace('**v**', '0,%s,%s,%s' % \
                     (q(field.name), q('asc'), q(filterKey)))"
           src=":url('sortDown.gif')" class="clickable"/>
      <img if="(sortKey != field.name) or (sortOrder == 'asc')"
           onclick=":navBaseCall.replace('**v**', '0,%s,%s,%s' % \
                     (q(field.name), q('desc'), q(filterKey)))"
           src=":url('sortUp.gif')" class="clickable"/>
     </x>
     <x if="filterable"
        var2="filterId='%s_%s' % (ajaxHookId, field.name);
              filterIdIcon='%s_icon' % filterId">
      <!-- Pressing the "enter" key in the field clicks the icon (onkeydown)--> 
      <input type="text" size="7" id=":filterId"
             value=":filterKey == field.name and filterValue or ''"
             onkeydown=":'if (event.keyCode==13) document.getElementById ' \
                         '(%s).click()' % q(filterIdIcon)"/>
      <img id=":filterIdIcon" class="clickable" src=":url('funnel')"
           onclick=":navBaseCall.replace('**v**', '0, %s,%s,%s' % \
                     (q(sortKey), q(sortOrder), q(field.name)))"/>
     </x>''')

    # Buttons for navigating among a list of objects (from a Ref field or a
    # query): next,back,first,last...
    pxNavigate = Px('''
     <div if="totalNumber &gt; batchSize" align=":dright"
          var2="mustSortAndFilter=ajaxHookId == 'queryResult';
                sortAndFilter=mustSortAndFilter and \
                    ',%s,%s,%s' % (q(sortKey),q(sortOrder),q(filterKey)) or ''">
       
      <!-- Go to the first page -->
      <img if="(startNumber != 0) and (startNumber != batchSize)"
           class="clickable" src=":url('arrowsLeft')" title=":_('goto_first')"
           onClick=":navBaseCall.replace('**v**', '0'+sortAndFilter)"/>

      <!-- Go to the previous page -->
      <img var="sNumber=startNumber - batchSize" if="startNumber != 0"
           class="clickable" src=":url('arrowLeft')" title=":_('goto_previous')"
           onClick=":navBaseCall.replace('**v**', str(sNumber)+sortAndFilter)"/>

      <!-- Explain which elements are currently shown -->
      <span class="discreet"> 
       <x>:startNumber + 1</x> <img src=":url('to')"/> 
       <x>:startNumber + batchNumber</x> <b>//</b> 
       <x>:totalNumber</x>
      </span>

      <!-- Go to the next page -->
      <img var="sNumber=startNumber + batchSize" if="sNumber &lt; totalNumber"
           class="clickable" src=":url('arrowRight')" title=":_('goto_next')"
           onClick=":navBaseCall.replace('**v**', str(sNumber)+sortAndFilter)"/>

      <!-- Go to the last page -->
      <img var="lastPageIsIncomplete=totalNumber % batchSize;
                nbOfCompletePages=totalNumber/batchSize;
                nbOfCountedPages=lastPageIsIncomplete and \
                                 nbOfCompletePages or nbOfCompletePages-1;
                sNumber= nbOfCountedPages * batchSize"
           if="(startNumber != sNumber) and \
               (startNumber != sNumber-batchSize)" class="clickable"
           src=":url('arrowsRight')" title=":_('goto_last')"
           onClick=":navBaseCall.replace('**v**', str(sNumber)+sortAndFilter)"/>

      <!-- Go to the element number... -->
      <x var="gotoNumber=gotoNumber|False" if="gotoNumber"
         var2="sourceUrl=obj.url">:obj.pxGotoNumber</x>
     </div>''')

    # --------------------------------------------------------------------------
    # PXs for graphical elements shown on every page
    # --------------------------------------------------------------------------
    # Global elements included in every page.
    pxPagePrologue = Px('''
     <!-- Include type-specific CSS and JS. -->
     <x if="cssJs">
      <link for="cssFile in cssJs['css']" rel="stylesheet" type="text/css"
            href=":url(cssFile)"/>
      <script for="jsFile in cssJs['js']" type="text/javascript"
              src=":url(jsFile)"></script></x>

     <!-- Javascript messages -->
     <script type="text/javascript">::ztool.getJavascriptMessages()</script>

     <!-- Global form for deleting an object -->
     <form id="deleteForm" method="post" action="do">
      <input type="hidden" name="action" value="Delete"/>
      <input type="hidden" name="objectUid"/>
     </form>
     <!-- Global form for deleting an event from an object's history -->
     <form id="deleteEventForm" method="post" action="do">
      <input type="hidden" name="action" value="DeleteEvent"/>
      <input type="hidden" name="objectUid"/>
      <input type="hidden" name="eventTime"/>
     </form>
     <!-- Global form for (un)linking (an) object(s) -->
     <form id="linkForm" method="post" action="do">
      <input type="hidden" name="action" value="Link"/>
      <input type="hidden" name="linkAction"/>
      <input type="hidden" name="sourceUid"/>
      <input type="hidden" name="fieldName"/>
      <input type="hidden" name="targetUid"/>
      <input type="hidden" name="semantics"/>
     </form>
     <!-- Global form for unlocking a page -->
     <form id="unlockForm" method="post" action="do">
      <input type="hidden" name="action" value="Unlock"/>
      <input type="hidden" name="objectUid"/>
      <input type="hidden" name="pageName"/>
     </form>
     <!-- Global form for generating/freezing a document from a pod template -->
     <form id="podForm" name="podForm" method="post"
           action=":ztool.absolute_url() + '/doPod'">
      <input type="hidden" name="objectUid"/>
      <input type="hidden" name="fieldName"/>
      <input type="hidden" name="template"/>
      <input type="hidden" name="podFormat"/>
      <input type="hidden" name="queryData"/>
      <input type="hidden" name="customParams"/>
      <input type="hidden" name="showSubTitles" value="true"/>
      <input type="hidden" name="checkedUids"/>
      <input type="hidden" name="checkedSem"/>
      <input type="hidden" name="mailing"/>
      <input type="hidden" name="action" value="generate"/>
     </form>''')

    pxPageBottom = Px('''
     <script var="info=zobj.getSlavesRequestInfo(page)"
             type="text/javascript">:'initSlaves(%s,%s,%s,%s)' % \
                    (q(zobj.absolute_url()), q(layoutType), info[0], info[1])
     </script>''')

    pxPortlet = Px('''
     <x var="toolUrl=tool.url;
             queryUrl='%s/query' % toolUrl;
             currentSearch=req.get('search', None);
             currentClass=req.get('className', None);
             currentPage=req['PATH_INFO'].rsplit('/',1)[-1];
             rootClasses=ztool.getRootClasses()">

      <!-- One section for every searchable root class -->
      <x for="rootClass in rootClasses" if="ztool.userMaySearch(rootClass)"
         var2="className=ztool.getPortalType(rootClass)">

       <!-- A separator if required -->
       <div class="portletSep" if="loop.rootClass.nb != 0"></div>

       <!-- Section title (link triggers the default search) -->
       <div class="portletContent"
            var="searchInfo=ztool.getGroupedSearches(rootClass)">
        <div class="portletTitle">
         <a var="queryParam=searchInfo.default and \
                            searchInfo.default.name or ''"
            href=":'%s?className=%s&amp;search=%s' % \
                   (queryUrl, className, queryParam)"
            class=":(not currentSearch and (currentClass==className) and \
                    (currentPage=='query')) and \
                    'current' or ''">::_(className + '_plural')</a>
        </div>

        <!-- Create instances of this class -->
        <form if="ztool.userMayCreate(rootClass) and \
                  ('form' in ztool.getCreateMeans(rootClass))" class="addForm"
              var2="target=ztool.getLinksTargetInfo(rootClass)"
              action=":'%s/do' % toolUrl" target=":target.target">
         <input type="hidden" name="action" value="Create"/>
         <input type="hidden" name="className" value=":className"/>
         <input type="hidden" name="popup"
               value=":(inPopup or (target.target != '_self')) and '1' or '0'"/>
         <input type="submit" class="buttonSmall button"
                var="label=_('query_create')" value=":label"
                onclick=":target.openPopup"
                style=":'%s; %s' % (url('add', bg=True), \
                                    ztool.getButtonWidth(label))"/>
        </form>

        <!-- Searches -->
        <x if="ztool.advancedSearchEnabledFor(rootClass)">
         <!-- Live search -->
         <form action=":'%s/do' % toolUrl">
          <input type="hidden" name="action" value="SearchObjects"/>
          <input type="hidden" name="className" value=":className"/>
          <table cellpadding="0" cellspacing="0">
           <tr valign="bottom">
            <td><input type="text" size="14" name="w_SearchableText"
                       class="inputSearch"/></td>
            <td>
             <input type="image" class="clickable" src=":url('search')"
                    title=":_('search_button')"/></td>
           </tr>
          </table>
         </form>

         <!-- Advanced search -->
         <div var="highlighted=(currentClass == className) and \
                               (currentPage == 'search')"
              class=":highlighted and 'portletSearch current' or \
                     'portletSearch'"
              align=":dright" style="margin-bottom: 4px">
          <a var="text=_('search_title')" style="font-size: 88%"
             href=":'%s/search?className=%s' % (toolUrl, className)"
             title=":text"><x>:text</x>...</a>
         </div>
        </x>

        <!-- Predefined searches -->
        <x for="search in searchInfo.searches" var2="field=search">
         <x if="search.type == 'group'">:search.px</x>
         <x if="search.type != 'group'">:search.pxView</x>
        </x>
        <!-- Portlet bottom, potentially customized by the app -->
        <x>::ztool.portletBottom(rootClass)</x>
       </div>
      </x>
     </x>''')

    # The message that is shown when a user triggers an action.
    pxMessage = Px('''
     <div class=":inPopup and 'messagePopup message' or 'message'"
          style="display:none" id="appyMessage">
      <!-- The icon for closing the message -->
      <img src=":url('close')" align=":dright" class="clickable"
           onclick="this.parentNode.style.display='none'"/>
      <!-- The message content -->
      <div id="appyMessageContent"></div>
     </div>
     <script type="text/javascript" var="messages=ztool.consumeMessages()"
             if="messages">::'showAppyMessage(%s)' % q(messages)</script>''')

    # The page footer.
    pxFooter = Px('''
     <table cellpadding="0" cellspacing="0" width="100%" class="footer">
      <tr>
       <td align=":dright">Made with
        <a href="http://appyframework.org" target="_blank">Appy</a></td></tr>
     </table>''')

    # Hook for defining a PX that proposes additional links, after the links
    # corresponding to top-level pages.
    pxLinks = Px('')

    # Hook for defining a PX that proposes additional icons after standard
    # icons in the user strip.
    pxIcons = Px('')

    # Displays the content of a layouted object (a page or a field). If the
    # layouted object is a page, the "layout target" (where to look for PXs)
    # will be the object whose page is shown; if the layouted object is a field,
    # the layout target will be this field.
    pxLayoutedObject = Px('''
     <table var="layoutCss=layout.css_class;
                 isCell=layoutType == 'cell'"
            cellpadding=":layout.cellpadding"
            cellspacing=":layout.cellspacing"
            width=":not isCell and layout.width or ''"
            align=":not isCell and \
                   ztool.flipLanguageDirection(layout.align, dir) or ''"
            class=":tagCss and ('%s %s' % (tagCss, layoutCss)).strip() or \
                   layoutCss"
            style=":layout.style" id=":tagId" name=":tagName">

      <!-- The table header row -->
      <tr if="layout.headerRow" valign=":layout.headerRow.valign">
       <th for="cell in layout.headerRow.cells" width=":cell.width"
           align=":ztool.flipLanguageDirection(cell.align, dir)">
       </th>
      </tr>
      <!-- The table content -->
      <tr for="row in layout.rows" valign=":row.valign">
       <td for="cell in row.cells" colspan=":cell.colspan"
           align=":ztool.flipLanguageDirection(cell.align, dir)"
           class=":not loop.cell.last and 'cellGap' or ''">
        <x for="pxName in cell.content">
         <x var="px=(pxName == '?') and 'px%s' % layoutType.capitalize() \
                                    or pxName">:getattr(layoutTarget, px)</x>
         <img if="not loop.pxName.last" src=":url('space.gif')"/>
        </x>
       </td>
      </tr>
     </table>''')

    pxHome = Px('''
     <table>
      <tr valign="middle">
       <td align="center">::_('front_page_text')</td>
      </tr>
     </table>''', template=AbstractWrapper.pxTemplate, hook='content')

    # Show on query list or grid, the field content for a given object.
    pxQueryField = Px('''
     <!-- Title -->
     <x if="field.name == 'title'">
      <x if="mayView"
         var2="navInfo='search.%s.%s.%d.%d' % \
                (className, searchName, startNumber+currentNumber, totalNumber);
               titleMode=inPopup and 'select' or 'link';
               pageName=zobj.getDefaultViewPage();
               selectJs=inPopup and 'onSelectObject(%s,%s,%s)' % (q(cbId), \
                          q(rootHookId), q(uiSearch.initiator.url))">
       <x var="sup=zobj.getSupTitle(navInfo)" if="sup">::sup</x>
       <x>::zobj.getListTitle(mode=titleMode, nav=navInfo, target=target, \
                          page=pageName, inPopup=inPopup, selectJs=selectJs)</x>
       <span style=":showSubTitles and 'display:inline' or 'display:none'"
             name="subTitle" var="sub=zobj.getSubTitle()" if="sub">::sub</span>

       <!-- Actions -->
       <table class="noStyle" if="not inPopup and zobj.mayAct()">
        <tr>
         <!-- Edit -->
         <td if="zobj.mayEdit()">
          <a var="navInfo='search.%s.%s.%d.%d' % \
               (className, searchName, loop.zobj.nb+1+startNumber, totalNumber);
                  linkInPopup=inPopup or (target.target != '_self')"
             target=":target.target" onclick=":target.openPopup"
             href=":zobj.getUrl(mode='edit', page=zobj.getDefaultEditPage(), \
                                nav=navInfo, inPopup=linkInPopup)">
          <img src=":url('edit')" title=":_('object_edit')"/></a>
         </td>
         <td>
          <!-- Delete -->
          <img if="zobj.mayDelete()" class="clickable" src=":url('delete')"
               title=":_('object_delete')"
               onClick=":'onDeleteObject(%s)' % q(zobj.id)"/>
         </td>
         <!-- Workflow transitions -->
         <td if="zobj.showTransitions('result')"
             var2="targetObj=zobj;
                   buttonsMode='small'">:targetObj.appy().pxTransitions</td>
        </tr>
       </table>
      </x>
      <x if="not mayView">
       <img src=":url('fake')" style="margin-right: 5px"/>
       <x>:_('unauthorized')</x>
      </x>
     </x>
     <!-- Any other field -->
     <x if="(field.name != 'title') and mayView">
      <x var="layoutType='cell'; innerRef=True"
         if="field.isShowable(zobj, 'result')">:field.pxRender</x>
     </x>''')

    # Show query results as a list.
    pxQueryResultList = Px('''
     <x var="showHeaders=showHeaders|True;
             checkboxes=uiSearch.search.checkboxes;
             checkboxesId=rootHookId + '_objs';
             cbShown=uiSearch.showCheckboxes();
             cbDisplay=cbShown and 'display:table-cell' or 'display:none'">
      <table class="list" width="100%">
       <!-- Headers, with filters and sort arrows -->
       <tr if="showHeaders">
        <th if="checkboxes" class="cbCell" style=":cbDisplay">
         <img src=":url('checkall')" class="clickable"
              title=":_('check_uncheck')"
              onclick=":'toggleAllCbs(%s)' % q(checkboxesId)"/>
        </th>
        <th for="column in columns"
            var2="field=column.field;
                  sortable=ztool.isSortable(field.name, className, 'search');
                  filterable=field.filterable"
            width=":column.width" align=":column.align">
         <x>::ztool.truncateText(_(field.labelId))</x>
         <x>:tool.pxSortAndFilter</x><x>:tool.pxShowDetails</x>
        </th>
       </tr>

       <!-- Results -->
       <tr if="not zobjects">
        <td colspan=":len(columns)+1">:_('query_no_result')</td>
       </tr>
       <tr for="zobj in zobjects" valign="top"
           var2="@currentNumber=currentNumber + 1;
                 obj=zobj.appy(); mayView=zobj.mayView();
                 cbId='%s_%s' % (checkboxesId, currentNumber)"
           class=":loop.zobj.odd and 'even' or 'odd'">
        <!-- A checkbox if required -->
        <td if="checkboxes" class="cbCell" id=":cbId" style=":cbDisplay">
         <input type="checkbox" name=":checkboxesId" checked="checked"
                value=":zobj.id" onclick="toggleCb(this)"/>
        </td>
        <td for="column in columns"
            var2="field=column.field" id=":'field_%s' % field.name"
            width=":column.width"
            align=":column.align">:tool.pxQueryField</td>
       </tr>
      </table>
      <!-- The button for selecting objects and closing the popup. -->
      <div if="inPopup and cbShown" align=":dleft">
       <input type="button" class="button"
              var="label=_('object_link_many')"
              value=":label"
              onclick=":'onSelectObjects(%s,%s,%s,%s,%s,%s,%s)' % \
               (q(rootHookId), q(uiSearch.initiator.url), \
                q(uiSearch.initiatorMode), q(sortKey), q(sortOrder), \
                q(filterKey), q(filterValue))"
              style=":'%s; %s' % (url('linkMany', bg=True), \
                                  ztool.getButtonWidth(label))"/>
      </div>
      <!-- Init checkboxes if present. -->
      <script if="checkboxes">:'initCbs(%s)' % q(checkboxesId)</script>
      <script>:'initFocus(%s)' % q(ajaxHookId)</script></x>''')

    # Show query results as a grid.
    pxQueryResultGrid = Px('''
     <table width="100%"
            var="modeElems=resultMode.split('_');
                 cols=(len(modeElems)==2) and int(modeElems[1]) or 4;
                 rows=ztool.splitList(zobjects, cols)">
      <tr for="row in rows" valign="middle">
       <td for="zobj in row" width=":'%d%%' % (100/cols)" align="center"
           style="padding-top: 25px"
           var2="obj=zobj.appy(); mayView=zobj.mayView()">
        <x var="currentNumber=currentNumber + 1"
           for="column in columns"
           var2="field=column.field">:tool.pxQueryField</x>
       </td>
      </tr>
     </table>''')

    # Show paginated query results as a list or grid.
    pxQueryResult = Px('''
     <div var="ajaxHookId='queryResult';
               _=ztool.translate;
               className=req['className'];
               searchName=req.get('search', '');
               uiSearch=uiSearch|ztool.getSearch(className,searchName,ui=True);
               rootHookId=uiSearch.getRootHookId();
               refInfo=ztool.getRefInfo();
               refObject=refInfo[0];
               refField=refInfo[1];
               refUrlPart=refObject and ('&amp;ref=%s:%s' % (refObject.id, \
                                                             refField)) or '';
               startNumber=req.get('startNumber', '0');
               startNumber=int(startNumber);
               sortKey=req.get('sortKey', '');
               sortOrder=req.get('sortOrder', 'asc');
               filterKey=req.get('filterKey', '');
               filterValue=req.get('filterValue', '');
               queryResult=ztool.executeQuery(className, \
                   search=uiSearch.search, startNumber=startNumber, \
                   remember=True, sortBy=sortKey, sortOrder=sortOrder, \
                   filterKey=filterKey, filterValue=filterValue, \
                   refObject=refObject, refField=refField);
               zobjects=queryResult.objects;
               totalNumber=queryResult.totalNumber;
               batchSize=queryResult.batchSize;
               batchNumber=len(zobjects);
               navBaseCall='askQueryResult(%s,%s,%s,%s,%s,**v**)' % \
                 (q(ajaxHookId), q(ztool.absolute_url()), q(className), \
                  q(searchName),int(inPopup));
               showNewSearch=showNewSearch|True;
               newSearchUrl='%s/search?className=%s%s' % \
                   (ztool.absolute_url(), className, refUrlPart);
               showSubTitles=req.get('showSubTitles', 'true') == 'true';
               resultMode=ztool.getResultMode(className);
               target=ztool.getLinksTargetInfo(ztool.getAppyClass(className))"
          id=":ajaxHookId">

      <x if="zobjects or filterValue">
       <!-- Display here POD templates if required. -->
       <table var="fields=ztool.getResultPodFields(className);
                   layoutType='view'"
              if="not inPopup and zobjects and fields" align=":dright">
        <tr>
         <td var="zobj=zobjects[0]; obj=zobj.appy()"
             for="field in fields"
             class=":not loop.field.last and 'pod' or ''">:field.pxRender</td>
        </tr>
       </table>

       <!-- The title of the search -->
       <p if="not inPopup">
        <x>::uiSearch.translated</x> (<span class="discreet">:totalNumber</span>)
        <x if="showNewSearch and (searchName == 'customSearch')">&nbsp;&mdash;
         &nbsp;<i><a href=":newSearchUrl">:_('search_new')</a></i>
        </x>
       </p>
       <table width="100%">
        <tr valign="top">
         <!-- Search description -->
         <td if="uiSearch.translatedDescr">
          <span class="discreet">:uiSearch.translatedDescr</span><br/>
         </td>
         <!-- (Top) navigation -->
         <td align=":dright" width="150px">:tool.pxNavigate</td>
        </tr>
       </table>

       <!-- Results, as a list or grid -->
       <x var="columnLayouts=ztool.getResultColumnsLayouts(className, refInfo);
               columns=ztool.getColumnsSpecifiers(className,columnLayouts, dir);
               currentNumber=0">
        <x if="resultMode == 'list'">:tool.pxQueryResultList</x>
        <x if="resultMode != 'list'">:tool.pxQueryResultGrid</x>
       </x>

       <!-- (Bottom) navigation -->
       <x>:tool.pxNavigate</x>
      </x>

      <x if="not zobjects and not filterValue">
       <x>:_('query_no_result')</x>
       <x if="showNewSearch and (searchName == 'customSearch')"><br/>
        <i class="discreet"><a href=":newSearchUrl">:_('search_new')</a></i></x>
      </x>
    </div>''')

    pxQuery = Px('''
     <div var="className=req['className'];
               searchName=req.get('search', '');
               uiSearch=ztool.getSearch(className, searchName, ui=True);
               rootHookId=uiSearch.getRootHookId();
               cssJs=None"
          id=":rootHookId">
      <script type="text/javascript">:uiSearch.search.getCbJsInit(rootHookId)
      </script>
      <x>:tool.pxPagePrologue</x><x>:tool.pxQueryResult</x>
     </div>''', template=AbstractWrapper.pxTemplate, hook='content')

    pxSearch = Px('''
     <x var="className=req['className'];
             refInfo=req.get('ref', None);
             searchInfo=ztool.getSearchInfo(className, refInfo);
             cssJs={};
             layoutType='search';
             x=ztool.getCssJs(searchInfo.fields, 'edit', cssJs)">

      <!-- Include type-specific CSS and JS. -->
      <link for="cssFile in cssJs['css']" rel="stylesheet" type="text/css"
            href=":url(cssFile)"/>
      <script for="jsFile in cssJs['js']" type="text/javascript"
              src=":url(jsFile)"></script>

      <!-- Search title -->
      <h1><x>:_('%s_plural'%className)</x> &ndash;
          <x>:_('search_title')</x></h1>
      <!-- Form for searching objects of request/className. -->
      <form name="search" action=":ztool.absolute_url()+'/do'" method="post">
       <input type="hidden" name="action" value="SearchObjects"/>
       <input type="hidden" name="className" value=":className"/>
       <input if="refInfo" type="hidden" name="ref" value=":refInfo"/>

       <table width="100%">
        <tr for="searchRow in ztool.getGroupedSearchFields(searchInfo)"
            valign="top">
         <td for="field in searchRow" class="search"
             var2="scolspan=field and field.scolspan or 1"
             colspan=":scolspan"
             width=":'%d%%' % ((100/searchInfo.nbOfColumns)*scolspan)">
           <x if="field">:field.pxRender</x>
           <br class="discreet"/>
         </td>
        </tr>
       </table>

       <!-- Submit button -->
       <p align=":dright"><br/>
        <input type="submit" class="button" var="label=_('search_button')"
               value=":label"
               style=":'%s; %s' % (url('search', bg=True), \
                                   ztool.getButtonWidth(label))"/>
       </p>
      </form>
     </x>''', template=AbstractWrapper.pxTemplate, hook='content')

    pxBack = Px('''
     <html>
      <head>
       <script src=":ztool.getIncludeUrl('appy.js')" type="text/javascript">
       </script>
      </head>
      <body>
       <script type="text/javascript">backFromPopup()</script>
      </body>
     </html>''')

    def isManager(self):
        '''Some pages on the tool can only be accessed by managers.'''
        if self.user.has_role('Manager'): return 'view'

    def isManagerEdit(self):
        '''Some pages on the tool can only be accessed by managers, also in
           edit mode.'''
        if self.user.has_role('Manager'): return True

    def computeConnectedUsers(self):
        '''Computes a table showing users that are currently connected.'''
        res = '<table cellpadding="0" cellspacing="0" class="list">' \
              '<tr><th></th><th>%s</th></tr>' % \
              self.translate('last_user_access')
        rows = []
        for userId, lastAccess in self.o.loggedUsers.items():
            user = self.search1('User', noSecurity=True, login=userId)
            if not user: continue # Could have been deleted in the meanwhile
            fmt = '%s (%s)' % (self.dateFormat, self.hourFormat)
            access = time.strftime(fmt, time.localtime(lastAccess))
            rows.append('<tr><td><a href="%s">%s</a></td><td>%s</td></tr>' % \
                        (user.o.absolute_url(), user.title,access))
        return res + '\n'.join(rows) + '</table>'

    def getObject(self, uid):
        '''Allow to retrieve an object from its unique identifier p_uid.'''
        return self.o.getObject(uid, appy=True)

    def getDiskFolder(self):
        '''Returns the disk folder where the Appy application is stored.'''
        return self.o.config.diskFolder

    def getClass(self, zopeName):
        '''Gets the Appy class corresponding to technical p_zopeName.'''
        return self.o.getAppyClass(zopeName)

    def getAvailableLanguages(self):
        '''Returns the list of available languages for this application.'''
        return [(t.id, t.title) for t in self.translations]

    def convert(self, fileName, format):
        '''Launches a UNO-enabled Python interpreter as defined in the self for
           converting, using OpenOffice in server mode, a file named p_fileName
           into an output p_format.'''
        convScript = '%s/pod/converter.py' % os.path.dirname(appy.__file__)
        cmd = '%s %s "%s" %s -p%d' % (self.unoEnabledPython, convScript,
                                      fileName, format, self.openOfficePort)
        self.log('executing %s...' % cmd)
        return executeCommand(cmd) # The result can contain an error message

    def sendMail(self, to, subject, body, attachments=None):
        '''Sends a mail. See doc for appy.gen.mail.sendMail.'''
        mailConfig = self.o.getProductConfig(True).mail
        sendMail(mailConfig, to, subject, body, attachments=attachments,
                 log=self.log)

    def formatDate(self, date, format=None, withHour=True, language=None):
        '''Check doc @ToolMixin::formatDate.'''
        if not date: return
        return self.o.formatDate(date, format, withHour, language)

    def getUserName(self, login=None, normalized=False):
        return self.o.getUserName(login=login, normalized=normalized)

    def refreshCatalog(self, startObject=None):
        '''Reindex all Appy objects. For some unknown reason, method
           catalog.refreshCatalog is not able to recatalog Appy objects.'''
        if not startObject:
            # This is a global refresh. Clear the catalog completely, and then
            # reindex all Appy-managed objects, ie those in folders "config"
            # and "data".
            # First, clear the catalog.
            self.log('recomputing the whole catalog...')
            app = self.o.getParentNode()
            app.catalog._catalog.clear()
            nb = 1
            failed = []
            for obj in app.config.objectValues():
                subNb, subFailed = self.refreshCatalog(startObject=obj)
                nb += subNb
                failed += subFailed
            try:
                app.config.reindex()
            except:
                failed.append(app.config)
            # Then, refresh objects in the "data" folder.
            for obj in app.data.objectValues():
                subNb, subFailed = self.refreshCatalog(startObject=obj)
                nb += subNb
                failed += subFailed
            # Re-try to index all objects for which reindexation has failed.
            for obj in failed: obj.reindex()
            if failed:
                failMsg = ' (%d retried)' % len(failed)
            else:
                failMsg = ''
            self.log('%d object(s) reindexed%s.' % (nb, failMsg))
        else:
            nb = 1
            failed = []
            for obj in startObject.objectValues():
                subNb, subFailed = self.refreshCatalog(startObject=obj)
                nb += subNb
                failed += subFailed
            try:
                startObject.reindex()
            except Exception, e:
                failed.append(startObject)
            return nb, failed
Exemple #10
0
class Siblings:
    '''Abstract class containing information for navigating from one object to
       its siblings.'''
    siblingTypes = ('previous', 'next', 'first', 'last')

    # Buttons for going to siblings of the current object.
    pxNavigate = Px('''
      <!-- Go to the source URL (search or referred object) -->
      <a if="not inPopup" href=":self.sourceUrl"><img
         var="goBack='%s - %s' % (self.getBackText(), _('goto_source'))"
         src=":url('gotoSource')" title=":goBack"/></a>

      <!-- Go to the first or previous page -->
      <a if="self.firstUrl" href=":self.firstUrl"><img title=":_('goto_first')"
         src=":url('arrowsLeft')"/></a><a
         if="self.previousUrl" href=":self.previousUrl"><img
         title=":_('goto_previous')" src=":url('arrowLeft')"/></a>

      <!-- Explain which element is currently shown -->
      <span class="discreet"> 
       <x>:self.number</x> <b>//</b> 
       <x>:self.total</x> </span>

      <!-- Go to the next or last page -->
      <a if="self.nextUrl" href=":self.nextUrl"><img title=":_('goto_next')"
         src=":url('arrowRight')"/></a><a
         if="self.lastUrl" href=":self.lastUrl"><img title=":_('goto_last')"
         src=":url('arrowsRight')"/></a>

      <!-- Go to the element number... -->
      <x if="self.showGotoNumber()"
         var2="field=self.field; sourceUrl=self.sourceObject.absolute_url();
               totalNumber=self.total"><br/><x>:obj.pxGotoNumber</x></x>''')

    @staticmethod
    def get(nav, tool, inPopup):
        '''This method analyses the navigation info p_nav and returns the
           corresponding concrete Siblings instance.'''
        elems = nav.split('.')
        params = elems[1:]
        if elems[0] == 'ref': return RefSiblings(tool, inPopup, *params)
        elif elems[0] == 'search':
            return SearchSiblings(tool, inPopup, *params)

    def computeStartNumber(self):
        '''Returns the start number of the batch where the current element
           lies.'''
        # First index starts at O, so we calibrate self.number
        number = self.number - 1
        batchSize = self.getBatchSize()
        res = 0
        while (res < self.total):
            if (number < res + batchSize): return res
            res += batchSize
        return res

    def __init__(self, tool, inPopup, number, total):
        self.tool = tool
        self.request = tool.REQUEST
        # Are we in a popup window or not?
        self.inPopup = inPopup
        # The number of the current element
        self.number = int(number)
        # The total number of siblings
        self.total = int(total)
        # Do I need to navigate to first, previous, next and/or last sibling ?
        self.previousNeeded = False  # Previous ?
        self.previousIndex = self.number - 2
        if (self.previousIndex > -1) and (self.total > self.previousIndex):
            self.previousNeeded = True
        self.nextNeeded = False  # Next ?
        self.nextIndex = self.number
        if self.nextIndex < self.total: self.nextNeeded = True
        self.firstNeeded = False  # First ?
        self.firstIndex = 0
        if self.previousIndex > 0: self.firstNeeded = True
        self.lastNeeded = False  # Last ?
        self.lastIndex = self.total - 1
        if (self.nextIndex < self.lastIndex): self.lastNeeded = True
        # Compute the UIDs of the siblings of the current object
        self.siblings = self.getSiblings()
        # Compute back URL and URLs to siblings
        self.sourceUrl = self.getSourceUrl()
        siblingNav = self.getNavKey()
        siblingPage = self.request.get('page', 'main')
        for urlType in self.siblingTypes:
            exec 'needIt = self.%sNeeded' % urlType
            urlKey = '%sUrl' % urlType
            setattr(self, urlKey, None)
            if not needIt: continue
            exec 'index = self.%sIndex' % urlType
            uid = None
            try:
                # self.siblings can be a list (ref) or a dict (search)
                uid = self.siblings[index]
            except KeyError:
                continue
            except IndexError:
                continue
            if not uid: continue
            sibling = self.tool.getObject(uid)
            if not sibling: continue
            setattr(
                self, urlKey,
                sibling.getUrl(nav=siblingNav % (index + 1),
                               page=siblingPage,
                               inPopup=inPopup))
Exemple #11
0
class List(Field):
    '''A list.'''

    # PX for rendering a single row.
    pxRow = Px('''
     <tr valign="top" style="(rowIndex==-1) and 'display: none' or ''">
      <td align="center" for="info in field.fields"
          var2="field=info[1];
                tagCss='noStyle';
                widgetName='%s*%d' % (field.name, rowIndex)">:field.pxView</td>
      <!-- Icon for removing the row -->
      <td if="layoutType=='edit'" align=":dright">
       <img class="clickable" src=":url(delete')" title=":_('object_delete')"
            onclick=":'deleteRow(%s, this)' % q('list_%s' % name)"/>
      </td>
     </tr>''')

    # PX for rendering the list (shared between pxView and pxEdit).
    pxTable = Px('''
     <table var="isEdit=layoutType == 'edit'" if="isEdit or value"
            id=":'list_%s' % name" class="isEdit and 'grid' or 'list'">
      <!-- Header -->
      <tr valign="bottom">
       <th for="info in field.fields">::_(info[1].labelId)</th>
       <!-- Icon for adding a new row. -->
       <th if="isEdit">
        <img class="clickable" src=":url('plus')" title=":_('add_ref')"
             onclick=":'insertRow(%s)' % q('list_%s' % name)"/>
       </th>
      </tr>

      <!-- Template row (edit only) -->
      <x var="rowIndex=-1" if="isEdit">:field.pxRow</x>
      <tr height="7px" if="isEdit"><td></td></tr>

      <!-- Rows of data -->
      <x var="rows=inRequest and requestValue or value"
         for="row in rows" var2="rowIndex=loop.row.nb">:field.pxRow</x>
     </table>''')

    pxView = pxCell = Px('''<x>:field.pxTable</x>''')
    pxEdit = Px('''<x>
     <!-- This input makes Appy aware that this field is in the request -->
     <input type="hidden" name=":name" value=""/><x>:field.pxTable</x>
    </x>''')

    pxSearch = ''

    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 getField(self, name):
        '''Gets the field definition whose name is p_name.'''
        for n, field in self.fields:
            if n == name: return field

    def getRequestValue(self, request, requestName=None):
        '''Concatenates the list from distinct form elements in the request.'''
        name = requestName or self.name  # A List may be into another List (?)
        prefix = name + '*' + self.fields[0][0] + '*'
        res = {}
        for key in request.keys():
            if not key.startswith(prefix): continue
            # I have found a row. Gets its index
            row = Object()
            if '_' in key: key = key[:key.index('_')]
            rowIndex = int(key.split('*')[-1])
            if rowIndex == -1: continue  # Ignore the template row.
            for subName, subField in self.fields:
                keyName = '%s*%s*%s' % (name, subName, rowIndex)
                v = subField.getRequestValue(request, requestName=keyName)
                setattr(row, subName, v)
            res[rowIndex] = row
        # Produce a sorted list.
        keys = res.keys()
        keys.sort()
        res = [res[key] for key in keys]
        # I store in the request this computed value. This way, when individual
        # subFields will need to get their value, they will take it from here,
        # instead of taking it from the specific request key. Indeed, specific
        # request keys contain row indexes that may be wrong after row deletions
        # by the user.
        request.set(name, res)
        return res

    def getStorableValue(self, value):
        '''Gets p_value in a form that can be stored in the database.'''
        res = []
        for v in value:
            sv = Object()
            for name, field in self.fields:
                setattr(sv, name, field.getStorableValue(getattr(v, name)))
            res.append(sv)
        return res

    def getInnerValue(self, outerValue, name, i):
        '''Returns the value of inner field named p_name in row number p_i
           within the whole list of values p_outerValue.'''
        if i == -1: return ''
        if not outerValue: return ''
        if i >= len(outerValue): return ''
        return getattr(outerValue[i], name, '')

    def getCss(self, layoutType, res):
        '''Gets the CSS required by sub-fields if any.'''
        for name, field in self.fields:
            field.getCss(layoutType, res)

    def getJs(self, layoutType, res):
        '''Gets the JS required by sub-fields if any.'''
        for name, field in self.fields:
            field.getJs(layoutType, res)
Exemple #12
0
class Ogone(Field):
    '''This field allows to perform payments with the Ogone (r) system.'''
    urlTypes = ('accept', 'decline', 'exception', 'cancel')

    pxView = pxCell = Px('''<x>
     <!-- var "value" is misused and contains the contact params for Ogone -->
     <!-- The form for sending the payment request to Ogone -->
     <form method="post" id="form1" name="form1" var="env=value['env']"
           action=":'https://secure.ogone.com/ncol/%s/orderstandard.asp'% env">
       <input type="hidden" for="item in value.items()" if="item[0] != 'env'"
              id=":item[0]" name=":item[0]" value=":item[1]"/>
       <!-- Submit image -->
       <input type="image" id="submit2" name="submit2" src=":url('ogone.gif')"
              title=":_('custom_pay')"/>
     </form>
    </x>''')

    pxEdit = pxSearch = ''

    def __init__(self,
                 orderMethod,
                 responseMethod,
                 show='view',
                 page='main',
                 group=None,
                 layouts=None,
                 move=0,
                 specificReadPermission=False,
                 specificWritePermission=False,
                 width=None,
                 height=None,
                 colspan=1,
                 master=None,
                 masterValue=None,
                 focus=False,
                 mapping=None,
                 label=None):
        Field.__init__(self, None, (0, 1), None, show, page, group, layouts,
                       move, False, False, specificReadPermission,
                       specificWritePermission, width, height, None, colspan,
                       master, masterValue, focus, False, True, mapping, label,
                       None, None, None, None)
        # orderMethod must contain a method returning a dict containing info
        # about the order. Following keys are mandatory:
        #   * orderID   An identifier for the order. Don't use the object UID
        #               for this, use a random number, because if the payment
        #               is canceled, Ogone will not allow you to reuse the same
        #               orderID for the next tentative.
        #   * amount    An integer representing the price for this order,
        #               multiplied by 100 (no floating point value, no commas
        #               are tolerated. Dont't forget to multiply the amount by
        #               100!
        self.orderMethod = orderMethod
        # responseMethod must contain a method accepting one param, let's call
        # it "response". The response method will be called when we will get
        # Ogone's response about the status of the payment. Param "response" is
        # an object whose attributes correspond to all parameters that you have
        # chosen to receive in your Ogone merchant account. After the payment,
        # the user will be redirected to the object's view page, excepted if
        # your method returns an alternatve URL.
        self.responseMethod = responseMethod

    noShaInKeys = ('env', )
    noShaOutKeys = ('name', 'SHASIGN')

    def createShaDigest(self, values, passphrase, keysToIgnore=()):
        '''Creates an Ogone-compliant SHA-1 digest based on key-value pairs in
           dict p_values and on some p_passphrase.'''
        # Create a new dict by removing p_keysToIgnore from p_values, and by
        # upperizing all keys.
        shaRes = {}
        for k, v in values.iteritems():
            if k in keysToIgnore: continue
            # Ogone: we must not include empty values.
            if (v == None) or (v == ''): continue
            shaRes[k.upper()] = v
        # Create a sorted list of keys
        keys = shaRes.keys()
        keys.sort()
        shaList = []
        for k in keys:
            shaList.append('%s=%s' % (k, shaRes[k]))
        shaObject = sha.new(passphrase.join(shaList) + passphrase)
        res = shaObject.hexdigest()
        return res

    def getValue(self, obj):
        '''The "value" of the Ogone field is a dict that collects all the
           necessary info for making the payment.'''
        tool = obj.getTool()
        # Basic Ogone parameters were generated in the app config module.
        res = obj.getProductConfig(True).ogone.copy()
        shaKey = res['shaInKey']
        # Remove elements from the Ogone config that we must not send in the
        # payment request.
        del res['shaInKey']
        del res['shaOutKey']
        res.update(self.callMethod(obj, self.orderMethod))
        # Add user-related information
        res['CN'] = str(tool.getUserName(normalized=True))
        user = obj.appy().appyUser
        res['EMAIL'] = user.email or user.login
        # Add standard back URLs
        siteUrl = tool.getSiteUrl()
        res['catalogurl'] = siteUrl
        res['homeurl'] = siteUrl
        # Add redirect URLs
        for t in self.urlTypes:
            res['%surl' % t] = '%s/onProcess' % obj.absolute_url()
        # Add additional parameter that we want Ogone to give use back in all
        # of its responses: the name of this Appy Ogone field. This way, Appy
        # will be able to call method m_process below, that will process
        # Ogone's response.
        res['paramplus'] = 'name=%s' % self.name
        # Ensure every value is a str
        for k in res.iterkeys():
            if not isinstance(res[k], str):
                res[k] = str(res[k])
        # Compute a SHA-1 key as required by Ogone and add it to the res
        res['SHASign'] = self.createShaDigest(res,
                                              shaKey,
                                              keysToIgnore=self.noShaInKeys)
        return res

    def ogoneResponseOk(self, obj):
        '''Returns True if the SHA-1 signature from Ogone matches retrieved
           params.'''
        response = obj.REQUEST.form
        shaKey = obj.getProductConfig(True).ogone['shaOutKey']
        digest = self.createShaDigest(response,
                                      shaKey,
                                      keysToIgnore=self.noShaOutKeys)
        return digest.lower() == response['SHASIGN'].lower()

    def process(self, obj):
        '''Processes a response from Ogone.'''
        # Call the response method defined in this Ogone field.
        if not self.ogoneResponseOk(obj):
            obj.log('Ogone response SHA failed. REQUEST: %s' % \
                    str(obj.REQUEST.form))
            raise Exception('Failure, possible fraud detection, an ' \
                            'administrator has been contacted.')
        # Create a nice object from the form.
        response = Object()
        for k, v in obj.REQUEST.form.iteritems():
            setattr(response, k, v)
        # Call the field method that handles the response received from Ogone.
        url = self.responseMethod(obj.appy(), response)
        # Redirect the user to the correct page. If the field method returns
        # some URL, use it. Else, use the view page of p_obj.
        if not url: url = obj.absolute_url()
        obj.goto(url)
Exemple #13
0
class Float(Field):
    allowedDecimalSeps = (',', '.')
    allowedThousandsSeps = (' ', '')

    pxView = pxCell = Px('''
     <x><x>:value</x>
      <input type="hidden" if="masterCss" class=":masterCss" value=":value"
             name=":name" id=":name"/>
     </x>''')

    pxEdit = Px('''
     <input id=":name" name=":name" size=":field.width"
            maxlength=":field.maxChars"
            value=":inRequest and requestValue or value" type="text"/>''')

    pxSearch = Px('''
     <!-- From -->
     <x var="fromName='%s*float' % widgetName">
      <label lfor=":fromName">:_('search_from')</label>
      <input type="text" name=":fromName" maxlength=":field.maxChars"
             value=":field.sdefault[0]" size=":field.swidth"/>
     </x>
     <!-- To -->
     <x var="toName='%s_to' % name">
      <label lfor=":toName">:_('search_to')</label>
      <input type="text" name=":toName" maxlength=":field.maxChars"
             value=":field.sdefault[1]" size="field.swidth"/>
     </x><br/>''')

    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=5, height=None,
                 maxChars=13, colspan=1, master=None, masterValue=None,
                 focus=False, historized=False, mapping=None, label=None,
                 sdefault=('',''), scolspan=1, swidth=None, sheight=None,
                 persist=True, precision=None, sep=(',', '.'), tsep=' '):
        # The precision is the number of decimal digits. This number is used
        # for rendering the float, but the internal float representation is not
        # rounded.
        self.precision = precision
        # The decimal separator can be a tuple if several are allowed, ie
        # ('.', ',')
        if type(sep) not in sutils.sequenceTypes:
            self.sep = (sep,)
        else:
            self.sep = sep
        # Check that the separator(s) are among allowed decimal separators
        for sep in self.sep:
            if sep not in Float.allowedDecimalSeps:
                raise Exception('Char "%s" is not allowed as decimal ' \
                                'separator.' % sep)
        self.tsep = tsep
        Field.__init__(self, validator, multiplicity, default, show, page,
                       group, layouts, move, indexed, False,
                       specificReadPermission, specificWritePermission, width,
                       height, maxChars, colspan, master, masterValue, focus,
                       historized, mapping, label, sdefault, scolspan, swidth,
                       sheight, persist)
        self.pythonType = float

    def getFormattedValue(self, obj, value, showChanges=False, language=None):
        return sutils.formatNumber(value, sep=self.sep[0],
                                   precision=self.precision, tsep=self.tsep)

    def validateValue(self, obj, value):
        # Replace used separator with the Python separator '.'
        for sep in self.sep: value = value.replace(sep, '.')
        value = value.replace(self.tsep, '')
        try:
            value = self.pythonType(value)
        except ValueError:
            return obj.translate('bad_%s' % self.pythonType.__name__)

    def getStorableValue(self, obj, value):
        if not self.isEmptyValue(obj, value):
            for sep in self.sep: value = value.replace(sep, '.')
            value = value.replace(self.tsep, '')
            return self.pythonType(value)
Exemple #14
0
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) &gt; 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)
Exemple #15
0
class Action(Field):
    '''An action is a Python method that can be triggered by the user on a
       given gen-class. An action is rendered as a button.'''

    # PX for viewing the Action button.
    pxView = pxCell = Px('''
     <form name="executeAppyAction"
           var="formId='%s_%s_form' % (zobj.UID(), name);
                label=_(field.labelId)"
           id=":formId" action=":ztool.absolute_url() + '/do'">
      <input type="hidden" name="action" value="ExecuteAppyAction"/>
      <input type="hidden" name="objectUid" value=":zobj.UID()"/>
      <input type="hidden" name="fieldName" value=":name"/>
      <input if="field.confirm" type="button" class="button"
         var="labelConfirm=_(field.labelId + '_confirm')"
         value=":ztool.truncateValue(label)" title=":label"
         style=":url('buttonAction', bg=True)"
         onclick=":'askConfirm(%s,%s,%s)' % (q('form'), q(formId), \
                                             q(labelConfirm))"/>
      <input if="not field.confirm" type="submit" class="button" name="do"
             value=":ztool.truncateValue(label)" title=":label"
             style=":url('buttonAction', bg=True)"/>
     </form>''')

    # It is not possible to edit an action, not to search it.
    pxEdit = pxSearch = ''

    def __init__(self, validator=None, multiplicity=(1,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, action=None, result='computation',
                 confirm=False, master=None, masterValue=None, focus=False,
                 historized=False, mapping=None, label=None):
        # Can be a single method or a list/tuple of methods
        self.action = action
        # For the 'result' param:
        #  * value 'computation' means that the action will simply compute
        #    things and redirect the user to the same page, with some status
        #    message about execution of the action;
        #  * 'file' means that the result is the binary content of a file that
        #    the user will download.
        #  * 'filetmp' is similar to file, but the file is a temp file and Appy
        #    will delete it as soon as it will be served to the browser.
        #  * 'redirect' means that the action will lead to the user being
        #    redirected to some other page.
        self.result = result
        # If following field "confirm" is True, a popup will ask the user if
        # she is really sure about triggering this action.
        self.confirm = confirm
        Field.__init__(self, None, (0,1), default, show, page, group, layouts,
                       move, indexed, False, specificReadPermission,
                       specificWritePermission, width, height, None, colspan,
                       master, masterValue, focus, historized, False, mapping,
                       label, None, None, None, None)
        self.validable = False

    def getDefaultLayouts(self): return {'view': 'l-f', 'edit': 'lrv-f'}

    def __call__(self, obj):
        '''Calls the action on p_obj.'''
        if type(self.action) in sutils.sequenceTypes:
            # There are multiple Python methods
            res = [True, '']
            for act in self.action:
                actRes = act(obj)
                if type(actRes) in sutils.sequenceTypes:
                    res[0] = res[0] and actRes[0]
                    if self.result.startswith('file'):
                        res[1] = res[1] + actRes[1]
                    else:
                        res[1] = res[1] + '\n' + actRes[1]
                else:
                    res[0] = res[0] and actRes
        else:
            # There is only one Python method
            actRes = self.action(obj)
            if type(actRes) in sutils.sequenceTypes:
                res = list(actRes)
            else:
                res = [actRes, '']
        # If res is None (ie the user-defined action did not return anything),
        # we consider the action as successfull.
        if res[0] == None: res[0] = True
        return res

    def isShowable(self, obj, layoutType):
        if layoutType == 'edit': return False
        else: return Field.isShowable(self, obj, layoutType)
Exemple #16
0
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
Exemple #17
0
class UiSearch:
    '''Instances of this class are generated on-the-fly for manipulating a
       Search from the User Interface.'''
    # PX for rendering a search.
    pxView = Px('''
     <div class="portletSearch">
      <a href=":'%s?className=%s&amp;search=%s' % \
                 (queryUrl, className, search.name)"
         class=":(search.name == currentSearch) and 'current' or ''"
         title=":search.translatedDescr">:search.translated</a>
     </div>''')

    def __init__(self, search, className, tool):
        self.search = search
        self.name = search.name
        self.type = 'search'
        self.colspan = search.colspan
        if search.translated:
            self.translated = search.translated
            self.translatedDescr = search.translatedDescr
        else:
            # The label may be specific in some special cases.
            labelDescr = ''
            if search.name == 'allSearch':
                label = '%s_plural' % className
            elif search.name == 'customSearch':
                label = 'search_results'
            elif search.name == '_field_':
                label = None
            else:
                label = '%s_search_%s' % (className, search.name)
                labelDescr = label + '_descr'
            _ = tool.translate
            self.translated = label and _(label) or ''
            self.translatedDescr = labelDescr and _(labelDescr) or ''

    def setInitiator(self, initiator, field, mode):
        '''If the search is defined in an attribute Ref.select, we receive here
           the p_initiator object, its Ref p_field and the p_mode, that can be:
           - "repl" if the objects selected in the popup will replace already
                    tied objects;
           - "add"  if those objects will be added to the already tied ones.
           .'''
        self.initiator = initiator
        self.initiatorField = field
        self.initiatorMode = mode
        # "initiatorHook" is the ID of the initiator field's XHTML tag.
        self.initiatorHook = '%s_%s' % (initiator.uid, field.name)

    def getRootHookId(self):
        '''If an initiator field is there, return the initiator hook.
           Else, simply return the name of the search.'''
        return getattr(self, 'initiatorHook', self.name)

    def showCheckboxes(self):
        '''If checkboxes are enabled for this search (and if an initiator field
           is there), they must be visible only if the initiator field is
           multivalued. Indeed, if it is not the case, it has no sense to select
           multiple objects. But in this case, we still want checkboxes to be in
           the DOM because they store object UIDs.'''
        if not self.search.checkboxes: return
        return not self.initiator or self.initiatorField.isMultiValued()
Exemple #18
0
class List(Field):
    '''A list.'''

    # PX for rendering a single row.
    pxRow = Px('''
     <tr valign="top" style=":(rowIndex==-1) and 'display: none' or ''">
      <td for="info in subFields" if="info[1]" align="center"
          var2="field=info[1];
                fieldName='%s*%d' % (field.name, rowIndex);
                tagCss='noStyle'">:field.pxRender</td>
      <!-- Icon for removing the row -->
      <td if="layoutType=='edit'" align=":dright">
       <img class="clickable" src=":url('delete')" title=":_('object_delete')"
            onclick=":'deleteRow(%s, this)' % q('list_%s' % name)"/>
      </td>
     </tr>''')

    # PX for rendering the list (shared between pxView and pxEdit).
    pxTable = Px('''
     <table var="isEdit=layoutType == 'edit'" if="isEdit or value"
            id=":'list_%s' % name" class=":isEdit and 'grid' or 'list'"
            width=":field.width"
            var2="subFields=field.getSubFields(zobj, layoutType)">
      <!-- Header -->
      <tr valign="bottom">
       <th for="info in subFields" if="info[1]"
           width=":field.widths[loop.info.nb]">::_(info[1].labelId)</th>
       <!-- Icon for adding a new row. -->
       <th if="isEdit">
        <img class="clickable" src=":url('plus')" title=":_('add_ref')"
             onclick=":'insertRow(%s)' % q('list_%s' % name)"/>
       </th>
      </tr>

      <!-- Template row (edit only) -->
      <x var="rowIndex=-1" if="isEdit">:field.pxRow</x>
      <tr height="7px" if="isEdit"><td></td></tr>

      <!-- Rows of data -->
      <x var="rows=inRequest and requestValue or value"
         for="row in rows" var2="rowIndex=loop.row.nb">:field.pxRow</x>
     </table>''')

    pxView = pxCell = Px('''<x>:field.pxTable</x>''')
    pxEdit = Px('''<x>
     <!-- This input makes Appy aware that this field is in the request -->
     <input type="hidden" name=":name" value=""/><x>:field.pxTable</x>
    </x>''')

    pxSearch = ''

    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 getField(self, name):
        '''Gets the field definition whose name is p_name.'''
        for n, field in self.fields:
            if n == name: return field

    def getSubFields(self, obj, layoutType):
        '''Returns the sub-fields (name, Field) that are showable among
           field.fields on the given p_layoutType. Fields that cannot appear in
           the result are nevertheless present as a tuple (name, None). This
           way, it keeps a nice layouting of the table.'''
        res = []
        for n, field in self.fields:
            elem = (n, None)
            if field.isShowable(obj, layoutType):
                elem = (n, field)
            res.append(elem)
        return res

    def getRequestValue(self, obj, requestName=None):
        '''Concatenates the list from distinct form elements in the request.'''
        request = obj.REQUEST
        name = requestName or self.name  # A List may be into another List (?)
        prefix = name + '*' + self.fields[0][0] + '*'
        res = {}
        for key in request.keys():
            if not key.startswith(prefix): continue
            # I have found a row. Gets its index
            row = Object()
            if '_' in key: key = key[:key.index('_')]
            rowIndex = int(key.split('*')[-1])
            if rowIndex == -1: continue  # Ignore the template row.
            for subName, subField in self.fields:
                keyName = '%s*%s*%s' % (name, subName, rowIndex)
                v = subField.getRequestValue(obj, requestName=keyName)
                setattr(row, subName, v)
            res[rowIndex] = row
        # Produce a sorted list.
        keys = res.keys()
        keys.sort()
        res = [res[key] for key in keys]
        # I store in the request this computed value. This way, when individual
        # subFields will need to get their value, they will take it from here,
        # instead of taking it from the specific request key. Indeed, specific
        # request keys contain row indexes that may be wrong after row deletions
        # by the user.
        request.set(name, res)
        return res

    def getStorableValue(self, obj, value):
        '''Gets p_value in a form that can be stored in the database.'''
        res = []
        for v in value:
            sv = Object()
            for name, field in self.fields:
                subValue = getattr(v, name)
                try:
                    setattr(sv, name, field.getStorableValue(obj, subValue))
                except ValueError:
                    # The value for this field for this specific row is
                    # incorrect. It can happen in the process of validating the
                    # whole List field (a call to m_getStorableValue occurs at
                    # this time). We don't care about it, because later on we
                    # will have sub-field specific validation that will also
                    # detect the error and will prevent storing the wrong value
                    # in the database.
                    setattr(sv, name, subValue)
            res.append(sv)
        return res

    def getInnerValue(self, obj, outerValue, name, i):
        '''Returns the value of inner field named p_name in row number p_i
           within the whole list of values p_outerValue.'''
        if i == -1: return ''
        if not outerValue: return ''
        if i >= len(outerValue): return ''
        # Return the value, or a potential default value.
        return getattr(outerValue[i], name, '') or \
               self.getField(name).getValue(obj) or ''

    def getCss(self, layoutType, res):
        '''Gets the CSS required by sub-fields if any.'''
        for name, field in self.fields:
            field.getCss(layoutType, res)

    def getJs(self, layoutType, res):
        '''Gets the JS required by sub-fields if any.'''
        for name, field in self.fields:
            field.getJs(layoutType, res)
Exemple #19
0
class File(Field):

    pxView = pxCell = Px('''
     <x var="info=field.getFileInfo(value);
             empty=not info.size;
             imgSrc='%s/download?name=%s' % (zobj.absolute_url(), name)">
      <x if="not empty and not field.isImage">
       <a href=":imgSrc">:info.filename</a>&nbsp;&nbsp;-
       <i class="discreet">'%sKb' % (info.size / 1024)"></i>
      </x>
      <x if="not empty and field.isImage"><img src=":imgSrc"/></x>
      <x if="empty">-</x>
     </x>''')

    pxEdit = Px('''
     <x var="info=field.getFileInfo(value);
             empty= not info.size;
             fName=q('%s_file' % name)">

      <x if="not empty">:field.pxView</x><br/>
      <x if="not empty">
       <!-- Keep the file unchanged. -->
       <input type="radio" value="nochange"
              checked=":(info.size != 0) and 'checked' or None"
              name=":'%s_delete' % name" id=":'%s_nochange' % name"
              onclick=":'document.getElementById(%s).disabled=true' % fName"/>
       <label lfor=":'%s_nochange' % name">Keep the file unchanged</label><br/>
       <!-- Delete the file. -->
       <x if="not field.required">
        <input type="radio" value="delete"
               name=":'%s_delete' % name" id=":'%s_delete' % name"
               onclick=":'document.getElementById(%s).disabled=true' % fName"/>
        <label lfor=":'%s_delete' % name">Delete the file</label><br/>
       </x>
       <!-- Replace with a new file. -->
       <input type="radio" value=""
              checked=":(info.size == 0) and 'checked' or None"
              name=":'%s_delete' % name" id=":'%s_upload' % name"
              onclick=":'document.getElementById(%s).disabled=false' % fName"/>
       <label lfor=":'%s_upload' % name">Replace it with a new file</label><br/>
      </x>
      <!-- The upload field. -->
      <input type="file" name=":'%s_file' % name" id=":'%s_file' % name"
             size=":field.width"/>
      <script var="isDisabled=empty and 'false' or 'true'"
              type="text/javascript">:document.getElementById(%s).disabled=%s'%\
                                      (q(fName), q(isDisabled))">
      </script>
     </x>''')

    pxSearch = ''

    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,
                 isImage=False, sdefault='', scolspan=1, swidth=None,
                 sheight=None):
        self.isImage = isImage
        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, sdefault, scolspan,
                       swidth, sheight)

    @staticmethod
    def getFileObject(filePath, fileName=None, zope=False):
        '''Returns a File instance as can be stored in the database or
           manipulated in code, filled with content from a file on disk,
           located at p_filePath. If you want to give it a name that is more
           sexy than the actual basename of filePath, specify it in
           p_fileName.

           If p_zope is True, it will be the raw Zope object = an instance of
           OFS.Image.File. Else, it will be a FileWrapper instance from Appy.'''
        f = file(filePath, 'rb')
        if not fileName:
            fileName = os.path.basename(filePath)
        fileId = 'file.%f' % time.time()
        import OFS.Image
        res = OFS.Image.File(fileId, fileName, f)
        res.filename = fileName
        res.content_type = mimetypes.guess_type(fileName)[0]
        f.close()
        if not zope: res = sutils.FileWrapper(res)
        return res

    def getValue(self, obj):
        value = Field.getValue(self, obj)
        if value: value = sutils.FileWrapper(value)
        return value

    def getFormattedValue(self, obj, value, showChanges=False):
        if not value: return value
        return value._zopeFile

    def getRequestValue(self, request, requestName=None):
        name = requestName or self.name
        return request.get('%s_file' % name)

    def getDefaultLayouts(self): return {'view':'l-f','edit':'lrv-f'}

    def isEmptyValue(self, value, obj=None):
        '''Must p_value be considered as empty?'''
        if not obj: return Field.isEmptyValue(self, value)
        if value: return False
        # If "nochange", the value must not be considered as empty
        return obj.REQUEST.get('%s_delete' % self.name) != 'nochange'

    imageExts = ('.jpg', '.jpeg', '.png', '.gif')
    def validateValue(self, obj, value):
        form = obj.REQUEST.form
        action = '%s_delete' % self.name
        if (not value or not value.filename) and form.has_key(action) and \
            not form[action]:
            # If this key is present but empty, it means that the user selected
            # "replace the file with a new one". So in this case he must provide
            # a new file to upload.
            return obj.translate('file_required')
        # Check that, if self.isImage, the uploaded file is really an image
        if value and value.filename and self.isImage:
            ext = os.path.splitext(value.filename)[1].lower()
            if ext not in File.imageExts:
                return obj.translate('image_required')

    defaultMimeType = 'application/octet-stream'
    def store(self, obj, value):
        '''Stores the p_value that represents some file. p_value can be:
           * an instance of Zope class ZPublisher.HTTPRequest.FileUpload. In
             this case, it is file content coming from a HTTP POST;
           * an instance of Zope class OFS.Image.File;
           * an instance of appy.shared.utils.FileWrapper, which wraps an
             instance of OFS.Image.File and adds useful methods for manipulating
             it;
           * a string. In this case, the string represents the path of a file
             on disk;
           * a 2-tuple (fileName, fileContent) where:
             - fileName is the name of the file (ie "myFile.odt")
             - fileContent is the binary or textual content of the file or an
                           open file handler.
           * a 3-tuple (fileName, fileContent, mimeType) where
             - fileName and fileContent have the same meaning than above;
             - mimeType is the MIME type of the file.
        '''
        if value:
            ZFileUpload = obj.o.getProductConfig().FileUpload
            OFSImageFile = obj.o.getProductConfig().File
            if isinstance(value, ZFileUpload):
                # The file content comes from a HTTP POST.
                # Retrieve the existing value, or create one if None
                existingValue = getattr(obj.aq_base, self.name, None)
                if not existingValue:
                    existingValue = OFSImageFile(self.name, '', '')
                # Set mimetype
                if value.headers.has_key('content-type'):
                    mimeType = value.headers['content-type']
                else:
                    mimeType = File.defaultMimeType
                existingValue.content_type = mimeType
                # Set filename
                fileName = value.filename
                filename= fileName[max(fileName.rfind('/'),fileName.rfind('\\'),
                                       fileName.rfind(':'))+1:]
                existingValue.filename = fileName
                # Set content
                existingValue.manage_upload(value)
                setattr(obj, self.name, existingValue)
            elif isinstance(value, OFSImageFile):
                setattr(obj, self.name, value)
            elif isinstance(value, sutils.FileWrapper):
                setattr(obj, self.name, value._zopeFile)
            elif isinstance(value, basestring):
                setattr(obj, self.name, File.getFileObject(value, zope=True))
            elif type(value) in sutils.sequenceTypes:
                # It should be a 2-tuple or 3-tuple
                fileName = None
                mimeType = None
                if len(value) == 2:
                    fileName, fileContent = value
                elif len(value) == 3:
                    fileName, fileContent, mimeType = value
                else:
                    raise WRONG_FILE_TUPLE
                if fileName:
                    fileId = 'file.%f' % time.time()
                    zopeFile = OFSImageFile(fileId, fileName, fileContent)
                    zopeFile.filename = fileName
                    if not mimeType:
                        mimeType = mimetypes.guess_type(fileName)[0]
                    zopeFile.content_type = mimeType
                    setattr(obj, self.name, zopeFile)
        else:
            # I store value "None", excepted if I find in the request the desire
            # to keep the file unchanged.
            action = None
            rq = getattr(obj, 'REQUEST', None)
            if rq: action = rq.get('%s_delete' % self.name, None)
            if action == 'nochange': pass
            else: setattr(obj, self.name, None)

    def getFileInfo(self, fileObject):
        '''Returns filename and size of p_fileObject.'''
        if not fileObject: return Object(filename='', size=0)
        return Object(filename=fileObject.filename, size=fileObject.size)
Exemple #20
0
class Integer(Field):

    pxView = pxCell = Px('''
     <x><x>:value</x>
      <input type="hidden" if="masterCss"
             class=":masterCss" value=":value" name=":name" id=":name"/>
     </x>''')

    pxEdit = Px('''
     <input id=":name" name=":name" size=":field.width"
            maxlength=":field.maxChars"
            value=":inRequest and requestValue or value" type="text"/>''')

    pxSearch = Px('''
     <!-- From -->
     <x var="fromName='%s*int' % widgetName">
      <label lfor=":fromName">:_('search_from')</label>
      <input type="text" name=":fromName" maxlength=":field.maxChars"
             value=":field.sdefault[0]" size=":field.swidth"/>
     </x>
     <!-- To -->
     <x var="toName='%s_to' % name">
      <label lfor=":toName">:_('search_to')</label>
      <input type="text" name=":toName" maxlength=":field.maxChars"
             value=":field.sdefault[1]" size=":field.swidth"/>
     </x><br/>''')

    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=5,
                 height=None,
                 maxChars=13,
                 colspan=1,
                 master=None,
                 masterValue=None,
                 focus=False,
                 historized=False,
                 mapping=None,
                 label=None,
                 sdefault=('', ''),
                 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, maxChars, colspan, master, masterValue, focus,
                       historized, mapping, label, sdefault, scolspan, swidth,
                       sheight, persist)
        self.pythonType = long

    def validateValue(self, obj, value):
        try:
            value = self.pythonType(value)
        except ValueError:
            return obj.translate('bad_%s' % self.pythonType.__name__)

    def getStorableValue(self, value):
        if not self.isEmptyValue(value): return self.pythonType(value)

    def getFormattedValue(self, obj, value, showChanges=False):
        if self.isEmptyValue(value): return ''
        return str(value)
Exemple #21
0
class UiSearch:
    '''Instances of this class are generated on-the-fly for manipulating a
       Search from the User Interface.'''
    # Tied sub-classes
    RefInitiator = RefInitiator
    TemplateInitiator = TemplateInitiator
    pxByMode = {'list': 'pxResultList', 'grid': 'pxResultGrid'}

    # Rendering a search
    pxView = Px('''
     <div class="portletSearch">
      <a href=":'%s?className=%s&amp;search=%s' % \
                 (queryUrl, className, search.name)"
         class=":(search.name == currentSearch) and 'current' or ''"
         onclick="clickOn(this)"
         title=":search.translatedDescr">:search.translated</a>
     </div>''')

    # Search results, as a list (used by pxResult below)
    pxResultList = Px('''
     <table class=":ztool.getResultCss(className, layoutType)" width="100%">
      <!-- Headers, with filters and sort arrows -->
      <tr if="showHeaders">
       <th if="checkboxes" class="cbCell" style=":'display:%s' % cbDisplay">
        <img src=":url('checkall')" class="clickable"
             title=":_('check_uncheck')"
             onclick=":'toggleAllCbs(%s)' % q(checkboxesId)"/>
       </th>
       <th for="column in columns"
           var2="field=column.field;
                 sortable=field.isSortable(usage='search')"
           width=":column.width" align=":column.align">
        <x>::ztool.truncateText(_(field.labelId))</x>
        <!-- Sort icons -->
        <x if="sortable and (totalNumber &gt; 1)">
         <img if="(sortKey != field.name) or (sortOrder == 'desc')"
              onclick=":'askBunchSorted(%s, %s, %s)' % \
                        (q(ajaxHookId), q(field.name), q('asc'))"
              src=":url('sortDown')" class="clickable"/>
         <img if="(sortKey != field.name) or (sortOrder == 'asc')"
              onclick=":'askBunchSorted(%s, %s, %s)' % \
                        (q(ajaxHookId), q(field.name), q('desc'))"
              src=":url('sortUp')" class="clickable"/>
        </x>
        <!-- Filter widget -->
        <x if="field.filterPx and ((totalNumber &gt; 1) or \
               filters)">:getattr(field, field.filterPx)</x>
        <x>:tool.pxShowDetails</x>
       </th>
      </tr>

      <!-- Results -->
      <tr if="not zobjects">
       <td colspan=":len(columns)+1">:_('query_no_result')</td>
      </tr>
      <x for="zobj in zobjects"
         var2="rowCss=loop.zobj.odd and 'even' or 'odd';
              @currentNumber=currentNumber + 1">:zobj.appy().pxViewAsResult</x>
     </table>
     <!-- The button for selecting objects and closing the popup -->
     <div if="inPopup and cbShown" align=":dleft">
      <input type="button"
             var="label=_('object_link_many'); css=ztool.getButtonCss(label)"
             value=":label" class=":css" style=":url('linkMany', bg=True)"
             onclick=":uiSearch.initiator.jsSelectMany(\
                   q, sortKey, sortOrder, uiSearch.getFiltersString(filters))"/>
     </div>
     <!-- Init checkboxes if present -->
     <script if="checkboxes">:'initCbs(%s)' % q(checkboxesId)</script>
     <script>:'initFocus(%s)' % q(ajaxHookId)</script>''')

    # Search results, as a grid (used by pxResult below)
    pxResultGrid = Px('''
     <table width="100%"
            var="modeElems=resultMode.split('_');
                 cols=(len(modeElems)==2) and int(modeElems[1]) or 4;
                 rows=ztool.splitList(zobjects, cols)">
      <tr for="row in rows" valign="middle">
       <td for="zobj in row" width=":'%d%%' % (100/cols)" align="center"
           style="padding-top: 25px"
           var2="obj=zobj.appy(); mayView=zobj.mayView()">
        <x var="@currentNumber=currentNumber + 1"
           for="column in columns"
           var2="field=column.field">:field.pxRenderAsResult</x>
       </td>
      </tr>
     </table>''')

    # Render search results
    pxResult = Px('''
     <div var="ajaxHookId='queryResult';
               layoutType='view';
               className=className|req['className'];
               klass=ztool.getAppyClass(className);
               searchName=req.get('search', '') or field.name | '';
               uiSearch=field|ztool.getSearch(className, searchName, ui=True);
               resultMode=uiSearch.getResultMode(klass, req);
               customPx=resultMode not in uiSearch.pxByMode;
               maxResults=customPx and 'NO_LIMIT' or None;
               rootHookId=uiSearch.getRootHookId();
               refInfo=ztool.getRefInfo();
               refObject=refInfo[0];
               refField=refInfo[1];
               refUrlPart=refObject and ('&amp;ref=%s:%s' % (refObject.id, \
                                                             refField)) or '';
               startNumber=req.get('startNumber', '0');
               startNumber=int(startNumber);
               sortKey=req.get('sortKey', '');
               sortOrder=req.get('sortOrder', 'asc');
               filters=uiSearch.getFiltersDict(req.get('filters'));
               queryResult=ztool.executeQuery(className, \
                 search=uiSearch.search, startNumber=startNumber, \
                 maxResults=maxResults, remember=True, sortBy=sortKey, \
                 sortOrder=sortOrder, filters=filters, refObject=refObject, \
                 refField=refField);
               zobjects=queryResult.objects;
               objects=maxResults and [z.appy() for z in zobjects];
               totalNumber=queryResult.totalNumber;
               batchSize=queryResult.batchSize;
               batchNumber=len(zobjects);
               showNewSearch=showNewSearch|True;
               newSearchUrl='%s/search?className=%s%s' % \
                   (ztool.absolute_url(), className, refUrlPart);
               showSubTitles=req.get('showSubTitles', 'true') == 'true';
               target=ztool.getLinksTargetInfo(klass);
               showHeaders=showHeaders|True;
               checkboxes=uiSearch.checkboxes;
               checkboxesId=rootHookId + '_objs';
               cbShown=uiSearch.showCheckboxes();
               cbDisplay=cbShown and 'table-cell' or 'none'"
          id=":ajaxHookId">
      <script>:uiSearch.getAjaxData(ajaxHookId, ztool, popup=inPopup and '1', \
        checkboxes=checkboxes, checkboxesId=checkboxesId, cbDisplay=cbDisplay, \
        search=searchName, searchName=searchName, startNumber=startNumber, \
        filters=filters, sortKey=sortKey, sortOrder=sortOrder, \
        totalNumber=totalNumber)</script>

      <x if="zobjects or filters"> <!-- Pod templates -->
       <table var="fields=ztool.getResultPodFields(className)"
              if="not inPopup and zobjects and fields" align=":dright">
        <tr>
         <td var="zobj=zobjects[0]; obj=zobj.appy()"
             for="field in fields" var2="fieldName=field.name"
             class=":not loop.field.last and 'pod' or ''">:field.pxRender</td>
        </tr>
       </table>

       <!-- The title of the search -->
       <p if="not inPopup">
       <x>::uiSearch.translated</x> (<span class="discreet">:totalNumber</span>)
        <x if="showNewSearch and (searchName == 'customSearch')">&nbsp;&mdash;
         &nbsp;<i><a href=":newSearchUrl">:_('search_new')</a></i>
        </x>
       </p>
       <table width="100%">
        <tr valign="top">
         <!-- Search description -->
         <td if="uiSearch.translatedDescr.strip()">
          <span class="discreet">:uiSearch.translatedDescr</span><br/>
         </td>
         <!-- (Top) navigation -->
         <td if="not customPx"
             align=":dright" width="200px">:tool.pxNavigate</td>
        </tr>
       </table>

       <!-- Results -->
       <x var="columnLayouts=ztool.getResultColumnsLayouts(className, refInfo,
                                                           searchName);
               columns=ztool.getColumnsSpecifiers(className,columnLayouts,dir);
               currentNumber=0">:uiSearch.getPx(resultMode, klass)</x>

       <!-- (Bottom) navigation -->
       <x if="not customPx">:tool.pxNavigate</x>
      </x>

      <x if="not zobjects and not filters">
       <x>:_('query_no_result')</x>
       <x if="showNewSearch and (searchName == 'customSearch')"><br/>
        <i class="discreet"><a href=":newSearchUrl">:_('search_new')</a></i></x>
      </x>
    </div>''')

    def __init__(self, search, className, tool, initiator=None):
        self.search = search
        self.name = search.name
        self.type = 'search'
        self.colspan = search.colspan
        self.className = className
        # Property "display" of the div tag containing actions for every search
        # result.
        self.showActions = search.showActions
        if search.showActions == True: self.showActions = 'block'
        if search.translated:
            self.translated = search.translated
            self.translatedDescr = search.translatedDescr or ''
        else:
            # The label may be specific in some special cases
            labelDescr = ''
            if search.name == 'allSearch': label = '%s_plural' % className
            elif search.name == 'customSearch': label = 'search_results'
            elif (not search.name or search.klass): label = None
            else:
                label = '%s_search_%s' % (className, search.name)
                labelDescr = label + '_descr'
            _ = tool.translate
            self.translated = label and _(label) or ''
            self.translatedDescr = labelDescr and _(labelDescr) or ''
        # Strip the description (a single space may be present)
        self.translatedDescr = self.translatedDescr.strip()
        # An initiator instance if the query is in a popup
        self.initiator = initiator
        # When query results are shown in a popup, checkboxes must be present
        # even when not shown. Indeed, we want them in the DOM because object
        # ids are stored on it.
        if initiator:
            self.checkboxes = True
            self.checkboxesDefault = False
        else:
            self.checkboxes = search.checkboxes
            self.checkboxesDefault = search.checkboxesDefault

    def getRootHookId(self):
        '''If there is an initiator, return the hook as defined by it. Else,
           return the name of the search.'''
        if not self.initiator:
            return self.name or 'search'
        else:
            return self.initiator.popupHook

    def getAllResultModes(self, klass):
        '''How must we show the result? As a list, grid, or a custom px ?'''
        return getattr(klass, 'resultModes', ('list', ))

    def getResultMode(self, klass, req):
        '''Get the current result mode'''
        res = req.get('resultMode')
        if not res: res = self.getAllResultModes(klass)[0]
        return res

    def getPx(self, mode, klass):
        '''What is the PX to show, according to the current result p_mode?'''
        if mode in UiSearch.pxByMode:
            return getattr(UiSearch, UiSearch.pxByMode[mode])
        # It must be a custom PX on p_klass
        return getattr(klass, mode)

    def showCheckboxes(self):
        '''When must checkboxes be shown ?'''
        if not self.initiator: return self.checkboxes
        return self.initiator.showCheckboxes()

    def getCbJsInit(self, hookId):
        '''Returns the code that creates JS data structures for storing the
           status of checkboxes for every result of this search.'''
        default = self.checkboxesDefault and 'unchecked' or 'checked'
        return '''var node=findNode(this, '%s');
                  node['_appy_objs_cbs'] = {};
                  node['_appy_objs_sem'] = '%s';''' % (hookId, default)

    def getFiltersDict(self, filterData):
        '''Converts p_filterData as encoded in the request to a dict'''
        return sutils.getDictFrom(filterData)

    def getFiltersString(self, filters):
        '''Converts dict p_filters into its string representation'''
        if not filters: return ''
        res = []
        for k, v in filters.items():
            res.append('%s:%s' % (k, v))
        return ','.join(res)

    def getAjaxData(self, hook, ztool, **params):
        '''Initializes an AjaxData object on the DOM node corresponding to
           p_hook = the whole search result.'''
        # Complete params with default ones. For performing a complete Ajax
        # request, "className" is not needed because included in the PX name.
        # But it is requested by sub-Ajax queries at the row level.
        params['className'] = self.className
        # Add initiator-specific params
        if self.initiator:
            initatorParams = self.initiator.getAjaxParams()
            if initatorParams: params.update(initatorParams)
        # Convert params into a JS dict
        name = params['searchName']
        params = sutils.getStringFrom(params)
        px = '%s:%s:pxResult' % (self.className, name)
        return "new AjaxData('%s', '%s', %s, null, '%s')" % \
               (hook, px, params, ztool.absolute_url())

    def getAjaxDataRow(self, zobj, parentHook, **params):
        '''Initializes an AjaxData object on the DOM node corresponding to
           p_hook = a row within the list of results.'''
        hook = zobj.id
        return "new AjaxData('%s', 'pxViewAsResultFromAjax', %s, '%s', '%s')"% \
               (hook, sutils.getStringFrom(params), parentHook,
                zobj.absolute_url())

    def getModeText(self, mode, _):
        '''Gets the i18n text corresponding to p_mode'''
        if mode in UiSearch.pxByMode: return _('result_mode_%s' % mode)
        return _('custom_%s' % mode)
Exemple #22
0
class Action(Field):
    '''An action is a Python method that can be triggered by the user on a
       given gen-class. An action is rendered as a button.'''

    # PX for viewing the Action button.
    pxView = pxCell = Px('''
     <form var="formId='%s_%s_form' % (zobj.id, name);
                label=_(field.labelId);
                buttonWidth=ztool.getButtonWidth(label)"
           id=":formId" action=":ztool.absolute_url() + '/do'">
      <input type="hidden" name="action" value="ExecuteAction"/>
      <input type="hidden" name="objectUid" value=":zobj.id"/>
      <input type="hidden" name="fieldName" value=":name"/>
      <input if="field.confirm" type="button" class="button"
         var="labelConfirm=_(field.labelId + '_confirm')" value=":label"
         style=":'%s; %s' % (url('action', bg=True), buttonWidth)"
         onclick=":'askConfirm(%s,%s,%s)' % (q('form'), q(formId), \
                                             q(labelConfirm))"/>
      <input if="not field.confirm" type="submit" class="button" name="do"
             value=":label"
             style=":'%s; %s' % (url('action', bg=True), buttonWidth)"/>
     </form>''')

    # It is not possible to edit an action, not to search it.
    pxEdit = pxSearch = ''

    def __init__(self, validator=None, multiplicity=(1,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, action=None, result='computation',
                 confirm=False, master=None, masterValue=None, focus=False,
                 historized=False, mapping=None, label=None):
        # Can be a single method or a list/tuple of methods
        self.action = action
        # For the 'result' param:
        #  * value 'computation' means that the action will simply compute
        #    things and redirect the user to the same page, with some status
        #    message about execution of the action;
        #  * 'file' means that the result is the binary content of a file that
        #    the user will download.
        #  * 'redirect' means that the action will lead to the user being
        #    redirected to some other page.
        self.result = result
        # If following field "confirm" is True, a popup will ask the user if
        # she is really sure about triggering this action.
        self.confirm = confirm
        Field.__init__(self, None, (0,1), default, show, page, group, layouts,
                       move, indexed, False, specificReadPermission,
                       specificWritePermission, width, height, None, colspan,
                       master, masterValue, focus, historized, mapping, label,
                       None, None, None, None, False)
        self.validable = False
        self.renderLabel = False # Label is rendered directly within the button.

    def getDefaultLayouts(self): return {'view': 'l-f', 'edit': 'lrv-f'}

    def __call__(self, obj):
        '''Calls the action on p_obj.'''
        if type(self.action) in sutils.sequenceTypes:
            # There are multiple Python methods
            res = [True, '']
            for act in self.action:
                actRes = act(obj)
                if type(actRes) in sutils.sequenceTypes:
                    res[0] = res[0] and actRes[0]
                    if self.result.startswith('file'):
                        res[1] = res[1] + actRes[1]
                    else:
                        res[1] = res[1] + '\n' + actRes[1]
                else:
                    res[0] = res[0] and actRes
        else:
            # There is only one Python method
            actRes = self.action(obj)
            if type(actRes) in sutils.sequenceTypes:
                res = list(actRes)
            else:
                res = [actRes, '']
        # If res is None (ie the user-defined action did not return anything),
        # we consider the action as successfull.
        if res[0] == None: res[0] = True
        return res

    def isShowable(self, obj, layoutType):
        if layoutType == 'edit': return
        return Field.isShowable(self, obj, layoutType)

    def onUiRequest(self, obj, rq):
        '''This method is called when a user triggers the execution of this
           action from the user interface.'''
        # Execute the action (method __call__).
        actionRes = self(obj.appy())
        parent = obj.getParentNode()
        parentAq = getattr(parent, 'aq_base', parent)
        if not hasattr(parentAq, obj.id):
            # The action has led to obj's deletion.
            obj.reindex()
        # Unwrap action results.
        successfull, msg = actionRes
        if not msg:
            # Use the default i18n messages.
            suffix = successfull and 'done' or 'ko'
            msg = obj.translate('action_%s' % suffix)
        if (self.result == 'computation') or not successfull:
            obj.say(msg)
            return obj.goto(obj.getUrl(rq['HTTP_REFERER']))
        elif self.result == 'file':
            # msg does not contain a message, but a file instance.
            response = rq.RESPONSE
            response.setHeader('Content-Type', sutils.getMimeType(msg.name))
            response.setHeader('Content-Disposition', 'inline;filename="%s"' %\
                               os.path.basename(msg.name))
            response.write(msg.read())
            msg.close()
        elif self.result == 'redirect':
            # msg does not contain a message, but the URL where to redirect
            # the user.
            return obj.goto(msg)
Exemple #23
0
class Date(Field):

    pxView = pxCell = Px('''<x>:value</x>''')
    pxEdit = Px('''
     <x var="years=field.getSelectableYears()">
      <!-- Day -->
      <select var="days=range(1,32)"
              name=":'%s_day' % name" id=":'%s_day' % name">
       <option value="">-</option>
       <option for="day in days"
               var2="zDay=str(day).zfill(2)" value=":zDay"
               selected="field.isSelected(zobj, 'day', day, \
                                          rawValue)">:zDay</option>
      </select>

      <!-- Month -->
      <select var="months=range(1,13)"
              name=":'%s_month' % name" id=":'%s_month' % name">
       <option value="">-</option>
       <option for="month in months"
               var2="zMonth=str(month).zfill(2)" value=":zMonth"
               selected="field.isSelected(zobj, 'month', month, \
                                          rawValue)">:zMonth</option>
      </select>

      <!-- Year -->
      <select name=":'%s_year' % name" id=":'%s_year' % name">
       <option value="">-</option>
       <option for="year in years" value=":year"
               selected="field.isSelected(zobj, name, 'year', year, \
                                          rawValue)">:year</option>
      </select>

      <!-- The icon for displaying the calendar popup -->
      <x if="field.calendar">
       <input type="hidden" id=":name" name=":name"/>
       <img id=":'%s_img' % name" src=":url('calendar.gif')"/>
       <script type="text/javascript">:field.getJsInit(name, years)</script>
      </x>

      <!-- Hour and minutes -->
      <x if="field.format == 0">
       <select var="hours=range(0,24)" name=":'%s_hour' % name"
               id=":'%s_hour' % name">
        <option value="">-</option>
        <option for="hour in hours"
                var2="zHour=str(hour).zfill(2)" value=":zHour"
                selected=":field.isSelected(zobj, 'hour', hour, \
                                            rawValue)">:zHour</option>
       </select> :
       <select var="minutes=range(0,60,5)" name=":'%s_minute' % name"
               id=":'%s_minute' % name">
        <option value="">-</option>
        <option for="minute in minutes"
                var2="zMinute=str(minute).zfill(2)" value=":zMinute"
                selected=":field.isSelected(zobj, 'minute', minute,\
                                            rawValue)">:zMinute</option>
       </select>
      </x>
     </x>''')

    pxSearch = Px('''
     <x var="years=range(field.startYear, field.endYear+1)">
      <label>:_(field.labelId)</label>
      <table>
       <!-- From -->
       <tr var="fromName='%s_from' % name;
                dayFromName='%s_from_day' % name;
                monthFromName='%s_from_month' % name;
                yearFromName='%s*date' % widgetName">
        <td width="10px">&nbsp;</td>
        <td><label>:_('search_from')"></label></td>
        <td>
         <select id=":dayFromName" name=":dayFromName">
          <option value="">--</option>
          <option for="value in [str(v).zfill(2) for v in range(1, 32)]"
                  value=":value">:value</option>
         </select> /
         <select id=":monthFromName" name=":monthFromName">
          <option value="">--</option>
          <option for="value in [str(v).zfill(2) for v in range(1, 13)]"
                  value=":value">:value</option>
         </select> /
         <select id=":yearFromName" name=":yearFromName">
          <option value="">--</option>
          <option for="value in range(field.startYear, field.endYear+1)"
                  value=":value">:value</option>
         </select>
         <!-- The icon for displaying the calendar popup -->
         <x if="field.calendar">
          <input type="hidden" id=":fromName" name=":fromName"/>
          <img id=":'%s_img' % fromName" src=":url('calendar.gif')"/>
          <script type="text/javascript">:field.getJsInit(fromName, years)
          </script>
         </x>
        </td>
       </tr>

       <!-- To -->
       <tr var="toName='%s_to' % name;
                dayToName='%s_to_day' % name;
                monthToName='%s_to_month' % name;
                yearToName='%s_to_year' % name">
        <td></td>
        <td><label>_('search_to')"></label>&nbsp;&nbsp;&nbsp;&nbsp;</td>
        <td height="20px">
         <select id=":dayToName" name=":dayToName">
          <option value="">--</option>
          <option for="value in [str(v).zfill(2) for v in range(1, 32)]"
                  value=":value">:value</option>
         </select> /
         <select id=":monthToName" name=":monthToName">
          <option value="">--</option>
          <option for="value in [str(v).zfill(2) for v in range(1, 13)]"
                  value=":value">:value</option>
         </select> /
         <select id=":yearToName" name=":yearToName">
          <option value="">--</option>
          <option for="value in range(field.startYear, field.endYear+1)"
                  value=":value">:value</option>
         </select>
         <!-- The icon for displaying the calendar popup -->
         <x if="widget.calendar">
          <input type="hidden" id=":toName" name=":toName"/>
          <img id=":'%s_img' % toName" src=":url('calendar.gif')"/>
          <script type="text/javascript">:field.getJsInit(toName, years)">
          </script>
         </x>
        </td>
       </tr>
      </table>
     </x>''')

    # Required CSS and Javascript files for this type.
    cssFiles = {'edit': ('jscalendar/calendar-blue.css', )}
    jsFiles = {
        'edit': ('jscalendar/calendar.js', 'jscalendar/lang/calendar-en.js',
                 'jscalendar/calendar-setup.js')
    }
    # Possible values for "format"
    WITH_HOUR = 0
    WITHOUT_HOUR = 1
    dateParts = ('year', 'month', 'day')
    hourParts = ('hour', 'minute')

    def __init__(self,
                 validator=None,
                 multiplicity=(0, 1),
                 default=None,
                 format=WITH_HOUR,
                 calendar=True,
                 startYear=time.localtime()[0] - 10,
                 endYear=time.localtime()[0] + 10,
                 reverseYears=False,
                 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=None,
                 scolspan=1,
                 swidth=None,
                 sheight=None):
        self.format = format
        self.calendar = calendar
        self.startYear = startYear
        self.endYear = endYear
        # If reverseYears is True, in the selection box, available years, from
        # self.startYear to self.endYear will be listed in reverse order.
        self.reverseYears = reverseYears
        Field.__init__(self, validator, multiplicity, default, show, page,
                       group, layouts, move, indexed, searchable,
                       specificReadPermission, specificWritePermission, width,
                       height, None, colspan, master, masterValue, focus,
                       historized, True, mapping, label, sdefault, scolspan,
                       swidth, sheight)

    def getCss(self, layoutType, res):
        # CSS files are only required if the calendar must be shown.
        if self.calendar: Field.getCss(self, layoutType, res)

    def getJs(self, layoutType, res):
        # Javascript files are only required if the calendar must be shown.
        if self.calendar: Field.getJs(self, layoutType, res)

    def getSelectableYears(self):
        '''Gets the list of years one may select for this field.'''
        res = range(self.startYear, self.endYear + 1)
        if self.reverseYears: res.reverse()
        return res

    def validateValue(self, obj, value):
        DateTime = obj.getProductConfig().DateTime
        try:
            value = DateTime(value)
        except DateTime.DateError, ValueError:
            return obj.translate('bad_date')
Exemple #24
0
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)&gt;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 &gt; 0" class="clickable" src=":url('arrowUp')"
             title=":_('move_up')"
             onclick=":ajaxBaseCall.replace('**v**', 'up')"/>
        <img if="objectIndex &lt; (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&amp;className=%s&amp;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)&lt;=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&amp;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/>&nbsp;&nbsp;
     <!-- 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())
Exemple #25
0
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
Exemple #26
0
class Table:
    '''Represents a table where to dispose graphical elements'''
    simpleParams = ('style', 'css_class', 'cellpadding', 'cellspacing', 'width',
                    'align')
    derivedRepls = {'view': 'hrvd', 'search': 'hrvd', 'cell': 'ldc'}

    # Render this Table instance, known in the context as "layout". If the
    # layouted object is a page, the "layout target" (where to look for sub-PXs)
    # will be the object whose page is shown; if the layouted object is a field,
    # the layout target will be this field.

    # Warning: when the layout type is "cell", the cell width and alignment must
    # not be defined by the cell layout, but by the outer column layout.
    pxRender = Px('''
     <table var="layoutCss=layout.css_class;
                 inTd=(layoutType == 'cell') and bool(column)|False"
       cellpadding=":layout.cellpadding"
       cellspacing=":layout.cellspacing"
       width=":not inTd and layout.width or ''"
       align=":not inTd and ztool.flipLanguageDirection(layout.align,dir) or ''"
       class=":tagCss and ('%s %s' % (tagCss, layoutCss)).strip() or layoutCss"
       style=":layout.style" id=":tagId" name=":tagName">
      <!-- The table header row -->
      <tr if="layout.headerRow" valign=":layout.headerRow.valign">
       <th for="cell in layout.headerRow.cells" width=":cell.width"
           align=":ztool.flipLanguageDirection(cell.align, dir)">
       </th>
      </tr>
      <!-- The table content -->
      <tr for="row in layout.rows" valign=":row.valign">
       <td for="cell in row.cells" colspan=":cell.colspan"
           align=":ztool.flipLanguageDirection(cell.align, dir)"
           class=":not loop.cell.last and 'cellGap' or ''">
        <x for="c in cell.content">
         <x>::cell.renderContent(c, layoutType, layoutTarget)</x>
         <img if="not loop.c.last" src=":url('space.gif')"/>
        </x>
       </td>
      </tr>
     </table>''')

    def __init__(self, layoutString=None, style=None, css_class='',
                 cellpadding=0, cellspacing=0, width='100%', align='left',
                 other=None, derivedType=None):
        if other:
            # We need to create a Table instance from another Table instance,
            # given in p_other. In this case, we ignore previous params.
            if derivedType != None:
                # We will not simply mimic p_other. If p_derivedType is:
                # - "view", p_other is an "edit" layout, and we must create the
                #           corresponding "view" layout;
                # - "cell" or "search", p_derivedFrom is a "view" layout.
                self.layoutString = Table.deriveLayout(other.layoutString,
                                                       derivedType)
            else:
                self.layoutString = other.layoutString
            source = 'other.'
        else:
            source = ''
            self.layoutString = layoutString
        # Initialise simple params, either from the true params, either from
        # the p_other Table instance.
        for param in Table.simpleParams:
            exec('self.%s = %s%s' % (param, source, param))
        # The following attribute will store a special Row instance used for
        # defining column properties.
        self.headerRow = None
        # The content rows will be stored hereafter.
        self.rows = []
        self.decodeRows(self.layoutString)

    @staticmethod
    def deriveLayout(layout, derivedType):
        '''Returns a layout derived from p_layout'''
        res = layout
        for letter in Table.derivedRepls[derivedType]:
            res = res.replace(letter, '')
        # Strip the derived layout
        res = res.lstrip(rowDelms); res = res.lstrip(cellDelms)
        if derivedType == 'cell':
            res = res.rstrip(rowDelms); res = res.rstrip(cellDelms)
        return res

    def addCssClasses(self, css_class):
        '''Adds a single or a group of p_css_class.'''
        if not self.css_class: self.css_class = css_class
        else:
            self.css_class += ' ' + css_class
            # Ensures that every class appears once
            self.css_class = ' '.join(set(self.css_class.split()))

    def isHeaderRow(self, rowContent):
        '''Determines if p_rowContent specified the table header row or a
           content row.'''
        # Find the first char that is a number or a letter
        for char in rowContent:
            if char not in cellDelimiters:
                if char.isdigit(): return True
                else:              return False
        return True

    def decodeRows(self, layoutString):
        '''Decodes the given p_layoutString and produces a list of Row
           instances.'''
        # Split the p_layoutString with the row delimiters
        rowContent = ''
        for char in layoutString:
            if char in rowDelimiters:
                valign = rowDelimiters[char]
                if self.isHeaderRow(rowContent):
                    if not self.headerRow:
                        self.headerRow = Row(rowContent, valign, isHeader=True)
                else:
                    self.rows.append(Row(rowContent, valign))
                rowContent = ''
            else:
                rowContent += char
        # Manage the last row if any
        if rowContent:
            self.rows.append(Row(rowContent, 'middle'))

    def removeElement(self, elem):
        '''Removes given p_elem from myself'''
        macroToRemove = pxDict[elem]
        for row in self.rows:
            for cell in row.cells:
                if macroToRemove in cell.content:
                    cell.content.remove(macroToRemove)
        if elem in self.layoutString:
            self.layoutString = self.layoutString.replace(elem, '')

    def __repr__(self): return '<Table %s>' % self.layoutString
Exemple #27
0
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] &gt; 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
Exemple #28
0
class List(Field):
    '''A list, stored as a list of Object instances ~[Object]~. Every object in
       the list has attributes named according to the sub-fields defined in this
       List.'''
    Totals = Totals
    # List is an "outer" field, made of inner fields
    outer = True
    gdLayouts = {'view': 'fl', 'edit': Table('dv=f;', width=None)}

    # A 1st invisible cell containing a virtual field allowing to detect this
    # row at server level.
    pxFirstCell = Px('''<td style="display: none">
     <table var="rid='%s*-row-*%s' % (field.name, rowId)"
            id=":'%s_%s' % (obj.id, rid)">
      <tr><td><input type="hidden" id=":rid" name=":rid"/></td></tr>
     </table></td>''')

    # PX for rendering a single row
    pxRow = Px('''
     <tr valign="top" style=":(rowId == -1) and 'display: none' or ''"
         class=":loop.row.odd and 'odd' or 'even'">
      <x>:field.pxFirstCell</x>
      <td if="showTotals"></td>
      <td for="info in subFields" if="info[1]" align="center"
          var2="dummy=field.updateTotals(totals, obj, info, row, loop.row.last);
                field=info[1];
                fieldName='%s*%d' % (field.name, rowId)">:field.pxRender</td>
      <!-- Icons -->
      <td if="layoutType=='edit'" align=":dright">
       <img class="clickable" src=":url('delete')" title=":_('object_delete')"
            onclick=":field.jsDeleteConfirm(q, tableId)"/>
       <img class="clickable" src=":url('arrowUp')" title=":_('move_up')"
            onclick=":'moveRow(%s,%s,this)' % (q('up'), q(tableId))"/>
       <img class="clickable" src=":url('arrowDown')" title=":_('move_down')"
            onclick=":'moveRow(%s,%s,this)' % (q('down'), q(tableId))"/>
       <img var="do='insertRow(%s,this)' % q(tableId)" class="clickable"
            src=":url('addBelow')" title=":_('object_add_below')" tabindex="0"
            onclick=":do" onkeydown=":'(event.keyCode == 13)? %s: null' % do"/>
      </td></tr>''')

    # Display totals (on view only) when defined
    pxTotals = Px('''
     <tr for="totalRow in field.totalRows" var2="total=totals[totalRow.id]">
      <th>:_(totalRow.label)</th>
      <th for="info in subFields">:field.getTotal(total, info)</th>
     </tr>''')

    # PX for rendering the list (shared between pxView and pxEdit)
    pxTable = Px('''
     <table var="isEdit=layoutType == 'edit';
                 tableId='list_%s' % name" if="isEdit or value"
            id=":tableId" class="grid" width=":field.width"
            var2="subFields=field.getSubFields(zobj, layoutType);
                  totals=field.createTotals(isEdit);
                  showTotals=not isEdit and totals">
      <!-- Header -->
      <tr valign="middle">
       <th if="showTotals"></th>
       <th for="info in subFields" if="info[1]"
           width=":field.widths[loop.info.nb]">::_(info[1].labelId)</th>
       <!-- Icon for adding a new row -->
       <th if="isEdit">
        <img class="clickable" src=":url('plus')" title=":_('object_add')"
             onclick=":'insertRow(%s)' % q(tableId)"/>
       </th>
      </tr>

      <!-- Template row (edit only) -->
      <x var="rowId=-1" if="isEdit">:field.pxRow</x>

      <!-- Rows of data -->
      <x var="rows=field.getInputValue(inRequest, requestValue, value)"
         for="row in rows" var2="rowId=loop.row.nb">:field.pxRow</x>

      <!-- Totals -->
      <x if="showTotals">:field.pxTotals</x>
     </table>''')

    pxView = pxCell = Px('''<x>:field.pxTable</x>''')
    pxEdit = Px('''<x>
     <!-- This input makes Appy aware that this field is in the request -->
     <input type="hidden" name=":name" value=""/><x>:field.pxTable</x>
    </x>''')

    pxSearch = ''

    def __init__(self,
                 fields,
                 validator=None,
                 multiplicity=(0, 1),
                 default=None,
                 show=True,
                 page='main',
                 group=None,
                 layouts=None,
                 move=0,
                 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('f;r;v', width=None),
                 widths=None,
                 view=None,
                 cell=None,
                 xml=None,
                 deleteConfirm=False,
                 totalRows=None):
        Field.__init__(self, validator, multiplicity, default, show, page,
                       group, layouts, move, False, True, None, False,
                       specificReadPermission, specificWritePermission, width,
                       height, None, colspan, master, masterValue, focus,
                       historized, mapping, label, None, None, None, None,
                       True, False, view, cell, xml)
        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.
        self.computeWidths(widths)
        # When deleting a row, must we display a popup for confirming it ?
        self.deleteConfirm = deleteConfirm
        # If you want to specify additional rows representing totals, give in
        # "totalRows" a list of Totals instances (see above).
        self.totalRows = totalRows or []

    def computeWidths(self, widths):
        '''Set given p_widths or compute default ones if not given'''
        if not widths:
            self.widths = [''] * len(self.fields)
        else:
            self.widths = widths

    def getField(self, name):
        '''Gets the field definition whose name is p_name'''
        for n, field in self.fields:
            if n == name: return field

    def getSubFields(self, obj, layoutType):
        '''Returns the sub-fields (name, Field) that are showable among
           field.fields on the given p_layoutType. Fields that cannot appear in
           the result are nevertheless present as a tuple (name, None). This
           way, it keeps a nice layouting of the table.'''
        res = []
        for n, field in self.fields:
            elem = (n, None)
            if field.isShowable(obj, layoutType):
                elem = (n, field)
            res.append(elem)
        return res

    def getRequestValue(self, obj, requestName=None):
        '''Concatenates the list from distinct form elements in the request'''
        request = obj.REQUEST
        name = requestName or self.name  # A List may be into another List (?)
        prefix = name + '*-row-*'  # Allows to detect a row of data for this List
        res = {}
        isDict = True  # We manage both List and Dict
        for key in request.keys():
            if not key.startswith(prefix): continue
            # I have found a row: get its index
            row = Object()
            rowId = key.split('*')[-1]
            if rowId == '-1': continue  # Ignore the template row
            for subName, subField in self.fields:
                keyName = '%s*%s*%s' % (name, subName, rowId)
                if keyName + subField.getRequestSuffix() in request:
                    v = subField.getRequestValue(obj, requestName=keyName)
                    setattr(row, subName, v)
            if rowId.isdigit():
                rowId = int(rowId)
                isDict = False
            res[rowId] = row
        # Produce a sorted list (List only)
        if not isDict:
            keys = res.keys()
            keys.sort()
            res = [res[key] for key in keys]
        # I store in the request this computed value. This way, when individual
        # subFields will need to get their value, they will take it from here,
        # instead of taking it from the specific request key. Indeed, specific
        # request keys contain row indexes that may be wrong after row deletions
        # by the user.
        if res: request.set(name, res)
        return res

    def setRequestValue(self, obj):
        '''Sets in the request, the field value on p_obj in its
           "request-carriable" form.'''
        value = self.getValue(obj)
        if value != None:
            req = obj.REQUEST
            name = self.name
            i = 0
            for row in value:
                req['%s*-row-*%d' % (name, i)] = ''
                for n, v in row.__dict__.items():
                    key = '%s*%s*%d' % (name, n, i)
                    req[key] = v
                i += 1

    def getStorableRowValue(self, obj, requestValue):
        '''Gets a ready-to-store Object instance representing a single row,
           from p_requestValue.'''
        res = Object()
        for name, field in self.fields:
            if not hasattr(requestValue, name): continue
            subValue = getattr(requestValue, name)
            try:
                setattr(res, name, field.getStorableValue(obj, subValue))
            except ValueError:
                # The value for this field for this specific row is incorrect.
                # It can happen in the process of validating the whole List
                # field (a call to m_getStorableValue occurs at this time). We
                # don't care about it, because later on we will have sub-field
                # specific validation that will also detect the error and will
                # prevent storing the wrong value in the database.
                setattr(res, name, subValue)
        return res

    def getStorableValue(self, obj, value):
        '''Gets p_value in a form that can be stored in the database'''
        return [self.getStorableRowValue(obj, v) for v in value]

    def getCopyValue(self, obj):
        '''Return a (deep) copy of field value on p_obj'''
        r = getattr(obj.aq_base, self.name, None)
        if r: return copy.deepcopy(r)

    def getCss(self, layoutType, res, config):
        '''Gets the CSS required by sub-fields if any'''
        for name, field in self.fields:
            field.getCss(layoutType, res, config)

    def getJs(self, layoutType, res, config):
        '''Gets the JS required by sub-fields if any'''
        for name, field in self.fields:
            field.getJs(layoutType, res, config)

    def jsDeleteConfirm(self, q, tableId):
        '''Gets the JS code to call when the user wants to delete a row'''
        confirm = self.deleteConfirm and 'true' or 'false'
        return 'deleteRow(%s,this,%s)' % (q(tableId), confirm)

    def subValidate(self, obj, value, errors):
        '''Validates inner fields'''
        i = -1
        for row in value:
            i += 1
            for name, subField in self.fields:
                message = subField.validate(obj, getattr(row, name, None))
                if message:
                    setattr(errors, '%s*%d' % (subField.name, i), message)

    def createTotals(self, isEdit):
        '''When rendering the List field, if total rows are defined, create a
           Total instance for every sub-field for which a total must be
           computed.'''
        if isEdit or not self.totalRows: return
        res = {}  # Keyed by Totals.name
        for totals in self.totalRows:
            subTotals = Object()
            for name in totals.subFields:
                totalObj = Total(name, self.getField(name), totals.initValue)
                setattr(subTotals, name, totalObj)
            res[totals.id] = subTotals
        return res

    def updateTotals(self, totals, obj, info, row, last):
        '''Every time a cell is encountered while rendering the List, this
           method is called to update totals when needed'''
        if not totals: return
        # Browse Totals instances
        for totalRow in self.totalRows:
            # Are there totals to update ?
            total = getattr(totals[totalRow.id], info[0], None)
            if total:
                totalRow.onCell(obj, row, total, last)

    def getTotal(self, totals, info):
        '''Get the total for the field p_info if available'''
        total = getattr(totals, info[0], None)
        if total: return total.value
        return ''
Exemple #29
0
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': 'fl!'}
    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 ''"
          src=":url(fmt + iconSuffix)" class="clickable"
          title=":field.getIconTitle(obj, fmt, frozen)"
          onclick=":'generatePod(%s,%s,%s,%s,%s)' % (q(uid), q(name), \
                    q(info.template), q(fmt), q(ztool.getQueryInfo()))"/>''')

    pxView = pxCell = Px('''
     <x var="uid=obj.uid"
        for="info in field.getVisibleTemplates(obj)">
      <x for="fmt in info.formats"
         var2="freezeAllowed=(fmt in info.freezeFormats) and \
                             (field.show != 'result');
               frozen=field.isFrozen(obj, info.template, fmt)">
       <!-- A clickable icon if no freeze action is allowed -->
       <x if="not freezeAllowed">:field.pxIcon</x>
       <!-- A clickable icon and a dropdown menu else. -->
       <span if="freezeAllowed" 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="75px">
         <!-- Unfreeze -->
         <tr if="frozen" valign="top">
          <td>
           <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 align="center"><img src=":url('unfreeze')"/></td>
         </tr>
         <!-- (Re-)freeze -->
         <tr valign="top">
          <td>
           <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 align="center"><img src=":url('freeze')"/></td>
         </tr>
         <!-- (Re-)upload -->
         <tr valign="top">
          <td>
           <a onclick=":'uploadPod(%s,%s,%s,%s)' % (q(uid), q(name), \
                        q(info.template), q(fmt))"
              class="smaller">:_('uploadField')</a>
          </td>
          <td align="center"><img src=":url('upload')"/></td>
         </tr>
        </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) &gt; 1"
            class=":not loop.info.last and 'pod smaller' or \
                    'smaller'">:field.getTemplateName(obj, info.template)</span>
     </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, context=None, stylesMapping={},
                 formats=None):
        # Param "template" stores the path to the pod template(s).
        if not template: raise Exception(Pod.NO_TEMPLATE)
        if isinstance(template, basestring):
            self.template = [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
        # 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
            if self.template[0].endswith('.ods'):
                self.formats = ('xls', 'ods')
            else:
                self.formats = ('pdf', 'doc', 'odt')
        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 getAllFormats(self, template):
        '''Gets all the outputy formats that are available for a given
           p_template.'''
        ext = os.path.splitext(template)[1]
        return self.allFormats[ext]

    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 getDownloadName(self, obj, template, format, queryRelated):
        '''Gets the name of the pod result as will be seen by the user that will
           download it.'''
        fileName = self.getTemplateName(obj, template)
        if not queryRelated:
            # This is a POD for a single object: personalize the file name with
            # the object title.
            fileName = '%s-%s' % (obj.title, fileName)
        return obj.tool.normalize(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 any format.
            for template in self.template:
                res.append(Object(template=template,
                        formats=self.getAllFormats(template),
                        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
                formats = isManager and self.getAllFormats(template) or formats
                if isinstance(formats, basestring): formats = (formats,)
                res.append(Object(template=template, formats=formats,
                           freezeFormats=self.getFreezeFormats(obj, template)))
        return res

    def getValue(self, obj, template=None, format=None, result=None,
                 queryData=None, customParams=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;
           * p_customParams may be specified. Every custom param must have form
             "name:value". Custom params 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 = os.path.join(diskFolder, template)
        if not os.path.isfile(templatePath):
            raise Exception(self.TEMPLATE_NOT_FOUND % templatePath)
        # 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}
        # 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]
        # 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.
        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': 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)
Exemple #30
0
class Computed(Field):
    WRONG_METHOD = 'Wrong value "%s". Param "method" must contain a method ' \
                   'or a PX.'
    pxView = pxCell = pxEdit = Px('''<x if="field.plainText">:value</x>
      <x if="not field.plainText">::value</x>''')

    pxSearch = Px('''
     <input type="text" name=":'%s*string' % widgetName"
            maxlength=":field.maxChars" size=":field.width"
            value=":field.sdefault"/>''')

    def __init__(self,
                 validator=None,
                 multiplicity=(0, 1),
                 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,
                 method=None,
                 formatMethod=None,
                 plainText=False,
                 master=None,
                 masterValue=None,
                 focus=False,
                 historized=False,
                 mapping=None,
                 label=None,
                 sdefault='',
                 scolspan=1,
                 swidth=None,
                 sheight=None,
                 context=None):
        # The Python method used for computing the field value, or a PX.
        self.method = method
        # A specific method for producing the formatted value of this field.
        # This way, if, for example, the value is a DateTime instance which is
        # indexed, you can specify in m_formatMethod the way to format it in
        # the user interface while m_method computes the value stored in the
        # catalog.
        self.formatMethod = formatMethod
        if isinstance(self.method, basestring):
            # A legacy macro identifier. Raise an exception
            raise Exception(self.WRONG_METHOD % self.method)
        # Does field computation produce plain text or XHTML?
        self.plainText = plainText
        if isinstance(method, Px):
            # When field computation is done with a PX, the result is XHTML.
            self.plainText = False
        # If method is a PX, its context can be given in p_context.
        self.context = context
        Field.__init__(self, None, 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, False)
        self.validable = False

    def getValue(self, obj):
        '''Computes the value instead of getting it in the database.'''
        if not self.method: return
        if isinstance(self.method, Px):
            obj = obj.appy()
            tool = obj.tool
            req = obj.request
            # Get the context of the currently executed PX if present
            try:
                ctx = req.pxContext
            except AttributeError:
                # Create some standard context
                ctx = {
                    'obj': obj,
                    'zobj': obj.o,
                    'field': self,
                    'req': req,
                    'tool': tool,
                    'ztool': tool.o,
                    '_': tool.translate,
                    'url': tool.o.getIncludeUrl
                }
            if self.context: ctx.update(self.context)
            return self.method(ctx)
        else:
            # self.method is a method that will return the field value
            return self.callMethod(obj, self.method, cache=False)

    def getFormattedValue(self, obj, value, showChanges=False, language=None):
        if self.formatMethod:
            res = self.formatMethod(obj, value)
        else:
            res = value
        if not isinstance(res, basestring): res = str(res)
        return res