WebSection.py 17.8 KB
Newer Older
1
# -*- coding: utf-8 -*-
Jean-Paul Smets committed
2 3
##############################################################################
#
4 5
# Copyright (c) 2002-2008 Nexedi SA and Contributors. All Rights Reserved.
#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
Jean-Paul Smets committed
6 7 8 9 10 11 12 13
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
14 15 16 17 18 19 20 21 22 23 24 25 26 27
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
Jean-Paul Smets committed
28 29 30
##############################################################################

from AccessControl import ClassSecurityInfo
31
from Products.ERP5Type import Permissions, PropertySheet
Jean-Paul Smets committed
32
from Products.ERP5.Document.Domain import Domain
33
from Products.ERP5.mixin.document_extensible_traversable import DocumentExtensibleTraversableMixin
34
from Acquisition import aq_base, aq_inner
35
from Products.ERP5Type.UnrestrictedMethod import unrestricted_apply
36
from AccessControl import Unauthorized
37
from OFS.Traversable import NotFound
38 39
from Persistence import Persistent
from ZPublisher import BeforeTraverse
40
from Products.CMFCore.utils import _checkConditionalGET, _setCacheHeaders, _ViewEmulator
Jean-Paul Smets committed
41

42
from Products.ERP5Type.Cache import getReadOnlyTransactionCache
43

44 45
# Global keys used for URL generation
WEBSECTION_KEY = 'web_section_value'
46
MARKER = []
47
WEB_SECTION_PORTAL_TYPE_TUPLE = ('Web Section', 'Web Site')
48

49
class WebSectionTraversalHook(Persistent):
50 51
  """Traversal hook to change the skin selection for this websection.
  """
52 53 54 55
  def __call__(self, container, request):
    if not request.get('ignore_layout', None):
      # If a skin selection is defined in this web section, change the skin now.
      skin_selection_name = container.getSkinSelectionName()
56 57 58
      if skin_selection_name and \
         ((request.get('portal_skin', None) is None) or \
          container.getPortalType() not in WEB_SECTION_PORTAL_TYPE_TUPLE):
59 60
        container.getPortalObject().changeSkin(skin_selection_name)

61
class WebSection(Domain, DocumentExtensibleTraversableMixin):
Jean-Paul Smets committed
62 63
    """
      A Web Section is a Domain with an extended API intended to
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
      support the creation of Web front ends to
      server ERP5 contents through a pretty and configurable
      user interface.

      WebSection uses the following scripts for customisation:

      - WebSection_getBreadcrumbItemList

      - WebSection_getDocumentValueList

      - WebSection_getPermanentURL

      - WebSection_getDocumentValue

      - WebSection_getDefaultDocumentValue

Kazuhiko Shiozaki committed
80
      - WebSection_getWebSectionValue
81 82 83 84 85 86 87 88 89
      - WebSection_getWebSiteValue

      It defines the following REQUEST global variables:

      - current_web_section

      - current_web_document

      - is_web_section_default_document
Jean-Paul Smets committed
90 91 92 93 94 95 96 97 98 99 100 101 102 103
    """
    # CMF Type Definition
    meta_type = 'ERP5 Web Section'
    portal_type = 'Web Section'

    # Declarative security
    security = ClassSecurityInfo()
    security.declareObjectProtected(Permissions.AccessContentsInformation)

    # Default Properties
    property_sheets = ( PropertySheet.Base
                      , PropertySheet.XMLObject
                      , PropertySheet.CategoryCore
                      , PropertySheet.DublinCore
104
                      , PropertySheet.WebSection
Jean-Paul Smets committed
105
                      , PropertySheet.SortIndex
106
                      , PropertySheet.Predicate
Jean-Paul Smets committed
107 108
                      )

109
    web_section_key = WEBSECTION_KEY
Jean-Paul Smets committed
110

111 112
    security.declareProtected(Permissions.View, '__bobo_traverse__')
    def __bobo_traverse__(self, request, name):
113
      """
114 115
        If no subobject is found through Folder API
        then try to lookup the object by invoking getDocumentValue
116 117
      """
      # Register current web site physical path for later URL generation
118
      if request.get(self.web_section_key, MARKER) is MARKER:
119
        request[self.web_section_key] = self.getPhysicalPath()
