[gen] Moved Appy fields into appy/fields together with their PX.
This commit is contained in:
parent
2b5d286668
commit
25b4edfc1d
24 changed files with 3795 additions and 2503 deletions
2496
gen/__init__.py
2496
gen/__init__.py
File diff suppressed because it is too large
Load diff
463
gen/calendar.py
463
gen/calendar.py
|
@ -1,463 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
import types
|
||||
from appy import Object
|
||||
from appy.gen import Type
|
||||
from DateTime import DateTime
|
||||
from BTrees.IOBTree import IOBTree
|
||||
from persistent.list import PersistentList
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class Calendar(Type):
|
||||
'''This field allows to produce an agenda (monthly view) and view/edit
|
||||
events on it.'''
|
||||
jsFiles = {'view': ('widgets/calendar.js',)}
|
||||
|
||||
def __init__(self, eventTypes, eventNameMethod=None, validator=None,
|
||||
default=None, show='view', page='main', group=None,
|
||||
layouts=None, move=0, specificReadPermission=False,
|
||||
specificWritePermission=False, width=None, height=300,
|
||||
colspan=1, master=None, masterValue=None, focus=False,
|
||||
mapping=None, label=None, maxEventLength=50,
|
||||
otherCalendars=None, additionalInfo=None, startDate=None,
|
||||
endDate=None, defaultDate=None, preCompute=None,
|
||||
applicableEvents=None):
|
||||
Type.__init__(self, validator, (0,1), default, show, page, group,
|
||||
layouts, move, False, False, specificReadPermission,
|
||||
specificWritePermission, width, height, None, colspan,
|
||||
master, masterValue, focus, False, True, mapping, label,
|
||||
None, None, None, None)
|
||||
# eventTypes can be a "static" list or tuple of strings that identify
|
||||
# the types of events that are supported by this calendar. It can also
|
||||
# be a method that computes such a "dynamic" list or tuple. When
|
||||
# specifying a static list, an i18n label will be generated for every
|
||||
# event type of the list. When specifying a dynamic list, you must also
|
||||
# give, in p_eventNameMethod, a method that will accept a single arg
|
||||
# (=one of the event types from your dynamic list) and return the "name"
|
||||
# of this event as it must be shown to the user.
|
||||
self.eventTypes = eventTypes
|
||||
self.eventNameMethod = eventNameMethod
|
||||
if (type(eventTypes) == types.FunctionType) and not eventNameMethod:
|
||||
raise Exception("When param 'eventTypes' is a method, you must " \
|
||||
"give another method in param 'eventNameMethod'.")
|
||||
# It is not possible to create events that span more days than
|
||||
# maxEventLength.
|
||||
self.maxEventLength = maxEventLength
|
||||
# When displaying a given month for this agenda, one may want to
|
||||
# pre-compute, once for the whole month, some information that will then
|
||||
# be given as arg for other methods specified in subsequent parameters.
|
||||
# This mechanism exists for performance reasons, to avoid recomputing
|
||||
# this global information several times. If you specify a method in
|
||||
# p_preCompute, it will be called every time a given month is shown, and
|
||||
# will receive 2 args: the first day of the currently shown month (as a
|
||||
# DateTime instance) and the grid of all shown dates (as a list of lists
|
||||
# of DateTime instances, one sub-list by row in the month view). This
|
||||
# grid may hold a little more than dates of the current month.
|
||||
# Subsequently, the return of your method will be given as arg to other
|
||||
# methods that you may specify as args of other parameters of this
|
||||
# Calendar class (see comments below).
|
||||
self.preCompute = preCompute
|
||||
# If a method is specified in the following parameters, it must accept
|
||||
# a single arg (the result of self.preCompute) and must return a list of
|
||||
# calendars whose events must be shown within this agenda.
|
||||
# Every element in this list must be a sub-list [object, name, color]
|
||||
# (not a tuple):
|
||||
# - object must refer to the other object on which the other calendar
|
||||
# field is defined;
|
||||
# - name is the name of the field on this object that stores the
|
||||
# calendar;
|
||||
# - color must be a string containing the HTML color (including the
|
||||
# leading "#" when relevant) into which events of the calendar must
|
||||
# appear.
|
||||
self.otherCalendars = otherCalendars
|
||||
# One may want to add, day by day, custom information in the calendar.
|
||||
# When a method is given in p_additionalInfo, for every cell of the
|
||||
# month view, this method will be called with 2 args: the cell's date
|
||||
# and the result of self.preCompute. The method's result (a string that
|
||||
# can hold text or a chunk of XHTML) will be inserted in the cell.
|
||||
self.additionalInfo = additionalInfo
|
||||
# One may limit event encoding and viewing to some period of time,
|
||||
# via p_startDate and p_endDate. Those parameters, if given, must hold
|
||||
# methods accepting no arg and returning a Zope DateTime instance. The
|
||||
# startDate and endDate will be converted to UTC at 00.00.
|
||||
self.startDate = startDate
|
||||
self.endDate = endDate
|
||||
# If a default date is specified, it must be a method accepting no arg
|
||||
# and returning a DateTime instance. As soon as the calendar is shown,
|
||||
# the month where this date is included will be shown. If not default
|
||||
# date is specified, it will be 'now' at the moment the calendar is
|
||||
# shown.
|
||||
self.defaultDate = defaultDate
|
||||
# For a specific day, all event types may not be applicable. If this is
|
||||
# the case, one may specify here a method that defines, for a given day,
|
||||
# a sub-set of all event types. This method must accept 3 args: the day
|
||||
# in question (as a DateTime instance), the list of all event types,
|
||||
# which is a copy of the (possibly computed) self.eventTypes) and
|
||||
# the result of calling self.preCompute. The method must modify
|
||||
# the 2nd arg and remove from it potentially not applicable events.
|
||||
# This method can also return a message, that will be shown to the user
|
||||
# for explaining him why he can, for this day, only create events of a
|
||||
# sub-set of the possible event types (or even no event at all).
|
||||
self.applicableEvents = applicableEvents
|
||||
|
||||
def getPreComputedInfo(self, obj, monthDayOne, grid):
|
||||
'''Returns the result of calling self.preComputed, or None if no such
|
||||
method exists.'''
|
||||
if self.preCompute:
|
||||
return self.preCompute(obj.appy(), monthDayOne, grid)
|
||||
|
||||
def getSiblingMonth(self, month, prevNext):
|
||||
'''Gets the next or previous month (depending of p_prevNext) relative
|
||||
to p_month.'''
|
||||
dayOne = DateTime('%s/01 UTC' % month)
|
||||
if prevNext == 'previous':
|
||||
refDate = dayOne - 1
|
||||
elif prevNext == 'next':
|
||||
refDate = dayOne + 33
|
||||
return refDate.strftime('%Y/%m')
|
||||
|
||||
weekDays = ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun')
|
||||
def getNamesOfDays(self, obj, short=True):
|
||||
res = []
|
||||
for day in self.weekDays:
|
||||
if short:
|
||||
suffix = '_short'
|
||||
else:
|
||||
suffix = ''
|
||||
res.append(obj.translate('day_%s%s' % (day, suffix)))
|
||||
return res
|
||||
|
||||
def getMonthGrid(self, month):
|
||||
'''Creates a list of lists of DateTime objects representing the calendar
|
||||
grid to render for a given p_month.'''
|
||||
# Month is a string "YYYY/mm".
|
||||
currentDay = DateTime('%s/01 UTC' % month)
|
||||
currentMonth = currentDay.month()
|
||||
res = [[]]
|
||||
dayOneNb = currentDay.dow() or 7 # This way, Sunday is 7 and not 0.
|
||||
if dayOneNb != 1:
|
||||
previousDate = DateTime(currentDay)
|
||||
# If the 1st day of the month is not a Monday, start the row with
|
||||
# the last days of the previous month.
|
||||
for i in range(1, dayOneNb):
|
||||
previousDate = previousDate - 1
|
||||
res[0].insert(0, previousDate)
|
||||
finished = False
|
||||
while not finished:
|
||||
# Insert currentDay in the grid
|
||||
if len(res[-1]) == 7:
|
||||
# Create a new row
|
||||
res.append([currentDay])
|
||||
else:
|
||||
res[-1].append(currentDay)
|
||||
currentDay = currentDay + 1
|
||||
if currentDay.month() != currentMonth:
|
||||
finished = True
|
||||
# Complete, if needed, the last row with the first days of the next
|
||||
# month.
|
||||
if len(res[-1]) != 7:
|
||||
while len(res[-1]) != 7:
|
||||
res[-1].append(currentDay)
|
||||
currentDay = currentDay + 1
|
||||
return res
|
||||
|
||||
def getOtherCalendars(self, obj, preComputed):
|
||||
'''Returns the list of other calendars whose events must also be shown
|
||||
on this calendar.'''
|
||||
if self.otherCalendars:
|
||||
res = self.otherCalendars(obj.appy(), preComputed)
|
||||
# Replace field names with field objects
|
||||
for i in range(len(res)):
|
||||
res[i][1] = res[i][0].getField(res[i][1])
|
||||
return res
|
||||
|
||||
def getAdditionalInfoAt(self, obj, date, preComputed):
|
||||
'''If the user has specified a method in self.additionalInfo, we call
|
||||
it for displaying this additional info in the calendar, at some
|
||||
p_date.'''
|
||||
if not self.additionalInfo: return
|
||||
return self.additionalInfo(obj.appy(), date, preComputed)
|
||||
|
||||
def getEventTypes(self, obj):
|
||||
'''Returns the (dynamic or static) event types as defined in
|
||||
self.eventTypes.'''
|
||||
if type(self.eventTypes) == types.FunctionType:
|
||||
return self.eventTypes(obj.appy())
|
||||
else:
|
||||
return self.eventTypes
|
||||
|
||||
def getApplicableEventsTypesAt(self, obj, date, allEventTypes, preComputed,
|
||||
forBrowser=False):
|
||||
'''Returns the event types that are applicable at a given p_date. More
|
||||
precisely, it returns an object with 2 attributes:
|
||||
* "events" is the list of applicable event types;
|
||||
* "message", not empty if some event types are not applicable,
|
||||
contains a message explaining those event types are
|
||||
not applicable.
|
||||
'''
|
||||
if not self.applicableEvents:
|
||||
eventTypes = allEventTypes
|
||||
message = None
|
||||
else:
|
||||
eventTypes = allEventTypes[:]
|
||||
message = self.applicableEvents(obj.appy(), date, eventTypes,
|
||||
preComputed)
|
||||
res = Object(eventTypes=eventTypes, message=message)
|
||||
if forBrowser:
|
||||
res.eventTypes = ','.join(res.eventTypes)
|
||||
if not res.message:
|
||||
res.message = ''
|
||||
else:
|
||||
res.message = obj.formatText(res.message, format='js')
|
||||
return res.__dict__
|
||||
return res
|
||||
|
||||
def getEventsAt(self, obj, date, asDict=True):
|
||||
'''Returns the list of events that exist at some p_date (=day).'''
|
||||
obj = obj.o # Ensure p_obj is not a wrapper.
|
||||
if not hasattr(obj.aq_base, self.name): return
|
||||
years = getattr(obj, self.name)
|
||||
year = date.year()
|
||||
if year not in years: return
|
||||
months = years[year]
|
||||
month = date.month()
|
||||
if month not in months: return
|
||||
days = months[month]
|
||||
day = date.day()
|
||||
if day not in days: return
|
||||
if asDict:
|
||||
res = [e.__dict__ for e in days[day]]
|
||||
else:
|
||||
res = days[day]
|
||||
return res
|
||||
|
||||
def getEventTypeAt(self, obj, date):
|
||||
'''Returns the event type of the first event defined at p_day, or None
|
||||
if unspecified.'''
|
||||
events = self.getEventsAt(obj, date, asDict=False)
|
||||
if not events: return
|
||||
return events[0].eventType
|
||||
|
||||
def getEventsByType(self, obj, eventType, minDate=None, maxDate=None,
|
||||
sorted=True, groupSpanned=False):
|
||||
'''Returns all the events of a given p_eventType. If p_eventType is
|
||||
None, it returns events of all types. The return value is a list of
|
||||
2-tuples whose 1st elem is a DateTime instance and whose 2nd elem is
|
||||
the event.
|
||||
If p_sorted is True, the list is sorted in chronological order. Else,
|
||||
the order is random, but the result is computed faster.
|
||||
If p_minDate and/or p_maxDate is/are specified, it restricts the
|
||||
search interval accordingly.
|
||||
If p_groupSpanned is True, events spanned on several days are
|
||||
grouped into a single event. In this case, tuples in the result
|
||||
are 3-tuples: (DateTime_startDate, DateTime_endDate, event).
|
||||
'''
|
||||
# Prevent wrong combinations of parameters
|
||||
if groupSpanned and not sorted:
|
||||
raise Exception('Events must be sorted if you want to get ' \
|
||||
'spanned events to be grouped.')
|
||||
obj = obj.o # Ensure p_obj is not a wrapper.
|
||||
res = []
|
||||
if not hasattr(obj, self.name): return res
|
||||
# Compute "min" and "max" tuples
|
||||
if minDate:
|
||||
minYear = minDate.year()
|
||||
minMonth = (minYear, minDate.month())
|
||||
minDay = (minYear, minDate.month(), minDate.day())
|
||||
if maxDate:
|
||||
maxYear = maxDate.year()
|
||||
maxMonth = (maxYear, maxDate.month())
|
||||
maxDay = (maxYear, maxDate.month(), maxDate.day())
|
||||
# Browse years
|
||||
years = getattr(obj, self.name)
|
||||
for year in years.keys():
|
||||
# Don't take this year into account if outside interval
|
||||
if minDate and (year < minYear): continue
|
||||
if maxDate and (year > maxYear): continue
|
||||
months = years[year]
|
||||
# Browse this year's months
|
||||
for month in months.keys():
|
||||
# Don't take this month into account if outside interval
|
||||
thisMonth = (year, month)
|
||||
if minDate and (thisMonth < minMonth): continue
|
||||
if maxDate and (thisMonth > maxMonth): continue
|
||||
days = months[month]
|
||||
# Browse this month's days
|
||||
for day in days.keys():
|
||||
# Don't take this day into account if outside interval
|
||||
thisDay = (year, month, day)
|
||||
if minDate and (thisDay < minDay): continue
|
||||
if maxDate and (thisDay > maxDay): continue
|
||||
events = days[day]
|
||||
# Browse this day's events
|
||||
for event in events:
|
||||
# Filter unwanted events
|
||||
if eventType and (event.eventType != eventType):
|
||||
continue
|
||||
# We have found a event.
|
||||
date = DateTime('%d/%d/%d UTC' % (year, month, day))
|
||||
if groupSpanned:
|
||||
singleRes = [date, None, event]
|
||||
else:
|
||||
singleRes = (date, event)
|
||||
res.append(singleRes)
|
||||
# Sort the result if required
|
||||
if sorted: res.sort(key=lambda x: x[0])
|
||||
# Group events spanned on several days if required
|
||||
if groupSpanned:
|
||||
# Browse events in reverse order and merge them when appropriate
|
||||
i = len(res) - 1
|
||||
while i > 0:
|
||||
currentDate = res[i][0]
|
||||
lastDate = res[i][1]
|
||||
previousDate = res[i-1][0]
|
||||
currentType = res[i][2].eventType
|
||||
previousType = res[i-1][2].eventType
|
||||
if (previousDate == (currentDate-1)) and \
|
||||
(previousType == currentType):
|
||||
# A merge is needed
|
||||
del res[i]
|
||||
res[i-1][1] = lastDate or currentDate
|
||||
i -= 1
|
||||
return res
|
||||
|
||||
def hasEventsAt(self, obj, date, otherEvents):
|
||||
'''Returns True if, at p_date, an event is found of the same type as
|
||||
p_otherEvents.'''
|
||||
if not otherEvents: return False
|
||||
events = self.getEventsAt(obj, date, asDict=False)
|
||||
if not events: return False
|
||||
return events[0].eventType == otherEvents[0]['eventType']
|
||||
|
||||
def getOtherEventsAt(self, obj, date, otherCalendars):
|
||||
'''Gets events that are defined in p_otherCalendars at some p_date.'''
|
||||
res = []
|
||||
for o, field, color in otherCalendars:
|
||||
events = field.getEventsAt(o.o, date, asDict=False)
|
||||
if events:
|
||||
eventType = events[0].eventType
|
||||
eventName = field.getEventName(o.o, eventType)
|
||||
info = Object(name=eventName, color=color)
|
||||
res.append(info.__dict__)
|
||||
return res
|
||||
|
||||
def getEventName(self, obj, eventType):
|
||||
'''Gets the name of the event corresponding to p_eventType as it must
|
||||
appear to the user.'''
|
||||
if self.eventNameMethod:
|
||||
return self.eventNameMethod(obj.appy(), eventType)
|
||||
else:
|
||||
return obj.translate('%s_event_%s' % (self.labelId, eventType))
|
||||
|
||||
def getStartDate(self, obj):
|
||||
'''Get the start date for this calendar if defined.'''
|
||||
if self.startDate:
|
||||
d = self.startDate(obj.appy())
|
||||
# Return the start date without hour, in UTC.
|
||||
return DateTime('%d/%d/%d UTC' % (d.year(), d.month(), d.day()))
|
||||
|
||||
def getEndDate(self, obj):
|
||||
'''Get the end date for this calendar if defined.'''
|
||||
if self.endDate:
|
||||
d = self.endDate(obj.appy())
|
||||
# Return the end date without hour, in UTC.
|
||||
return DateTime('%d/%d/%d UTC' % (d.year(), d.month(), d.day()))
|
||||
|
||||
def getDefaultDate(self, obj):
|
||||
'''Get the default date that must appear as soon as the calendar is
|
||||
shown.'''
|
||||
if self.defaultDate:
|
||||
return self.defaultDate(obj.appy())
|
||||
else:
|
||||
return DateTime() # Now
|
||||
|
||||
def createEvent(self, obj, date, eventType=None, eventSpan=None,
|
||||
handleEventSpan=True):
|
||||
'''Create a new event in the calendar, at some p_date (day).
|
||||
If p_eventType is given, it is used; else, rq['eventType'] is used.
|
||||
If p_handleEventSpan is True, we will use p_eventSpan (or
|
||||
rq["eventSpan"] if p_eventSpan is not given) and also
|
||||
create the same event for successive days.'''
|
||||
obj = obj.o # Ensure p_obj is not a wrapper.
|
||||
rq = obj.REQUEST
|
||||
# Get values from parameters
|
||||
if not eventType: eventType = rq['eventType']
|
||||
if handleEventSpan and not eventSpan:
|
||||
eventSpan = rq.get('eventSpan', None)
|
||||
# Split the p_date into separate parts
|
||||
year, month, day = date.year(), date.month(), date.day()
|
||||
# Check that the "preferences" dict exists or not.
|
||||
if not hasattr(obj.aq_base, self.name):
|
||||
# 1st level: create a IOBTree whose keys are years.
|
||||
setattr(obj, self.name, IOBTree())
|
||||
yearsDict = getattr(obj, self.name)
|
||||
# Get the sub-dict storing months for a given year
|
||||
if year in yearsDict:
|
||||
monthsDict = yearsDict[year]
|
||||
else:
|
||||
yearsDict[year] = monthsDict = IOBTree()
|
||||
# Get the sub-dict storing days of a given month
|
||||
if month in monthsDict:
|
||||
daysDict = monthsDict[month]
|
||||
else:
|
||||
monthsDict[month] = daysDict = IOBTree()
|
||||
# Get the list of events for a given day
|
||||
if day in daysDict:
|
||||
events = daysDict[day]
|
||||
else:
|
||||
daysDict[day] = events = PersistentList()
|
||||
# Create and store the event, excepted if an event already exists.
|
||||
if not events:
|
||||
event = Object(eventType=eventType)
|
||||
events.append(event)
|
||||
# Span the event on the successive days if required
|
||||
if handleEventSpan and eventSpan:
|
||||
nbOfDays = min(int(eventSpan), self.maxEventLength)
|
||||
for i in range(nbOfDays):
|
||||
date = date + 1
|
||||
self.createEvent(obj, date, handleEventSpan=False)
|
||||
|
||||
def deleteEvent(self, obj, date, handleEventSpan=True):
|
||||
'''Deletes an event. It actually deletes all events at p_date.
|
||||
If p_handleEventSpan is True, we will use rq["deleteNext"] to
|
||||
delete successive events, too.'''
|
||||
obj = obj.o # Ensure p_obj is not a wrapper.
|
||||
if not self.getEventsAt(obj, date): return
|
||||
daysDict = getattr(obj, self.name)[date.year()][date.month()]
|
||||
# Remember events, in case we must delete similar ones for next days.
|
||||
events = self.getEventsAt(obj, date)
|
||||
del daysDict[date.day()]
|
||||
rq = obj.REQUEST
|
||||
if handleEventSpan and rq.has_key('deleteNext') and \
|
||||
(rq['deleteNext'] == 'True'):
|
||||
while True:
|
||||
date = date + 1
|
||||
if self.hasEventsAt(obj, date, events):
|
||||
self.deleteEvent(obj, date, handleEventSpan=False)
|
||||
else:
|
||||
break
|
||||
|
||||
def process(self, obj):
|
||||
'''Processes an action coming from the calendar widget, ie, the creation
|
||||
or deletion of a calendar event.'''
|
||||
rq = obj.REQUEST
|
||||
action = rq['actionType']
|
||||
# Get the date for this action
|
||||
if action == 'createEvent':
|
||||
return self.createEvent(obj, DateTime(rq['day']))
|
||||
elif action == 'deleteEvent':
|
||||
return self.deleteEvent(obj, DateTime(rq['day']))
|
||||
|
||||
def getCellStyle(self, obj, date, today):
|
||||
'''What CSS classes must apply to the table cell representing p_date
|
||||
in the calendar?'''
|
||||
res = []
|
||||
# We must distinguish between past and future dates.
|
||||
if date < today:
|
||||
res.append('even')
|
||||
else:
|
||||
res.append('odd')
|
||||
# Week-end days must have a specific style.
|
||||
if date.aDay() in ('Sat', 'Sun'):
|
||||
res.append('cellDashed')
|
||||
return ' '.join(res)
|
||||
# ------------------------------------------------------------------------------
|
|
@ -55,7 +55,7 @@ class ClassDescriptor(Descriptor):
|
|||
except AttributeError:
|
||||
attrValue = getattr(self.modelClass, attrName)
|
||||
hookClass = self.modelClass
|
||||
if isinstance(attrValue, gen.Type):
|
||||
if isinstance(attrValue, gen.Field):
|
||||
if not condition or eval(condition):
|
||||
attrs.append( (attrName, attrValue, hookClass) )
|
||||
# Then, add attributes from parent classes
|
||||
|
@ -124,7 +124,7 @@ class ClassDescriptor(Descriptor):
|
|||
attrValue = getattr(self.klass, attrName)
|
||||
except AttributeError:
|
||||
attrValue = getattr(self.modelClass, attrName)
|
||||
if not isinstance(attrValue, gen.Type): continue
|
||||
if not isinstance(attrValue, gen.Field): continue
|
||||
FieldDescriptor(attrName, attrValue, self).generate()
|
||||
|
||||
def isAbstract(self):
|
||||
|
|
|
@ -158,7 +158,7 @@ class Generator:
|
|||
workflow.'''
|
||||
res = 'none'
|
||||
for attrValue in klass.__dict__.itervalues():
|
||||
if isinstance(attrValue, gen.Type):
|
||||
if isinstance(attrValue, gen.Field):
|
||||
res = 'class'
|
||||
elif isinstance(attrValue, gen.State):
|
||||
res = 'workflow'
|
||||
|
@ -218,7 +218,7 @@ class Generator:
|
|||
# programmatically
|
||||
moreAttrs = []
|
||||
for eName, eValue in moduleElem.__dict__.iteritems():
|
||||
if isinstance(eValue, gen.Type) and (eName not in attrs):
|
||||
if isinstance(eValue, gen.Field) and (eName not in attrs):
|
||||
moreAttrs.append(eName)
|
||||
# Sort them in alphabetical order: else, order would be random
|
||||
moreAttrs.sort()
|
||||
|
|
|
@ -385,7 +385,7 @@ class ZopeInstaller:
|
|||
for baseClass in klass.wrapperClass.__bases__:
|
||||
if baseClass.__name__ == 'AbstractWrapper': continue
|
||||
for name, appyType in baseClass.__dict__.iteritems():
|
||||
if not isinstance(appyType, gen.Type) or \
|
||||
if not isinstance(appyType, gen.Field) or \
|
||||
(isinstance(appyType, gen.Ref) and appyType.isBack):
|
||||
continue # Back refs are initialised within fw refs
|
||||
appyType.init(name, baseClass, appName)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import os, os.path, sys, re, time, random, types, base64, urllib
|
||||
from appy import Object
|
||||
import appy.gen
|
||||
from appy.gen import Type, Search, Selection, String, Page
|
||||
from appy.gen import Search, String, Page
|
||||
from appy.gen.utils import SomeObjects, getClassName, GroupDescr, SearchDescr
|
||||
from appy.gen.mixins import BaseMixin
|
||||
from appy.gen.wrappers import AbstractWrapper
|
||||
|
@ -476,6 +476,11 @@ class ToolMixin(BaseMixin):
|
|||
sub-lists of p_sub elements.'''
|
||||
return splitList(l, sub)
|
||||
|
||||
def quote(self, s):
|
||||
'''Returns the quoted version of p_s.'''
|
||||
if "'" in s: return '"%s"' % s
|
||||
return "'%s'" % s
|
||||
|
||||
def getLayoutType(self):
|
||||
'''Guess the current layout type, according to actual URL.'''
|
||||
url = self.REQUEST['ACTUAL_URL']
|
||||
|
|
149
gen/ogone.py
149
gen/ogone.py
|
@ -1,149 +0,0 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
import sha
|
||||
from appy import Object
|
||||
from appy.gen import Type
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class OgoneConfig:
|
||||
'''If you plan, in your app, to perform on-line payments via the Ogone (r)
|
||||
system, create an instance of this class in your app and place it in the
|
||||
'ogone' attr of your appy.gen.Config instance.'''
|
||||
def __init__(self):
|
||||
# self.env refers to the Ogone environment and can be "test" or "prod".
|
||||
self.env = 'test'
|
||||
# You merchant Ogone ID
|
||||
self.PSPID = None
|
||||
# Default currency for transactions
|
||||
self.currency = 'EUR'
|
||||
# Default language
|
||||
self.language = 'en_US'
|
||||
# SHA-IN key (digest will be generated with the SHA-1 algorithm)
|
||||
self.shaInKey = ''
|
||||
# SHA-OUT key (digest will be generated with the SHA-1 algorithm)
|
||||
self.shaOutKey = ''
|
||||
|
||||
def __repr__(self): return str(self.__dict__)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
class Ogone(Type):
|
||||
'''This field allows to perform payments with the Ogone (r) system.'''
|
||||
urlTypes = ('accept', 'decline', 'exception', 'cancel')
|
||||
|
||||
def __init__(self, orderMethod, responseMethod, show='view', page='main',
|
||||
group=None, layouts=None, move=0, specificReadPermission=False,
|
||||
specificWritePermission=False, width=None, height=None,
|
||||
colspan=1, master=None, masterValue=None, focus=False,
|
||||
mapping=None, label=None):
|
||||
Type.__init__(self, None, (0,1), None, show, page, group, layouts, move,
|
||||
False, False,specificReadPermission,
|
||||
specificWritePermission, width, height, None, colspan,
|
||||
master, masterValue, focus, False, True, mapping, label,
|
||||
None, None, None, None)
|
||||
# orderMethod must contain a method returning a dict containing info
|
||||
# about the order. Following keys are mandatory:
|
||||
# * orderID An identifier for the order. Don't use the object UID
|
||||
# for this, use a random number, because if the payment
|
||||
# is canceled, Ogone will not allow you to reuse the same
|
||||
# orderID for the next tentative.
|
||||
# * amount An integer representing the price for this order,
|
||||
# multiplied by 100 (no floating point value, no commas
|
||||
# are tolerated. Dont't forget to multiply the amount by
|
||||
# 100!
|
||||
self.orderMethod = orderMethod
|
||||
# responseMethod must contain a method accepting one param, let's call
|
||||
# it "response". The response method will be called when we will get
|
||||
# Ogone's response about the status of the payment. Param "response" is
|
||||
# an object whose attributes correspond to all parameters that you have
|
||||
# chosen to receive in your Ogone merchant account. After the payment,
|
||||
# the user will be redirected to the object's view page, excepted if
|
||||
# your method returns an alternatve URL.
|
||||
self.responseMethod = responseMethod
|
||||
|
||||
noShaInKeys = ('env',)
|
||||
noShaOutKeys = ('name', 'SHASIGN')
|
||||
def createShaDigest(self, values, passphrase, keysToIgnore=()):
|
||||
'''Creates an Ogone-compliant SHA-1 digest based on key-value pairs in
|
||||
dict p_values and on some p_passphrase.'''
|
||||
# Create a new dict by removing p_keysToIgnore from p_values, and by
|
||||
# upperizing all keys.
|
||||
shaRes = {}
|
||||
for k, v in values.iteritems():
|
||||
if k in keysToIgnore: continue
|
||||
# Ogone: we must not include empty values.
|
||||
if (v == None) or (v == ''): continue
|
||||
shaRes[k.upper()] = v
|
||||
# Create a sorted list of keys
|
||||
keys = shaRes.keys()
|
||||
keys.sort()
|
||||
shaList = []
|
||||
for k in keys:
|
||||
shaList.append('%s=%s' % (k, shaRes[k]))
|
||||
shaObject = sha.new(passphrase.join(shaList) + passphrase)
|
||||
res = shaObject.hexdigest()
|
||||
return res
|
||||
|
||||
def getValue(self, obj):
|
||||
'''The "value" of the Ogone field is a dict that collects all the
|
||||
necessary info for making the payment.'''
|
||||
tool = obj.getTool()
|
||||
# Basic Ogone parameters were generated in the app config module.
|
||||
res = obj.getProductConfig().ogone.copy()
|
||||
shaKey = res['shaInKey']
|
||||
# Remove elements from the Ogone config that we must not send in the
|
||||
# payment request.
|
||||
del res['shaInKey']
|
||||
del res['shaOutKey']
|
||||
res.update(self.callMethod(obj, self.orderMethod))
|
||||
# Add user-related information
|
||||
res['CN'] = str(tool.getUserName(normalized=True))
|
||||
user = obj.appy().appyUser
|
||||
res['EMAIL'] = user.email or user.login
|
||||
# Add standard back URLs
|
||||
siteUrl = tool.getSiteUrl()
|
||||
res['catalogurl'] = siteUrl
|
||||
res['homeurl'] = siteUrl
|
||||
# Add redirect URLs
|
||||
for t in self.urlTypes:
|
||||
res['%surl' % t] = '%s/onProcess' % obj.absolute_url()
|
||||
# Add additional parameter that we want Ogone to give use back in all
|
||||
# of its responses: the name of this Appy Ogone field. This way, Appy
|
||||
# will be able to call method m_process below, that will process
|
||||
# Ogone's response.
|
||||
res['paramplus'] = 'name=%s' % self.name
|
||||
# Ensure every value is a str
|
||||
for k in res.iterkeys():
|
||||
if not isinstance(res[k], str):
|
||||
res[k] = str(res[k])
|
||||
# Compute a SHA-1 key as required by Ogone and add it to the res
|
||||
res['SHASign'] = self.createShaDigest(res, shaKey,
|
||||
keysToIgnore=self.noShaInKeys)
|
||||
return res
|
||||
|
||||
def ogoneResponseOk(self, obj):
|
||||
'''Returns True if the SHA-1 signature from Ogone matches retrieved
|
||||
params.'''
|
||||
response = obj.REQUEST.form
|
||||
shaKey = obj.getProductConfig().ogone['shaOutKey']
|
||||
digest = self.createShaDigest(response, shaKey,
|
||||
keysToIgnore=self.noShaOutKeys)
|
||||
return digest.lower() == response['SHASIGN'].lower()
|
||||
|
||||
def process(self, obj):
|
||||
'''Processes a response from Ogone.'''
|
||||
# Call the response method defined in this Ogone field.
|
||||
if not self.ogoneResponseOk(obj):
|
||||
obj.log('Ogone response SHA failed. REQUEST: %s' % \
|
||||
str(obj.REQUEST.form))
|
||||
raise Exception('Failure, possible fraud detection, an ' \
|
||||
'administrator has been contacted.')
|
||||
# Create a nice object from the form.
|
||||
response = Object()
|
||||
for k, v in obj.REQUEST.form.iteritems():
|
||||
setattr(response, k, v)
|
||||
# Call the field method that handles the response received from Ogone.
|
||||
url = self.responseMethod(obj.appy(), response)
|
||||
# Redirect the user to the correct page. If the field method returns
|
||||
# some URL, use it. Else, use the view page of p_obj.
|
||||
if not url: url = obj.absolute_url()
|
||||
obj.goto(url)
|
||||
# ------------------------------------------------------------------------------
|
|
@ -1,4 +1,4 @@
|
|||
<tal:comment replace="nothing">View macro</tal:comment>
|
||||
<tal:comment replace="nothing">Month view macro</tal:comment>
|
||||
<div metal:define-macro="viewMonth"
|
||||
tal:define="fieldName request/fieldName;
|
||||
ajaxHookId python: contextObj.UID() + fieldName;
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
import os, os.path, mimetypes
|
||||
import appy.pod
|
||||
from appy.gen import Type, Search, Ref, String, WorkflowAnonymous
|
||||
from appy.gen import Field, Search, Ref, String, WorkflowAnonymous
|
||||
from appy.gen.indexer import defaultIndexes
|
||||
from appy.gen.utils import createObject
|
||||
from appy.px import Px
|
||||
|
@ -370,7 +370,7 @@ class AbstractWrapper(object):
|
|||
appUrl=app.absolute_url(); appFolder=app.data;
|
||||
appName=ztool.getAppName(); _=ztool.translate;
|
||||
req=ztool.REQUEST; resp=req.RESPONSE;
|
||||
lang=ztool.getUserLanguage();
|
||||
lang=ztool.getUserLanguage(); q=ztool.quote;
|
||||
layoutType=ztool.getLayoutType();
|
||||
contextObj=ztool.getPublishedObject(layoutType) or \
|
||||
ztool.getHomeObject();
|
||||
|
@ -998,7 +998,7 @@ class AbstractWrapper(object):
|
|||
# Now, let's try to return a real attribute.
|
||||
res = object.__getattribute__(self, name)
|
||||
# If we got an Appy type, return the value of this type for this object
|
||||
if isinstance(res, Type):
|
||||
if isinstance(res, Field):
|
||||
o = self.o
|
||||
if isinstance(res, Ref):
|
||||
return res.getValue(o, noListIfSingleObj=True)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue