1
2
3 """ S3 Extensions for gluon.dal.Field, reusable fields
4
5 @requires: U{B{I{gluon}} <http://web2py.com>}
6
7 @copyright: 2009-2019 (c) Sahana Software Foundation
8 @license: MIT
9
10 Permission is hereby granted, free of charge, to any person
11 obtaining a copy of this software and associated documentation
12 files (the "Software"), to deal in the Software without
13 restriction, including without limitation the rights to use,
14 copy, modify, merge, publish, distribute, sublicense, and/or sell
15 copies of the Software, and to permit persons to whom the
16 Software is furnished to do so, subject to the following
17 conditions:
18
19 The above copyright notice and this permission notice shall be
20 included in all copies or substantial portions of the Software.
21
22 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
23 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
24 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
25 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
26 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
27 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
28 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
29 OTHER DEALINGS IN THE SOFTWARE.
30 """
31
32 import datetime
33 import sys
34 from itertools import chain
35 from uuid import uuid4
36
37 from gluon import current, A, DIV, Field, IS_EMPTY_OR, IS_IN_SET, TAG, URL, XML
38 from gluon.storage import Storage
39 from gluon.languages import lazyT
40
41 from s3dal import SQLCustomType
42 from s3datetime import S3DateTime
43 from s3navigation import S3ScriptItem
44 from s3utils import s3_auth_user_represent, s3_auth_user_represent_name, s3_unicode, s3_str, S3MarkupStripper
45 from s3validators import IS_ISO639_2_LANGUAGE_CODE, IS_ONE_OF, IS_UTC_DATE, IS_UTC_DATETIME
46 from s3widgets import S3CalendarWidget, S3DateWidget
50 """
51 S3 extensions of the gluon.sql.Field class
52 - add "sortby" attribute (used by IS_ONE_OF)
53
54 @todo: add parameters supported by newer PyDAL
55 """
56
57 - def __init__(self, fieldname,
58 type="string",
59 length=None,
60 default=None,
61 required=False,
62 requires="<default>",
63 ondelete="CASCADE",
64 notnull=False,
65 unique=False,
66 uploadfield=True,
67 widget=None,
68 label=None,
69 comment=None,
70 writable=True,
71 readable=True,
72 update=None,
73 authorize=None,
74 autodelete=False,
75 represent=None,
76 uploadfolder=None,
77 compute=None,
78 sortby=None):
79
80 self.sortby = sortby
81
82 Field.__init__(self,
83 fieldname,
84 type=type,
85 length=length,
86 default=default,
87 required=required,
88 requires=requires,
89 ondelete=ondelete,
90 notnull=notnull,
91 unique=unique,
92 uploadfield=uploadfield,
93 widget=widget,
94 label=label,
95 comment=comment,
96 writable=writable,
97 readable=readable,
98 update=update,
99 authorize=authorize,
100 autodelete=autodelete,
101 represent=represent,
102 uploadfolder=uploadfolder,
103 compute=compute,
104 )
105
108 """
109 Helper to attach a representation method to a Field.Method.
110
111 @param name: the field name
112 @param f: the field method
113 @param represent: the representation function
114 @param search_field: the field to use for searches
115 - only used by datatable_filter currently
116 - can only be a single field in the same table currently
117 """
118
119 if represent is None and search_field is None:
120 fieldmethod = Field.Method(name, f)
121
122 else:
123 class Handler(object):
124 def __init__(self, method, row):
125 self.method=method
126 self.row=row
127 def __call__(self, *args, **kwargs):
128 return self.method(self.row, *args, **kwargs)
129
130 if represent is not None:
131 if hasattr(represent, "bulk"):
132 Handler.represent = represent
133 else:
134 Handler.represent = staticmethod(represent)
135
136 if search_field is not None:
137 Handler.search_field = search_field
138
139 fieldmethod = Field.Method(name, f, handler=Handler)
140
141 return fieldmethod
142
145 """
146 DRY Helper for reusable fields:
147
148 This creates neither a Table nor a Field, but just
149 an argument store. The field is created with the __call__
150 method, which is faster than copying an existing field.
151 """
152
153 - def __init__(self, name, type="string", **attr):
154
155 self.name = name
156 self.__type = type
157 self.attr = Storage(attr)
158
159
161
162 if not name:
163 name = self.name
164
165 ia = dict(self.attr)
166
167 DEFAULT = "default"
168 widgets = ia.pop("widgets", {})
169
170 if attr:
171 empty = attr.pop("empty", True)
172 if not empty:
173 requires = ia.get("requires")
174 if requires:
175 if not isinstance(requires, (list, tuple)):
176 requires = [requires]
177 if requires:
178 r = requires[0]
179 if isinstance(r, IS_EMPTY_OR):
180 requires = r.other
181 ia["requires"] = requires
182 widget = attr.pop("widget", DEFAULT)
183 ia.update(**attr)
184 else:
185 widget = DEFAULT
186
187 if isinstance(widget, basestring):
188 if widget == DEFAULT and "widget" in ia:
189 widget = ia["widget"]
190 else:
191 if not isinstance(widgets, dict):
192 widgets = {DEFAULT: widgets}
193 if widget != DEFAULT and widget not in widgets:
194 raise NameError("Undefined widget: %s" % widget)
195 else:
196 widget = widgets.get(widget)
197 ia["widget"] = widget
198
199 script = ia.pop("script", None)
200 if script:
201 comment = ia.get("comment")
202 if comment:
203 ia["comment"] = TAG[""](comment,
204 S3ScriptItem(script=script),
205 )
206 else:
207 ia["comment"] = S3ScriptItem(script=script)
208
209 if ia.get("sortby") is not None:
210 return FieldS3(name, self.__type, **ia)
211 else:
212 return Field(name, self.__type, **ia)
213
216 """
217 Scalable universal field representation for option fields and
218 foreign keys. Can be subclassed and tailored to the particular
219 model where necessary.
220
221 @group Configuration (in the model): __init__
222 @group API (to apply the method): __call__,
223 multiple,
224 bulk,
225 render_list
226 @group Prototypes (to adapt in subclasses): lookup_rows,
227 represent_row,
228 link
229 @group Internal Methods: _setup,
230 _lookup
231 """
232
233 - def __init__(self,
234 lookup = None,
235 key = None,
236 fields = None,
237 labels = None,
238 options = None,
239 translate = False,
240 linkto = None,
241 show_link = False,
242 multiple = False,
243 hierarchy = False,
244 default = None,
245 none = None,
246 field_sep = " "
247 ):
248 """
249 Constructor
250
251 @param lookup: the name of the lookup table
252 @param key: the field name of the primary key of the lookup table,
253 a field name
254 @param fields: the fields to extract from the lookup table, a list
255 of field names
256 @param labels: string template or callable to represent rows from
257 the lookup table, callables must return a string
258 @param options: dictionary of options to lookup the representation
259 of a value, overrides lookup and key
260 @param multiple: web2py list-type (all values will be lists)
261 @param hierarchy: render a hierarchical representation, either
262 True or a string template like "%s > %s"
263 @param translate: translate all representations (using T)
264 @param linkto: a URL (as string) to link representations to,
265 with "[id]" as placeholder for the key
266 @param show_link: whether to add a URL to representations
267 @param default: default representation for unknown options
268 @param none: representation for empty fields (None or empty list)
269 @param field_sep: separator to use to join fields
270 """
271
272 self.tablename = lookup
273 self.table = None
274 self.key = key
275 self.fields = fields
276 self.labels = labels
277 self.options = options
278 self.list_type = multiple
279 self.hierarchy = hierarchy
280 self.translate = translate
281 self.linkto = linkto
282 self.show_link = show_link
283 self.default = default
284 self.none = none
285 self.field_sep = field_sep
286 self.setup = False
287 self.theset = None
288 self.queries = 0
289 self.lazy = []
290 self.lazy_show_link = False
291
292 self.rows = {}
293
294 self.clabels = None
295 self.slabels = None
296 self.htemplate = None
297
298
299
300 self.func_code = Storage(co_argcount = 1)
301 self.func_defaults = None
302
303
304 if self.lookup_rows.__func__ is not S3Represent.lookup_rows.__func__:
305 self.custom_lookup = True
306 else:
307 self.custom_lookup = False
308
309
311 """
312 Lookup all rows referenced by values.
313 (in foreign key representations)
314
315 @param key: the key Field
316 @param values: the values
317 @param fields: the fields to retrieve
318 """
319
320 if fields is None:
321 fields = []
322 fields.append(key)
323
324 if len(values) == 1:
325 query = (key == values[0])
326 else:
327 query = key.belongs(values)
328 rows = current.db(query).select(*fields)
329 self.queries += 1
330 return rows
331
332
334 """
335 Represent the referenced row.
336 (in foreign key representations)
337
338 @param row: the row
339 @param prefix: prefix for hierarchical representation
340
341 @return: the representation of the Row, or None if there
342 is an error in the Row
343 """
344
345 labels = self.labels
346
347 translated = False
348
349 if self.slabels:
350
351 try:
352 row_dict = row.as_dict()
353 except AttributeError:
354
355 row_dict = row
356
357
358 none = self.none
359 for k, v in row_dict.items():
360 if v is None:
361 row_dict[k] = none
362
363 v = labels % row_dict
364
365 elif self.clabels:
366
367 v = labels(row)
368
369 else:
370
371 values = [row[f] for f in self.fields if row[f] not in (None, "")]
372
373 if len(values) > 1:
374
375 if self.translate:
376
377 T = current.T
378 values = [T(v) if not type(v) is lazyT else v for v in values]
379 translated = True
380 sep = self.field_sep
381 v = sep.join(s3_str(value) for value in values)
382 elif values:
383 v = s3_str(values[0])
384 else:
385 v = self.none
386
387 if not translated and self.translate and not type(v) is lazyT:
388 output = current.T(v)
389 else:
390 output = v
391
392 if prefix and self.hierarchy:
393 return self.htemplate % (prefix, output)
394
395 return output
396
397
398 - def link(self, k, v, row=None):
399 """
400 Represent a (key, value) as hypertext link.
401
402 - Typically, k is a foreign key value, and v the
403 representation of the referenced record, and the link
404 shall open a read view of the referenced record.
405
406 - In the base class, the linkto-parameter expects a URL (as
407 string) with "[id]" as placeholder for the key.
408
409 @param k: the key
410 @param v: the representation of the key
411 @param row: the row with this key (unused in the base class)
412 """
413
414 if self.linkto:
415 k = s3_str(k)
416 return A(v, _href=self.linkto.replace("[id]", k) \
417 .replace("%5Bid%5D", k))
418 else:
419 return v
420
421
422 - def __call__(self, value, row=None, show_link=True):
423 """
424 Represent a single value (standard entry point).
425
426 @param value: the value
427 @param row: the referenced row (if value is a foreign key)
428 @param show_link: render the representation as link
429 """
430
431 self._setup()
432 show_link = show_link and self.show_link
433
434 if self.list_type:
435
436 return self.multiple(value,
437 rows=row,
438 list_type=False,
439 show_link=show_link)
440
441
442 if row and self.table:
443 value = row[self.key]
444
445
446 if value:
447 rows = [row] if row is not None else None
448 items = self._lookup([value], rows=rows)
449 if value in items:
450 k, v = value, items[value]
451 r = self.link(k, v, row=self.rows.get(k)) \
452 if show_link else items[value]
453 else:
454 r = self.default
455 return r
456 return self.none
457
458
459 - def multiple(self, values, rows=None, list_type=True, show_link=True):
460 """
461 Represent multiple values as a comma-separated list.
462
463 @param values: list of values
464 @param rows: the referenced rows (if values are foreign keys)
465 @param show_link: render each representation as link
466 """
467
468 self._setup()
469 show_link = show_link and self.show_link
470
471
472 if rows and self.table:
473 key = self.key
474 values = [row[key] for row in rows]
475 elif self.list_type and list_type:
476 try:
477 hasnone = None in values
478 if hasnone:
479 values = [i for i in values if i != None]
480 values = list(set(chain.from_iterable(values)))
481 if hasnone:
482 values.append(None)
483 except TypeError:
484 raise ValueError("List of lists expected, got %s" % values)
485 else:
486 values = [values] if type(values) is not list else values
487
488
489 if values:
490 default = self.default
491 items = self._lookup(values, rows=rows)
492 if show_link:
493 link = self.link
494 rows = self.rows
495 labels = [[link(k, s3_str(items[k]), row=rows.get(k)), ", "]
496 if k in items else [default, ", "]
497 for k in values]
498 if labels:
499 return TAG[""](list(chain.from_iterable(labels))[:-1])
500 else:
501 return ""
502 else:
503 labels = [s3_str(items[k])
504 if k in items else default for k in values]
505 if labels:
506 return ", ".join(labels)
507 return self.none
508
509
510 - def bulk(self, values, rows=None, list_type=True, show_link=True):
511 """
512 Represent multiple values as dict {value: representation}
513
514 @param values: list of values
515 @param rows: the rows
516 @param show_link: render each representation as link
517
518 @return: a dict {value: representation}
519
520 @note: for list-types, the dict keys will be the individual
521 values within all lists - and not the lists (simply
522 because lists can not be dict keys). Thus, the caller
523 would still have to construct the final string/HTML.
524 """
525
526 self._setup()
527 show_link = show_link and self.show_link
528
529
530 if rows and self.table:
531 key = self.key
532 _rows = self.rows
533 values = set()
534 add_value = values.add
535 for row in rows:
536 value = row[key]
537 _rows[value] = row
538 add_value(value)
539 values = list(values)
540 elif self.list_type and list_type:
541 try:
542 hasnone = None in values
543 if hasnone:
544 values = [i for i in values if i != None]
545 values = list(set(chain.from_iterable(values)))
546 if hasnone:
547 values.append(None)
548 except TypeError:
549 raise ValueError("List of lists expected, got %s" % values)
550 else:
551 values = [values] if type(values) is not list else values
552
553
554 if values:
555 labels = self._lookup(values, rows=rows)
556 if show_link:
557 link = self.link
558 rows = self.rows
559 labels = dict((k, link(k, v, rows.get(k)))
560 for k, v in labels.items())
561 for k in values:
562 if k not in labels:
563 labels[k] = self.default
564 else:
565 labels = {}
566 labels[None] = self.none
567 return labels
568
569
571 """
572 Helper method to render list-type representations from
573 bulk()-results.
574
575 @param value: the list
576 @param labels: the labels as returned from bulk()
577 @param show_link: render references as links, should
578 be the same as used with bulk()
579 """
580
581 show_link = show_link and self.show_link
582 if show_link:
583 labels = [(labels[v], ", ")
584 if v in labels else (self.default, ", ")
585 for v in value]
586 if labels:
587 return TAG[""](list(chain.from_iterable(labels))[:-1])
588 else:
589 return ""
590 else:
591 return ", ".join([s3_str(labels[v])
592 if v in labels else self.default
593 for v in value])
594
595
597 """ Lazy initialization of defaults """
598
599 if self.setup:
600 return
601
602 self.queries = 0
603
604
605 messages = current.messages
606 if self.default is None:
607 self.default = s3_str(messages.UNKNOWN_OPT)
608 if self.none is None:
609 self.none = messages["NONE"]
610
611
612 if self.options is not None:
613 if self.translate:
614 T = current.T
615 self.theset = dict((opt, T(label))
616 if isinstance(label, basestring) else (opt, label)
617 for opt, label in self.options.items()
618 )
619 else:
620 self.theset = self.options
621 else:
622 self.theset = {}
623
624
625 if self.table is None:
626 tablename = self.tablename
627 if tablename:
628 table = current.s3db.table(tablename)
629 if table is not None:
630 if self.key is None:
631 self.key = table._id.name
632 if not self.fields:
633 if "name" in table:
634 self.fields = ["name"]
635 else:
636 self.fields = [self.key]
637 self.table = table
638 if self.linkto is None and self.show_link:
639 c, f = tablename.split("_", 1)
640 self.linkto = URL(c=c, f=f, args=["[id]"], extension="")
641
642
643 labels = self.labels
644
645 self.slabels = isinstance(labels, (basestring, lazyT))
646
647 self.clabels = callable(labels)
648
649
650 if isinstance(self.hierarchy, basestring):
651 self.htemplate = self.hierarchy
652 else:
653 self.htemplate = "%s > %s"
654
655 self.setup = True
656
657
658 - def _lookup(self, values, rows=None):
659 """
660 Lazy lookup values.
661
662 @param values: list of values to lookup
663 @param rows: rows referenced by values (if values are foreign keys)
664 optional
665 """
666
667 theset = self.theset
668
669 keys = {}
670 items = {}
671 lookup = {}
672
673
674 table = self.table
675 for _v in values:
676 v = _v
677 if v is not None and table and isinstance(v, basestring):
678 try:
679 v = int(_v)
680 except ValueError:
681 pass
682 keys[v] = _v
683 if v is None:
684 items[_v] = self.none
685 elif v in theset:
686 items[_v] = theset[v]
687 else:
688 lookup[v] = True
689
690 if table is None or not lookup:
691 return items
692
693 if table and self.hierarchy:
694
695 from s3hierarchy import S3Hierarchy
696 h = S3Hierarchy(table._tablename)
697 if h.config:
698 def lookup_parent(node_id):
699 parent = h.parent(node_id)
700 if parent and \
701 parent not in theset and \
702 parent not in lookup:
703 lookup[parent] = False
704 lookup_parent(parent)
705 return
706 for node_id in lookup.keys():
707 lookup_parent(node_id)
708 else:
709 h = None
710 else:
711 h = None
712
713
714 pkey = self.key
715 ogetattr = object.__getattribute__
716 try:
717 key = ogetattr(table, pkey)
718 except AttributeError:
719 return items
720
721
722 pop = lookup.pop
723 represent_row = self.represent_row
724 represent_path = self._represent_path
725 if rows and not self.custom_lookup:
726 rows_ = dict((row[key], row) for row in rows)
727 self.rows.update(rows_)
728 for row in rows:
729 k = row[key]
730 if k not in theset:
731 if h:
732 theset[k] = represent_path(k,
733 row,
734 rows = rows_,
735 hierarchy = h,
736 )
737 else:
738 theset[k] = represent_row(row)
739 if pop(k, None):
740 items[keys.get(k, k)] = theset[k]
741
742
743 if lookup:
744 if not self.custom_lookup:
745 try:
746
747 fields = [ogetattr(table, f) for f in self.fields]
748 except AttributeError:
749
750 current.log.error(sys.exc_info()[1])
751 fields = [ogetattr(table, f)
752 for f in self.fields if hasattr(table, f)]
753 else:
754 fields = []
755 rows = self.lookup_rows(key, lookup.keys(), fields=fields)
756 rows = dict((row[key], row) for row in rows)
757 self.rows.update(rows)
758 if h:
759 for k, row in rows.items():
760 if lookup.pop(k, None):
761 items[keys.get(k, k)] = represent_path(k,
762 row,
763 rows = rows,
764 hierarchy = h,
765 )
766 else:
767 for k, row in rows.items():
768 lookup.pop(k, None)
769 items[keys.get(k, k)] = theset[k] = represent_row(row)
770
771
772 if lookup:
773 for k in lookup:
774 items[keys.get(k, k)] = self.default
775
776 return items
777
778
810
813 """
814 Lazy Representation of a field value, utilizes the bulk-feature
815 of S3Represent-style representation methods
816 """
817
819 """
820 Constructor
821
822 @param value: the value
823 @param renderer: the renderer (S3Represent instance)
824 """
825
826 self.value = value
827 self.renderer = renderer
828
829 self.multiple = False
830 renderer.lazy.append(value)
831
832
836
837
839 """ Represent as string """
840
841 value = self.value
842 renderer = self.renderer
843 if renderer.lazy:
844 labels = renderer.bulk(renderer.lazy, show_link=False)
845 renderer.lazy = []
846 else:
847 labels = renderer.theset
848 if renderer.list_type:
849 if self.multiple:
850 return renderer.multiple(value, show_link=False)
851 else:
852 return renderer.render_list(value, labels, show_link=False)
853 else:
854 if self.multiple:
855 return renderer.multiple(value, show_link=False)
856 else:
857 return renderer(value, show_link=False)
858
859
861 """ Render as HTML """
862
863 value = self.value
864 renderer = self.renderer
865 if renderer.lazy:
866 labels = renderer.bulk(renderer.lazy)
867 renderer.lazy = []
868 else:
869 labels = renderer.theset
870 if renderer.list_type:
871 if not value:
872 value = []
873 if self.multiple:
874 if len(value) and type(value[0]) is not list:
875 value = [value]
876 return renderer.multiple(value)
877 else:
878 return renderer.render_list(value, labels)
879 else:
880 if self.multiple:
881 return renderer.multiple(value)
882 else:
883 return renderer(value)
884
885
887 """
888 Render as text or attribute of an XML element
889
890 @param element: the element
891 @param attributes: the attributes dict of the element
892 @param name: the attribute name
893 """
894
895
896 text = s3_unicode(self.represent())
897
898
899 if text and "<" in text:
900 try:
901 stripper = S3MarkupStripper()
902 stripper.feed(text)
903 text = stripper.stripped()
904 except:
905 pass
906
907
908 if text is not None:
909 if element is not None:
910 element.text = text
911 else:
912 attributes[name] = text
913 return
914
915
916
917
918
919 s3uuid = SQLCustomType(type = "string",
920 native = "VARCHAR(128)",
921 encoder = lambda x: "%s" % (uuid4().urn
922 if x == ""
923 else str(x.encode("utf-8"))),
924 decoder = lambda x: x)
925
926
927 auth_group_represent = S3Represent(lookup="auth_group", fields=["role"])
928
929 ALL_META_FIELD_NAMES = ("uuid",
930 "mci",
931 "deleted",
932 "deleted_fk",
933 "deleted_rb",
934 "created_on",
935 "created_by",
936 "modified_on",
937 "modified_by",
938 "approved_by",
939 "owned_by_user",
940 "owned_by_group",
941 "realm_entity",
942 )
1243
1253
1262
1267 """
1268 Role Required to access a resource
1269 - used by GIS for map layer permissions management
1270 """
1271
1272 T = current.T
1273 gtable = current.auth.settings.table_group
1274 represent = S3Represent(lookup="auth_group", fields=["role"])
1275 return FieldS3("role_required", gtable,
1276 sortby="role",
1277 requires = IS_EMPTY_OR(
1278 IS_ONE_OF(current.db, "auth_group.id",
1279 represent,
1280 zero=T("Public"))),
1281
1282
1283
1284 represent = represent,
1285 label = T("Role Required"),
1286 comment = DIV(_class="tooltip",
1287 _title="%s|%s" % (T("Role Required"),
1288 T("If this record should be restricted then select which role is required to access the record here."),
1289 ),
1290 ),
1291 ondelete = "RESTRICT",
1292 )
1293
1296 """
1297 List of Roles Permitted to access a resource
1298 - used by CMS
1299 """
1300
1301 T = current.T
1302 represent = S3Represent(lookup="auth_group", fields=["role"])
1303 if "label" not in attr:
1304 attr["label"] = T("Roles Permitted")
1305 if "sortby" not in attr:
1306 attr["sortby"] = "role"
1307 if "represent" not in attr:
1308 attr["represent"] = represent
1309 if "requires" not in attr:
1310 attr["requires"] = IS_EMPTY_OR(IS_ONE_OF(current.db,
1311 "auth_group.id",
1312 represent,
1313 multiple=True))
1314 if "comment" not in attr:
1315 attr["comment"] = DIV(_class="tooltip",
1316 _title="%s|%s" % (T("Roles Permitted"),
1317 T("If this record should be restricted then select which role(s) are permitted to access the record here.")))
1318 if "ondelete" not in attr:
1319 attr["ondelete"] = "RESTRICT"
1320
1321 return FieldS3(name, "list:reference auth_group", **attr)
1322
1351
1354 """
1355 Return a standard Currency field
1356
1357 @ToDo: Move to a Finance module?
1358 """
1359
1360 settings = current.deployment_settings
1361
1362 if "label" not in attr:
1363 attr["label"] = current.T("Currency")
1364 if "default" not in attr:
1365 attr["default"] = settings.get_fin_currency_default()
1366 if "requires" not in attr:
1367 currency_opts = settings.get_fin_currencies()
1368 attr["requires"] = IS_IN_SET(currency_opts.keys(),
1369 zero=None)
1370 if "writable" not in attr:
1371 attr["writable"] = settings.get_fin_currency_writable()
1372
1373 return Field(name, length=3, **attr)
1374
1377 """
1378 Return a standard Language field
1379 """
1380
1381 if "label" not in attr:
1382 attr["label"] = current.T("Language")
1383 if "default" not in attr:
1384 attr["default"] = current.deployment_settings.get_L10n_default_language()
1385 empty = attr.pop("empty", None)
1386 if empty:
1387 zero = ""
1388 else:
1389 zero = None
1390 list_from_settings = attr.pop("list_from_settings", True)
1391 select = attr.pop("select", None)
1392 translate = attr.pop("translate", True)
1393 if select or not list_from_settings:
1394 requires = IS_ISO639_2_LANGUAGE_CODE(select = select,
1395 sort = True,
1396 translate = translate,
1397 zero = zero,
1398 )
1399 else:
1400
1401 requires = IS_ISO639_2_LANGUAGE_CODE(sort = True,
1402 translate = translate,
1403 zero = zero,
1404 )
1405 if "requires" not in attr:
1406 if empty is False:
1407 attr["requires"] = requires
1408 else:
1409
1410 attr["requires"] = IS_EMPTY_OR(requires)
1411 if "represent" not in attr:
1412 attr["represent"] = requires.represent
1413
1414 return Field(name, length=8, **attr)
1415
1416
1417 -def s3_date(name="date", **attr):
1418 """
1419 Return a standard date-field
1420
1421 @param name: the field name
1422
1423 @keyword default: the field default, can be specified as "now" for
1424 current date, or as Python date
1425 @keyword past: number of selectable past months
1426 @keyword future: number of selectable future months
1427 @keyword widget: the form widget for the field, can be specified
1428 as "date" for S3DateWidget, "calendar" for
1429 S3CalendarWidget, or as a web2py FormWidget,
1430 defaults to "calendar"
1431 @keyword calendar: the calendar to use for this widget, defaults
1432 to current.calendar
1433 @keyword start_field: CSS selector for the start field for interval
1434 selection
1435 @keyword default_interval: the default interval
1436 @keyword default_explicit: whether the user must click the field
1437 to set the default, or whether it will
1438 automatically be set when the value for
1439 start_field is set
1440 @keyword set_min: CSS selector for another date/time widget to
1441 dynamically set the minimum selectable date/time to
1442 the value selected in this widget
1443 @keyword set_max: CSS selector for another date/time widget to
1444 dynamically set the maximum selectable date/time to
1445 the value selected in this widget
1446
1447 @note: other S3ReusableField keywords are also supported (in addition
1448 to the above)
1449
1450 @note: calendar-option requires widget="calendar" (default), otherwise
1451 Gregorian calendar is enforced for the field
1452
1453 @note: set_min/set_max only supported for widget="calendar" (default)
1454
1455 @note: interval options currently not supported by S3CalendarWidget,
1456 only available with widget="date"
1457 @note: start_field and default_interval should be given together
1458
1459 @note: sets a default field label "Date" => use label-keyword to
1460 override if necessary
1461 @note: sets a default validator IS_UTC_DATE => use requires-keyword
1462 to override if necessary
1463 @note: sets a default representation S3DateTime.date_represent => use
1464 represent-keyword to override if necessary
1465
1466 @ToDo: Different default field name in case we need to start supporting
1467 Oracle, where 'date' is a reserved word
1468 """
1469
1470 attributes = dict(attr)
1471
1472
1473 calendar = attributes.pop("calendar", None)
1474
1475
1476 past = attributes.pop("past", None)
1477 future = attributes.pop("future", None)
1478
1479
1480 if "label" not in attributes:
1481 attributes["label"] = current.T("Date")
1482
1483
1484 WIDGET_OPTIONS = ("start_field",
1485 "default_interval",
1486 "default_explicit",
1487 "set_min",
1488 "set_max",
1489 )
1490
1491
1492 widget = attributes.get("widget", "calendar")
1493 widget_options = {}
1494 if widget == "date":
1495
1496
1497
1498
1499 calendar = "Gregorian"
1500
1501
1502 if past is not None:
1503 widget_options["past"] = past
1504 if future is not None:
1505 widget_options["future"] = future
1506
1507
1508 SUPPORTED_OPTIONS = ("start_field",
1509 "default_interval",
1510 "default_explicit",
1511 )
1512 for option in WIDGET_OPTIONS:
1513 if option in attributes:
1514 if option in SUPPORTED_OPTIONS:
1515 widget_options[option] = attributes[option]
1516 del attributes[option]
1517
1518 widget = S3DateWidget(**widget_options)
1519
1520 elif widget == "calendar":
1521
1522
1523 widget_options["calendar"] = calendar
1524
1525
1526 if past is not None:
1527 widget_options["past_months"] = past
1528 if future is not None:
1529 widget_options["future_months"] = future
1530
1531
1532 SUPPORTED_OPTIONS = ("set_min",
1533 "set_max",
1534 )
1535 for option in WIDGET_OPTIONS:
1536 if option in attributes:
1537 if option in SUPPORTED_OPTIONS:
1538 widget_options[option] = attributes[option]
1539 del attributes[option]
1540
1541 widget = S3CalendarWidget(**widget_options)
1542
1543 else:
1544
1545 for option in WIDGET_OPTIONS:
1546 attributes.pop(option, None)
1547
1548 attributes["widget"] = widget
1549
1550
1551 now = current.request.utcnow.date()
1552 if attributes.get("default") == "now":
1553 attributes["default"] = now
1554
1555
1556 if "represent" not in attributes:
1557 attributes["represent"] = lambda dt: \
1558 S3DateTime.date_represent(dt,
1559 utc=True,
1560 calendar=calendar,
1561 )
1562
1563
1564 if "requires" not in attributes:
1565
1566 if past is None and future is None:
1567 requires = IS_UTC_DATE(calendar=calendar)
1568 else:
1569 from dateutil.relativedelta import relativedelta
1570 minimum = maximum = None
1571 if past is not None:
1572 minimum = now - relativedelta(months = past)
1573 if future is not None:
1574 maximum = now + relativedelta(months = future)
1575 requires = IS_UTC_DATE(calendar=calendar,
1576 minimum=minimum,
1577 maximum=maximum,
1578 )
1579
1580 empty = attributes.pop("empty", None)
1581 if empty is False:
1582 attributes["requires"] = requires
1583 else:
1584
1585 attributes["requires"] = IS_EMPTY_OR(requires)
1586
1587 return Field(name, "date", **attributes)
1588
1591 """
1592 Return a standard datetime field
1593
1594 @param name: the field name
1595
1596 @keyword default: the field default, can be specified as "now" for
1597 current date/time, or as Python date
1598
1599 @keyword past: number of selectable past hours
1600 @keyword future: number of selectable future hours
1601
1602 @keyword widget: form widget option, can be specified as "date"
1603 for date-only, or "datetime" for date+time (default),
1604 or as a web2py FormWidget
1605 @keyword calendar: the calendar to use for this field, defaults
1606 to current.calendar
1607 @keyword set_min: CSS selector for another date/time widget to
1608 dynamically set the minimum selectable date/time to
1609 the value selected in this widget
1610 @keyword set_max: CSS selector for another date/time widget to
1611 dynamically set the maximum selectable date/time to
1612 the value selected in this widget
1613
1614 @note: other S3ReusableField keywords are also supported (in addition
1615 to the above)
1616
1617 @note: sets a default field label "Date" => use label-keyword to
1618 override if necessary
1619 @note: sets a default validator IS_UTC_DATE/IS_UTC_DATETIME => use
1620 requires-keyword to override if necessary
1621 @note: sets a default representation S3DateTime.date_represent or
1622 S3DateTime.datetime_represent respectively => use the
1623 represent-keyword to override if necessary
1624
1625 @ToDo: Different default field name in case we need to start supporting
1626 Oracle, where 'date' is a reserved word
1627 """
1628
1629 attributes = dict(attr)
1630
1631
1632 calendar = attributes.pop("calendar", None)
1633
1634
1635 limits = {}
1636 for keyword in ("past", "future", "min", "max"):
1637 if keyword in attributes:
1638 limits[keyword] = attributes[keyword]
1639 del attributes[keyword]
1640
1641
1642 widget = attributes.pop("widget", None)
1643 now = current.request.utcnow
1644 if widget == "date":
1645
1646
1647
1648 def limit(delta):
1649 current_month = now.month
1650 years, hours = divmod(-delta, 8760)
1651 months = divmod(hours, 744)[0]
1652 if months > current_month:
1653 years += 1
1654 month = divmod((current_month - months) + 12, 12)[1]
1655 year = now.year - years
1656 return now.replace(month=month, year=year)
1657
1658 earliest = limits.get("min")
1659 if not earliest:
1660 past = limits.get("past")
1661 if past is not None:
1662 earliest = limit(-past)
1663 latest = limits.get("max")
1664 if not latest:
1665 future = limits.get("future")
1666 if future is not None:
1667 latest = limit(future)
1668 else:
1669
1670 earliest = limits.get("min")
1671 if not earliest:
1672 past = limits.get("past")
1673 if past is not None:
1674 earliest = now - datetime.timedelta(hours=past)
1675 latest = limits.get("max")
1676 if not latest:
1677 future = limits.get("future")
1678 if future is not None:
1679 latest = now + datetime.timedelta(hours=future)
1680
1681
1682 if "label" not in attributes:
1683 attributes["label"] = current.T("Date")
1684
1685
1686 set_min = attributes.pop("set_min", None)
1687 set_max = attributes.pop("set_max", None)
1688 date_only = False
1689 if widget == "date":
1690 date_only = True
1691 widget = S3CalendarWidget(calendar = calendar,
1692 timepicker = False,
1693 minimum = earliest,
1694 maximum = latest,
1695 set_min = set_min,
1696 set_max = set_max,
1697 )
1698 elif widget is None or widget == "datetime":
1699 widget = S3CalendarWidget(calendar = calendar,
1700 timepicker = True,
1701 minimum = earliest,
1702 maximum = latest,
1703 set_min = set_min,
1704 set_max = set_max,
1705 )
1706 attributes["widget"] = widget
1707
1708
1709 if attributes.get("default") == "now":
1710 attributes["default"] = now
1711
1712
1713 represent = attributes.pop("represent", None)
1714 represent_method = None
1715 if represent == "date" or represent is None and date_only:
1716 represent_method = S3DateTime.date_represent
1717 elif represent is None:
1718 represent_method = S3DateTime.datetime_represent
1719 if represent_method:
1720 represent = lambda dt: represent_method(dt,
1721 utc=True,
1722 calendar=calendar,
1723 )
1724 attributes["represent"] = represent
1725
1726
1727 if "requires" not in attributes:
1728 if date_only:
1729 validator = IS_UTC_DATE
1730 else:
1731 validator = IS_UTC_DATETIME
1732 requires = validator(calendar=calendar,
1733 minimum=earliest,
1734 maximum=latest,
1735 )
1736 empty = attributes.pop("empty", None)
1737 if empty is False:
1738 attributes["requires"] = requires
1739 else:
1740 attributes["requires"] = IS_EMPTY_OR(requires)
1741
1742 return Field(name, "datetime", **attributes)
1743
1744
1745