2009-06-29 07:06:01 -05:00
|
|
|
# ------------------------------------------------------------------------------
|
2013-09-06 09:19:56 -05:00
|
|
|
import os, os.path, sys, re, time, random, types, base64
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
from appy import Object
|
2010-09-02 09:16:08 -05:00
|
|
|
import appy.gen
|
2013-09-06 09:19:56 -05:00
|
|
|
from appy.gen import Search, UiSearch, String, Page
|
2013-08-21 05:35:30 -05:00
|
|
|
from appy.gen.layout import ColumnLayout
|
|
|
|
from appy.gen import utils as gutils
|
2011-12-05 08:11:29 -06:00
|
|
|
from appy.gen.mixins import BaseMixin
|
|
|
|
from appy.gen.wrappers import AbstractWrapper
|
|
|
|
from appy.gen.descriptors import ClassDescriptor
|
2014-10-21 02:25:37 -05:00
|
|
|
from appy.gen.navigate import Siblings
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
from appy.shared import mimeTypes
|
2013-08-21 05:35:30 -05:00
|
|
|
from appy.shared import utils as sutils
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
from appy.shared.data import languages
|
2014-10-07 06:14:16 -05:00
|
|
|
from appy.shared.ldap_connector import LdapConnector
|
2011-11-25 11:01:20 -06:00
|
|
|
try:
|
|
|
|
from AccessControl.ZopeSecurityPolicy import _noroles
|
|
|
|
except ImportError:
|
|
|
|
_noroles = []
|
2010-08-05 11:23:17 -05:00
|
|
|
|
2014-05-02 05:35:09 -05:00
|
|
|
# Global JS internationalized messages that will be computed in every page -----
|
|
|
|
jsMessages = ('no_elem_selected', 'action_confirm', 'save_confirm',
|
|
|
|
'warn_leave_form')
|
2014-10-24 08:55:45 -05:00
|
|
|
USER_NOT_FOUND = 'User %s not found. Probably a problem implying several ' \
|
|
|
|
'Appy apps put behind the same domain name or dev machine.'
|
2009-06-29 07:06:01 -05:00
|
|
|
|
|
|
|
# ------------------------------------------------------------------------------
|
2010-10-14 07:43:56 -05:00
|
|
|
class ToolMixin(BaseMixin):
|
2010-08-05 11:23:17 -05:00
|
|
|
_appy_meta_type = 'Tool'
|
2013-06-25 16:22:33 -05:00
|
|
|
xhtmlEncoding = 'text/html;charset=UTF-8'
|
|
|
|
|
2010-09-02 09:16:08 -05:00
|
|
|
def getPortalType(self, metaTypeOrAppyClass):
|
|
|
|
'''Returns the name of the portal_type that is based on
|
2010-10-14 07:43:56 -05:00
|
|
|
p_metaTypeOrAppyType.'''
|
2010-09-02 09:16:08 -05:00
|
|
|
appName = self.getProductConfig().PROJECTNAME
|
2011-02-01 04:09:54 -06:00
|
|
|
res = metaTypeOrAppyClass
|
2010-09-02 09:16:08 -05:00
|
|
|
if not isinstance(metaTypeOrAppyClass, basestring):
|
2013-08-21 05:35:30 -05:00
|
|
|
res = gutils.getClassName(metaTypeOrAppyClass, appName)
|
2011-12-05 03:52:18 -06:00
|
|
|
if res.find('_wrappers') != -1:
|
2010-09-02 09:16:08 -05:00
|
|
|
elems = res.split('_')
|
|
|
|
res = '%s%s' % (elems[1], elems[4])
|
2011-11-28 15:50:01 -06:00
|
|
|
if res in ('User', 'Group', 'Translation'): res = appName + res
|
2010-09-02 09:16:08 -05:00
|
|
|
return res
|
|
|
|
|
2013-06-25 05:04:23 -05:00
|
|
|
def home(self):
|
|
|
|
'''Returns the content of px ToolWrapper.pxHome.'''
|
|
|
|
tool = self.appy()
|
2013-08-21 05:35:30 -05:00
|
|
|
return tool.pxHome({'obj': None, 'tool': tool})
|
2013-06-25 05:04:23 -05:00
|
|
|
|
2013-06-27 04:57:39 -05:00
|
|
|
def query(self):
|
|
|
|
'''Returns the content of px ToolWrapper.pxQuery.'''
|
|
|
|
tool = self.appy()
|
2013-08-21 05:35:30 -05:00
|
|
|
return tool.pxQuery({'obj': None, 'tool': tool})
|
2013-06-27 04:57:39 -05:00
|
|
|
|
2013-06-28 08:00:02 -05:00
|
|
|
def search(self):
|
|
|
|
'''Returns the content of px ToolWrapper.pxSearch.'''
|
|
|
|
tool = self.appy()
|
2013-08-21 05:35:30 -05:00
|
|
|
return tool.pxSearch({'obj': None, 'tool': tool})
|
2013-06-28 08:00:02 -05:00
|
|
|
|
2012-02-18 12:48:00 -06:00
|
|
|
def getHomePage(self):
|
|
|
|
'''Return the home page when a user hits the app.'''
|
|
|
|
# If the app defines a method "getHomePage", call it.
|
2013-08-21 05:35:30 -05:00
|
|
|
tool = self.appy()
|
2014-05-13 09:41:59 -05:00
|
|
|
url = None
|
2012-02-18 12:48:00 -06:00
|
|
|
try:
|
2013-08-21 05:35:30 -05:00
|
|
|
url = tool.getHomePage()
|
2012-02-18 12:48:00 -06:00
|
|
|
except AttributeError:
|
2014-05-13 09:41:59 -05:00
|
|
|
pass
|
|
|
|
if not url:
|
2013-08-21 15:25:27 -05:00
|
|
|
# Bring Managers to the config, lead others to pxHome.
|
2012-02-18 12:48:00 -06:00
|
|
|
user = self.getUser()
|
2014-10-24 08:55:45 -05:00
|
|
|
if not user:
|
|
|
|
raise Exception(USER_NOT_FOUND % self.identifyUser()[0])
|
2012-02-18 12:48:00 -06:00
|
|
|
if user.has_role('Manager'):
|
|
|
|
url = self.goto(self.absolute_url())
|
|
|
|
else:
|
2013-08-21 15:25:27 -05:00
|
|
|
url = self.goto('%s/home' % self.absolute_url())
|
2012-02-18 12:48:00 -06:00
|
|
|
return url
|
|
|
|
|
2014-07-18 09:54:11 -05:00
|
|
|
def getHomeObject(self, inPopup=False):
|
2013-06-28 08:00:02 -05:00
|
|
|
'''The concept of "home object" is the object where the user must "be",
|
|
|
|
even if he is "nowhere". For example, if the user is on a search
|
|
|
|
screen, there is no contextual object. In this case, if we have a
|
|
|
|
home object for him, we will use it as contextual object, and its
|
|
|
|
portlet menu will nevertheless appear: the user will not have the
|
|
|
|
feeling of being lost.'''
|
2014-07-18 09:54:11 -05:00
|
|
|
# If we are in the popup, we do not want any home object in the way.
|
|
|
|
if inPopup: return
|
2013-06-28 08:00:02 -05:00
|
|
|
# If the app defines a method "getHomeObject", call it.
|
|
|
|
try:
|
2013-08-21 05:35:30 -05:00
|
|
|
return self.appy().getHomeObject()
|
2013-06-28 08:00:02 -05:00
|
|
|
except AttributeError:
|
|
|
|
# For managers, the home object is the config. For others, there is
|
|
|
|
# no default home object.
|
2013-08-21 05:35:30 -05:00
|
|
|
if self.getUser().has_role('Manager'): return self.appy()
|
2013-06-28 08:00:02 -05:00
|
|
|
|
2011-11-25 11:01:20 -06:00
|
|
|
def getCatalog(self):
|
|
|
|
'''Returns the catalog object.'''
|
|
|
|
return self.getParentNode().catalog
|
|
|
|
|
2011-09-18 08:00:05 -05:00
|
|
|
def getApp(self):
|
2011-11-25 11:01:20 -06:00
|
|
|
'''Returns the root Zope object.'''
|
|
|
|
return self.getPhysicalRoot()
|
2011-09-18 08:00:05 -05:00
|
|
|
|
2010-11-22 02:36:14 -06:00
|
|
|
def getSiteUrl(self):
|
|
|
|
'''Returns the absolute URL of this site.'''
|
2011-11-25 11:01:20 -06:00
|
|
|
return self.getApp().absolute_url()
|
2010-11-22 02:36:14 -06:00
|
|
|
|
2013-07-20 12:56:17 -05:00
|
|
|
def getIncludeUrl(self, name, bg=False):
|
|
|
|
'''Gets the full URL of an external resource, like an image, a
|
|
|
|
Javascript or a CSS file, named p_name. If p_bg is True, p_name is
|
|
|
|
an image that is meant to be used in a "style" attribute for defining
|
|
|
|
the background image of some XHTML tag.'''
|
|
|
|
# If no extension is found in p_name, we suppose it is a png image.
|
2013-07-15 04:23:29 -05:00
|
|
|
if '.' not in name: name += '.png'
|
2013-07-23 03:29:39 -05:00
|
|
|
url = '%s/ui/%s' % (self.getPhysicalRoot().absolute_url(), name)
|
2013-07-15 04:23:29 -05:00
|
|
|
if not bg: return url
|
|
|
|
return 'background-image: url(%s)' % url
|
|
|
|
|
2014-03-24 16:55:00 -05:00
|
|
|
def doPod(self):
|
|
|
|
'''Performs an action linked to a pod field: generate, freeze,
|
|
|
|
unfreeze... a document from a pod field.'''
|
2010-10-14 07:43:56 -05:00
|
|
|
rq = self.REQUEST
|
2014-03-24 16:55:00 -05:00
|
|
|
# Get the object that is the target of this action.
|
2011-02-16 06:43:58 -06:00
|
|
|
obj = self.getObject(rq.get('objectUid'), appy=True)
|
2014-03-25 06:05:07 -05:00
|
|
|
return obj.getField(rq.get('fieldName')).onUiRequest(obj, rq)
|
2009-06-29 07:06:01 -05:00
|
|
|
|
2009-10-25 15:42:08 -05:00
|
|
|
def getAppName(self):
|
2011-11-25 11:01:20 -06:00
|
|
|
'''Returns the name of the application.'''
|
2009-10-25 15:42:08 -05:00
|
|
|
return self.getProductConfig().PROJECTNAME
|
|
|
|
|
2011-11-25 11:01:20 -06:00
|
|
|
def getPath(self, path):
|
|
|
|
'''Returns the folder or object whose absolute path p_path.'''
|
|
|
|
res = self.getPhysicalRoot()
|
|
|
|
if path == '/': return res
|
|
|
|
path = path[1:]
|
|
|
|
if '/' not in path: return res._getOb(path) # For performance
|
|
|
|
for elem in path.split('/'): res = res._getOb(elem)
|
|
|
|
return res
|
|
|
|
|
2012-02-21 05:09:42 -06:00
|
|
|
def showLanguageSelector(self):
|
|
|
|
'''We must show the language selector if the app config requires it and
|
|
|
|
it there is more than 2 supported languages. Moreover, on some pages,
|
|
|
|
switching the language is not allowed.'''
|
2013-07-24 08:53:19 -05:00
|
|
|
cfg = self.getProductConfig(True)
|
2012-02-21 05:09:42 -06:00
|
|
|
if not cfg.languageSelector: return
|
|
|
|
if len(cfg.languages) < 2: return
|
|
|
|
page = self.REQUEST.get('ACTUAL_URL').split('/')[-1]
|
2012-06-27 06:27:24 -05:00
|
|
|
return page not in ('edit', 'query', 'search', 'do')
|
2012-02-21 05:09:42 -06:00
|
|
|
|
2012-07-27 04:01:35 -05:00
|
|
|
def showForgotPassword(self):
|
|
|
|
'''We must show link "forgot password?" when the app requires it.'''
|
2013-07-24 08:53:19 -05:00
|
|
|
return self.getProductConfig(True).activateForgotPassword
|
2012-07-27 04:01:35 -05:00
|
|
|
|
2011-11-25 11:01:20 -06:00
|
|
|
def getLanguages(self):
|
|
|
|
'''Returns the supported languages. First one is the default.'''
|
2013-07-24 08:53:19 -05:00
|
|
|
return self.getProductConfig(True).languages
|
2011-11-25 11:01:20 -06:00
|
|
|
|
2014-09-03 11:18:27 -05:00
|
|
|
def getLanguageName(self, code, lowerize=False):
|
2011-11-25 11:01:20 -06:00
|
|
|
'''Gets the language name (in this language) from a 2-chars language
|
|
|
|
p_code.'''
|
2014-09-03 11:18:27 -05:00
|
|
|
res = languages.get(code)[2]
|
|
|
|
if not lowerize: return res
|
|
|
|
return res.lower()
|
2011-11-25 11:01:20 -06:00
|
|
|
|
2012-05-29 13:50:18 -05:00
|
|
|
def changeLanguage(self):
|
|
|
|
'''Sets the language cookie with the new desired language code that is
|
|
|
|
in request["language"].'''
|
|
|
|
rq = self.REQUEST
|
|
|
|
rq.RESPONSE.setCookie('_ZopeLg', rq['language'], path='/')
|
|
|
|
return self.goto(rq['HTTP_REFERER'])
|
|
|
|
|
2012-06-27 06:27:24 -05:00
|
|
|
def flipLanguageDirection(self, align, dir):
|
|
|
|
'''According to language direction p_dir ('ltr' or 'rtl'), this method
|
|
|
|
turns p_align from 'left' to 'right' (or the inverse) when
|
|
|
|
required.'''
|
|
|
|
if dir == 'ltr': return align
|
|
|
|
if align == 'left': return 'right'
|
|
|
|
if align == 'right': return 'left'
|
|
|
|
return align
|
|
|
|
|
2014-01-20 09:30:14 -06:00
|
|
|
def getGlobalCssJs(self, dir):
|
2012-02-21 05:09:42 -06:00
|
|
|
'''Returns the list of CSS and JS files to include in the main template.
|
2014-01-20 09:30:14 -06:00
|
|
|
The method ensures that appy.css and appy.js come first. If p_dir
|
|
|
|
(=language *dir*rection) is "rtl" (=right-to-left), the stylesheet
|
|
|
|
for rtl languages is also included.'''
|
2012-02-21 05:09:42 -06:00
|
|
|
names = self.getPhysicalRoot().ui.objectIds('File')
|
2014-01-20 09:30:14 -06:00
|
|
|
# The single Appy Javascript file
|
2012-02-21 05:09:42 -06:00
|
|
|
names.remove('appy.js'); names.insert(0, 'appy.js')
|
2014-01-20 09:30:14 -06:00
|
|
|
# CSS changes for left-to-right languages
|
|
|
|
names.remove('appyrtl.css')
|
|
|
|
if dir == 'rtl': names.insert(0, 'appyrtl.css')
|
2012-02-21 05:09:42 -06:00
|
|
|
names.remove('appy.css'); names.insert(0, 'appy.css')
|
|
|
|
return names
|
|
|
|
|
2011-12-05 11:15:45 -06:00
|
|
|
def consumeMessages(self):
|
|
|
|
'''Returns the list of messages to show to a web page and clean it in
|
|
|
|
the session.'''
|
|
|
|
rq = self.REQUEST
|
|
|
|
res = rq.SESSION.get('messages', '')
|
|
|
|
if res:
|
|
|
|
del rq.SESSION['messages']
|
|
|
|
res = ' '.join([m[1] for m in res])
|
2011-11-25 11:01:20 -06:00
|
|
|
return res
|
2009-06-29 07:06:01 -05:00
|
|
|
|
2009-10-18 07:52:27 -05:00
|
|
|
def getRootClasses(self):
|
|
|
|
'''Returns the list of root classes for this application.'''
|
2014-03-03 11:54:21 -06:00
|
|
|
cfg = self.getProductConfig().appConfig
|
|
|
|
rootClasses = cfg.rootClasses
|
|
|
|
if not rootClasses:
|
|
|
|
# We consider every class as being a root class.
|
2014-03-05 08:47:12 -06:00
|
|
|
rootClasses = self.getProductConfig().appClassNames
|
2014-03-03 11:54:21 -06:00
|
|
|
return [self.getAppyClass(k) for k in rootClasses]
|
2009-10-18 07:52:27 -05:00
|
|
|
|
2013-08-21 05:35:30 -05:00
|
|
|
def getSearchInfo(self, className, refInfo=None):
|
|
|
|
'''Returns, as an object:
|
|
|
|
- the list of searchable fields (some among all indexed fields);
|
2010-12-17 07:46:55 -06:00
|
|
|
- the number of columns for layouting those fields.'''
|
2010-12-06 04:11:40 -06:00
|
|
|
fields = []
|
|
|
|
if refInfo:
|
|
|
|
# The search is triggered from a Ref field.
|
2011-09-20 12:21:48 -05:00
|
|
|
refObject, fieldName = self.getRefInfo(refInfo)
|
|
|
|
refField = refObject.getAppyType(fieldName)
|
2010-12-06 04:11:40 -06:00
|
|
|
fieldNames = refField.queryFields or ()
|
|
|
|
nbOfColumns = refField.queryNbCols
|
|
|
|
else:
|
|
|
|
# The search is triggered from an app-wide search.
|
2013-09-21 10:46:42 -05:00
|
|
|
klass = self.getAppyClass(className)
|
|
|
|
fieldNames = getattr(klass, 'searchFields', None)
|
|
|
|
if not fieldNames:
|
|
|
|
# Gather all the indexed fields on this class.
|
|
|
|
fieldNames = [f.name for f in self.getAllAppyTypes(className) \
|
|
|
|
if f.indexed]
|
|
|
|
nbOfColumns = getattr(klass, 'numberOfSearchColumns', 3)
|
2010-10-14 07:43:56 -05:00
|
|
|
for name in fieldNames:
|
2013-08-21 05:35:30 -05:00
|
|
|
field = self.getAppyType(name, className=className)
|
|
|
|
fields.append(field)
|
|
|
|
return Object(fields=fields, nbOfColumns=nbOfColumns)
|
2010-10-14 07:43:56 -05:00
|
|
|
|
2011-11-25 11:01:20 -06:00
|
|
|
queryParamNames = ('className', 'search', 'sortKey', 'sortOrder',
|
2011-01-28 07:36:30 -06:00
|
|
|
'filterKey', 'filterValue')
|
|
|
|
def getQueryInfo(self):
|
|
|
|
'''If we are showing search results, this method encodes in a string all
|
|
|
|
the params in the request that are required for re-triggering the
|
|
|
|
search.'''
|
|
|
|
rq = self.REQUEST
|
|
|
|
res = ''
|
|
|
|
if rq.has_key('search'):
|
|
|
|
res = ';'.join([rq.get(key,'').replace(';','') \
|
|
|
|
for key in self.queryParamNames])
|
|
|
|
return res
|
|
|
|
|
2013-03-09 09:06:12 -06:00
|
|
|
def getResultMode(self, className):
|
2013-08-21 15:25:27 -05:00
|
|
|
'''Must we show, on pxQueryResult, instances of p_className as a list or
|
2013-03-09 09:06:12 -06:00
|
|
|
as a grid?'''
|
|
|
|
klass = self.getAppyClass(className)
|
2013-09-21 10:46:42 -05:00
|
|
|
return getattr(klass, 'resultMode', 'list')
|
2013-03-09 09:06:12 -06:00
|
|
|
|
2013-09-22 09:33:32 -05:00
|
|
|
def showPortlet(self, obj, layoutType):
|
|
|
|
'''When must the portlet be shown? p_obj and p_layoutType can be None
|
|
|
|
if we are not browing any objet (ie, we are on the home page).'''
|
2012-11-29 13:45:21 -06:00
|
|
|
# Not on 'edit' pages.
|
|
|
|
if layoutType == 'edit': return
|
2010-08-12 04:56:42 -05:00
|
|
|
res = True
|
2013-09-22 09:33:32 -05:00
|
|
|
if obj and hasattr(obj, 'showPortlet'):
|
|
|
|
res = obj.showPortlet()
|
2012-05-29 13:50:18 -05:00
|
|
|
else:
|
2013-09-22 09:33:32 -05:00
|
|
|
tool = self.appy()
|
|
|
|
if hasattr(tool, 'showPortletAt'):
|
|
|
|
res = tool.showPortletAt(self.REQUEST['ACTUAL_URL'])
|
2010-08-12 04:56:42 -05:00
|
|
|
return res
|
2009-06-29 07:06:01 -05:00
|
|
|
|
2011-11-25 11:01:20 -06:00
|
|
|
def getObject(self, uid, appy=False, brain=False):
|
2009-11-17 03:05:19 -06:00
|
|
|
'''Allows to retrieve an object from its p_uid.'''
|
2011-11-25 11:01:20 -06:00
|
|
|
res = self.getPhysicalRoot().catalog(UID=uid)
|
|
|
|
if not res: return
|
|
|
|
res = res[0]
|
|
|
|
if brain: return res
|
2012-02-09 09:36:50 -06:00
|
|
|
res = res._unrestrictedGetObject()
|
2011-11-25 11:01:20 -06:00
|
|
|
if not appy: return res
|
|
|
|
return res.appy()
|
|
|
|
|
2011-11-28 15:50:01 -06:00
|
|
|
def getAllowedValue(self):
|
2013-09-22 09:33:32 -05:00
|
|
|
'''Gets, for the current user, the value of index "Allowed".'''
|
2011-11-28 15:50:01 -06:00
|
|
|
user = self.getUser()
|
2014-05-15 02:51:11 -05:00
|
|
|
# Get the user roles. If we do not make a copy of the list here, we will
|
|
|
|
# really add user logins among user roles!
|
|
|
|
res = user.getRoles()[:]
|
2013-09-22 09:33:32 -05:00
|
|
|
# Get the user logins
|
|
|
|
if user.login != 'anon':
|
|
|
|
for login in user.getLogins():
|
|
|
|
res.append('user:%s' % login)
|
2011-11-28 15:50:01 -06:00
|
|
|
return res
|
|
|
|
|
2011-11-25 11:01:20 -06:00
|
|
|
def executeQuery(self, className, searchName=None, startNumber=0,
|
2010-10-14 07:43:56 -05:00
|
|
|
search=None, remember=False, brainsOnly=False,
|
|
|
|
maxResults=None, noSecurity=False, sortBy=None,
|
2010-12-06 04:11:40 -06:00
|
|
|
sortOrder='asc', filterKey=None, filterValue=None,
|
2011-09-20 12:21:48 -05:00
|
|
|
refObject=None, refField=None):
|
2012-11-14 10:40:52 -06:00
|
|
|
'''Executes a query on instances of a given p_className in the catalog.
|
|
|
|
If p_searchName is specified, it corresponds to:
|
2011-11-25 11:01:20 -06:00
|
|
|
1) a search defined on p_className: additional search criteria
|
2009-12-30 10:12:18 -06:00
|
|
|
will be added to the query, or;
|
2012-12-12 10:26:01 -06:00
|
|
|
2) "customSearch": in this case, additional search criteria will
|
|
|
|
also be added to the query, but those criteria come from the
|
2013-08-21 15:25:27 -05:00
|
|
|
session (in key "searchCriteria") and came from pxSearch.
|
2009-12-30 10:12:18 -06:00
|
|
|
|
|
|
|
We will retrieve objects from p_startNumber. If p_search is defined,
|
|
|
|
it corresponds to a custom Search instance (instead of a predefined
|
|
|
|
named search like in p_searchName). If both p_searchName and p_search
|
|
|
|
are given, p_search is ignored.
|
|
|
|
|
2013-08-21 15:25:27 -05:00
|
|
|
This method returns a list of objects in the form of an instance of
|
|
|
|
SomeObjects (see in appy.gen.utils). If p_brainsOnly is True, it
|
|
|
|
returns a list of brains instead (can be useful for some usages like
|
|
|
|
knowing the number of objects without needing to get information
|
|
|
|
about them). If no p_maxResults is specified, the method returns
|
|
|
|
maximum self.numberOfResultsPerPage. The method returns all objects
|
|
|
|
if p_maxResults equals string "NO_LIMIT".
|
2010-01-12 14:15:14 -06:00
|
|
|
|
|
|
|
If p_noSecurity is True, it gets all the objects, even those that the
|
2010-04-30 05:05:29 -05:00
|
|
|
currently logged user can't see.
|
|
|
|
|
|
|
|
The result is sorted according to the potential sort key defined in
|
2012-05-25 07:27:53 -05:00
|
|
|
the Search instance (Search.sortBy, together with Search.sortOrder).
|
|
|
|
But if parameter p_sortBy is given, it defines or overrides the sort.
|
|
|
|
In this case, p_sortOrder gives the order (*asc*ending or
|
|
|
|
*desc*ending).
|
2010-04-30 05:05:29 -05:00
|
|
|
|
|
|
|
If p_filterKey is given, it represents an additional search parameter
|
|
|
|
to take into account: the corresponding search value is in
|
2010-12-06 04:11:40 -06:00
|
|
|
p_filterValue.
|
|
|
|
|
2011-09-20 12:21:48 -05:00
|
|
|
If p_refObject and p_refField are given, the query is limited to the
|
|
|
|
objects that are referenced from p_refObject through p_refField.'''
|
2012-11-14 10:40:52 -06:00
|
|
|
params = {'ClassName': className}
|
2014-11-28 07:42:32 -06:00
|
|
|
klass = self.getAppyClass(className, wrapper=True)
|
2009-11-24 15:41:42 -06:00
|
|
|
if not brainsOnly: params['batch'] = True
|
2009-11-03 08:02:18 -06:00
|
|
|
# Manage additional criteria from a search when relevant
|
2012-11-14 10:40:52 -06:00
|
|
|
if searchName: search = self.getSearch(className, searchName)
|
2009-11-06 04:33:56 -06:00
|
|
|
if search:
|
2014-11-28 07:42:32 -06:00
|
|
|
# Add in params search and sort criteria
|
|
|
|
search.updateSearchCriteria(params, klass)
|
|
|
|
# Determine or override sort if specified
|
2010-04-30 05:05:29 -05:00
|
|
|
if sortBy:
|
2014-11-28 07:42:32 -06:00
|
|
|
params['sort_on'] = Search.getIndexName(sortBy, klass, usage='sort')
|
|
|
|
params['sort_order'] = (sortOrder == 'desc') and 'reverse' or None
|
|
|
|
# If defined, add the filter among search parameters
|
2010-04-30 05:05:29 -05:00
|
|
|
if filterKey:
|
2014-11-28 07:42:32 -06:00
|
|
|
filterKey = Search.getIndexName(filterKey, klass)
|
|
|
|
filterValue = Search.getSearchValue(filterKey, filterValue, klass)
|
2010-04-30 05:05:29 -05:00
|
|
|
params[filterKey] = filterValue
|
|
|
|
# TODO This value needs to be merged with an existing one if already
|
|
|
|
# in params, or, in a first step, we should avoid to display the
|
|
|
|
# corresponding filter widget on the screen.
|
2011-09-20 12:21:48 -05:00
|
|
|
if refObject:
|
|
|
|
refField = refObject.getAppyType(refField)
|
2011-09-26 14:19:34 -05:00
|
|
|
params['UID'] = getattr(refObject, refField.name).data
|
2011-11-28 15:50:01 -06:00
|
|
|
# Use index "Allowed" if noSecurity is False
|
|
|
|
if not noSecurity: params['Allowed'] = self.getAllowedValue()
|
|
|
|
brains = self.getPath("/catalog")(**params)
|
2014-07-25 08:07:31 -05:00
|
|
|
# Compute maxResults
|
2010-12-06 04:11:40 -06:00
|
|
|
if not maxResults:
|
|
|
|
if refField: maxResults = refField.maxPerPage
|
2014-07-25 08:07:31 -05:00
|
|
|
else: maxResults = search.maxPerPage or \
|
|
|
|
self.appy().numberOfResultsPerPage
|
|
|
|
elif maxResults == 'NO_LIMIT':
|
|
|
|
maxResults = None
|
2014-11-28 07:42:32 -06:00
|
|
|
# Return brains only if required
|
2014-07-25 08:07:31 -05:00
|
|
|
if brainsOnly:
|
|
|
|
if not maxResults: return brains
|
|
|
|
else: return brains[:maxResults]
|
2013-08-21 05:35:30 -05:00
|
|
|
res = gutils.SomeObjects(brains, maxResults, startNumber,
|
|
|
|
noSecurity=noSecurity)
|
2009-11-03 08:02:18 -06:00
|
|
|
res.brainsToObjects()
|
2009-11-17 03:05:19 -06:00
|
|
|
# In some cases (p_remember=True), we need to keep some information
|
|
|
|
# about the query results in the current user's session, allowing him
|
|
|
|
# to navigate within elements without re-triggering the query every
|
|
|
|
# time a page for an element is consulted.
|
|
|
|
if remember:
|
|
|
|
if not searchName:
|
2014-10-21 02:25:37 -05:00
|
|
|
if not search:
|
2012-12-12 10:26:01 -06:00
|
|
|
searchName = className
|
|
|
|
else:
|
2014-10-21 02:25:37 -05:00
|
|
|
searchName = search.getSessionKey(className, full=False)
|
2009-11-17 03:05:19 -06:00
|
|
|
uids = {}
|
|
|
|
i = -1
|
|
|
|
for obj in res.objects:
|
|
|
|
i += 1
|
2014-10-21 02:25:37 -05:00
|
|
|
uids[startNumber+i] = obj.id
|
2010-12-17 07:46:55 -06:00
|
|
|
self.REQUEST.SESSION['search_%s' % searchName] = uids
|
2013-08-21 15:25:27 -05:00
|
|
|
return res
|
2009-06-29 07:06:01 -05:00
|
|
|
|
2013-08-21 05:35:30 -05:00
|
|
|
def getResultColumnsLayouts(self, className, refInfo):
|
2012-11-02 16:27:54 -05:00
|
|
|
'''Returns the column layouts for displaying objects of
|
2013-08-21 05:35:30 -05:00
|
|
|
p_className.'''
|
2012-11-02 16:27:54 -05:00
|
|
|
if refInfo[0]:
|
2013-08-21 05:35:30 -05:00
|
|
|
return refInfo[0].getAppyType(refInfo[1]).shownInfo
|
2012-11-02 16:27:54 -05:00
|
|
|
else:
|
2013-09-18 05:06:07 -05:00
|
|
|
k = self.getAppyClass(className)
|
2014-11-28 07:42:32 -06:00
|
|
|
if not hasattr(k, 'listColumns'): return ('title',)
|
|
|
|
if callable(k.listColumns): return k.listColumns(self.appy())
|
|
|
|
return k.listColumns
|
2009-06-29 07:06:01 -05:00
|
|
|
|
2014-05-02 05:35:09 -05:00
|
|
|
def truncateValue(self, value, width=20):
|
|
|
|
'''Truncates the p_value according to p_width. p_value has to be
|
|
|
|
unicode-encoded for being truncated (else, one char may be spread on
|
|
|
|
2 chars).'''
|
|
|
|
# Param p_width can be None.
|
|
|
|
if not width: width = 20
|
2011-12-01 13:53:13 -06:00
|
|
|
if isinstance(value, str): value = value.decode('utf-8')
|
2014-05-02 05:35:09 -05:00
|
|
|
if len(value) > width: return value[:width] + '...'
|
|
|
|
return value
|
2010-08-05 11:23:17 -05:00
|
|
|
|
2014-05-02 05:35:09 -05:00
|
|
|
def truncateText(self, text, width=20):
|
2010-09-20 04:33:54 -05:00
|
|
|
'''Truncates p_text to max p_width chars. If the text is longer than
|
2014-05-02 05:35:09 -05:00
|
|
|
p_width, the truncated part is put in a "acronym" html tag. p_text
|
|
|
|
has to be unicode-encoded for being truncated (else, one char may be
|
|
|
|
spread on 2 chars).'''
|
|
|
|
# Param p_width can be None.
|
|
|
|
if not width: width = 20
|
|
|
|
if isinstance(text, str): text = text.decode('utf-8')
|
|
|
|
if len(text) <= width: return text
|
|
|
|
return '<acronym title="%s">%s...</acronym>' % (text, text[:width])
|
2010-09-20 04:33:54 -05:00
|
|
|
|
2013-03-09 09:06:12 -06:00
|
|
|
def splitList(self, l, sub):
|
|
|
|
'''Returns a list made of the same elements as p_l, but grouped into
|
|
|
|
sub-lists of p_sub elements.'''
|
2013-08-21 05:35:30 -05:00
|
|
|
return sutils.splitList(l, sub)
|
2013-03-09 09:06:12 -06:00
|
|
|
|
2014-06-15 17:58:45 -05:00
|
|
|
def quote(self, s, escapeWithEntity=True):
|
2013-07-08 16:39:16 -05:00
|
|
|
'''Returns the quoted version of p_s.'''
|
2013-08-21 05:35:30 -05:00
|
|
|
if not isinstance(s, basestring): s = str(s)
|
2014-06-15 17:58:45 -05:00
|
|
|
repl = escapeWithEntity and ''' or "\\'"
|
|
|
|
s = s.replace('\r\n', '').replace('\n', '').replace("'", repl)
|
2013-07-08 16:39:16 -05:00
|
|
|
return "'%s'" % s
|
|
|
|
|
2012-11-29 13:45:21 -06:00
|
|
|
def getLayoutType(self):
|
|
|
|
'''Guess the current layout type, according to actual URL.'''
|
2013-06-28 08:00:02 -05:00
|
|
|
url = self.REQUEST['ACTUAL_URL']
|
|
|
|
if url.endswith('/view'): return 'view'
|
|
|
|
if url.endswith('/edit') or url.endswith('/do'): return 'edit'
|
2012-11-29 13:45:21 -06:00
|
|
|
|
2011-11-25 11:01:20 -06:00
|
|
|
def getZopeClass(self, name):
|
|
|
|
'''Returns the Zope class whose name is p_name.'''
|
|
|
|
exec 'from Products.%s.%s import %s as C'% (self.getAppName(),name,name)
|
|
|
|
return C
|
|
|
|
|
|
|
|
def getAppyClass(self, zopeName, wrapper=False):
|
|
|
|
'''Gets the Appy class corresponding to the Zope class named p_name.
|
|
|
|
If p_wrapper is True, it returns the Appy wrapper. Else, it returns
|
|
|
|
the user-defined class.'''
|
2014-03-03 11:54:21 -06:00
|
|
|
# p_zopeName may be the name of the Zope class *or* the name of the Appy
|
|
|
|
# class (shorter, not prefixed with the underscored package path).
|
|
|
|
classes = self.getProductConfig().allShortClassNames
|
|
|
|
if zopeName in classes: zopeName = classes[zopeName]
|
2011-11-25 11:01:20 -06:00
|
|
|
zopeClass = self.getZopeClass(zopeName)
|
|
|
|
if wrapper: return zopeClass.wrapperClass
|
|
|
|
else: return zopeClass.wrapperClass.__bases__[-1]
|
2010-04-26 11:19:34 -05:00
|
|
|
|
2014-02-26 16:40:27 -06:00
|
|
|
def getAllClassNames(self):
|
|
|
|
'''Returns the name of all classes within this app, including default
|
|
|
|
Appy classes (Tool, Translation, Page, etc).'''
|
|
|
|
return self.getProductConfig().allClassNames + [self.__class__.__name__]
|
|
|
|
|
2013-08-23 11:57:27 -05:00
|
|
|
def getCreateMeans(self, klass):
|
2014-03-05 09:48:54 -06:00
|
|
|
'''Gets the different ways objects of p_klass can be created (currently:
|
|
|
|
via a web form or programmatically only). Result is a list.'''
|
|
|
|
res = []
|
2013-08-23 11:57:27 -05:00
|
|
|
if not klass.__dict__.has_key('create'):
|
2014-03-05 09:48:54 -06:00
|
|
|
return ['form']
|
2009-10-20 09:57:00 -05:00
|
|
|
else:
|
2014-03-06 07:09:19 -06:00
|
|
|
means = klass.create
|
2009-10-20 09:57:00 -05:00
|
|
|
if means:
|
2014-03-05 09:48:54 -06:00
|
|
|
if isinstance(means, basestring): res = [means]
|
|
|
|
else: res = means
|
2009-10-20 09:57:00 -05:00
|
|
|
return res
|
|
|
|
|
2013-09-22 09:33:32 -05:00
|
|
|
def userMaySearch(self, klass):
|
|
|
|
'''May the user search among instances of root p_klass ?'''
|
2013-08-23 11:57:27 -05:00
|
|
|
# When editing a form, one should avoid annoying the user with this.
|
2011-09-28 14:17:15 -05:00
|
|
|
url = self.REQUEST['ACTUAL_URL']
|
|
|
|
if url.endswith('/edit') or url.endswith('/do'): return
|
2013-09-22 09:33:32 -05:00
|
|
|
if hasattr(klass, 'maySearch'): return klass.maySearch(self.appy())
|
2010-08-05 11:23:17 -05:00
|
|
|
return True
|
|
|
|
|
2013-08-23 11:57:27 -05:00
|
|
|
def userMayCreate(self, klass):
|
2013-09-22 09:33:32 -05:00
|
|
|
'''May the logged user create instances of p_klass ? This information
|
|
|
|
can be defined on p_klass, in static attribute "creators".
|
|
|
|
1. If this attr holds a list, we consider it to be a list of roles,
|
|
|
|
and we check that the user has at least one of those roles.
|
|
|
|
2. If this attr holds a boolean, we consider that the user can create
|
|
|
|
instances of this class if the boolean is True.
|
|
|
|
3. If this attr stores a method, we execute the method, and via its
|
|
|
|
result, we fall again in cases 1 or 2.
|
|
|
|
|
|
|
|
If p_klass does not define this attr "creators", we will use a
|
|
|
|
default list of roles as defined in the config.'''
|
|
|
|
# Get the value of attr "creators", or a default value if not present.
|
|
|
|
if hasattr(klass, 'creators'):
|
|
|
|
creators = klass.creators
|
|
|
|
else:
|
|
|
|
creators = self.getProductConfig().appConfig.defaultCreators
|
|
|
|
# Resolve case (3): if "creators" is a method, execute it.
|
|
|
|
if callable(creators): creators = creators(self.appy())
|
|
|
|
# Resolve case (2)
|
|
|
|
if isinstance(creators, bool) or not creators: return creators
|
|
|
|
# Resolve case (1): checks whether the user has at least one of the
|
|
|
|
# roles listed in "creators".
|
2013-08-23 11:57:27 -05:00
|
|
|
for role in self.getUser().getRoles():
|
2013-09-22 09:33:32 -05:00
|
|
|
if role in creators:
|
2013-08-23 11:57:27 -05:00
|
|
|
return True
|
|
|
|
|
2012-10-31 15:17:31 -05:00
|
|
|
def subTitleIsUsed(self, className):
|
|
|
|
'''Does class named p_className define a method "getSubTitle"?'''
|
|
|
|
klass = self.getAppyClass(className)
|
|
|
|
return hasattr(klass, 'getSubTitle')
|
|
|
|
|
2010-01-07 13:25:18 -06:00
|
|
|
def _searchValueIsEmpty(self, key):
|
|
|
|
'''Returns True if request value in key p_key can be considered as
|
|
|
|
empty.'''
|
|
|
|
rq = self.REQUEST.form
|
2010-01-14 10:54:18 -06:00
|
|
|
if key.endswith('*int') or key.endswith('*float'):
|
2010-01-07 13:25:18 -06:00
|
|
|
# We return True if "from" AND "to" values are empty.
|
2010-01-14 10:54:18 -06:00
|
|
|
toKey = '%s_to' % key[2:key.find('*')]
|
2010-01-07 13:25:18 -06:00
|
|
|
return not rq[key].strip() and not rq[toKey].strip()
|
|
|
|
elif key.endswith('*date'):
|
|
|
|
# We return True if "from" AND "to" values are empty. A value is
|
|
|
|
# considered as not empty if at least the year is specified.
|
|
|
|
toKey = '%s_to_year' % key[2:-5]
|
|
|
|
return not rq[key] and not rq[toKey]
|
|
|
|
else:
|
|
|
|
return not rq[key]
|
|
|
|
|
|
|
|
def _getDateTime(self, year, month, day, setMin):
|
|
|
|
'''Gets a valid DateTime instance from date information coming from the
|
|
|
|
request as strings in p_year, p_month and p_day. Returns None if
|
|
|
|
p_year is empty. If p_setMin is True, when some
|
|
|
|
information is missing (month or day), we will replace it with the
|
|
|
|
minimum value (=1). Else, we will replace it with the maximum value
|
|
|
|
(=12, =31).'''
|
|
|
|
if not year: return None
|
|
|
|
if not month:
|
|
|
|
if setMin: month = 1
|
|
|
|
else: month = 12
|
|
|
|
if not day:
|
|
|
|
if setMin: day = 1
|
|
|
|
else: day = 31
|
|
|
|
DateTime = self.getProductConfig().DateTime
|
2011-02-15 06:27:36 -06:00
|
|
|
# Set the hour
|
|
|
|
if setMin: hour = '00:00'
|
|
|
|
else: hour = '23:59'
|
2010-01-07 13:25:18 -06:00
|
|
|
# We loop until we find a valid date. For example, we could loop from
|
|
|
|
# 2009/02/31 to 2009/02/28.
|
|
|
|
dateIsWrong = True
|
|
|
|
while dateIsWrong:
|
|
|
|
try:
|
2011-02-15 06:27:36 -06:00
|
|
|
res = DateTime('%s/%s/%s %s' % (year, month, day, hour))
|
2010-01-07 13:25:18 -06:00
|
|
|
dateIsWrong = False
|
|
|
|
except:
|
|
|
|
day = int(day)-1
|
|
|
|
return res
|
|
|
|
|
2013-02-18 08:03:26 -06:00
|
|
|
def _getDefaultSearchCriteria(self):
|
|
|
|
'''We are about to perform an advanced search on instances of a given
|
|
|
|
class. Check, on this class, if in field Class.searchAdvanced, some
|
|
|
|
default criteria (field values, sort filters, etc) exist, and, if
|
|
|
|
yes, return it.'''
|
|
|
|
res = {}
|
|
|
|
rq = self.REQUEST
|
|
|
|
if 'className' not in rq.form: return res
|
|
|
|
klass = self.getAppyClass(rq.form['className'])
|
|
|
|
if not hasattr(klass, 'searchAdvanced'): return res
|
2013-04-30 04:12:02 -05:00
|
|
|
# In klass.searchAdvanced, we have the Search instance representing
|
|
|
|
# default advanced search criteria.
|
|
|
|
wrapperClass = self.getAppyClass(rq.form['className'], wrapper=True)
|
|
|
|
klass.searchAdvanced.updateSearchCriteria(res, wrapperClass,
|
|
|
|
advanced=True)
|
2013-02-18 08:03:26 -06:00
|
|
|
return res
|
|
|
|
|
2010-04-16 10:07:34 -05:00
|
|
|
transformMethods = {'uppercase': 'upper', 'lowercase': 'lower',
|
|
|
|
'capitalize': 'capitalize'}
|
2013-04-09 03:57:21 -05:00
|
|
|
def storeSearchCriteria(self):
|
|
|
|
'''Stores the search criteria coming from the request into the
|
|
|
|
session.'''
|
2009-12-30 10:12:18 -06:00
|
|
|
rq = self.REQUEST
|
2010-01-06 11:36:16 -06:00
|
|
|
# Store the search criteria in the session
|
2013-02-18 08:03:26 -06:00
|
|
|
criteria = self._getDefaultSearchCriteria()
|
2014-03-18 05:22:08 -05:00
|
|
|
for name in rq.form.keys():
|
|
|
|
if name.startswith('w_') and not self._searchValueIsEmpty(name):
|
|
|
|
hasStar = name.find('*') != -1
|
|
|
|
fieldName = not hasStar and name[2:] or name[2:name.find('*')]
|
|
|
|
field = self.getAppyType(fieldName, rq.form['className'])
|
2014-05-17 09:44:56 -05:00
|
|
|
if field and not field.persist and not field.indexed: continue
|
2010-01-07 13:25:18 -06:00
|
|
|
# We have a(n interval of) value(s) that is not empty for a
|
2014-03-18 05:22:08 -05:00
|
|
|
# given field or index.
|
|
|
|
value = rq.form[name]
|
|
|
|
if hasStar:
|
|
|
|
value = value.strip()
|
2010-03-19 07:13:36 -05:00
|
|
|
# The type of the value is encoded after char "*".
|
2014-03-18 05:22:08 -05:00
|
|
|
name, type = name.split('*')
|
|
|
|
if type == 'bool':
|
|
|
|
exec 'value = %s' % value
|
|
|
|
elif type in ('int', 'float'):
|
2010-01-07 13:25:18 -06:00
|
|
|
# Get the "from" value
|
2014-03-18 05:22:08 -05:00
|
|
|
if not value: value = None
|
2010-01-14 10:54:18 -06:00
|
|
|
else:
|
2014-03-18 05:22:08 -05:00
|
|
|
exec 'value = %s(value)' % type
|
2010-01-07 13:25:18 -06:00
|
|
|
# Get the "to" value
|
2014-03-18 05:22:08 -05:00
|
|
|
toValue = rq.form['%s_to' % name[2:]].strip()
|
2010-01-14 10:54:18 -06:00
|
|
|
if not toValue: toValue = None
|
|
|
|
else:
|
2014-03-18 05:22:08 -05:00
|
|
|
exec 'toValue = %s(toValue)' % type
|
|
|
|
value = (value, toValue)
|
|
|
|
elif type == 'date':
|
|
|
|
prefix = name[2:]
|
2010-01-07 13:25:18 -06:00
|
|
|
# Get the "from" value
|
2014-03-18 05:22:08 -05:00
|
|
|
year = value
|
2010-01-07 13:25:18 -06:00
|
|
|
month = rq.form['%s_from_month' % prefix]
|
|
|
|
day = rq.form['%s_from_day' % prefix]
|
|
|
|
fromDate = self._getDateTime(year, month, day, True)
|
|
|
|
# Get the "to" value"
|
|
|
|
year = rq.form['%s_to_year' % prefix]
|
|
|
|
month = rq.form['%s_to_month' % prefix]
|
|
|
|
day = rq.form['%s_to_day' % prefix]
|
|
|
|
toDate = self._getDateTime(year, month, day, False)
|
2014-03-18 05:22:08 -05:00
|
|
|
value = (fromDate, toDate)
|
|
|
|
elif type.startswith('string'):
|
2010-04-16 10:07:34 -05:00
|
|
|
# In the case of a string, it could be necessary to
|
|
|
|
# apply some text transform.
|
2014-03-18 05:22:08 -05:00
|
|
|
if len(type) > 6:
|
|
|
|
transform = type.split('-')[1]
|
|
|
|
if (transform != 'none') and value:
|
|
|
|
exec 'value = value.%s()' % \
|
2010-04-16 10:07:34 -05:00
|
|
|
self.transformMethods[transform]
|
2014-03-18 05:22:08 -05:00
|
|
|
if isinstance(value, list):
|
2010-03-19 07:13:36 -05:00
|
|
|
# It is a list of values. Check if we have an operator for
|
|
|
|
# the field, to see if we make an "and" or "or" for all
|
|
|
|
# those values. "or" will be the default.
|
2014-03-18 05:22:08 -05:00
|
|
|
operKey = 'o_%s' % name[2:]
|
2010-03-19 07:13:36 -05:00
|
|
|
oper = ' %s ' % rq.form.get(operKey, 'or').upper()
|
2014-03-18 05:22:08 -05:00
|
|
|
value = oper.join(value)
|
|
|
|
criteria[name[2:]] = value
|
2010-12-06 04:11:40 -06:00
|
|
|
# Complete criteria with Ref info if the search is restricted to
|
|
|
|
# referenced objects of a Ref field.
|
|
|
|
refInfo = rq.get('ref', None)
|
|
|
|
if refInfo: criteria['_ref'] = refInfo
|
2010-01-06 11:36:16 -06:00
|
|
|
rq.SESSION['searchCriteria'] = criteria
|
2013-04-09 03:57:21 -05:00
|
|
|
|
|
|
|
def onSearchObjects(self):
|
|
|
|
'''This method is called when the user triggers a search from
|
2013-08-21 15:25:27 -05:00
|
|
|
pxSearch.'''
|
2013-04-09 03:57:21 -05:00
|
|
|
rq = self.REQUEST
|
|
|
|
self.storeSearchCriteria()
|
2010-10-14 07:43:56 -05:00
|
|
|
# Go to the screen that displays search results
|
2013-08-21 05:35:30 -05:00
|
|
|
backUrl = '%s/query?className=%s&&search=customSearch' % \
|
2011-11-25 11:01:20 -06:00
|
|
|
(self.absolute_url(), rq['className'])
|
2009-12-30 10:12:18 -06:00
|
|
|
return self.goto(backUrl)
|
|
|
|
|
2009-10-20 09:57:00 -05:00
|
|
|
def getJavascriptMessages(self):
|
|
|
|
'''Returns the translated version of messages that must be shown in
|
|
|
|
Javascript popups.'''
|
|
|
|
res = ''
|
|
|
|
for msg in jsMessages:
|
|
|
|
res += 'var %s = "%s";\n' % (msg, self.translate(msg))
|
|
|
|
return res
|
2009-11-03 08:02:18 -06:00
|
|
|
|
2013-08-21 05:35:30 -05:00
|
|
|
def getColumnsSpecifiers(self, className, columnLayouts, dir):
|
|
|
|
'''Extracts and returns, from a list of p_columnLayouts, info required
|
|
|
|
for displaying columns of field values for instances of p_className,
|
|
|
|
either in a result screen or for a Ref field.'''
|
|
|
|
res = []
|
|
|
|
for info in columnLayouts:
|
|
|
|
fieldName, width, align = ColumnLayout(info).get()
|
|
|
|
align = self.flipLanguageDirection(align, dir)
|
|
|
|
field = self.getAppyType(fieldName, className)
|
|
|
|
if not field:
|
2014-09-01 07:14:32 -05:00
|
|
|
self.log('field "%s", used in a column specifier, was not ' \
|
2013-08-21 05:35:30 -05:00
|
|
|
'found.' % fieldName, type='warning')
|
|
|
|
else:
|
|
|
|
res.append(Object(field=field, width=width, align=align))
|
|
|
|
return res
|
|
|
|
|
2010-12-06 04:11:40 -06:00
|
|
|
def getRefInfo(self, refInfo=None):
|
|
|
|
'''When a search is restricted to objects referenced through a Ref
|
|
|
|
field, this method returns information about this reference: the
|
2013-08-21 05:35:30 -05:00
|
|
|
source class and the Ref field. If p_refInfo is not given, we search
|
|
|
|
it among search criteria in the session.'''
|
2012-12-12 10:26:01 -06:00
|
|
|
if not refInfo and (self.REQUEST.get('search', None) == 'customSearch'):
|
2010-12-06 04:11:40 -06:00
|
|
|
criteria = self.REQUEST.SESSION.get('searchCriteria', None)
|
|
|
|
if criteria and criteria.has_key('_ref'): refInfo = criteria['_ref']
|
2013-08-21 05:35:30 -05:00
|
|
|
if not refInfo: return None, None
|
2011-09-20 12:21:48 -05:00
|
|
|
objectUid, fieldName = refInfo.split(':')
|
|
|
|
obj = self.getObject(objectUid)
|
|
|
|
return obj, fieldName
|
2010-12-06 04:11:40 -06:00
|
|
|
|
2013-08-23 11:57:27 -05:00
|
|
|
def getGroupedSearches(self, klass):
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
'''Returns an object with 2 attributes:
|
2013-08-23 11:57:27 -05:00
|
|
|
* "searches" stores the searches that are defined for p_klass;
|
2012-11-06 04:32:39 -06:00
|
|
|
* "default" stores the search defined as the default one.
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
Every item representing a search is a dict containing info about a
|
|
|
|
search or about a group of searches.
|
|
|
|
'''
|
2012-11-14 04:36:48 -06:00
|
|
|
res = []
|
[gen] Added param Search.default allowing to define a default Search. The default search, if present, will be triggered when clicking on the main link for a class, instead of the query that collects all instances of this class; appy.gen.Type: removed 3 obsolete params: 'index', 'editDefault' and 'optional'. For achieving the same result than using 'editDefault', one may define 'by hand' an attribute on the Tool for storing the editable default value, and define, on the appropriate field in param 'default', a method that returns the value of the tool attribute; Added Type.defaultForSearch, allowing, for some sub-types, to define a default value when displaying the corresponding widget on the search screen; added a default 'state' field allowing to include workflow state among search criteria in the search screens; removed obsolete test applications.
2012-10-31 07:20:25 -05:00
|
|
|
default = None # Also retrieve the default one here.
|
2012-11-14 04:36:48 -06:00
|
|
|
groups = {} # The already encountered groups
|
2013-09-24 05:26:31 -05:00
|
|
|
page = Page('searches') # A dummy page required by class UiGroup
|
2013-06-07 09:37:00 -05:00
|
|
|
# Get the searches statically defined on the class
|
2013-09-24 05:26:31 -05:00
|
|
|
className = self.getPortalType(klass)
|
2013-08-23 11:57:27 -05:00
|
|
|
searches = ClassDescriptor.getSearches(klass, tool=self.appy())
|
2013-06-07 09:37:00 -05:00
|
|
|
# Get the dynamically computed searches
|
2013-08-23 11:57:27 -05:00
|
|
|
if hasattr(klass, 'getDynamicSearches'):
|
|
|
|
searches += klass.getDynamicSearches(self.appy())
|
2012-11-14 10:40:52 -06:00
|
|
|
for search in searches:
|
|
|
|
# Create the search descriptor
|
2013-08-21 05:35:30 -05:00
|
|
|
uiSearch = UiSearch(search, className, self)
|
2012-11-14 04:36:48 -06:00
|
|
|
if not search.group:
|
|
|
|
# Insert the search at the highest level, not in any group.
|
2013-08-21 05:35:30 -05:00
|
|
|
res.append(uiSearch)
|
2009-11-06 04:33:56 -06:00
|
|
|
else:
|
2013-08-21 05:35:30 -05:00
|
|
|
uiGroup = search.group.insertInto(res, groups, page, className,
|
2013-09-24 05:26:31 -05:00
|
|
|
content='searches')
|
|
|
|
uiGroup.addElement(uiSearch)
|
2012-11-14 04:36:48 -06:00
|
|
|
# Is this search the default search?
|
2013-08-21 05:35:30 -05:00
|
|
|
if search.default: default = uiSearch
|
|
|
|
return Object(searches=res, default=default)
|
2009-11-06 04:33:56 -06:00
|
|
|
|
2013-08-21 05:35:30 -05:00
|
|
|
def getSearch(self, className, name, ui=False):
|
|
|
|
'''Gets the Search instance (or a UiSearch instance if p_ui is True)
|
|
|
|
corresponding to the search named p_name, on class p_className.'''
|
2014-07-25 08:07:31 -05:00
|
|
|
initiator = None
|
2012-12-12 10:26:01 -06:00
|
|
|
if name == 'customSearch':
|
|
|
|
# It is a custom search whose parameters are in the session.
|
2012-11-14 10:40:52 -06:00
|
|
|
fields = self.REQUEST.SESSION['searchCriteria']
|
|
|
|
res = Search('customSearch', **fields)
|
2014-07-18 09:54:11 -05:00
|
|
|
elif ':' in name:
|
2014-07-25 08:07:31 -05:00
|
|
|
# The search is defined in a Ref field with link=popup. Get the
|
|
|
|
# search, the initiator object and the Ref field.
|
2014-07-28 05:29:16 -05:00
|
|
|
uid, ref, mode = name.split(':')
|
2014-07-25 08:07:31 -05:00
|
|
|
initiator = self.getObject(uid, appy=True)
|
|
|
|
initiatorField = initiator.getField(ref)
|
|
|
|
res = getattr(initiator.klass, ref).select
|
2012-11-14 10:40:52 -06:00
|
|
|
elif name:
|
|
|
|
appyClass = self.getAppyClass(className)
|
|
|
|
# Search among static searches
|
|
|
|
res = ClassDescriptor.getSearch(appyClass, name)
|
|
|
|
if not res and hasattr(appyClass, 'getDynamicSearches'):
|
|
|
|
# Search among dynamic searches
|
|
|
|
for search in appyClass.getDynamicSearches(self.appy()):
|
|
|
|
if search.name == name:
|
|
|
|
res = search
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
# It is the search for every instance of p_className
|
|
|
|
res = Search('allSearch')
|
2013-08-21 05:35:30 -05:00
|
|
|
# Return a UiSearch if required.
|
2014-07-25 08:07:31 -05:00
|
|
|
if ui:
|
|
|
|
res = UiSearch(res, className, self)
|
2014-07-28 05:29:16 -05:00
|
|
|
if initiator: res.setInitiator(initiator, initiatorField, mode)
|
2012-11-14 10:40:52 -06:00
|
|
|
return res
|
|
|
|
|
2013-08-23 11:57:27 -05:00
|
|
|
def advancedSearchEnabledFor(self, klass):
|
2013-06-07 09:37:00 -05:00
|
|
|
'''Is advanced search visible for p_klass ?'''
|
|
|
|
# By default, advanced search is enabled.
|
|
|
|
if not hasattr(klass, 'searchAdvanced'): return True
|
|
|
|
# Evaluate attribute "show" on this Search instance representing the
|
|
|
|
# advanced search.
|
|
|
|
return klass.searchAdvanced.isShowable(klass, self.appy())
|
|
|
|
|
2014-04-29 12:02:06 -05:00
|
|
|
def portletBottom(self, klass):
|
|
|
|
'''Is there a custom zone to display at the bottom of the portlet zone
|
|
|
|
for p_klass?'''
|
|
|
|
if not hasattr(klass, 'getPortletBottom'): return ''
|
|
|
|
res = klass.getPortletBottom(self.appy())
|
|
|
|
if not res: return ''
|
|
|
|
return res
|
|
|
|
|
2014-10-21 02:25:37 -05:00
|
|
|
def getNavigationInfo(self, nav, inPopup):
|
|
|
|
'''Produces a Siblings instance from navigation info p_nav.'''
|
|
|
|
return Siblings.get(nav, self, inPopup)
|
2010-01-06 11:36:16 -06:00
|
|
|
|
2012-12-14 02:23:33 -06:00
|
|
|
def getGroupedSearchFields(self, searchInfo):
|
2013-08-21 05:35:30 -05:00
|
|
|
'''This method transforms p_searchInfo.fields, which is a "flat"
|
2012-12-14 02:23:33 -06:00
|
|
|
list of fields, into a list of lists, where every sub-list having
|
2013-08-21 05:35:30 -05:00
|
|
|
length p_searchInfo.nbOfColumns. For every field, scolspan
|
2012-12-14 02:23:33 -06:00
|
|
|
(=colspan "for search") is taken into account.'''
|
2010-08-05 11:23:17 -05:00
|
|
|
res = []
|
|
|
|
row = []
|
2012-12-14 02:23:33 -06:00
|
|
|
rowLength = 0
|
2013-08-21 05:35:30 -05:00
|
|
|
for field in searchInfo.fields:
|
2012-12-14 02:23:33 -06:00
|
|
|
# Can I insert this field in the current row?
|
2013-08-21 05:35:30 -05:00
|
|
|
remaining = searchInfo.nbOfColumns - rowLength
|
|
|
|
if field.scolspan <= remaining:
|
2012-12-14 02:23:33 -06:00
|
|
|
# Yes.
|
|
|
|
row.append(field)
|
2013-08-21 05:35:30 -05:00
|
|
|
rowLength += field.scolspan
|
2012-12-14 02:23:33 -06:00
|
|
|
else:
|
|
|
|
# We must put the field on a new line. Complete the current one
|
|
|
|
# if not complete.
|
2013-08-21 05:35:30 -05:00
|
|
|
while rowLength < searchInfo.nbOfColumns:
|
2012-12-14 02:23:33 -06:00
|
|
|
row.append(None)
|
|
|
|
rowLength += 1
|
2010-01-06 11:36:16 -06:00
|
|
|
res.append(row)
|
2012-12-14 02:23:33 -06:00
|
|
|
row = [field]
|
2013-08-21 05:35:30 -05:00
|
|
|
rowLength = field.scolspan
|
2010-08-05 11:23:17 -05:00
|
|
|
# Complete the last unfinished line if required.
|
|
|
|
if row:
|
2013-08-21 05:35:30 -05:00
|
|
|
while rowLength < searchInfo.nbOfColumns:
|
2012-12-14 02:23:33 -06:00
|
|
|
row.append(None)
|
|
|
|
rowLength += 1
|
2010-08-05 11:23:17 -05:00
|
|
|
res.append(row)
|
|
|
|
return res
|
2010-01-06 11:36:16 -06:00
|
|
|
|
2011-11-25 11:01:20 -06:00
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
# Authentication-related methods
|
|
|
|
# --------------------------------------------------------------------------
|
2013-09-09 08:54:06 -05:00
|
|
|
def identifyUser(self, alsoSpecial=False):
|
|
|
|
'''To identify a user means: get its login and password. There are
|
|
|
|
several places to look for this information: http authentication,
|
|
|
|
cookie of credentials coming from the web form.
|
|
|
|
|
|
|
|
If no user could be identified, and p_alsoSpecial is True, we will
|
|
|
|
nevertheless identify a "special user": "system", representing the
|
|
|
|
system itself (running at startup or in batch mode) or "anon",
|
|
|
|
representing an anonymous user.'''
|
2013-09-06 09:19:56 -05:00
|
|
|
tool = self.appy()
|
|
|
|
req = tool.request
|
|
|
|
login = password = None
|
2013-09-09 08:54:06 -05:00
|
|
|
# a. Identify the user from http basic authentication.
|
2013-09-06 09:19:56 -05:00
|
|
|
if getattr(req, '_auth', None):
|
|
|
|
# HTTP basic authentication credentials are present (used when
|
|
|
|
# connecting to the ZMI). Decode it.
|
|
|
|
creds = req._auth
|
|
|
|
if creds.lower().startswith('basic '):
|
|
|
|
try:
|
|
|
|
creds = creds.split(' ')[-1]
|
|
|
|
login, password = base64.decodestring(creds).split(':', 1)
|
|
|
|
except Exception, e:
|
|
|
|
pass
|
2013-09-09 08:54:06 -05:00
|
|
|
# b. Identify the user from the authentication cookie.
|
2013-09-06 09:19:56 -05:00
|
|
|
if not login:
|
|
|
|
login, password = gutils.readCookie(req)
|
2013-09-09 08:54:06 -05:00
|
|
|
# c. Identify the user from the authentication form.
|
2013-09-06 09:19:56 -05:00
|
|
|
if not login:
|
|
|
|
login = req.get('__ac_name', None)
|
2014-05-19 05:12:33 -05:00
|
|
|
password = req.get('__ac_password', '')
|
2013-09-09 08:54:06 -05:00
|
|
|
# Stop identification here if we don't need to return a special user
|
|
|
|
if not alsoSpecial: return login, password
|
|
|
|
# d. All the identification methods failed. So identify the user as
|
2013-09-06 09:19:56 -05:00
|
|
|
# "anon" or "system".
|
2013-09-09 08:54:06 -05:00
|
|
|
if not login:
|
2014-03-18 06:11:21 -05:00
|
|
|
# If we have a fake request, we are at startup or in batch mode and
|
|
|
|
# the user is "system". Else, it is "anon". At Zope startup, Appy
|
|
|
|
# uses an Object instance as a fake request. In "zopectl run" mode
|
|
|
|
# (the Zope batch mode), Appy adds a param "_fake_" on the request
|
|
|
|
# object created by Zope.
|
|
|
|
if (req.__class__.__name__ == 'Object') or \
|
|
|
|
(hasattr(req, '_fake_') and req._fake_):
|
|
|
|
login = 'system'
|
|
|
|
else:
|
|
|
|
login = 'anon'
|
2013-09-09 08:54:06 -05:00
|
|
|
return login, password
|
2013-07-24 08:53:19 -05:00
|
|
|
|
2013-09-09 08:54:06 -05:00
|
|
|
def getLdapUser(self, login, password):
|
|
|
|
'''Returns a local User instance corresponding to a LDAP user if p_login
|
|
|
|
and p_password correspong to a valid LDAP user.'''
|
2014-11-26 03:49:59 -06:00
|
|
|
# Check if LDAP is configured
|
2013-09-06 09:19:56 -05:00
|
|
|
cfg = self.getProductConfig(True).ldap
|
2014-11-26 03:49:59 -06:00
|
|
|
if not cfg or not cfg.enabled: return
|
|
|
|
# Get a connector to the LDAP server and connect to the LDAP server
|
2013-09-06 09:19:56 -05:00
|
|
|
serverUri = cfg.getServerUri()
|
|
|
|
connector = LdapConnector(serverUri, tool=self)
|
|
|
|
success, msg = connector.connect(cfg.adminLogin, cfg.adminPassword)
|
|
|
|
if not success: return
|
|
|
|
# Check if the user corresponding to p_login exists in the LDAP.
|
|
|
|
filter = connector.getFilter(cfg.getUserFilterValues(login))
|
|
|
|
params = cfg.getUserAttributes()
|
|
|
|
ldapData = connector.search(cfg.baseDn, cfg.scope, filter, params)
|
|
|
|
if not ldapData: return
|
|
|
|
# The user exists. Try to connect to the LDAP with this user in order
|
|
|
|
# to validate its password.
|
|
|
|
userConnector = LdapConnector(serverUri, tool=self)
|
|
|
|
success, msg = userConnector.connect(ldapData[0][0], password)
|
|
|
|
if not success: return
|
|
|
|
# The password is correct. We can create/update our local user
|
|
|
|
# corresponding to this LDAP user.
|
2013-09-09 16:14:50 -05:00
|
|
|
userParams = cfg.getUserParams(ldapData[0][1])
|
2013-09-09 08:54:06 -05:00
|
|
|
tool = self.appy()
|
|
|
|
user = tool.search1('User', noSecurity=True, login=login)
|
2013-09-06 09:19:56 -05:00
|
|
|
if user:
|
|
|
|
# Update the user with fresh info about him from the LDAP
|
|
|
|
for name, value in userParams.iteritems():
|
|
|
|
setattr(user, name, value)
|
2013-09-09 16:14:50 -05:00
|
|
|
# Update user password
|
|
|
|
user.setPassword(password, log=False)
|
2013-09-06 09:19:56 -05:00
|
|
|
user.reindex()
|
|
|
|
else:
|
|
|
|
# Create the user
|
2013-09-09 16:14:50 -05:00
|
|
|
user = tool.create('users', noSecurity=True, login=login,
|
|
|
|
password1=password, source='ldap', **userParams)
|
2013-09-06 09:19:56 -05:00
|
|
|
return user
|
2013-07-24 08:53:19 -05:00
|
|
|
|
2013-09-09 08:54:06 -05:00
|
|
|
def getUser(self, authentify=False, source='zodb'):
|
|
|
|
'''Gets the current user. If p_authentify is True, in addition to
|
|
|
|
finding the logged user and returning it (=identification), we check
|
|
|
|
if found credentials are valid (=authentification).
|
|
|
|
|
|
|
|
If p_authentify is True and p_source is "zodb", authentication is
|
|
|
|
performed locally. Else (p_source is "ldap"), authentication is
|
2013-09-09 16:14:50 -05:00
|
|
|
performed on a LDAP (if a LDAP configuration is found). If p_source
|
|
|
|
is "any", authentication is performed on the local User object, be it
|
|
|
|
really local or a copy of a LDAP user.'''
|
2013-09-09 08:54:06 -05:00
|
|
|
tool = self.appy()
|
|
|
|
req = tool.request
|
|
|
|
# Try first to return the user that can be cached on the request. In
|
|
|
|
# this case, we suppose authentication has previously been done, and we
|
|
|
|
# just return the cached user.
|
|
|
|
if hasattr(req, 'user'): return req.user
|
|
|
|
# Identify the user (=find its login and password). If we don't need
|
|
|
|
# to authentify the user, we ask to identify a user or, if impossible,
|
|
|
|
# a special user.
|
|
|
|
login, password = self.identifyUser(alsoSpecial=not authentify)
|
2014-12-08 07:52:04 -06:00
|
|
|
# Stop here if no user was found and authentication was required
|
2013-09-09 08:54:06 -05:00
|
|
|
if authentify and not login: return
|
2014-12-08 07:52:04 -06:00
|
|
|
# Now, get the User instance
|
2013-09-09 08:54:06 -05:00
|
|
|
if source == 'zodb':
|
2013-09-09 16:14:50 -05:00
|
|
|
# Get the User object, but only if it is a true local user.
|
2013-09-09 08:54:06 -05:00
|
|
|
user = tool.search1('User', noSecurity=True, login=login)
|
2013-09-09 16:14:50 -05:00
|
|
|
if user and (user.source != 'zodb'): user = None # Not a local one.
|
2013-09-09 08:54:06 -05:00
|
|
|
elif source == 'ldap':
|
|
|
|
user = self.getLdapUser(login, password)
|
2013-09-09 16:14:50 -05:00
|
|
|
elif source == 'any':
|
|
|
|
# Get the user object, be it really local or a copy of a LDAP user.
|
|
|
|
user = tool.search1('User', noSecurity=True, login=login)
|
2013-09-09 08:54:06 -05:00
|
|
|
if not user: return
|
2014-12-08 07:52:04 -06:00
|
|
|
# Authentify the user if required
|
2013-09-09 08:54:06 -05:00
|
|
|
if authentify:
|
2014-09-11 09:41:08 -05:00
|
|
|
if (user.state == 'inactive') or (not user.checkPassword(password)):
|
2014-12-08 07:52:04 -06:00
|
|
|
# Disable the authentication cookie and remove credentials
|
|
|
|
# stored on the request.
|
2013-09-09 08:54:06 -05:00
|
|
|
req.RESPONSE.expireCookie('_appy_', path='/')
|
2014-12-08 07:52:04 -06:00
|
|
|
k = 'HTTP_AUTHORIZATION'
|
|
|
|
req._auth = req[k] = req._orig_env[k] = None
|
2013-09-09 08:54:06 -05:00
|
|
|
return
|
|
|
|
# Create an authentication cookie for this user.
|
|
|
|
gutils.writeCookie(login, password, req)
|
|
|
|
# Cache the user and some precomputed values, for performance.
|
|
|
|
req.user = user
|
|
|
|
req.userRoles = user.getRoles()
|
|
|
|
req.userLogins = user.getLogins()
|
|
|
|
req.zopeUser = user.getZopeUser()
|
|
|
|
return user
|
|
|
|
|
2011-02-15 08:59:55 -06:00
|
|
|
def performLogin(self):
|
|
|
|
'''Logs the user in.'''
|
|
|
|
rq = self.REQUEST
|
|
|
|
jsEnabled = rq.get('js_enabled', False) in ('1', 1)
|
|
|
|
cookiesEnabled = rq.get('cookies_enabled', False) in ('1', 1)
|
|
|
|
urlBack = rq['HTTP_REFERER']
|
|
|
|
if jsEnabled and not cookiesEnabled:
|
2012-06-03 11:34:56 -05:00
|
|
|
msg = self.translate('enable_cookies')
|
2011-12-08 09:01:57 -06:00
|
|
|
return self.goto(urlBack, msg)
|
2014-12-08 07:52:04 -06:00
|
|
|
# Authenticate the user
|
2013-09-09 08:54:06 -05:00
|
|
|
if self.getUser(authentify=True) or \
|
|
|
|
self.getUser(authentify=True, source='ldap'):
|
2012-06-03 11:34:56 -05:00
|
|
|
msg = self.translate('login_ok')
|
2014-09-01 07:14:32 -05:00
|
|
|
logMsg = 'logged in.'
|
2013-07-24 08:53:19 -05:00
|
|
|
else:
|
|
|
|
msg = self.translate('login_ko')
|
2014-09-01 07:14:32 -05:00
|
|
|
login = rq.get('__ac_name') or '<empty>'
|
|
|
|
logMsg = 'authentication failed with login %s.' % login
|
2011-02-15 08:59:55 -06:00
|
|
|
self.log(logMsg)
|
2012-02-18 12:48:00 -06:00
|
|
|
return self.goto(self.getApp().absolute_url(), msg)
|
2011-02-15 08:59:55 -06:00
|
|
|
|
|
|
|
def performLogout(self):
|
2010-01-17 15:01:14 -06:00
|
|
|
'''Logs out the current user when he clicks on "disconnect".'''
|
|
|
|
rq = self.REQUEST
|
2013-08-21 05:35:30 -05:00
|
|
|
userId = self.getUser().login
|
2010-01-17 15:01:14 -06:00
|
|
|
# Perform the logout in acl_users
|
2013-08-21 05:35:30 -05:00
|
|
|
rq.RESPONSE.expireCookie('_appy_', path='/')
|
2013-09-09 08:54:06 -05:00
|
|
|
# Invalidate the user session.
|
2012-06-02 07:36:49 -05:00
|
|
|
try:
|
|
|
|
sdm = self.session_data_manager
|
|
|
|
except AttributeError, ae:
|
|
|
|
# When ran in test mode, session_data_manager is not there.
|
|
|
|
sdm = None
|
|
|
|
if sdm:
|
|
|
|
session = sdm.getSessionData(create=0)
|
|
|
|
if session is not None:
|
|
|
|
session.invalidate()
|
2014-09-01 07:14:32 -05:00
|
|
|
self.log('logged out.')
|
2010-01-17 15:01:14 -06:00
|
|
|
# Remove user from variable "loggedUsers"
|
2013-09-09 16:14:50 -05:00
|
|
|
if self.loggedUsers.has_key(userId): del self.loggedUsers[userId]
|
2011-11-25 11:01:20 -06:00
|
|
|
return self.goto(self.getApp().absolute_url())
|
|
|
|
|
2013-09-09 16:14:50 -05:00
|
|
|
# This dict stores, for every logged user, the date/time of its last access
|
|
|
|
loggedUsers = {}
|
|
|
|
forgetAccessExtensions = ('.jpg', '.gif', '.png', '.js', '.css')
|
|
|
|
def rememberAccess(self, id, user):
|
|
|
|
'''Every time there is a hit on the server, this method is called in
|
|
|
|
order to update global dict loggedUsers (see above).'''
|
|
|
|
if not id: return
|
|
|
|
if os.path.splitext(id)[-1].lower() in self.forgetAccessExtensions:
|
|
|
|
return
|
|
|
|
self.loggedUsers[user.login] = time.time()
|
|
|
|
# "Touch" the SESSION object. Else, expiration won't occur.
|
|
|
|
session = self.REQUEST.SESSION
|
|
|
|
|
2011-11-25 11:01:20 -06:00
|
|
|
def validate(self, request, auth='', roles=_noroles):
|
|
|
|
'''This method performs authentication and authorization. It is used as
|
|
|
|
a replacement for Zope's AccessControl.User.BasicUserFolder.validate,
|
|
|
|
that allows to manage cookie-based authentication.'''
|
|
|
|
v = request['PUBLISHED'] # The published object
|
2013-09-09 16:14:50 -05:00
|
|
|
tool = self.getParentNode().config
|
2011-11-25 11:01:20 -06:00
|
|
|
# v is the object (value) we're validating access to
|
|
|
|
# n is the name used to access the object
|
|
|
|
# a is the object the object was accessed through
|
|
|
|
# c is the physical container of the object
|
|
|
|
a, c, n, v = self._getobcontext(v, request)
|
2013-09-06 09:19:56 -05:00
|
|
|
# Identify and authentify the user
|
2013-09-09 16:14:50 -05:00
|
|
|
user = tool.getUser(authentify=True, source='any')
|
2013-09-06 09:19:56 -05:00
|
|
|
if not user:
|
2011-11-25 11:01:20 -06:00
|
|
|
# Login and/or password incorrect. Try to authorize and return the
|
|
|
|
# anonymous user.
|
|
|
|
if self.authorize(self._nobody, a, c, n, v, roles):
|
|
|
|
return self._nobody.__of__(self)
|
|
|
|
else:
|
2013-09-06 09:19:56 -05:00
|
|
|
return
|
2011-11-25 11:01:20 -06:00
|
|
|
else:
|
|
|
|
# We found a user and his password was correct. Try to authorize him
|
2013-09-09 16:14:50 -05:00
|
|
|
# against the published object. By the way, remember its last access
|
|
|
|
# to this system.
|
|
|
|
tool.rememberAccess(a.getId(), user)
|
2013-09-06 09:19:56 -05:00
|
|
|
user = user.getZopeUser()
|
2011-11-25 11:01:20 -06:00
|
|
|
if self.authorize(user, a, c, n, v, roles):
|
|
|
|
return user.__of__(self)
|
2013-08-08 05:00:33 -05:00
|
|
|
# That didn't work. Try to authorize the anonymous user.
|
2011-11-25 11:01:20 -06:00
|
|
|
elif self.authorize(self._nobody, a, c, n, v, roles):
|
|
|
|
return self._nobody.__of__(self)
|
|
|
|
else:
|
|
|
|
return
|
|
|
|
|
|
|
|
# Patch BasicUserFolder with our version of m_validate above.
|
|
|
|
from AccessControl.User import BasicUserFolder
|
|
|
|
BasicUserFolder.validate = validate
|
2010-08-05 11:23:17 -05:00
|
|
|
|
2013-01-07 08:30:13 -06:00
|
|
|
def getUserName(self, login=None, normalized=False):
|
2012-07-26 10:22:22 -05:00
|
|
|
'''Gets the user name corresponding to p_login (or the currently logged
|
2013-08-23 11:57:27 -05:00
|
|
|
user if None), or the p_login itself if the user does not exist
|
2013-01-07 08:30:13 -06:00
|
|
|
anymore. If p_normalized is True, special chars in the first and last
|
|
|
|
names are normalized.'''
|
2012-07-26 10:22:22 -05:00
|
|
|
tool = self.appy()
|
2014-09-29 03:06:40 -05:00
|
|
|
if not login:
|
|
|
|
user = tool.user
|
|
|
|
else:
|
|
|
|
user = tool.search1('User', noSecurity=True, login=login)
|
2014-10-01 09:21:12 -05:00
|
|
|
if not user: return login
|
2014-09-29 03:06:40 -05:00
|
|
|
return user.getTitle(normalized=normalized)
|
2012-07-18 14:58:11 -05:00
|
|
|
|
2013-08-21 05:35:30 -05:00
|
|
|
def tempFile(self):
|
|
|
|
'''A temp file has been created in a temp folder. This method returns
|
|
|
|
this file to the browser.'''
|
|
|
|
rq = self.REQUEST
|
|
|
|
baseFolder = os.path.join(sutils.getOsTempFolder(), self.getAppName())
|
|
|
|
baseFolder = os.path.join(baseFolder, rq.SESSION.id)
|
|
|
|
fileName = os.path.join(baseFolder, rq.get('name', ''))
|
|
|
|
if os.path.exists(fileName):
|
|
|
|
f = file(fileName)
|
|
|
|
content = f.read()
|
|
|
|
f.close()
|
|
|
|
# Remove the temp file
|
|
|
|
os.remove(fileName)
|
|
|
|
return content
|
|
|
|
return 'File does not exist'
|
|
|
|
|
|
|
|
def getResultPodFields(self, contentType):
|
|
|
|
'''Finds, among fields defined on p_contentType, which ones are Pod
|
|
|
|
fields that need to be shown on a page displaying query results.'''
|
|
|
|
# Skip this if we are searching multiple content types.
|
|
|
|
if ',' in contentType: return ()
|
2014-02-27 05:54:05 -06:00
|
|
|
return [f for f in self.getAllAppyTypes(contentType) \
|
2013-08-21 05:35:30 -05:00
|
|
|
if (f.type == 'Pod') and (f.show == 'result')]
|
2013-09-09 16:14:50 -05:00
|
|
|
|
2014-09-05 10:13:23 -05:00
|
|
|
def formatDate(self, date, format=None, withHour=True, language=None):
|
|
|
|
'''Returns p_date formatted as specified by p_format, or tool.dateFormat
|
|
|
|
if not specified. If p_withHour is True, hour is appended, with a
|
|
|
|
format specified in tool.hourFormat.'''
|
2012-07-18 14:58:11 -05:00
|
|
|
tool = self.appy()
|
2014-09-05 10:13:23 -05:00
|
|
|
fmt = format or tool.dateFormat
|
|
|
|
# Resolve appy-specific formatting symbols used for getting translated
|
|
|
|
# names of days or months:
|
|
|
|
# - %dt: translated name of day
|
|
|
|
# - %DT: translated name of day, capitalized
|
|
|
|
# - %mt: translated name of month
|
|
|
|
# - %MT: translated name of month, capitalized
|
|
|
|
if ('%dt' in fmt) or ('%DT' in fmt):
|
|
|
|
day = self.translate('day_%s' % date._aday, language=language)
|
|
|
|
fmt = fmt.replace('%dt', day.lower()).replace('%DT', day)
|
|
|
|
if ('%mt' in fmt) or ('%MT' in fmt):
|
|
|
|
month = self.translate('month_%s' % date._amon, language=language)
|
|
|
|
fmt = fmt.replace('%mt', month.lower()).replace('%MT', month)
|
|
|
|
# Resolve all other, standard, symbols
|
|
|
|
res = date.strftime(fmt)
|
|
|
|
# Append hour from tool.hourFormat
|
2014-05-02 05:35:09 -05:00
|
|
|
if withHour: res += ' (%s)' % date.strftime(tool.hourFormat)
|
2012-07-18 14:58:11 -05:00
|
|
|
return res
|
|
|
|
|
2011-11-25 11:01:20 -06:00
|
|
|
def generateUid(self, className):
|
|
|
|
'''Generates a UID for an instance of p_className.'''
|
2012-04-24 09:22:12 -05:00
|
|
|
name = className.split('_')[-1]
|
2014-10-07 06:14:16 -05:00
|
|
|
randomNumber = str(random.random()).split('.')[1].replace('e-', '')
|
2011-11-25 11:01:20 -06:00
|
|
|
timestamp = ('%f' % time.time()).replace('.', '')
|
|
|
|
return '%s%s%s' % (name, timestamp, randomNumber)
|
2011-12-05 11:15:45 -06:00
|
|
|
|
|
|
|
def manageError(self, error):
|
|
|
|
'''Manages an error.'''
|
|
|
|
tb = sys.exc_info()
|
2013-05-31 02:59:25 -05:00
|
|
|
if error.type.__name__ == 'Unauthorized':
|
|
|
|
siteUrl = self.getSiteUrl()
|
2014-03-05 15:01:51 -06:00
|
|
|
htmlMessage = '<a href="/">Back</a> You are not allowed to ' \
|
|
|
|
'access this page.'
|
2013-08-21 05:35:30 -05:00
|
|
|
userId = self.appy().user.login
|
2014-09-01 07:14:32 -05:00
|
|
|
textMessage = 'unauthorized for %s @%s.' % \
|
2013-05-31 02:59:25 -05:00
|
|
|
(userId, self.REQUEST.get('PATH_INFO'))
|
|
|
|
else:
|
|
|
|
from zExceptions.ExceptionFormatter import format_exception
|
|
|
|
htmlMessage = format_exception(tb[0], tb[1], tb[2], as_html=1)
|
|
|
|
htmlMessage = '\n'.join(htmlMessage)
|
|
|
|
textMessage = format_exception(tb[0], tb[1], tb[2], as_html=0)
|
|
|
|
textMessage = ''.join(textMessage).strip()
|
|
|
|
self.log(textMessage, type='error')
|
2013-05-31 08:23:28 -05:00
|
|
|
return '<div class="error">%s</div>' % htmlMessage
|
2012-03-26 12:09:45 -05:00
|
|
|
|
|
|
|
def getMainPages(self):
|
|
|
|
'''Returns the main pages.'''
|
|
|
|
if hasattr(self.o.aq_base, 'pages') and self.o.pages:
|
|
|
|
return [self.getObject(uid) for uid in self.o.pages ]
|
|
|
|
return ()
|
2012-07-09 08:47:38 -05:00
|
|
|
|
|
|
|
def askPasswordReinit(self):
|
|
|
|
'''A user (anonymmous) does not remember its password. Here we will
|
|
|
|
send him a mail containing a link that will trigger password
|
|
|
|
re-initialisation.'''
|
|
|
|
login = self.REQUEST.get('login').strip()
|
|
|
|
appyTool = self.appy()
|
|
|
|
user = appyTool.search1('User', login=login, noSecurity=True)
|
|
|
|
msg = self.translate('reinit_mail_sent')
|
|
|
|
backUrl = self.REQUEST['HTTP_REFERER']
|
|
|
|
if not user:
|
|
|
|
# Return the message nevertheless. This way, malicious users can't
|
|
|
|
# deduce information about existing users.
|
|
|
|
return self.goto(backUrl, msg)
|
|
|
|
# If login is an email, use it. Else, use user.email instead.
|
|
|
|
email = user.login
|
|
|
|
if not String.EMAIL.match(email):
|
|
|
|
email = user.email
|
|
|
|
if not email:
|
|
|
|
# Impossible to re-initialise the password.
|
|
|
|
return self.goto(backUrl, msg)
|
|
|
|
# Create a temporary file whose name is the user login and whose
|
|
|
|
# content is a generated token.
|
2013-08-21 05:35:30 -05:00
|
|
|
f = file(os.path.join(sutils.getOsTempFolder(), login), 'w')
|
2012-07-09 08:47:38 -05:00
|
|
|
token = String().generatePassword()
|
|
|
|
f.write(token)
|
|
|
|
f.close()
|
|
|
|
# Send an email
|
|
|
|
initUrl = '%s/doPasswordReinit?login=%s&token=%s' % \
|
|
|
|
(self.absolute_url(), login, token)
|
|
|
|
subject = self.translate('reinit_password')
|
|
|
|
map = {'url':initUrl, 'siteUrl':self.getSiteUrl()}
|
|
|
|
body= self.translate('reinit_password_body', mapping=map, format='text')
|
2014-09-18 04:08:29 -05:00
|
|
|
appyTool.sendMail(email, subject, body)
|
2012-07-09 08:47:38 -05:00
|
|
|
return self.goto(backUrl, msg)
|
|
|
|
|
|
|
|
def doPasswordReinit(self):
|
|
|
|
'''Performs the password re-initialisation.'''
|
|
|
|
rq = self.REQUEST
|
|
|
|
login = rq['login']
|
|
|
|
token = rq['token']
|
|
|
|
# Check if such token exists in temp folder
|
2012-09-19 10:48:49 -05:00
|
|
|
res = None
|
|
|
|
siteUrl = self.getSiteUrl()
|
2013-08-21 05:35:30 -05:00
|
|
|
tokenFile = os.path.join(sutils.getOsTempFolder(), login)
|
2012-07-09 08:47:38 -05:00
|
|
|
if os.path.exists(tokenFile):
|
|
|
|
f = file(tokenFile)
|
|
|
|
storedToken = f.read()
|
|
|
|
f.close()
|
|
|
|
if storedToken == token:
|
|
|
|
# Generate a new password for this user
|
|
|
|
appyTool = self.appy()
|
|
|
|
user = appyTool.search1('User', login=login, noSecurity=True)
|
|
|
|
newPassword = user.setPassword()
|
|
|
|
# Send the new password by email
|
|
|
|
email = login
|
|
|
|
if not String.EMAIL.match(email):
|
|
|
|
email = user.email
|
|
|
|
subject = self.translate('new_password')
|
|
|
|
map = {'password': newPassword, 'siteUrl': siteUrl}
|
|
|
|
body = self.translate('new_password_body', mapping=map,
|
|
|
|
format='text')
|
2014-09-18 04:08:29 -05:00
|
|
|
appyTool.sendMail(email, subject, body)
|
2012-07-09 08:47:38 -05:00
|
|
|
os.remove(tokenFile)
|
2012-09-19 10:48:49 -05:00
|
|
|
res = self.goto(siteUrl, self.translate('new_password_sent'))
|
|
|
|
if not res:
|
|
|
|
res = self.goto(siteUrl, self.translate('wrong_password_reinit'))
|
|
|
|
return res
|
2012-09-17 14:11:54 -05:00
|
|
|
|
2013-04-29 14:32:05 -05:00
|
|
|
def getGoogleAnalyticsCode(self):
|
|
|
|
'''If the config defined a Google Analytics ID, this method returns the
|
|
|
|
Javascript code to be included in every page, allowing Google
|
|
|
|
Analytics to work.'''
|
|
|
|
# Disable Google Analytics when we are in debug mode.
|
|
|
|
if self.isDebug(): return
|
|
|
|
# Disable Google Analytics if no ID is found in the config.
|
2013-07-24 08:53:19 -05:00
|
|
|
gaId = self.getProductConfig(True).googleAnalyticsId
|
2013-04-29 14:37:55 -05:00
|
|
|
if not gaId: return
|
2013-04-29 14:32:05 -05:00
|
|
|
# Google Analytics must be enabled: return the chunk of Javascript
|
|
|
|
# code specified by Google.
|
|
|
|
code = "var _gaq = _gaq || [];\n" \
|
|
|
|
"_gaq.push(['_setAccount', '%s']);\n" \
|
|
|
|
"_gaq.push(['_trackPageview']);\n" \
|
|
|
|
"(function() {\n" \
|
|
|
|
" var ga = document.createElement('script'); " \
|
|
|
|
"ga.type = 'text/javascript'; ga.async = true;\n" \
|
|
|
|
" ga.src = ('https:' == document.location.protocol ? " \
|
|
|
|
"'https://ssl' : 'http://www') + " \
|
|
|
|
"'.google-analytics.com/ga.js';\n" \
|
|
|
|
" var s = document.getElementsByTagName('script')[0]; " \
|
|
|
|
"s.parentNode.insertBefore(ga, s);\n" \
|
|
|
|
"})();\n" % gaId
|
|
|
|
return code
|
2014-04-20 12:22:40 -05:00
|
|
|
|
|
|
|
def getButtonWidth(self, label):
|
|
|
|
'''Determine button width, in pixels, corresponding to the button
|
|
|
|
p_label.'''
|
2014-05-16 05:58:53 -05:00
|
|
|
# Set a minimum width for small labels.
|
|
|
|
if len(label) < 15: return 'width:130px'
|
|
|
|
return 'padding-left: 26px; padding-right: 8px'
|
2014-06-15 17:58:45 -05:00
|
|
|
|
|
|
|
def getLinksTargetInfo(self, klass):
|
|
|
|
'''Appy allows to open links to view or edit instances of p_klass
|
|
|
|
either via the same browser window, or via a popup. This method
|
|
|
|
returns info about that, as an object having 2 attributes:
|
|
|
|
- target is "_self" if the link leads to the same browser window,
|
|
|
|
"appyIFrame" if the link must be opened in a popup;
|
|
|
|
- openPopup is unused if target is "_self" and contains the
|
|
|
|
Javascript code to open the popup.'''
|
|
|
|
res = Object(target='_self', openPopup='')
|
|
|
|
if hasattr(klass, 'popup'):
|
|
|
|
res.target = 'appyIFrame'
|
|
|
|
d = klass.popup
|
|
|
|
if isinstance(d, basestring):
|
|
|
|
# Width only
|
|
|
|
params = int(d[:-2])
|
|
|
|
else:
|
|
|
|
# Width and height
|
|
|
|
params = "%s, %s" % (d[0][:-2], d[1][:-2])
|
|
|
|
res.openPopup = "openPopup('iframePopup',null,%s)" % params
|
|
|
|
return res
|
|
|
|
|
|
|
|
def backFromPopup(self):
|
|
|
|
'''Returns the PX allowing to close the iframe popup and refresh the
|
|
|
|
base page.'''
|
|
|
|
return self.appy().pxBack({'ztool': self})
|
2014-07-28 10:35:49 -05:00
|
|
|
|
|
|
|
ieRex = re.compile('MSIE\s+(\d\.\d)')
|
|
|
|
ieMin = '9' # We do not support IE below this version.
|
|
|
|
def getBrowserIncompatibility(self):
|
|
|
|
'''Produces an error message if the browser in use is not compatible
|
|
|
|
with Appy.'''
|
|
|
|
res = self.ieRex.search(self.REQUEST.get('HTTP_USER_AGENT'))
|
|
|
|
if not res: return
|
|
|
|
version = res.group(1)
|
|
|
|
if version < self.ieMin:
|
|
|
|
mapping = {'version': version, 'min': self.ieMin}
|
|
|
|
return self.translate('wrong_browser', mapping=mapping)
|
2014-08-05 07:53:08 -05:00
|
|
|
|
|
|
|
def executeAjaxAction(self, action, obj, field):
|
|
|
|
'''When PX "pxAjax" is called to get some chunk of XHTML via an Ajax
|
|
|
|
request, a server action can be executed before rendering the XHTML
|
|
|
|
chunk. This method executes this action.'''
|
|
|
|
if action.startswith(':'):
|
|
|
|
# The action corresponds to a method on Appy p_obj.
|
2014-11-10 06:34:52 -06:00
|
|
|
msg = getattr(obj, action[1:])()
|
2014-08-05 07:53:08 -05:00
|
|
|
else:
|
|
|
|
# The action must be executed on p_field if present, on obj.o else.
|
2014-11-10 06:34:52 -06:00
|
|
|
if field: msg = getattr(field, action)(obj.o)
|
|
|
|
else: msg = getattr(obj.o, action)()
|
|
|
|
return msg
|
2009-06-29 07:06:01 -05:00
|
|
|
# ------------------------------------------------------------------------------
|