120 121 122 123
        # Normalize web parameter in the request
        # Fix common user mistake and transform '1' string to boolean
        for web_param in ['ignore_layout', 'editable_mode']:
          if hasattr(request, web_param):
124 125 126 127
            param = getattr(request, web_param, None)
            if isinstance(param, (list, tuple)):
              param = param[0]
            if param in ('1', 1, True):
128 129 130
              request.set(web_param, True)
            else:
              request.set(web_param, False)
131

132
      document = None
133
      try:
134
        document = DocumentExtensibleTraversableMixin.__bobo_traverse__(self, request, name)
135 136
      except NotFound:
        not_found_page_ref = self.getLayoutProperty('layout_not_found_page_reference')
137
        if not_found_page_ref:
138
          document = DocumentExtensibleTraversableMixin.getDocumentValue(self, name=not_found_page_ref)
139 140
        if document is None:
          # if no document found, fallback on default page template
141
          document = DocumentExtensibleTraversableMixin.__bobo_traverse__(self, request,
142 143
            '404.error.page')
      return document
144

145 146
    security.declarePrivate( 'manage_beforeDelete' )
    def manage_beforeDelete(self, item, container):
147
      if item is self:
148 149 150 151 152 153
        handle = self.meta_type + '/' + self.getId()
        BeforeTraverse.unregisterBeforeTraverse(item, handle)
      super(WebSection, self).manage_beforeDelete(item, container)

    security.declarePrivate( 'manage_afterAdd' )
    def manage_afterAdd(self, item, container):
154
      if item is self:
155
        handle = self.meta_type + '/' + self.getId()
156
        BeforeTraverse.registerBeforeTraverse(item, self._getTraversalHookClass()(), handle)
157 158
      super(WebSection, self).manage_afterAdd(item, container)

159 160 161 162 163
    security.declarePrivate( 'manage_afterClone' )
    def manage_afterClone(self, item):
      self._cleanupBeforeTraverseHooks()
      super(WebSection, self).manage_afterClone(item)

164 165 166 167 168
    def _getTraversalHookClass(self):
      return WebSectionTraversalHook

    _traversal_hook_class = WebSectionTraversalHook

169 170 171 172 173
    def _cleanupBeforeTraverseHooks(self):
      # unregister all before traversal hooks that do not belong to us.
      my_handle = self.meta_type + '/' + self.getId()
      handle_to_unregister_list = []
      for (priority, handle), hook in self.__before_traverse__.items():
174
        if isinstance(hook, self._getTraversalHookClass()) and handle != my_handle:
175 176 177 178
          handle_to_unregister_list.append(handle)
      for handle in handle_to_unregister_list:
        BeforeTraverse.unregisterBeforeTraverse(self, handle)

179
    security.declareProtected(Permissions.AccessContentsInformation, 'getLayoutProperty')
180
    def getLayoutProperty(self, key, default=None):
181
      """
Jean-Paul Smets committed
182 183
        A simple method to get a property of the current by
        acquiring it from the current section or its parents.
184 185
      """
      section = aq_inner(self)
186
      while section.getPortalType() in ('Web Section', 'Web Site', 'Static Web Section', 'Static Web Site'):
187
        result = section.getProperty(key, MARKER)
188
        if result not in (MARKER, None):
189 190
          return result
        section = section.aq_parent
191
      return default
192

193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225
    security.declareProtected(Permissions.AccessContentsInformation, 'getWebSectionValue')
    def getWebSectionValue(self):
      """
        Returns the current web section (ie. self) though containment acquisition.

        To understand the misteries of acquisition and how the rule
        containment vs. acquisition works, please look at
        XXXX (Zope web site)
      """
      return self

    # Default view display
    security.declareProtected(Permissions.View, '__call__')
    def __call__(self):
      """
        If a Web Section has a default document, we render
        the default document instead of rendering the Web Section
        itself.

        The implementation is based on the presence of specific
        variables in the REQUEST (besides editable_mode and
        ignore_layout).

        current_web_section -- defines the Web Section which is
        used to display the current document.

        current_web_document -- defines the Document (ex. Web Page)
        which is being displayed within current_web_section.

        is_web_section_default_document -- a boolean which is
        set each time we display a default document as a section.

        We use REQUEST parameters so that they are reset for every
226
        Web transaction and can be accessed from widgets.
227
      """
