ConfiguratorTool.py 20.9 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
##############################################################################
#
# Copyright (c) 2006-2010 Nexedi SA and Contributors. All Rights Reserved.
#                    Romain Courteaud <romain@nexedi.com>
#                    Ivan Tyagov <ivan@nexedi.com>
#                    Rafael Monnerat <rafael@nexedi.com>
#
# 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
#
# 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.
#
##############################################################################

from AccessControl import ClassSecurityInfo
from Products.ERP5Type.Globals import DTMLFile
from Products.ERP5Type.Accessor.Constant import PropertyGetter as \
    ConstantGetter
from Products.ERP5Type.Tool.BaseTool import BaseTool
36
from Products.ERP5Type.Cache import CachingMethod
37 38 39 40 41 42 43 44 45 46 47
from Products.ERP5Type import Permissions
from Products.ERP5Configurator import _dtmldir
from Products.Formulator.Errors import FormValidationError
import cookielib
from base64 import encodestring
from urllib import quote
from DateTime import DateTime

# global (RAM) cookie storage
cookiejar = cookielib.CookieJar()
last_loggedin_user_and_password = None
Rafael Monnerat's avatar
Rafael Monnerat committed
48
referer = None
49
installation_status = {'bt5': {'current': 0,
Rafael Monnerat's avatar
Rafael Monnerat committed
50 51
                               'all': 0, },
                       'activity_list': [], }
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88

# cookie name to store user's preferred language name
LANGUAGE_COOKIE_NAME = 'configurator_user_preferred_language'
BUSINESS_CONFIGURATION_COOKIE_NAME = 'business_configuration_key'

def getAvailableLanguageFromHttpAcceptLanguage(http_accept_language,
                                               available_language_list,
                                               default='en'):
  for language_set in http_accept_language.split(','):
    language_tag = language_set.split(';')[0]
    language = language_tag.split('-')[0]
    if language in available_language_list:
      return language
  return default

def _isUserAcknowledged(cookiejar):
  """ Is user authenticated to remote system through a cookie. """
  for cookie in cookiejar:
    if cookie.name == '__ac' and cookie.value != '':
      return 1
  return 0

def _validateFormToRequest(form, REQUEST, **kw):
    """ Validate form to REQUEST. """
    form_kw = {}
    REQUEST.form = kw
    try:
      form.validate_all_to_request(REQUEST)
      validation_status = 0
      validation_errors = None
    except FormValidationError, validation_errors:
      ## not all fields valid
      validation_status = 1
    except Exception, validation_errors:
      ## missing fields
      validation_status = 2
    ## extract form arguments and remove leading prefixes
Rafael Monnerat's avatar
Rafael Monnerat committed
89
    if validation_status == 0:
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
      for field in form.get_fields():
        field_id = field.id
        value = getattr(REQUEST, field_id, None)
        for prefix in ('my_', 'your_',):
          if field_id.startswith(prefix):
            attr_id = field_id[len(prefix):]
            form_kw[attr_id] = value
            for del_key in (field.generate_field_key(validation=1), field_id):
              try:
                REQUEST.other.pop(del_key)
              except KeyError:
                pass
    return validation_status, form_kw, validation_errors


class ConfiguratorTool(BaseTool):
Rafael Monnerat's avatar
Rafael Monnerat committed
106 107
  """This tool provides a Configurator Tool.
  """
108 109 110 111

  id = 'portal_configurator'
  title = 'Configurator Tool'
  meta_type = 'ERP5 Configurator Tool'
Rafael Monnerat's avatar
Rafael Monnerat committed
112
  portal_type = 'Configurator Tool'
113 114 115 116 117 118

  isPortalContent = ConstantGetter('isPortalContent', value=True)

  security = ClassSecurityInfo()

  security.declareProtected(Permissions.ManagePortal, 'manage_overview')
