Contact: fumanchu@aminus.org

Log in as guest/misc to create tickets

root/alamode.py

Revision 18 (checked in by fumanchu, 8 years ago)

New alamode module.

Line 
1 """'A La Mode' is a helper module for web apps. This is version 1.0.16.
2
3 See alamode.html for more info.
4
5 LICENSE
6 -------
7 This work, including the source code, documentation
8 and related data, is placed into the public domain.
9
10 The original author is Robert Brewer, Amor Ministries.
11 fumanchu@amor.org
12 svn://casadeamor.com/alamode/trunk
13
14 THIS SOFTWARE IS PROVIDED AS-IS, WITHOUT WARRANTY
15 OF ANY KIND, NOT EVEN THE IMPLIED WARRANTY OF
16 MERCHANTABILITY. THE AUTHOR OF THIS SOFTWARE
17 ASSUMES _NO_ RESPONSIBILITY FOR ANY CONSEQUENCE
18 RESULTING FROM THE USE, MODIFICATION, OR
19 REDISTRIBUTION OF THIS SOFTWARE.
20 """
21
22 import datetime
23
24 import sys, traceback
25
26 # Quoting and escaping
27 from xml.sax.saxutils import escape, quoteattr as quote
28 escape_url_map = dict([(char, hex(ord(char)).replace("0x", "%"))
29                         for char in "/?,.:%"])
30 def escape_url(url):
31     return escape(url, escape_url_map)
32
33
34 def urljoin(*atoms):
35     """urljoin(*atoms) -> Join path atoms with / to form a complete url."""
36     return u"/".join([x.strip(r"\/") for x in atoms])
37
38 def selected(value):
39     """If value is True, return a valid XHTML 'selected' attribute, else u''."""
40     if value:
41         return u' selected="selected" '
42     return u''
43
44 def checked(value):
45     """If value is True, return a valid XHTML 'checked' attribute, else u''."""
46     if value:
47         return u' checked="checked" '
48     return u''
49
50 def escape_spaces(val, repl=' '):
51     """escape_spaces(val, repl=' ') -> val, made safe for HTML forms.
52     
53     Replace leading and trailing spaces with  , to get around browsers
54     like IE, Firefox and Lynx which strip leading and trailing spaces from
55     form element values upon submit. See unescape_spaces() for the reverse
56     transform.
57     
58     IE, Firefox: occurs with option elements (display only; if a separate
59     value argument is given (<option value=' x '>), there's no problem).
60     
61     Lynx: occurs with submit buttons.
62     """
63    
64     replfunc = lambda m: repl * len(m.group(0))
65     if isinstance(val, basestring):
66         val = re.sub(r'^( +)', replfunc, val)
67         val = re.sub(r'( +)$', replfunc, val)
68     return val
69
70 def unescape_spaces(val, esc='\xa0'):
71     """unescape_spaces(val, esc='\xa0') -> val, unescaped from an HTML form.
72     
73     The reverse of escape_spaces(). Replace leading and trailing
74     occurrences of the 'esc' arg with spaces.
75     
76     Notice that the default pattern is not '&nbsp;', as it is for
77     escape_spaces(). Most browsers/servers replace &nbsp; values with \xa0
78     when submitting form data.
79     """
80    
81     replfunc = lambda m: " " * len(m.group(0))
82     if isinstance(val, basestring):
83         val = re.sub(r'^(%s+)' % re.escape(esc), replfunc, val)
84         val = re.sub(r'(%s+)$' % re.escape(esc), replfunc, val)
85     return val
86
87
88 ###########################################################################
89 ##                                                                       ##
90 ##                               Adapters                                ##
91 ##                                                                       ##
92 ###########################################################################
93
94 try:
95     import fixedpoint
96 except ImportError:
97     pass
98
99 try:
100     import decimal
101 except ImportError:
102     pass
103
104 import re
105
106 def _pop_head_atom(aList):
107     try:
108         return aList.pop(0)
109     except IndexError:
110         return 0
111
112 latin1Replacements = {u'~a': u'\u00E1', u'~e': u'\u00E9',
113                       u'~i': u'\u00ED', u'~n': u'\u00F1',
114                       u'~o': u'\u00F3', u'~u': u'\u00FA',
115                       u'~y': u'\u00FD',
116                       u'~A': u'\u00C1', u'~E': u'\u00C9',
117                       u'~I': u'\u00CD', u'~N': u'\u00D1',
118                       u'~O': u'\u00D3', u'~U': u'\u00DA',
119                       u'~Y': u'\u00DD'}
120
121
122 class strtuple(tuple):
123     """A new type for HTML: a tuple of strings."""
124     pass
125
126 class inttuple(tuple):
127     """A new type for HTML: a tuple of string-ified ints."""
128     pass
129
130 def sane_year(yearAsString, lookahead=20):
131     """Sanity-check years entered as less than four digits.
132     
133     If users want to enter years between 0 A.D. and 99 A.D., they need to
134     enter it as a four-digit year (0000-0099). Dates entered as less than
135     four digits take the current century, unless that places them past the
136     current year by (lookahead) years or more, in which case they take the
137     previous century."""
138     yearAsInt = int(yearAsString)
139     if yearAsInt < 100 and len(yearAsString) < 4:
140         currentYear = datetime.date.today().year
141         currentCentury, rem = divmod(currentYear, 100)
142         yearAsInt += (currentCentury * 100)
143         if yearAsInt >= (currentYear + lookahead):
144             yearAsInt -= 100
145     return yearAsInt
146
147 def parse_date(dateAsString):
148     atoms = re.split(r"\D", dateAsString)
149     if len(atoms) != 3:
150         raise ValueError("Date values must be in the form: mm dd yy.")
151     atoms = sane_year(atoms[2]), int(atoms[0]), int(atoms[1])
152     return atoms
153
154
155 class Adapter(object):
156     """Transform values according to their type. Must be subclassed.
157     
158     In order for your subclass to work, you need to provide functions
159     named 'coerce_' + type, where 'type' refers to the type you wish to
160     support. Replace dots in the type name by underscores. For example,
161     to coerce datetime.date objects, you must provide a function in your
162     subclass named 'coerce_datetime_date'. For builtins, do not include
163     the module name '__builtin__', just use 'coerce_unicode', for example.
164     
165     If you try to coerce a value for whose type you have not provided a
166     coercion function, a TypeError is raised.
167     
168     When writing Adapters for Cation, you should at least provide
169     coerce_* functions for: bool, float, int, inttuple, long, NoneType,
170     str, strtuple, and unicode. For most applications, you should
171     also provide:
172         datetime.datetime
173                 .date
174                 .time
175     and:
176         fixedpoint.FixedPoint or decimal.Decimal (preferably both)
177     """
178    
179     def coerce(self, value, valuetype=None):
180         """coerce(value, valuetype=None) -> value, coerced by valuetype."""
181         if valuetype is None:
182             valuetype = type(value)
183        
184         mod = valuetype.__module__
185         if mod == "__builtin__":
186             xform = "coerce_%s" % valuetype.__name__
187         else:
188             xform = "coerce_%s_%s" % (mod, valuetype.__name__)
189         xform = xform.replace(".", "_")
190         try:
191             xform = getattr(self, xform)
192         except AttributeError:
193             raise TypeError("'%s' is not handled by %s." %
194                             (valuetype, self.__class__))
195         return xform(value)
196
197
198 class AdapterFromHTML(Adapter):
199     """Transform values according to their type from HTML strings."""
200    
201     def substitute_latin_1(self, value):
202         """Replace markup with high-ASCII equiv. (e.g.: ~n with \u00F1)."""
203         value = unicode(value)
204         for eachKey, eachValue in latin1Replacements.items():
205             value = value.replace(eachKey, eachValue)
206         return value
207    
208     def coerce_bool(self, value):
209         return value.lower() in ('true', 't', '1', '-1',
210                                  'y', 'yes', 'on', 'si')
211    
212     def coerce_datetime_datetime(self, value):
213         value = value.strip()
214         if value == '':
215             return None
216         try:
217             aDate, aTime = value.split(" ")
218             timeatoms = map(int, aTime.split(":"))
219         except ValueError:
220             aDate = value
221             timeatoms = [0, 0, 0]
222         try:
223             atoms = re.match(r"^([0-9][0-9]?)[/-]([0-9][0-9]?)[/-]([0-9][0-9]*)",
224                              aDate).groups(0)
225         except AttributeError:
226             return None
227        
228         month, day, year = [x for x in atoms]
229         month, day, year = int(month), int(day), sane_year(year)
230         hour = _pop_head_atom(timeatoms)
231         minute = _pop_head_atom(timeatoms)
232         second = _pop_head_atom(timeatoms)
233         if ('pm' in aTime or 'PM' in aTime) and hour < 12:
234             hour += 12
235         return datetime.datetime(year, month, day, hour, minute, second)
236    
237     def coerce_datetime_date(self, value):
238         value = value.strip()
239         if value == '':
240             return None
241         try:
242             atoms = re.match(r"^([0-9][0-9]?)[/-]([0-9][0-9]?)[/-]([0-9][0-9]*)",
243                              value).groups(0)
244         except AttributeError:
245             return None
246         month, day, year = [x for x in atoms]
247         month, day, year = int(month), int(day), sane_year(year)
248         return datetime.date(year, month, day)
249    
250     def coerce_datetime_time(self, value):
251         value = value.strip()
252         if value == '':
253             return None
254        
255         # Pull out am/pm indicator
256         if value[-1] in ('a', 'p'):
257             pm = (value[-1] == 'p')
258             value = value[:-1]
259         else:
260             pm = False
261        
262         timeatoms = map(int, value.split(":"))
263         hour = _pop_head_atom(timeatoms)
264         minute = _pop_head_atom(timeatoms)
265         second = _pop_head_atom(timeatoms)
266        
267         if pm and hour < 12:
268             hour += 12
269        
270         return datetime.time(hour, minute, second)
271    
272     def coerce_decimal_Decimal(self, value):
273         value = value.replace('$', '')
274         if value == '':
275             return None
276         return Decimal(value)
277    
278     def coerce_float(self, value):
279         value = value.replace('$', '')
280         if value == '':
281             return None
282         return float(value)
283    
284     def coerce_fixedpoint_FixedPoint(self, value):
285         value = value.replace('$', '')
286         if value == '':
287             return None
288         return fixedpoint.FixedPoint(value)
289    
290     def coerce_int(self, value):
291         if value == '':
292             return None
293         value = int(value)
294         if isinstance(value, long):
295             raise ValueError("invalid literal for int(): %s" % value)
296         return value
297    
298     def coerce_alamode_inttuple(self, value):
299         value = [int(x.strip()) for x in value.split(",")]
300         return tuple(value)
301    
302     def coerce_long(self, value):
303         if value == '':
304             return None
305         return long(value)
306    
307     coerce_NoneType = substitute_latin_1
308     coerce_str = substitute_latin_1
309    
310     def coerce_alamode_strtuple(self, value):
311         value = [x.strip() for x in value.split(",")]
312         return tuple(value)
313    
314     coerce_unicode = substitute_latin_1
315
316 from_html = AdapterFromHTML()
317
318
319 class AdapterToHTML(Adapter):
320     """Transform values according to their type to HTML strings."""
321    
322     def to_unicode(self, value):
323         return unicode(value)
324    
325     coerce_bool = to_unicode
326    
327     def coerce_datetime_datetime(self, value):
328         return ('%s/%s/%04d %s:%02d:%02d' %
329                 (value.month, value.day, value.year,
330                  value.hour, value.minute, value.second))
331    
332     def coerce_datetime_date(self, value):
333         return '%s/%s/%04d' % (value.month, value.day, value.year)
334    
335     def coerce_datetime_time(self, value):
336         if value is None:
337             return ""
338         if value.second:
339             return '%s:%02d:%02d' % (value.hour, value.minute, value.second)
340         else:
341             h = value.hour
342             m = value.minute
343             pm = (h >= 12)
344             if h > 12:
345                 h -= 12
346             return u"%d:%02d%s" % (h, m, ('a', 'p')[pm])
347    
348     coerce_decimal_Decimal = to_unicode
349     coerce_fixedpoint_FixedPoint = to_unicode
350     coerce_float = to_unicode
351     coerce_int = to_unicode
352     coerce_alamode_inttuple = to_unicode
353     coerce_long = to_unicode
354    
355     def coerce_NoneType(self, value):
356         return u''
357    
358     coerce_str = to_unicode
359     coerce_alamode_strtuple = to_unicode
360     coerce_unicode = to_unicode
361
362 to_html = AdapterToHTML()
363
364 missing=object()
365
366 def coerce_in(paramMap, key, type, default=missing):
367     """coerce_in(paramMap, key, type, default=missing) -> instance of type.
368     
369     Coerces an inbound parameter to the specified type. If the parameter
370     is not present, the default is returned instead.
371     """
372     try:
373         value = paramMap[key]
374     except KeyError, x:
375         if default is missing:
376             x.args += (u"A value named '%s' was expected "
377                        u"but not supplied." % key,)
378             raise x
379         else:
380             return default
381     return from_html.coerce(value, type)
382
383 def coerce_out(obj, attr=missing, default=missing):
384     """coerce_out(obj, attr=missing, default=missing) -> unicode.
385     
386     Convert an outbound value or attribute to a unicode string.
387     If attr is missing, the object itself is coerced. If attr is
388     present, that attribute of the object is retrieved and coerced.
389     If attr does not exist for obj, the default is returned.
390     """
391     if attr is missing:
392         return to_html.coerce(obj)
393     else:
394         if default is missing:
395             return to_html.coerce(getattr(obj, attr))
396         else:
397             return to_html.coerce(getattr(obj, attr, default))
Note: See TracBrowser for help on using the browser.