228 229 230
      # Register current web site physical path for later URL generation
      if self.REQUEST.get(self.web_section_key, MARKER) is MARKER:
        self.REQUEST[self.web_section_key] = self.getPhysicalPath()
231 232
      self.REQUEST.set('current_web_section', self)
      if not self.REQUEST.get('editable_mode') and not self.REQUEST.get('ignore_layout'):
233
        document = None
234 235 236 237 238 239 240 241 242
        if self.isDefaultPageDisplayed():
          # The following could be moved to a typed based method for more flexibility
          document = self.getDefaultDocumentValue()
          if document is None:
            # no document found for current user, still such document may exists
            # in some cases user (like Anonymous) can not view document according to portal catalog
            # but we may ask him to login if such a document exists
            isAuthorizationForced = getattr(self, 'isAuthorizationForced', None)
            if isAuthorizationForced is not None and isAuthorizationForced():
243
              if unrestricted_apply(self.getDefaultDocumentValue) is not None:
244 245
                # force user to login as specified in Web Section
                raise Unauthorized
246 247 248
          if document is not None and document.getReference() is not None:
            # we use web_site_module/site_id/section_id/page_reference
            # as the url of the default document.
249
            self.REQUEST.set('current_web_document', document)
250
            self.REQUEST.set('is_web_section_default_document', 1)
251
            document = aq_base(document.asContext(
252
                id=document.getReference(),
253 254
                original_container=document.getParentValue(),
                original_id=document.getId(),
255
                editable_absolute_url=document.absolute_url())).__of__(self)
256 257 258 259 260 261
        else:
          isAuthorizationForced = getattr(self, 'isAuthorizationForced', None)
          if isAuthorizationForced is not None and isAuthorizationForced():
            if self.getPortalObject().portal_membership.isAnonymousUser():
              # force anonymous to login
              raise Unauthorized
262 263 264 265 266
        # Try to use a custom renderer if any
        custom_render_method_id = self.getCustomRenderMethodId()
        if custom_render_method_id is not None:
          if document is None:
            document = self
267
          result = getattr(document, custom_render_method_id)()
268 269 270 271 272 273 274
          view = _ViewEmulator().__of__(self)
          # If we have a conditional get, set status 304 and return
          # no content
          if _checkConditionalGET(view, extra_context={}):
            return ''
          # call caching policy manager.
          _setCacheHeaders(view, {})
275
          return result
276 277
        elif document is not None:
          return document()
278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298
      return Domain.__call__(self)

    # Layout Selection API
    security.declareProtected(Permissions.AccessContentsInformation, 'getApplicableLayout')
    def getApplicableLayout(self):
      """
        The applicable layout on a section is the container layout.
      """
      return self.getContainerLayout()

    # WebSection API
    security.declareProtected(Permissions.View, 'getDefaultDocumentValue')
    def getDefaultDocumentValue(self):
      """
        Return the default document of the current
        section.

        This method must be implemented through a
        portal type dependent script:
          WebSection_getDefaultDocumentValue
      """
299
      cache = getReadOnlyTransactionCache()
300 301 302 303 304 305 306 307 308 309 310 311 312
      if cache is not None:
        key = ('getDefaultDocumentValue', self)
        try:
          return cache[key]
        except KeyError:
          pass

      result = self._getTypeBasedMethod('getDefaultDocumentValue',
                     fallback_script_id='WebSection_getDefaultDocumentValue')()

      if cache is not None:
        cache[key] = result

313 314 315
      if result is not None:
        result = result.__of__(self)

316 317 318 319 320 321 322 323 324 325 326 327 328 329 330
      return result

    security.declareProtected(Permissions.View, 'getDocumentValueList')
    def getDocumentValueList(self, **kw):
      """
        Return the list of documents which belong to the
        current section. The API is designed to
        support additional parameters so that it is possible
        to group documents by reference, version, language, etc.
        or to implement filtering of documents.

        This method must be implemented through a
        portal type dependent script:
          WebSection_getDocumentValueList
      """
331
      cache = getReadOnlyTransactionCache()