Rafael Monnerat's avatar
Rafael Monnerat committed
119
  manage_overview = DTMLFile('explainConfiguratorTool', _dtmldir)
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146

  def getConfiguratorUserPreferredLanguage(self):
    """ Get configuration language as selected by user """
    REQUEST = getattr(self, 'REQUEST', None)
    configurator_user_preferred_language = None
    if REQUEST is not None:
      # language value will be in cookie or REQUEST itself.
      configurator_user_preferred_language = REQUEST.get(LANGUAGE_COOKIE_NAME,
          None)
      if configurator_user_preferred_language is None:
        # Find a preferred language from HTTP_ACCEPT_LANGUAGE
        available_language_list = [i[1] for i in self\
            .ConfiguratorTool_getConfigurationLanguageList()]
        configurator_user_preferred_language = \
            getAvailableLanguageFromHttpAcceptLanguage(
          REQUEST.get('HTTP_ACCEPT_LANGUAGE', 'en'),
          available_language_list)
    if configurator_user_preferred_language is None:
      configurator_user_preferred_language = 'en'
    return configurator_user_preferred_language

  ######################################################
  ##               Navigation                         ##
  ######################################################
  def login(self, REQUEST):
    """ Login client and show next form. """
    password = REQUEST.get('field_my_ac_key', '')
147
    bc = REQUEST.get('field_your_business_configuration')
148
    if self._isCorrectConfigurationKey(password, bc):
149 150 151 152 153 154 155 156 157 158 159
      # set user preferred configuration language
      user_preferred_language = REQUEST.get(
          'field_my_user_preferred_language', None)
      if user_preferred_language:
        # Set language value to request so that next page after login
        # can get the value. Because cookie value is available from
        # next request.
        REQUEST.set(LANGUAGE_COOKIE_NAME, user_preferred_language)
        REQUEST.RESPONSE.setCookie(LANGUAGE_COOKIE_NAME,
                                   user_preferred_language,
                                   path='/',
Rafael Monnerat's avatar
Rafael Monnerat committed
160
                                   expires=(DateTime() + 30).rfc822())
161 162 163 164 165
      # set encoded __ac_key cookie at client's browser
      __ac_key = quote(encodestring(password))
      expires = (DateTime() + 1).toZone('GMT').rfc822()
      REQUEST.RESPONSE.setCookie('__ac_key',
                                 __ac_key,
Rafael Monnerat's avatar
Rafael Monnerat committed
166
                                 expires=expires)
167
      REQUEST.set('__ac_key', __ac_key)
Rafael Monnerat's avatar
Rafael Monnerat committed
168 169 170
      REQUEST.RESPONSE.setCookie(BUSINESS_CONFIGURATION_COOKIE_NAME,
                                 bc,
                                 expires=expires)
171 172 173
      REQUEST.set(BUSINESS_CONFIGURATION_COOKIE_NAME, bc)
      return self.next(REQUEST=REQUEST)
    else:
Rafael Monnerat's avatar
Rafael Monnerat committed
174
      REQUEST.set('portal_status_message',
175 176 177
                   self.Base_translateString('Incorrect Configuration Key'))
      return self.view()

178 179
  def _isCorrectConfigurationKey(self, password=None,
                                       business_configuration=None):
180 181 182
    """ Is configuration key correct """
    if password is None:
      password = self.REQUEST.get('__ac_key', None)
183 184
    else:
      password = quote(encodestring(password))
185
    # Not still not finished yet.
186
    if business_configuration is None:
Rafael Monnerat's avatar
Rafael Monnerat committed
187 188
      business_configuration = self.REQUEST.get(
               BUSINESS_CONFIGURATION_COOKIE_NAME, None)
189 190 191 192
    if None not in [password, business_configuration]:
      def is_key_valid(password, business_configuration):
        bc = self.getPortalObject().unrestrictedTraverse(business_configuration)
        return quote(encodestring(bc.getReference(''))) == password
Rafael Monnerat's avatar
Rafael Monnerat committed
193 194
      return CachingMethod(is_key_valid,
                           "ConfiguratorTool_is_key_valid",
195 196 197 198
                           cache_factory='erp5_content_long')(
                                     password, business_configuration)
    return False

199 200 201 202 203 204
  #security.declareProtected(Permissions.ModifyPortalContent, 'next')
  def next(self, REQUEST):
    """ Validate settings and return a new form to the user.  """
    # check if user is allowed to access service
    portal = self.getPortalObject()
    if not self._isCorrectConfigurationKey():
