Contact: fumanchu@aminus.org

Log in as guest/misc to create tickets

root/alamode.py

Revision 127 (checked in by fumanchu, 1 year ago)

License and email cleanups.

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