appypod-rattail/gen/__init__.py

616 lines
30 KiB
Python
Raw Normal View History

2009-06-29 07:06:01 -05:00
# ------------------------------------------------------------------------------
import re, time
2009-06-29 07:06:01 -05:00
from appy.gen.utils import sequenceTypes, PageDescr
from appy.shared.data import countries
2009-06-29 07:06:01 -05:00
# Default Appy permissions -----------------------------------------------------
r, w, d = ('read', 'write', 'delete')
digit = re.compile('[0-9]')
alpha = re.compile('[a-zA-Z0-9]')
letter = re.compile('[a-zA-Z]')
2009-06-29 07:06:01 -05:00
# Descriptor classes used for refining descriptions of elements in types
# (pages, groups,...) ----------------------------------------------------------
class Page:
'''Used for describing a page, its related phase, show condition, etc.'''
2009-06-29 07:06:01 -05:00
def __init__(self, name, phase='main', show=True):
self.name = name
self.phase = phase
self.show = show
class Import:
'''Used for describing the place where to find the data to use for creating
an object.'''
def __init__(self, path, columnMethod=None, columnHeaders=(),
sortMethod=None):
self.id = 'import'
self.path = path
self.columnMethod = columnMethod
# This method allows to split every element into subElements that can
# be shown as column values in a table.
self.columnHeaders = columnHeaders
self.sortMethod = sortMethod
class Search:
'''Used for specifying a search for a given type.'''
def __init__(self, name, group=None, sortBy='', limit=None, **fields):
self.name = name
self.group = group # Searches may be visually grouped in the portlet
self.sortBy = sortBy
self.limit = limit
self.fields = fields # This is a dict whose keys are indexed field
# names and whose values are search values.
2009-06-29 07:06:01 -05:00
# ------------------------------------------------------------------------------
class Type:
'''Basic abstract class for defining any appy type.'''
def __init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, move, indexed, searchable,
2009-06-29 07:06:01 -05:00
specificReadPermission, specificWritePermission, width,
height, master, masterValue, focus, historized):
2009-06-29 07:06:01 -05:00
# The validator restricts which values may be defined. It can be an
# interval (1,None), a list of string values ['choice1', 'choice2'],
# a regular expression, a custom function, a Selection instance, etc.
self.validator = validator
# Multiplicity is a tuple indicating the minimum and maximum
# occurrences of values.
self.multiplicity = multiplicity
# Type of the index on the values. If you want to represent a simple
# (ordered) list of values, specify None. If you want to
# index your values with unordered integers or with other types like
# strings (thus creating a dictionary of values instead of a list),
# specify a type specification for the index, like Integer() or
# String(). Note that this concept of "index" has nothing to do with
# the concept of "database index" (see fields "indexed" and
# "searchable" below). self.index is not yet used.
2009-06-29 07:06:01 -05:00
self.index = index
# Default value
self.default = default
# Is the field optional or not ?
self.optional = optional
# May the user configure a default value ?
self.editDefault = editDefault
# Must the field be visible or not?
self.show = show
# When displaying/editing the whole object, on what page and phase must
# this field value appear? Default is ('main', 'main'). pageShow
# indicates if the page must be shown or not.
self.page, self.phase, self.pageShow = PageDescr.getPageInfo(page, Page)
# Within self.page, in what group of fields must this field value
# appear?
self.group = group
# The following attribute allows to move a field back to a previous
# position (useful for content types that inherit from others).
self.move = move
# If indexed is True, a database index will be set on the field for
# fast access.
self.indexed = indexed
# If specified "searchable", the field will be added to some global
# index allowing to perform application-wide, keyword searches.
2009-06-29 07:06:01 -05:00
self.searchable = searchable
# Normally, permissions to read or write every attribute in a type are
# granted if the user has the global permission to read or
# create/edit instances of the whole type. If you want a given attribute
# to be protected by specific permissions, set one or the 2 next boolean
# values to "True".
self.specificReadPermission = specificReadPermission
self.specificWritePermission = specificWritePermission
# Widget width and height
self.width = width
self.height = height
# The behaviour of this field may depend on another, "master" field
self.master = master
if master:
self.master.slaves.append(self)
# When master has some value(s), there is impact on this field.
self.masterValue = masterValue
# If a field must retain attention in a particular way, set focus=True.
# It will be rendered in a special way.
self.focus = focus
# If we must keep track of changes performed on a field, "historized"
# must be set to True.
self.historized = historized
2009-06-29 07:06:01 -05:00
self.id = id(self)
self.type = self.__class__.__name__
self.pythonType = None # The True corresponding Python type
self.slaves = [] # The list of slaves of this field
self.selfClass = None # The Python class to which this Type definition
# is linked. This will be computed at runtime.
def isMultiValued(self):
'''Does this type definition allow to define multiple values?'''
res = False
maxOccurs = self.multiplicity[1]
if (maxOccurs == None) or (maxOccurs > 1):
res = True
return res
class Integer(Type):
def __init__(self, validator=None, multiplicity=(0,1), index=None,
default=None, optional=False, editDefault=False, show=True,
page='main', group=None, move=0, indexed=False,
searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None,
master=None, masterValue=None, focus=False, historized=False):
2009-06-29 07:06:01 -05:00
Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, move, indexed, searchable,
2009-06-29 07:06:01 -05:00
specificReadPermission, specificWritePermission, width,
height, master, masterValue, focus, historized)
2009-06-29 07:06:01 -05:00
self.pythonType = long
class Float(Type):
def __init__(self, validator=None, multiplicity=(0,1), index=None,
default=None, optional=False, editDefault=False, show=True,
page='main', group=None, move=0, indexed=False,
searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None,
master=None, masterValue=None, focus=False, historized=False):
2009-06-29 07:06:01 -05:00
Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, move, indexed, False,
2009-06-29 07:06:01 -05:00
specificReadPermission, specificWritePermission, width,
height, master, masterValue, focus, historized)
2009-06-29 07:06:01 -05:00
self.pythonType = float
class String(Type):
# Some predefined regular expressions that may be used as validators
c = re.compile
EMAIL = c('[a-zA-Z][\w\.-]*[a-zA-Z0-9]@[a-zA-Z0-9][\w\.-]*[a-zA-Z0-9]\.' \
'[a-zA-Z][a-zA-Z\.]*[a-zA-Z]')
ALPHANUMERIC = c('[\w-]+')
URL = c('(http|https):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*(\.[a-z]{2,5})?' \
'(([0-9]{1,5})?\/.*)?')
# Some predefined functions that may also be used as validators
@staticmethod
def _MODULO_97(obj, value, complement=False):
'''p_value must be a string representing a number, like a bank account.
this function checks that the 2 last digits are the result of
computing the modulo 97 of the previous digits. Any non-digit
character is ignored. If p_complement is True, it does compute the
complement of modulo 97 instead of modulo 97. p_obj is not used;
it will be given by the Appy validation machinery, so it must be
specified as parameter. The function returns True if the check is
successful.'''
if not value: return True # Plone calls me erroneously for
# non-mandatory fields.
# First, remove any non-digit char
v = ''
for c in value:
if digit.match(c): v += c
# There must be at least 3 digits for performing the check
if len(v) < 3: return False
# Separate the real number from the check digits
number = int(v[:-2])
checkNumber = int(v[-2:])
# Perform the check
if complement:
return (97 - (number % 97)) == checkNumber
else:
return (number % 97) == checkNumber
@staticmethod
def MODULO_97(obj, value): return String._MODULO_97(obj, value)
@staticmethod
def MODULO_97_COMPLEMENT(obj, value):
return String._MODULO_97(obj, value, True)
@staticmethod
def IBAN(obj, value):
'''Checks that p_value corresponds to a valid IBAN number. IBAN stands
for International Bank Account Number (ISO 13616). If the number is
valid, the method returns True.'''
if not value: return True # Plone calls me erroneously for
# non-mandatory fields.
# First, remove any non-digit or non-letter char
v = ''
for c in value:
if alpha.match(c): v += c
# Maximum size is 34 chars
if (len(v) < 8) or (len(v) > 34): return False
# 2 first chars must be a valid country code
if not countries.exists(v[:2].lower()): return False
# 2 next chars are a control code whose value must be between 0 and 96.
try:
code = int(v[2:4])
if (code < 0) or (code > 96): return False
except ValueError:
return False
# Perform the checksum
vv = v[4:] + v[:4] # Put the 4 first chars at the end.
nv = ''
for c in vv:
# Convert each letter into a number (A=10, B=11, etc)
# Ascii code for a is 65, so A=10 if we perform "minus 55"
if letter.match(c): nv += str(ord(c.upper()) - 55)
else: nv += c
return int(nv) % 97 == 1
@staticmethod
def BIC(obj, value):
'''Checks that p_value corresponds to a valid BIC number. BIC stands
for Bank Identifier Code (ISO 9362). If the number is valid, the
method returns True.'''
if not value: return True # Plone calls me erroneously for
# non-mandatory fields.
# BIC number must be 8 or 11 chars
if len(value) not in (8, 11): return False
# 4 first chars, representing bank name, must be letters
for c in value[:4]:
if not letter.match(c): return False
# 2 next chars must be a valid country code
if not countries.exists(value[4:6].lower()): return False
# Last chars represent some location within a country (a city, a
# province...). They can only be letters or figures.
for c in value[6:]:
if not alpha.match(c): return False
return True
2009-06-29 07:06:01 -05:00
# Possible values for "format"
LINE = 0
TEXT = 1
XHTML = 2
PASSWORD = 3
2009-06-29 07:06:01 -05:00
def __init__(self, validator=None, multiplicity=(0,1), index=None,
default=None, optional=False, editDefault=False, format=LINE,
show=True, page='main', group=None, move=0, indexed=False,
searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None,
master=None, masterValue=None, focus=False, historized=False):
2009-06-29 07:06:01 -05:00
Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, move, indexed, searchable,
2009-06-29 07:06:01 -05:00
specificReadPermission, specificWritePermission, width,
height, master, masterValue, focus, historized)
2009-06-29 07:06:01 -05:00
self.format = format
self.isSelect = self.isSelection()
2009-06-29 07:06:01 -05:00
def isSelection(self):
'''Does the validator of this type definition define a list of values
into which the user must select one or more values?'''
res = True
if type(self.validator) in (list, tuple):
for elem in self.validator:
if not isinstance(elem, basestring):
res = False
break
else:
if not isinstance(self.validator, Selection):
res = False
return res
class Boolean(Type):
def __init__(self, validator=None, multiplicity=(0,1), index=None,
default=None, optional=False, editDefault=False, show=True,
page='main', group=None, move=0, indexed=False,
searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None,
master=None, masterValue=None, focus=False, historized=False):
2009-06-29 07:06:01 -05:00
Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, move, indexed, searchable,
2009-06-29 07:06:01 -05:00
specificReadPermission, specificWritePermission, width,
height, master, masterValue, focus, historized)
2009-06-29 07:06:01 -05:00
self.pythonType = bool
class Date(Type):
# Possible values for "format"
WITH_HOUR = 0
WITHOUT_HOUR = 1
def __init__(self, validator=None, multiplicity=(0,1), index=None,
default=None, optional=False, editDefault=False,
format=WITH_HOUR, startYear=time.localtime()[0]-10,
endYear=time.localtime()[0]+10, show=True, page='main',
group=None, move=0, indexed=False, searchable=False,
2009-06-29 07:06:01 -05:00
specificReadPermission=False, specificWritePermission=False,
width=None, height=None, master=None, masterValue=None,
focus=False, historized=False):
2009-06-29 07:06:01 -05:00
Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, move, indexed, searchable,
2009-06-29 07:06:01 -05:00
specificReadPermission, specificWritePermission, width,
height, master, masterValue, focus, historized)
2009-06-29 07:06:01 -05:00
self.format = format
self.startYear = startYear
self.endYear = endYear
2009-06-29 07:06:01 -05:00
class File(Type):
def __init__(self, validator=None, multiplicity=(0,1), index=None,
default=None, optional=False, editDefault=False, show=True,
page='main', group=None, move=0, indexed=False,
searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None,
master=None, masterValue=None, focus=False, historized=False,
isImage=False):
2009-06-29 07:06:01 -05:00
Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, move, indexed, False,
2009-06-29 07:06:01 -05:00
specificReadPermission, specificWritePermission, width,
height, master, masterValue, focus, historized)
2009-06-29 07:06:01 -05:00
self.isImage = isImage
class Ref(Type):
def __init__(self, klass=None, attribute=None, validator=None,
multiplicity=(0,1), index=None, default=None, optional=False,
editDefault=False, add=False, link=True, unlink=False,
back=None, isBack=False, show=True, page='main', group=None,
showHeaders=False, shownInfo=(), wide=False, select=None,
maxPerPage=30, move=0, indexed=False, searchable=False,
2009-06-29 07:06:01 -05:00
specificReadPermission=False, specificWritePermission=False,
width=None, height=None, master=None, masterValue=None,
focus=False, historized=False):
2009-06-29 07:06:01 -05:00
Type.__init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, move, indexed, False,
2009-06-29 07:06:01 -05:00
specificReadPermission, specificWritePermission, width,
height, master, masterValue, focus, historized)
2009-06-29 07:06:01 -05:00
self.klass = klass
self.attribute = attribute
self.add = add # May the user add new objects through this ref ?
self.link = link # May the user link existing objects through this ref?
self.unlink = unlink # May the user unlink existing objects?
self.back = back
self.isBack = isBack # Should always be False
self.showHeaders = showHeaders # When displaying a tabular list of
# referenced objects, must we show the table headers?
self.shownInfo = shownInfo # When displaying referenced object(s),
# we will display its title + all other fields whose names are listed
# in this attribute.
self.wide = wide # If True, the table of references will be as wide
# as possible
self.select = select # If a method is defined here, it will be used to
# filter the list of available tied objects.
self.maxPerPage = maxPerPage # Maximum number of referenced objects
# shown at once.
2009-06-29 07:06:01 -05:00
class Computed(Type):
def __init__(self, validator=None, multiplicity=(0,1), index=None,
default=None, optional=False, editDefault=False, show='view',
page='main', group=None, move=0, indexed=False,
searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None,
method=None, plainText=True, master=None, masterValue=None,
focus=False, historized=False):
2009-06-29 07:06:01 -05:00
Type.__init__(self, None, multiplicity, index, default, optional,
False, show, page, group, move, indexed, False,
2009-06-29 07:06:01 -05:00
specificReadPermission, specificWritePermission, width,
height, master, masterValue, focus, historized)
2009-06-29 07:06:01 -05:00
self.method = method # The method used for computing the field value
self.plainText = plainText # Does field computation produce pain text
# or XHTML?
class Action(Type):
'''An action is a workflow-independent Python method that can be triggered
by the user on a given gen-class. For example, the custom installation
procedure of a gen-application is implemented by an action on the custom
tool class. An action is rendered as a button.'''
def __init__(self, validator=None, multiplicity=(1,1), index=None,
default=None, optional=False, editDefault=False, show=True,
page='main', group=None, move=0, indexed=False,
searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None,
action=None, result='computation', master=None,
masterValue=None, focus=False, historized=False):
2009-06-29 07:06:01 -05:00
Type.__init__(self, None, (0,1), index, default, optional,
False, show, page, group, move, indexed, False,
2009-06-29 07:06:01 -05:00
specificReadPermission, specificWritePermission, width,
height, master, masterValue, focus, historized)
2009-06-29 07:06:01 -05:00
self.action = action # Can be a single method or a list/tuple of methods
self.result = result # 'computation' means that the action will simply
# compute things and redirect the user to the same page, with some
# status message about execution of the action. 'file' means that the
# result is the binary content of a file that the user will download.
2009-06-29 07:06:01 -05:00
def __call__(self, obj):
'''Calls the action on p_obj.'''
try:
if type(self.action) in sequenceTypes:
# There are multiple Python methods
res = [True, '']
for act in self.action:
actRes = act(obj)
if type(actRes) in sequenceTypes:
res[0] = res[0] and actRes[0]
if self.result == 'file':
res[1] = res[1] + actRes[1]
else:
res[1] = res[1] + '\n' + actRes[1]
2009-06-29 07:06:01 -05:00
else:
res[0] = res[0] and actRes
else:
# There is only one Python method
actRes = self.action(obj)
if type(actRes) in sequenceTypes:
res = list(actRes)
else:
res = [actRes, '']
# If res is None (ie the user-defined action did not return
# anything), we consider the action as successfull.
2009-06-29 07:06:01 -05:00
if res[0] == None: res[0] = True
except Exception, e:
res = (False, str(e))
return res
class Info(Type):
'''An info is a field whose purpose is to present information
(text, html...) to the user.'''
def __init__(self, validator=None, multiplicity=(1,1), index=None,
default=None, optional=False, editDefault=False, show=True,
page='main', group=None, move=0, indexed=False,
searchable=False, specificReadPermission=False,
specificWritePermission=False, width=None, height=None,
master=None, masterValue=None, focus=False, historized=False):
2009-06-29 07:06:01 -05:00
Type.__init__(self, None, (0,1), index, default, optional,
False, show, page, group, move, indexed, False,
2009-06-29 07:06:01 -05:00
specificReadPermission, specificWritePermission, width,
height, master, masterValue, focus, historized)
2009-06-29 07:06:01 -05:00
# Workflow-specific types ------------------------------------------------------
class State:
def __init__(self, permissions, initial=False, phase='main', show=True):
self.permissions = permissions #~{s_permissionName:[s_roleName]}~ This
# dict gives, for every permission managed by a workflow, the list of
# roles for which the permission is granted in this state.
# Standard permissions are 'read', 'write' and 'delete'.
self.initial = initial
self.phase = phase
self.show = show
def getUsedRoles(self):
res = set()
for roleValue in self.permissions.itervalues():
if isinstance(roleValue, basestring):
res.add(roleValue)
elif roleValue:
for role in roleValue:
res.add(role)
return list(res)
def getTransitions(self, transitions, selfIsFromState=True):
'''Among p_transitions, returns those whose fromState is p_self (if
p_selfIsFromState is True) or those whose toState is p_self (if
p_selfIsFromState is False).'''
res = []
for t in transitions:
if self in t.getStates(selfIsFromState):
res.append(t)
return res
def getPermissions(self):
'''If you get the permissions mapping through self.permissions, dict
values may be of different types (a list of roles, a single role or
None). Iy you call this method, you will always get a list which
may be empty.'''
res = {}
for permission, roleValue in self.permissions.iteritems():
if roleValue == None:
res[permission] = []
elif isinstance(roleValue, basestring):
res[permission] = [roleValue]
else:
res[permission] = roleValue
return res
class Transition:
def __init__(self, states, condition=True, action=None, notify=None,
show=True):
2009-06-29 07:06:01 -05:00
self.states = states # In its simpler form, it is a tuple with 2
# states: (fromState, toState). But it can also be a tuple of several
# (fromState, toState) sub-tuples. This way, you may define only 1
# transition at several places in the state-transition diagram. It may
# be useful for "undo" transitions, for example.
self.condition = condition
self.action = action
self.notify = notify # If not None, it is a method telling who must be
# notified by email after the transition has been executed.
self.show = show # If False, the end user will not be able to trigger
# the transition. It will only be possible by code.
2009-06-29 07:06:01 -05:00
def getUsedRoles(self):
'''If self.condition is specifies a role.'''
res = []
if isinstance(self.condition, basestring):
res = [self.condition]
return res
def isSingle(self):
'''If this transitions is only define between 2 states, returns True.
Else, returns False.'''
return isinstance(self.states[0], State)
def getStates(self, fromStates=True):
'''Returns the fromState(s) if p_fromStates is True, the toState(s)
else. If you want to get the states grouped in tuples
(fromState, toState), simply use self.states.'''
res = []
stateIndex = 1
if fromStates:
stateIndex = 0
if self.isSingle():
res.append(self.states[stateIndex])
else:
for states in self.states:
theState = states[stateIndex]
if theState not in res:
res.append(theState)
return res
def hasState(self, state, isFrom):
'''If p_isFrom is True, this method returns True if p_state is a
starting state for p_self. If p_isFrom is False, this method returns
True if p_state is an ending state for p_self.'''
stateIndex = 1
if isFrom:
stateIndex = 0
if self.isSingle():
res = state == self.states[stateIndex]
else:
res = False
for states in self.states:
if states[stateIndex] == state:
res = True
break
return res
class Permission:
'''If you need to define a specific read or write permission of a given
attribute of an Appy type, you use the specific boolean parameters
"specificReadPermission" or "specificWritePermission" for this attribute.
When you want to refer to those specific read or write permissions when
defining a workflow, for example, you need to use instances of
"ReadPermission" and "WritePermission", the 2 children classes of this
class. For example, if you need to refer to write permission of
attribute "t1" of class A, write: "WritePermission("A.t1") or
WritePermission("x.y.A.t1") if class A is not in the same module as
where you instantiate the class.'''
def __init__(self, fieldDescriptor):
self.fieldDescriptor = fieldDescriptor
class ReadPermission(Permission): pass
class WritePermission(Permission): pass
# ------------------------------------------------------------------------------
class Selection:
'''Instances of this class may be given as validator of a String, in order
to tell Appy that the validator is a selection that will be computed
dynamically.'''
pass
# ------------------------------------------------------------------------------
class Tool:
'''If you want so define a custom tool class, she must inherit from this
class.'''
class Flavour:
'''A flavour represents a given group of configuration options. If you want
to define a custom flavour class, she must inherit from this class.'''
def __init__(self, name): self.name = name
# ------------------------------------------------------------------------------
class Config:
'''If you want to specify some configuration parameters for appy.gen and
your application, please create an instance of this class and modify its
attributes. You may put your instance anywhere in your application
(main package, sub-package, etc).'''
# The default Config instance, used if the application does not give one.
defaultConfig = None
def getDefault():
if not Config.defaultConfig:
Config.defaultConfig = Config()
return Config.defaultConfig
getDefault = staticmethod(getDefault)
def __init__(self):
# For every language code that you specify in this list, appy.gen will
# produce and maintain translation files.
self.languages = ['en']
# People having one of these roles will be able to create instances
# of classes defined in your application.
self.defaultCreators = ['Manager', 'Owner']
# If True, the following flag will produce a minimalist Plone, where
# some actions, portlets or other stuff less relevant for building
# web applications, are removed or hidden. Using this produces
# effects on your whole Plone site!
self.minimalistPlone = False
# If you want to replace the Plone front page with a page coming from
# your application, use the following parameter. Setting
# frontPage = True will replace the Plone front page with a page
# whose content will come fron i18n label "front_page_text".
self.frontPage = False
# If you don't need the portlet that appy.gen has generated for your
# application, set the following parameter to False.
self.showPortlet = True
# Default number of flavours. It will be used for generating i18n labels
# for classes in every flavour. Indeed, every flavour can name its
# concepts differently. For example, class Thing in flavour 2 may have
# i18n label "MyProject_Thing_2".
self.numberOfFlavours = 2
2009-06-29 07:06:01 -05:00
# ------------------------------------------------------------------------------