Rafael Monnerat's avatar
Rafael Monnerat committed
205
      REQUEST.set('portal_status_message',
206 207 208 209 210 211
                  self.Base_translateString('Incorrect Configuration Key'))
      return self.view()
    kw = self.REQUEST.form.copy()
    business_configuration = REQUEST.get(BUSINESS_CONFIGURATION_COOKIE_NAME)
    bc = portal.restrictedTraverse(business_configuration)
    if bc is None:
Rafael Monnerat's avatar
Rafael Monnerat committed
212
      REQUEST.set('portal_status_message',
213 214 215
                   self.Base_translateString(
                     'You cannot Continue. Unable to find your Business Configuration.'))
      return self.view()
Rafael Monnerat's avatar
Rafael Monnerat committed
216
    response = self._next(business_configuration=bc, kw=kw)
217
    ## Parse server response
218
    if response["command"] == "show":
219 220
      return self.ConfiguratorTool_dialogForm(previous=response['previous'],
                                        form_html=response["data"],
221 222
                                        next=response['next'])
    elif response["command"] == "install":
223 224 225 226 227 228 229 230 231
      return self.startInstallation(bc, REQUEST=REQUEST)

  def _next(self, business_configuration, kw):
    """ Return next configuration form and validate previous. """
    form_kw = {}
    need_validation = 1
    validation_errors = None
    response = {}

232 233
    business_configuration.initializeWorkflow()

234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
    ## initial state no previous form to validate
    if business_configuration.isInitialConfigurationState():
      need_validation = 0

    ## client can not go further hist business configuration is already built
    if business_configuration.isEndConfigurationState() or \
         business_configuration.getNextTransition() == None:
      return self._terminateConfigurationProcess(response,
          'no_available_transitions')

    isMultiEntryTransition = business_configuration._isMultiEntryTransition()
    ## validate multiple forms
    if isMultiEntryTransition:
      html_forms = []
      failed_forms_counter = 0
      transition = business_configuration.getNextTransition()
      form = getattr(business_configuration, transition.getTransitionFormId())
      for form_key in filter(lambda x: x.startswith('field_'), kw.keys()):
        form_kw[form_key] = kw[form_key]
      ## iterate all forms
      for form_counter in range(0, isMultiEntryTransition):
        single_form_kw = {}
Rafael Monnerat's avatar
Rafael Monnerat committed
256
        for key, value in form_kw.items():
257 258 259 260 261
          if isinstance(value, list) or isinstance(value, tuple):
            ## we have more than one form shown
            single_form_kw[key] = value[form_counter]
            # save original value in request in some cases of multiple forms
            # we need it for validation
Rafael Monnerat's avatar
Rafael Monnerat committed
262
            single_form_kw['_original_%s' % key] = value
263 264 265 266 267
          else:
            ## even though we have multiple entry transition customer wants
            ## ONE form!
            single_form_kw[key] = value
        ## update properly REQUEST with current form data
Rafael Monnerat's avatar
Rafael Monnerat committed
268
        for key, value in single_form_kw.items():
