| Home | Trees | Indices | Help |
|
|---|
|
|
1 # -*- coding: utf-8 -*-
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
47
48 # =============================================================================
49 -class FieldS3(Field):
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
106 # =============================================================================
107 -def s3_fieldmethod(name, f, represent=None, search_field=None):
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
143 # =============================================================================
144 -class S3ReusableField(object):
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
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
214 # =============================================================================
215 -class S3Represent(object):
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 # Attributes to simulate being a function for sqlhtml's represent()
299 # Make sure we indicate only 1 position argument
300 self.func_code = Storage(co_argcount = 1)
301 self.func_defaults = None
302
303 # Detect lookup_rows override
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 # String Template or lazyT
351 try:
352 row_dict = row.as_dict()
353 except AttributeError:
354 # Row just a dict/Storage after all? (e.g. custom lookup)
355 row_dict = row
356
357 # Represent None as self.none
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 # External Renderer
367 v = labels(row)
368
369 else:
370 # Default
371 values = [row[f] for f in self.fields if row[f] not in (None, "")]
372
373 if len(values) > 1:
374 # Multiple values => concatenate with separator
375 if self.translate:
376 # Translate items individually before concatenating
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 # -------------------------------------------------------------------------
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 # -------------------------------------------------------------------------
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 # Is a list-type => use multiple
436 return self.multiple(value,
437 rows=row,
438 list_type=False,
439 show_link=show_link)
440
441 # Prefer the row over the value
442 if row and self.table:
443 value = row[self.key]
444
445 # Lookup the representation
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 # -------------------------------------------------------------------------
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 # Get the values
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 # Lookup the representations
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 # -------------------------------------------------------------------------
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 # Get the values
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 # Lookup the representations
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 # Default representations
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 # Initialize theset
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 # Lookup table parameters and linkto
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 # What type of renderer do we use?
643 labels = self.labels
644 # String template?
645 self.slabels = isinstance(labels, (basestring, lazyT))
646 # External renderer?
647 self.clabels = callable(labels)
648
649 # Hierarchy template
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 # -------------------------------------------------------------------------
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 # Check whether values are already in theset
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 # Does the lookup table have a hierarchy?
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 # Get the primary key
714 pkey = self.key
715 ogetattr = object.__getattribute__
716 try:
717 key = ogetattr(table, pkey)
718 except AttributeError:
719 return items
720
721 # Use the given rows to lookup the values
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 # Retrieve additional rows as needed
743 if lookup:
744 if not self.custom_lookup:
745 try:
746 # Need for speed: assume all fields are in table
747 fields = [ogetattr(table, f) for f in self.fields]
748 except AttributeError:
749 # Ok - they are not: provide debug output and filter fields
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 # Anything left gets set to default
772 if lookup:
773 for k in lookup:
774 items[keys.get(k, k)] = self.default
775
776 return items
777
778 # -------------------------------------------------------------------------
780 """
781 Recursive helper method to represent value as path in
782 a hierarchy.
783
784 @param value: the value
785 @param row: the row containing the value
786 @param rows: all rows from _loopup as dict
787 @param hierarchy: the S3Hierarchy instance
788 """
789
790 theset = self.theset
791
792 if value in theset:
793 return theset[value]
794
795 prefix = None
796 parent = hierarchy.parent(value)
797
798 if parent:
799 if parent in theset:
800 prefix = theset[parent]
801 elif parent in rows:
802 prefix = self._represent_path(parent,
803 rows[parent],
804 rows=rows,
805 hierarchy=hierarchy)
806
807 result = self.represent_row(row, prefix=prefix)
808 theset[value] = result
809 return result
810
811 # =============================================================================
812 -class S3RepresentLazy(object):
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 # Render value
896 text = s3_unicode(self.represent())
897
898 # Strip markup + XML-escape
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 # Add to node
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 # Meta-fields
917 #
918 # Use URNs according to http://tools.ietf.org/html/rfc4122
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 # Representation of user roles (auth_group)
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 )
943
944 # -----------------------------------------------------------------------------
945 -class S3MetaFields(object):
946 """ Class to standardize meta-fields """
947
948 # -------------------------------------------------------------------------
949 @staticmethod
951 """
952 Universally unique record identifier according to RFC4122, as URN
953 (e.g. "urn:uuid:fd8f97ab-1252-4d62-9982-8e3f3025307f"); uuids are
954 mandatory for synchronization (incl. EdenMobile)
955 """
956
957 return Field("uuid", type=s3uuid,
958 default = "",
959 length = 128,
960 notnull = True,
961 unique = True,
962 readable = False,
963 writable = False,
964 )
965
966 # -------------------------------------------------------------------------
967 @staticmethod
969 """
970 Master-Copy-Index - whether this record has been created locally
971 or imported ("copied") from another source:
972 - mci=0 means "created here"
973 - mci>0 means "copied n times"
974 """
975
976 return Field("mci", "integer",
977 default = 0,
978 readable = False,
979 writable = False,
980 )
981
982 # -------------------------------------------------------------------------
983 @staticmethod
985 """
986 Deletion status (True=record is deleted)
987 """
988
989 return Field("deleted", "boolean",
990 default = False,
991 readable = False,
992 writable = False,
993 )
994
995 # -------------------------------------------------------------------------
996 @staticmethod
998 """
999 Foreign key values of this record before deletion (foreign keys
1000 are set to None during deletion to derestrict constraints)
1001 """
1002
1003 return Field("deleted_fk", #"text",
1004 readable = False,
1005 writable = False,
1006 )
1007
1008 # -------------------------------------------------------------------------
1009 @staticmethod
1011 """
1012 De-duplication: ID of the record that has replaced this record
1013 """
1014
1015 return Field("deleted_rb", "integer",
1016 readable = False,
1017 writable = False,
1018 )
1019
1020 # -------------------------------------------------------------------------
1021 @staticmethod
1023 """
1024 Date/time when the record was created
1025 """
1026
1027 return Field("created_on", "datetime",
1028 readable = False,
1029 writable = False,
1030 default = datetime.datetime.utcnow,
1031 )
1032
1033 # -------------------------------------------------------------------------
1034 @staticmethod
1036 """
1037 Date/time when the record was last modified
1038 """
1039
1040 return Field("modified_on", "datetime",
1041 readable = False,
1042 writable = False,
1043 default = datetime.datetime.utcnow,
1044 update = datetime.datetime.utcnow,
1045 )
1046
1047 # -------------------------------------------------------------------------
1048 @classmethod
1050 """
1051 Auth_user ID of the user who created the record
1052 """
1053
1054 return Field("created_by", current.auth.settings.table_user,
1055 readable = False,
1056 writable = False,
1057 requires = None,
1058 default = cls._current_user(),
1059 represent = cls._represent_user(),
1060 ondelete = "RESTRICT",
1061 )
1062
1063 # -------------------------------------------------------------------------
1064 @classmethod
1066 """
1067 Auth_user ID of the last user who modified the record
1068 """
1069
1070 current_user = cls._current_user()
1071 return Field("modified_by", current.auth.settings.table_user,
1072 readable = False,
1073 writable = False,
1074 requires = None,
1075 default = current_user,
1076 update = current_user,
1077 represent = cls._represent_user(),
1078 ondelete = "RESTRICT",
1079 )
1080
1081 # -------------------------------------------------------------------------
1082 @classmethod
1084 """
1085 Auth_user ID of the user who has approved the record:
1086 - None means unapproved
1087 - 0 means auto-approved
1088 """
1089
1090 return Field("approved_by", "integer",
1091 readable = False,
1092 writable = False,
1093 requires = None,
1094 represent = cls._represent_user(),
1095 )
1096
1097 # -------------------------------------------------------------------------
1098 @classmethod
1100 """
1101 Auth_user ID of the user owning the record
1102 """
1103
1104 return Field("owned_by_user", current.auth.settings.table_user,
1105 readable = False,
1106 writable = False,
1107 requires = None,
1108 default = cls._current_user(),
1109 represent = cls._represent_user(),
1110 ondelete = "RESTRICT",
1111 )
1112
1113 # -------------------------------------------------------------------------
1114 @staticmethod
1116 """
1117 Auth_group ID of the user role owning the record
1118 """
1119
1120 return Field("owned_by_group", "integer",
1121 default = None,
1122 readable = False,
1123 writable = False,
1124 requires = None,
1125 represent = auth_group_represent,
1126 )
1127
1128 # -------------------------------------------------------------------------
1129 @staticmethod
1131 """
1132 PE ID of the entity managing the record
1133 """
1134
1135 return Field("realm_entity", "integer",
1136 default = None,
1137 readable = False,
1138 writable = False,
1139 requires = None,
1140 # using a lambda here as we don't want the model
1141 # to be loaded yet:
1142 represent = lambda pe_id: \
1143 current.s3db.pr_pentity_represent(pe_id),
1144 )
1145
1146 # -------------------------------------------------------------------------
1147 @classmethod
1149 """
1150 Standard meta fields for all tables
1151
1152 @return: tuple of Fields
1153 """
1154
1155 return (cls.uuid(),
1156 cls.mci(),
1157 cls.deleted(),
1158 cls.deleted_fk(),
1159 cls.deleted_rb(),
1160 cls.created_on(),
1161 cls.created_by(),
1162 cls.modified_on(),
1163 cls.modified_by(),
1164 cls.approved_by(),
1165 cls.owned_by_user(),
1166 cls.owned_by_group(),
1167 cls.realm_entity(),
1168 )
1169
1170 # -------------------------------------------------------------------------
1171 @classmethod
1173 """
1174 Meta-fields required for sync
1175
1176 @return: tuple of Fields
1177 """
1178
1179 return (cls.uuid(),
1180 cls.mci(),
1181 cls.deleted(),
1182 cls.deleted_fk(),
1183 cls.deleted_rb(),
1184 cls.created_on(),
1185 cls.modified_on(),
1186 )
1187
1188 # -------------------------------------------------------------------------
1189 @classmethod
1191 """
1192 Record ownership meta-fields
1193
1194 @return: tuple of Fields
1195 """
1196
1197 return (cls.owned_by_user(),
1198 cls.owned_by_group(),
1199 cls.realm_entity(),
1200 )
1201
1202 # -------------------------------------------------------------------------
1203 @classmethod
1205 """
1206 Timestamp meta-fields
1207
1208 @return: tuple of Fields
1209 """
1210
1211 return (cls.created_on(),
1212 cls.modified_on(),
1213 )
1214
1215 # -------------------------------------------------------------------------
1216 @staticmethod
1218 """
1219 Get the user ID of the currently logged-in user
1220
1221 @return: auth_user ID
1222 """
1223
1224 if current.auth.is_logged_in():
1225 # Not current.auth.user to support impersonation
1226 return current.session.auth.user.id
1227 else:
1228 return None
1229
1230 # -------------------------------------------------------------------------
1231 @staticmethod
1233 """
1234 Representation method for auth_user IDs
1235
1236 @return: representation function
1237 """
1238
1239 if current.deployment_settings.get_ui_auth_user_represent() == "name":
1240 return s3_auth_user_represent_name
1241 else:
1242 return s3_auth_user_represent
1243
1244 # -----------------------------------------------------------------------------
1245 -def s3_meta_fields():
1246 """
1247 Shortcut commonly used in table definitions: *s3_meta_fields()
1248
1249 @return: tuple of Field instances
1250 """
1251
1252 return S3MetaFields.all_meta_fields()
1253
1255 """
1256 Shortcut commonly used to include/exclude meta fields
1257
1258 @return: tuple of field names
1259 """
1260
1261 return ALL_META_FIELD_NAMES
1262
1263 # =============================================================================
1264 # Reusable roles fields
1265
1266 -def s3_role_required():
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 #widget = S3AutocompleteWidget("admin",
1282 # "group",
1283 # fieldname="role"),
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
1294 # -----------------------------------------------------------------------------
1295 -def s3_roles_permitted(name="roles_permitted", **attr):
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
1323 # =============================================================================
1324 -def s3_comments(name="comments", **attr):
1325 """
1326 Return a standard Comments field
1327 """
1328
1329 T = current.T
1330 if "label" not in attr:
1331 attr["label"] = T("Comments")
1332 if "represent" not in attr:
1333 # Support HTML markup
1334 attr["represent"] = lambda comments: \
1335 XML(comments) if comments else current.messages["NONE"]
1336 if "widget" not in attr:
1337 from s3widgets import s3_comments_widget
1338 _placeholder = attr.pop("_placeholder", None)
1339 if _placeholder:
1340 attr["widget"] = lambda f, v: \
1341 s3_comments_widget(f, v, _placeholder=_placeholder)
1342 else:
1343 attr["widget"] = s3_comments_widget
1344 if "comment" not in attr:
1345 attr["comment"] = DIV(_class="tooltip",
1346 _title="%s|%s" % \
1347 (T("Comments"),
1348 T("Please use this field to record any additional information, including a history of the record if it is updated.")))
1349
1350 return Field(name, "text", **attr)
1351
1352 # =============================================================================
1353 -def s3_currency(name="currency", **attr):
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
1375 # =============================================================================
1376 -def s3_language(name="language", **attr):
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) # None = Full list
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 # Use deployment_settings to show a limited list
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 # Default
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 # Calendar
1473 calendar = attributes.pop("calendar", None)
1474
1475 # Past and future options
1476 past = attributes.pop("past", None)
1477 future = attributes.pop("future", None)
1478
1479 # Label
1480 if "label" not in attributes:
1481 attributes["label"] = current.T("Date")
1482
1483 # Widget-specific options (=not intended for S3ReusableField)
1484 WIDGET_OPTIONS = ("start_field",
1485 "default_interval",
1486 "default_explicit",
1487 "set_min",
1488 "set_max",
1489 )
1490
1491 # Widget
1492 widget = attributes.get("widget", "calendar")
1493 widget_options = {}
1494 if widget == "date":
1495 # Legacy: S3DateWidget
1496 # @todo: deprecate (once S3CalendarWidget supports all legacy options)
1497
1498 # Must use Gregorian calendar
1499 calendar = "Gregorian"
1500
1501 # Past/future options
1502 if past is not None:
1503 widget_options["past"] = past
1504 if future is not None:
1505 widget_options["future"] = future
1506
1507 # Supported additional widget options
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 # Default: calendar widget
1523 widget_options["calendar"] = calendar
1524
1525 # Past/future options
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 # Supported additional widget options
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 # Drop all widget options
1545 for option in WIDGET_OPTIONS:
1546 attributes.pop(option, None)
1547
1548 attributes["widget"] = widget
1549
1550 # Default value
1551 now = current.request.utcnow.date()
1552 if attributes.get("default") == "now":
1553 attributes["default"] = now
1554
1555 # Representation
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 # Validator
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 # Default
1585 attributes["requires"] = IS_EMPTY_OR(requires)
1586
1587 return Field(name, "date", **attributes)
1588
1589 # =============================================================================
1590 -def s3_datetime(name="date", **attr):
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 # Calendar
1632 calendar = attributes.pop("calendar", None)
1633
1634 # Limits
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 # Compute earliest/latest
1642 widget = attributes.pop("widget", None)
1643 now = current.request.utcnow
1644 if widget == "date":
1645 # Helper function to convert past/future hours into
1646 # earliest/latest datetime, retaining day of month and
1647 # time of day
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 # Compute earliest/latest
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 # Label
1682 if "label" not in attributes:
1683 attributes["label"] = current.T("Date")
1684
1685 # Widget
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 # Default value
1709 if attributes.get("default") == "now":
1710 attributes["default"] = now
1711
1712 # Representation
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 # Validator and empty-option
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 # END =========================================================================
1745
| Home | Trees | Indices | Help |
|
|---|
| Generated by Epydoc 3.0.1 on Fri Mar 15 08:52:04 2019 | http://epydoc.sourceforge.net |