2009-06-29 07:06:01 -05:00
|
|
|
# ------------------------------------------------------------------------------
|
2015-10-27 15:10:24 -05:00
|
|
|
import re, os, os.path, base64, urllib.request, urllib.parse, urllib.error
|
2015-01-26 10:26:05 -06:00
|
|
|
from appy.px import Px
|
2013-08-21 06:54:56 -05:00
|
|
|
from appy.shared import utils as sutils
|
2015-10-27 15:10:24 -05:00
|
|
|
import collections
|
2009-06-29 07:06:01 -05:00
|
|
|
|
2011-11-25 11:01:20 -06:00
|
|
|
# Function for creating a Zope object ------------------------------------------
|
2012-06-02 07:36:49 -05:00
|
|
|
def createObject(folder, id, className, appName, wf=True, noSecurity=False):
|
2011-11-25 11:01:20 -06:00
|
|
|
'''Creates, in p_folder, object with some p_id. Object will be an instance
|
|
|
|
of p_className from application p_appName. In a very special case (the
|
|
|
|
creation of the config object), computing workflow-related info is not
|
|
|
|
possible at this time. This is why this function can be called with
|
|
|
|
p_wf=False.'''
|
2015-10-27 15:10:24 -05:00
|
|
|
exec('from Products.%s.%s import %s as ZopeClass' % \
|
|
|
|
(appName, className, className))
|
2013-08-25 01:59:53 -05:00
|
|
|
# Get the tool. It may not be present yet, maybe are we creating it now.
|
|
|
|
if folder.meta_type.endswith('Folder'):
|
|
|
|
# p_folder is a standard Zope (temp) folder.
|
|
|
|
tool = getattr(folder, 'config', None)
|
|
|
|
else:
|
|
|
|
# p_folder is an instance of a gen-class.
|
|
|
|
tool = folder.getTool()
|
|
|
|
# Get the currently logged user
|
|
|
|
user = None
|
|
|
|
if tool:
|
|
|
|
user = tool.getUser()
|
|
|
|
# Checks whether the user an create this object if security is enabled.
|
2012-06-02 07:36:49 -05:00
|
|
|
if not noSecurity:
|
2013-08-23 11:57:27 -05:00
|
|
|
klass = ZopeClass.wrapperClass.__bases__[-1]
|
|
|
|
if not tool.userMayCreate(klass):
|
2012-06-02 07:36:49 -05:00
|
|
|
from AccessControl import Unauthorized
|
|
|
|
raise Unauthorized("User can't create instances of %s" % \
|
2013-08-23 11:57:27 -05:00
|
|
|
klass.__name__)
|
2013-08-25 01:59:53 -05:00
|
|
|
# Create the object
|
2011-11-25 11:01:20 -06:00
|
|
|
obj = ZopeClass(id)
|
2013-08-21 05:35:30 -05:00
|
|
|
folder._objects = folder._objects + ({'id':id, 'meta_type':className},)
|
2011-11-25 11:01:20 -06:00
|
|
|
folder._setOb(id, obj)
|
|
|
|
obj = folder._getOb(id) # Important. Else, obj is not really in the folder.
|
|
|
|
obj.portal_type = className
|
|
|
|
obj.id = id
|
|
|
|
obj._at_uid = id
|
2013-08-21 05:35:30 -05:00
|
|
|
# If no user object is there, we are at startup, before default User
|
|
|
|
# instances are created.
|
|
|
|
userId = user and user.login or 'system'
|
|
|
|
obj.creator = userId
|
2011-11-25 11:01:20 -06:00
|
|
|
from DateTime import DateTime
|
|
|
|
obj.created = DateTime()
|
2012-11-05 03:21:27 -06:00
|
|
|
obj.modified = obj.created
|
2013-09-22 15:08:48 -05:00
|
|
|
from persistent.mapping import PersistentMapping
|
|
|
|
obj.__ac_local_roles__ = PersistentMapping({ userId: ['Owner'] })
|
2014-04-21 05:11:41 -05:00
|
|
|
if wf: obj.initializeWorkflow()
|
2011-11-25 11:01:20 -06:00
|
|
|
return obj
|
|
|
|
|
2009-06-29 07:06:01 -05:00
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
upperLetter = re.compile('[A-Z]')
|
|
|
|
def produceNiceMessage(msg):
|
|
|
|
'''Transforms p_msg into a nice msg.'''
|
|
|
|
res = ''
|
|
|
|
if msg:
|
|
|
|
res = msg[0].upper()
|
|
|
|
for c in msg[1:]:
|
|
|
|
if c == '_':
|
|
|
|
res += ' '
|
|
|
|
elif upperLetter.match(c):
|
|
|
|
res += ' ' + c.lower()
|
|
|
|
else:
|
|
|
|
res += c
|
|
|
|
return res
|
|
|
|
|
2009-10-25 15:42:08 -05:00
|
|
|
# ------------------------------------------------------------------------------
|
2009-11-03 08:02:18 -06:00
|
|
|
class SomeObjects:
|
|
|
|
'''Represents a bunch of objects retrieved from a reference or a query in
|
2011-11-25 11:01:20 -06:00
|
|
|
the catalog.'''
|
2010-01-12 14:15:14 -06:00
|
|
|
def __init__(self, objects=None, batchSize=None, startNumber=0,
|
|
|
|
noSecurity=False):
|
2009-10-25 15:42:08 -05:00
|
|
|
self.objects = objects or [] # The objects
|
|
|
|
self.totalNumber = len(self.objects) # self.objects may only represent a
|
|
|
|
# part of all available objects.
|
2009-11-03 08:02:18 -06:00
|
|
|
self.batchSize = batchSize or self.totalNumber # The max length of
|
|
|
|
# self.objects.
|
|
|
|
self.startNumber = startNumber # The index of first object in
|
|
|
|
# self.objects in the whole list.
|
2010-01-12 14:15:14 -06:00
|
|
|
self.noSecurity = noSecurity
|
2009-11-03 08:02:18 -06:00
|
|
|
def brainsToObjects(self):
|
2011-11-25 11:01:20 -06:00
|
|
|
'''self.objects has been populated from brains from the catalog,
|
2009-11-03 08:02:18 -06:00
|
|
|
not from True objects. This method turns them (or some of them
|
2010-01-12 14:15:14 -06:00
|
|
|
depending on batchSize and startNumber) into real objects.
|
|
|
|
If self.noSecurity is True, it gets the objects even if the logged
|
|
|
|
user does not have the right to get them.'''
|
2009-11-03 08:02:18 -06:00
|
|
|
start = self.startNumber
|
|
|
|
brains = self.objects[start:start + self.batchSize]
|
2010-01-12 14:15:14 -06:00
|
|
|
if self.noSecurity: getMethod = '_unrestrictedGetObject'
|
|
|
|
else: getMethod = 'getObject'
|
|
|
|
self.objects = [getattr(b, getMethod)() for b in brains]
|
2010-03-25 10:34:37 -05:00
|
|
|
|
2015-01-02 09:16:48 -06:00
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
def splitIntoWords(text, ignore=2):
|
|
|
|
'''Split the cleaned index value p_text into words (returns a list of
|
|
|
|
words). Words whose length is below p_ignore are ignored, excepted digits
|
|
|
|
which are always kept. Duplicate words are removed (result is a set and
|
|
|
|
not a list).'''
|
|
|
|
# Split p_text into words
|
|
|
|
res = text.split()
|
|
|
|
# Remove shorter words not being figures
|
|
|
|
i = len(res) - 1
|
|
|
|
while i > -1:
|
|
|
|
if (len(res[i]) <= ignore) and not res[i].isdigit():
|
|
|
|
del res[i]
|
|
|
|
i -= 1
|
|
|
|
# Remove duplicates
|
|
|
|
return set(res)
|
|
|
|
|
2010-03-25 10:34:37 -05:00
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
class Keywords:
|
|
|
|
'''This class allows to handle keywords that a user enters and that will be
|
2012-09-26 16:13:02 -05:00
|
|
|
used as basis for performing requests in a TextIndex/XhtmlIndex.'''
|
2010-03-25 10:34:37 -05:00
|
|
|
|
|
|
|
toRemove = '?-+*()'
|
2015-01-02 09:16:48 -06:00
|
|
|
def __init__(self, keywords, operator='AND', ignore=2):
|
|
|
|
# Clean the p_keywords that the user has entered
|
2013-08-21 06:54:56 -05:00
|
|
|
words = sutils.normalizeText(keywords)
|
2010-03-25 10:34:37 -05:00
|
|
|
if words == '*': words = ''
|
|
|
|
for c in self.toRemove: words = words.replace(c, ' ')
|
2015-01-02 09:16:48 -06:00
|
|
|
self.keywords = splitIntoWords(words, ignore=ignore)
|
2010-03-25 10:34:37 -05:00
|
|
|
# Store the operator to apply to the keywords (AND or OR)
|
|
|
|
self.operator = operator
|
|
|
|
|
|
|
|
def merge(self, other, append=False):
|
|
|
|
'''Merges our keywords with those from p_other. If p_append is True,
|
|
|
|
p_other keywords are appended at the end; else, keywords are appended
|
|
|
|
at the begin.'''
|
|
|
|
for word in other.keywords:
|
|
|
|
if word not in self.keywords:
|
|
|
|
if append:
|
|
|
|
self.keywords.append(word)
|
|
|
|
else:
|
|
|
|
self.keywords.insert(0, word)
|
|
|
|
|
|
|
|
def get(self):
|
2012-09-26 16:13:02 -05:00
|
|
|
'''Returns the keywords as needed by the TextIndex.'''
|
2010-03-25 10:34:37 -05:00
|
|
|
if self.keywords:
|
|
|
|
op = ' %s ' % self.operator
|
|
|
|
return op.join(self.keywords)+'*'
|
|
|
|
return ''
|
2010-03-31 08:49:54 -05:00
|
|
|
|
2010-09-02 09:16:08 -05:00
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
def getClassName(klass, appName=None):
|
|
|
|
'''Generates, from appy-class p_klass, the name of the corresponding
|
2011-12-05 03:52:18 -06:00
|
|
|
Zope class. For some classes, name p_appName is required: it is
|
2010-09-02 09:16:08 -05:00
|
|
|
part of the class name.'''
|
|
|
|
moduleName = klass.__module__
|
2011-12-05 08:11:29 -06:00
|
|
|
if (moduleName == 'appy.gen.model') or moduleName.endswith('.wrappers'):
|
2010-09-02 09:16:08 -05:00
|
|
|
# This is a model (generation time or run time)
|
|
|
|
res = appName + klass.__name__
|
2014-10-24 09:34:47 -05:00
|
|
|
elif klass.__bases__ and (klass.__bases__[-1].__module__=='appy.gen.utils'):
|
2010-09-02 09:16:08 -05:00
|
|
|
# This is a customized class (inherits from appy.gen.Tool, User,...)
|
|
|
|
res = appName + klass.__bases__[-1].__name__
|
|
|
|
else: # This is a standard class
|
|
|
|
res = klass.__module__.replace('.', '_') + '_' + klass.__name__
|
|
|
|
return res
|
2011-09-26 14:19:34 -05:00
|
|
|
|
|
|
|
# ------------------------------------------------------------------------------
|
2013-06-09 17:13:29 -05:00
|
|
|
def callMethod(obj, method, klass=None, cache=True):
|
|
|
|
'''This function is used to call a p_method on some Appy p_obj. m_method
|
|
|
|
can be an instance method on p_obj; it can also be a static method. In
|
|
|
|
this latter case, p_obj is the tool and the static method, defined in
|
|
|
|
p_klass, will be called with the tool as unique arg.
|
|
|
|
|
|
|
|
A method cache is implemented on the request object (available at
|
|
|
|
p_obj.request). So while handling a single request from the ui, every
|
|
|
|
method is called only once. Some method calls must not be cached (ie,
|
|
|
|
values of Computed fields). In this case, p_cache will be False.'''
|
|
|
|
rq = obj.request
|
|
|
|
# Create the method cache if it does not exist on the request
|
|
|
|
if not hasattr(rq, 'methodCache'): rq.methodCache = {}
|
|
|
|
# If m_method is a static method or an instance method, unwrap the true
|
|
|
|
# Python function object behind it.
|
|
|
|
methodType = method.__class__.__name__
|
|
|
|
if methodType == 'staticmethod':
|
|
|
|
method = method.__get__(klass)
|
|
|
|
elif methodType == 'instancemethod':
|
2015-10-27 15:10:24 -05:00
|
|
|
method = method.__func__
|
2015-01-19 18:03:23 -06:00
|
|
|
# Call the method if cache is not needed
|
2013-06-09 17:13:29 -05:00
|
|
|
if not cache: return method(obj)
|
|
|
|
# If first arg of method is named "tool" instead of the traditional "self",
|
|
|
|
# we cheat and will call the method with the tool as first arg. This will
|
|
|
|
# allow to consider this method as if it was a static method on the tool.
|
|
|
|
# Every method call, even on different instances, will be cached in a unique
|
|
|
|
# key.
|
|
|
|
cheat = False
|
2015-10-27 15:10:24 -05:00
|
|
|
if not klass and (method.__code__.co_varnames[0] == 'tool'):
|
2013-06-09 17:13:29 -05:00
|
|
|
prefix = obj.klass.__name__
|
|
|
|
obj = obj.tool
|
|
|
|
cheat = True
|
|
|
|
# Build the key of this method call in the cache.
|
|
|
|
# First part of the key: the p_obj's uid (if p_method is an instance method)
|
|
|
|
# or p_className (if p_method is a static method).
|
|
|
|
if not cheat:
|
|
|
|
if klass:
|
|
|
|
prefix = klass.__name__
|
|
|
|
else:
|
|
|
|
prefix = obj.uid
|
|
|
|
# Second part of the key: p_method name
|
2015-10-27 15:10:24 -05:00
|
|
|
key = '%s:%s' % (prefix, method.__name__)
|
2013-06-09 17:13:29 -05:00
|
|
|
# Return the cached value if present in the method cache.
|
|
|
|
if key in rq.methodCache:
|
|
|
|
return rq.methodCache[key]
|
|
|
|
# No cached value: call the method, cache the result and return it
|
|
|
|
res = method(obj)
|
|
|
|
rq.methodCache[key] = res
|
|
|
|
return res
|
2013-08-21 05:35:30 -05:00
|
|
|
|
|
|
|
# Functions for manipulating the authentication cookie -------------------------
|
|
|
|
def readCookie(request):
|
|
|
|
'''Returns the tuple (login, password) read from the authentication
|
|
|
|
cookie received in p_request. If no user is logged, its returns
|
|
|
|
(None, None).'''
|
|
|
|
cookie = request.get('_appy_', None)
|
|
|
|
if not cookie: return None, None
|
2015-10-27 15:10:24 -05:00
|
|
|
cookieValue = base64.decodestring(urllib.parse.unquote(cookie))
|
2013-08-21 05:35:30 -05:00
|
|
|
if ':' in cookieValue: return cookieValue.split(':')
|
|
|
|
return None, None
|
|
|
|
|
|
|
|
def writeCookie(login, password, request):
|
|
|
|
'''Encode p_login and p_password into the cookie set in the p_request.'''
|
|
|
|
cookieValue = base64.encodestring('%s:%s' % (login, password)).rstrip()
|
2015-10-27 15:10:24 -05:00
|
|
|
cookieValue = urllib.parse.quote(cookieValue)
|
2013-08-21 05:35:30 -05:00
|
|
|
request.RESPONSE.setCookie('_appy_', cookieValue, path='/')
|
2013-08-21 06:54:56 -05:00
|
|
|
|
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
def initMasterValue(v):
|
2014-03-03 11:54:21 -06:00
|
|
|
'''Standardizes p_v as a list of strings, excepted if p_v is a method.'''
|
2015-10-27 15:10:24 -05:00
|
|
|
if isinstance(v, collections.Callable): return v
|
2013-08-21 06:54:56 -05:00
|
|
|
if not isinstance(v, bool) and not v: res = []
|
|
|
|
elif type(v) not in sutils.sequenceTypes: res = [v]
|
|
|
|
else: res = v
|
|
|
|
return [str(v) for v in res]
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
class No:
|
|
|
|
'''When you write a workflow condition method and you want to return False
|
|
|
|
but you want to give to the user some explanations about why a transition
|
|
|
|
can't be triggered, do not return False, return an instance of No
|
|
|
|
instead. When creating such an instance, you can specify an error
|
|
|
|
message.'''
|
2014-05-03 08:18:41 -05:00
|
|
|
def __init__(self, msg): self.msg = msg
|
2015-10-27 15:10:24 -05:00
|
|
|
def __bool__(self): return False
|
2014-05-03 08:18:41 -05:00
|
|
|
def __repr__(self): return '<No: %s>' % self.msg
|
2014-10-24 08:55:45 -05:00
|
|
|
|
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
class Model: pass
|
|
|
|
class Tool(Model):
|
|
|
|
'''Subclass me to extend or modify the Tool class.'''
|
|
|
|
class User(Model):
|
|
|
|
'''Subclass me to extend or modify the User class.'''
|
2015-01-26 10:26:05 -06:00
|
|
|
|
2009-06-29 07:06:01 -05:00
|
|
|
# ------------------------------------------------------------------------------
|
2015-01-26 10:26:05 -06:00
|
|
|
class Collapsible:
|
|
|
|
'''Represents a chunk of HTML code that can be collapsed/expanded via a
|
|
|
|
plus/minus icon.'''
|
|
|
|
|
|
|
|
# Plus/minus icon allowing to collapse/expand a chunk of HTML
|
|
|
|
px = Px('''
|
|
|
|
<img id=":'%s_img' % collapse.id" class="clickable" align=":dleft"
|
|
|
|
onclick=":'toggleCookie(%s,%s,%s)' % \
|
|
|
|
(q(collapse.id), q(collapse.display), q(collapse.default))"
|
|
|
|
src=":collapse.expanded and url('collapse.gif') or url('expand.gif')"
|
|
|
|
style="padding-right:4px"/>''')
|
2014-10-24 08:55:45 -05:00
|
|
|
|
2015-01-26 10:26:05 -06:00
|
|
|
def __init__(self, id, request, default='collapsed', display='block'):
|
|
|
|
'''p_display is the value of style attribute "display" for the XHTML
|
|
|
|
element when it must be displayed. By default it is "block"; for a
|
|
|
|
table it must be "table", etc.'''
|
|
|
|
self.id = id # The ID of the collapsible HTML element
|
|
|
|
self.request = request # The request object
|
|
|
|
self.default = default
|
|
|
|
self.display = display
|
|
|
|
# Must the element be collapsed or expanded ?
|
|
|
|
self.expanded = request.get(id, default) == 'expanded'
|
|
|
|
self.style = 'display:%s' % (self.expanded and self.display or 'none')
|
|
|
|
# ------------------------------------------------------------------------------
|