269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298
          self.REQUEST.set(key, value)
        ## get validation status
        validation_status, dummy, validation_errors = \
           business_configuration._validateNextForm(**single_form_kw)

        ## clean up REQUEST from traces from validate_all_to_request
        ## otherwise next form will use previous forms details
        cleanup_keys = filter(lambda x: x.startswith('my_') or
                                x.startswith('your_'),
                                self.REQUEST.other.keys())
        for key in cleanup_keys:
          self.REQUEST.other.pop(key, None)
        ## render HTML code
        if validation_status != 0:
          failed_forms_counter += 1
          ## XXX: form can fail because a new
          ## http://localhost:9080/erp5/portal_wizard/next is issued
          ## without arguments. Improve this
          try:
            self.REQUEST.set('field_errors',
                form.ErrorFields(validation_errors))
          except:
            pass
          single_form_html = form()
          self.REQUEST.other.pop('field_errors', None)
          self.REQUEST.form = {}
        else:
          single_form_html = form()
        ## wrap in form template
        single_form_html = self.Base_mainConfiguratorFormTemplate(
Rafael Monnerat's avatar
Rafael Monnerat committed
299
                                current_form_number = form_counter + 1,
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316
                                max_form_numbers = isMultiEntryTransition,
                                form_html = single_form_html)
        ## add to list of forms as html code
        html_forms.append(single_form_html)
      ## return if failure
      if failed_forms_counter > 0:
        next_state = self.restrictedTraverse(business_configuration.getNextTransition()\
            .getDestination())
        html_data = self.Base_mainConfiguratorTemplate(
            form_html = "\n".join(html_forms),
            current_state = next_state,
            business_configuration = business_configuration)
        response.update(command = "show",
                  previous = self.Base_translateString("Previous"),
                  next = self.Base_translateString(transition.getTitle()),
                  data = html_data)
        return response
Rafael Monnerat's avatar
Rafael Monnerat committed
317

318 319 320 321 322 323 324 325 326 327
    ## show next form in transitions
    rendered = False
    while rendered is False:
      if need_validation == 1:
        if isMultiEntryTransition:
          ## multiple forms must be validated before
          validation_status = 0
        else:
          validation_status, form_kw, validation_errors = \
              business_configuration._validateNextForm(**kw)
Rafael Monnerat's avatar
Rafael Monnerat committed
328
        if validation_status == 1:
329
          need_validation = 0
Rafael Monnerat's avatar
Rafael Monnerat committed
330
        elif validation_status == 2:
331 332 333 334 335 336
          rendered = True
          need_validation = 0
          if business_configuration.getNextTransition() == None:
            ### client can not continue at the momen
            return self._terminateConfigurationProcess(response,
                reason='no_available_transitions')
337 338
          response["previous"], html, form_title, response["next"] \
                  = business_configuration._displayNextForm()
339 340 341 342 343 344 345 346 347 348
        else:
          ## validation passed
          need_validation = 0
          business_configuration._executeTransition(form_kw=form_kw, request_kw=kw)
      elif need_validation == 0:
        if business_configuration.getNextTransition() == None:
          return self._terminateConfigurationProcess(response,
              'no_available_transitions')
        ## validation failure
        rendered = True
349 350 351
        response["previous"], html, form_title, response["next"] =\
            business_configuration._displayNextForm(
                validation_errors=validation_errors)
352 353 354

    if html is None:
      ## we have no more forms proceed to build
355
      response.update(command="install", data=None)
356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371
    else:
      ## we have more forms
      next_state = self.restrictedTraverse(business_configuration.getNextTransition()\
          .getDestination())
      html_data = self.Base_mainConfiguratorTemplate(
          form_html = html,
          current_state = next_state,
          business_configuration = business_configuration)
      response.update(command = "show", data = html_data)
    return response

  def _terminateConfigurationProcess(self, response, reason=''):
    """ Terminate process and return some explanations to client why
        he can no longer continue. """
    if reason == 'no_available_transitions':
      form_html = self.BusinessConfiguration_viewStopForm()
372 373
      response.update(command="show", next=None, \
                      previous=None, data=form_html)
374 375
    elif reason == 'authentification_failure':
      form_html = self.BusinessConfiguration_viewUnauthenticatedForm()
376 377
      response.update(command="show", data=form_html,
                      next=None, previous=None,)
378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403

    return response

  #security.declareProtected(Permissions.ModifyPortalContent, 'previous')
  def previous(self, REQUEST):
    """ Display the previous form. """
    # check if user is allowed to access service
    portal = self.getPortalObject()
    if not self._isCorrectConfigurationKey():
      REQUEST.set('portal_status_message',
                  self.Base_translateString('Incorrect Configuration Key'))
      return self.view()
    kw = self.REQUEST.form.copy()
    business_configuration = REQUEST.get(BUSINESS_CONFIGURATION_COOKIE_NAME)
    bc = portal.restrictedTraverse(business_configuration)
    response = self._previous(business_configuration=bc, kw=kw)
    return self.ConfiguratorTool_dialogForm(previous=response['previous'],
                                      form_html=response['data'],
                                      next=response['next'])

  def _previous(self, business_configuration, kw):
    """ Returns previous form. """
    response = {}
    ## is client is not allowed access ?
    if business_configuration is None:
      form_html = self.BusinessConfiguration_viewUnauthenticatedForm()
