Contact: fumanchu@aminus.org

Log in as guest/misc to create tickets

root/alamode.py

Revision 21 (checked in by fumanchu, 7 years ago)

Added today(), now() builtins for use with "from alamode import *".

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