332 333 334 335 336 337 338 339 340 341 342 343 344
      if cache is not None:
        key = ('getDocumentValueList', self) + tuple(kw.items())
        try:
          return cache[key]
        except KeyError:
          pass

      result = self._getTypeBasedMethod('getDocumentValueList',
                     fallback_script_id='WebSection_getDocumentValueList')(**kw)

      if cache is not None:
        cache[key] = result

345
      if result is not None and not kw.get('src__', 0):
346 347
        result = [doc.__of__(self) for doc in result]

348 349 350
      return result

    security.declareProtected(Permissions.View, 'getPermanentURL')
Fabien Morin committed
351
    def getPermanentURL(self, document, view=True):
352 353 354 355 356 357 358
      """
        Return a permanent URL of document in the context
        of the current section.

        This method must be implemented through a
        portal type dependent script:
          WebSection_getPermanentURL
359 360 361 362

        XXX The following argument is obsoleted because we no longer need /view.
        If view is True, the url returned point to html content and can be
        opened in a browser (ie. + '/view' for ooo documents)
363
      """
364
      cache = getReadOnlyTransactionCache()
365
      if cache is not None:
366
        key = ('getPermanentURL', self, document.getPath())
367 368 369 370 371
        try:
          return cache[key]
        except KeyError:
          pass

372
      document = document.getObject().__of__(self)
373
      result = document._getTypeBasedMethod('getPermanentURL',
374
                     fallback_script_id='WebSection_getPermanentURL')(document)
375 376 377 378 379 380 381

      if cache is not None:
        cache[key] = result

      return result

    security.declareProtected(Permissions.View, 'getBreadcrumbItemList')
382
    def getBreadcrumbItemList(self, document=None):
383 384 385 386 387 388 389 390
      """
        Return a section dependent breadcrumb in the form
        of a list of (title, document) tuples.

        This method must be implemented through a
        portal type dependent script:
          WebSection_getBreadcrumbItemList
      """
391 392
      if document is None:
        document = self
393
      cache = getReadOnlyTransactionCache()
394
      if cache is not None:
395
        key = ('getBreadcrumbItemList', self, document.getPath())
396 397 398 399 400 401 402 403 404 405 406 407
        try:
          return cache[key]
        except KeyError:
          pass

      result = self._getTypeBasedMethod('getBreadcrumbItemList',
                     fallback_script_id='WebSection_getBreadcrumbItemList')(document)

      if cache is not None:
        cache[key] = result

      return result
408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437

    security.declareProtected(Permissions.View, 'getSiteMapTree')
    def getSiteMapTree(self, **kw):
      """
        Return a site map tree section dependent breadcrumb in the
        form of a list of dicts whose structure is provided as a tree
        so that it is easy to implement recursive call with TAL/METAL:

        [
          {
            'url'      : '/erp5/web_site_module/site/section',
            'level'    : 1,
            'translated_title' : 'Section Title',
            'subsection' : [
              {
                'url'      : '/erp5/web_site_module/site/section/reference',
                'level'    : 2,
                'translated_title' : 'Sub Section Title',
                'subsection' : None,
              },
              ...
            ],
          }
          ...
        ]

        This method must be implemented through a
        portal type dependent script:
          WebSection_getSiteMapTree
      """
438
      cache = getReadOnlyTransactionCache()
439 440 441 442 443 444 445 446 447 448 449 450 451 452
      if cache is not None:
        key = ('getSiteMapTree', self) + tuple(kw.items())
        try:
          return cache[key]
        except KeyError:
          pass

      result = self._getTypeBasedMethod('getSiteMapTree',
                     fallback_script_id='WebSection_getSiteMapTree')(**kw)

      if cache is not None:
        cache[key] = result

      return result
453 454

    def _edit(self, **kw):
455 456
      # XXX it is unclear if we should keep this behavior in other potential subclasses.
      # Probably yes.
457
      if self.getPortalType() in WEB_SECTION_PORTAL_TYPE_TUPLE:
458 459 460
        if getattr(self, '__before_traverse__', None) is None:
          # migrate beforeTraverse hook if missing
          handle = self.meta_type + '/' + self.getId()
461
          BeforeTraverse.registerBeforeTraverse(self, self._getTraversalHookClass()(), handle)
462 463 464
        else:
          # cleanup beforeTraverse hooks that may exist after this document was cloned.
          self._cleanupBeforeTraverseHooks()
465
      super(WebSection, self)._edit(**kw)