404
      return self.ConfiguratorTool_dialogForm(form_html=form_html)
405 406 407 408 409 410
    ## client can not go further his business configuration is already built
    if business_configuration.isEndConfigurationState():
      form_html = self.BusinessConfiguration_viewStopForm()
      return self.ConfiguratorTool_dialogForm(form_html = form_html,
                                        next = "Next")

411
    response['previous'], form_html, form_title, response['next'] = \
412 413 414 415 416 417
        business_configuration._displayPreviousForm()

    next_state = self.restrictedTraverse(
        business_configuration.getNextTransition().getDestination())

    response['data'] = self.Base_mainConfiguratorTemplate(
418 419 420
        form_html=form_html,
        current_state=next_state,
        business_configuration=business_configuration)
421 422 423 424 425 426 427 428 429 430 431
    return response

  security.declarePublic(Permissions.AccessContentsInformation,
                         'getInstallationStatusReport')
  def getInstallationStatusReport(self,
                          active_process_id=None, REQUEST=None):
    """ Query local ERP5 instance for installation status.
        If installation is over the installation activities and reindexing
        activities should not exists.
    """
    global installation_status
432 433
    portal_activities = self.getPortalObject().portal_activities

434
    is_bt5_installation_over = (portal_activities.countMessageWithTag(
Rafael Monnerat's avatar
Rafael Monnerat committed
435
      'initialERP5Setup') == 0)
436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465
    if 0 == len(portal_activities.getMessageList()) and \
        is_bt5_installation_over:
      html = self.ConfiguratorTool_viewSuccessfulConfigurationMessageRenderer()
    else:
      if is_bt5_installation_over:
        # only if bt5s are installed start tracking number of activities
        activity_list = portal_activities.getMessageList()
        installation_status['activity_list'].append(len(activity_list))
      html = self.ConfiguratorTool_viewRunningInstallationMessage(
          installation_status = installation_status)
    # set encoding as this is usually called from asynchronous JavaScript call
    self.REQUEST.RESPONSE.setHeader('Content-Type',
        'text/html; charset=utf-8')
    return html

  security.declareProtected(Permissions.ModifyPortalContent, 'startInstallation')
  def startInstallation(self, business_configuration, REQUEST):
    """ Start installation process as an activity which will query generation
        server and download/install bt5 template files and meanwhile offer
        user a nice GUI to observe what's happening. """
    global installation_status
    # init installation status
    bt5_file_list = len(business_configuration.contentValues(
                                portal_types=["File", "Link"])) or 1
    installation_status['bt5']['all'] = bt5_file_list
    installation_status['bt5']['current'] = 0
    installation_status['activity_list'] = []
    active_process = self.portal_activities.newActiveProcess()
    REQUEST.set('active_process_id', active_process.getId())
    request_restore_dict = {'__ac_key': REQUEST.get('__ac_key',
Rafael Monnerat's avatar
Rafael Monnerat committed
466
                                                       None), }
467
    self.activate(active_process=active_process, tag='initialERP5Setup'
468 469 470 471 472 473 474 475 476 477 478 479 480 481 482
        ).initialERP5Setup(business_configuration.getRelativeUrl(), request_restore_dict)
    return self.ConfiguratorTool_viewInstallationStatus(REQUEST)

  security.declareProtected(Permissions.ModifyPortalContent,
      'initialERP5Setup')
  def initialERP5Setup(self, business_configuration, request_restore_dict={}):
    """ Get from remote generation server customized bt5 template files
        and then install them. """
    # restore some REQUEST variables as this method is executed in an activity
    # and there's no access to real original REQUEST
    for key, value in request_restore_dict.items():
      self.REQUEST.set(key, value)

    bc = self.restrictedTraverse(business_configuration)
    bc.build()