Package s3 :: Module s3fields
[frames] | no frames]

Source Code for Module s3.s3fields

   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
153 - def __init__(self, name, type="string", **attr):
154 155 self.name = name 156 self.__type = type 157 self.attr = Storage(attr)
158 159 # -------------------------------------------------------------------------
160 - def __call__(self, name=None, **attr):
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 # -------------------------------------------------------------------------
310 - def lookup_rows(self, key, values, fields=None):
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 # -------------------------------------------------------------------------
333 - def represent_row(self, row, prefix=None):
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 # ------------------------------------------------------------------------- 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 # 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 # -------------------------------------------------------------------------
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 # 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 # -------------------------------------------------------------------------
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 # 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 # -------------------------------------------------------------------------
570 - def render_list(self, value, labels, show_link=True):
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 # -------------------------------------------------------------------------
596 - def _setup(self):
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 # -------------------------------------------------------------------------
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 # 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 # -------------------------------------------------------------------------
779 - def _represent_path(self, value, row, rows=None, hierarchy=None):
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
818 - def __init__(self, value, renderer):
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 # -------------------------------------------------------------------------
833 - def __repr__(self):
834 835 return s3_str(self.represent())
836 837 # -------------------------------------------------------------------------
838 - def represent(self):
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 # -------------------------------------------------------------------------
860 - def render(self):
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 # -------------------------------------------------------------------------
886 - def render_node(self, element, attributes, name):
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
950 - def uuid():
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
968 - def mci():
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
984 - def deleted():
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
997 - def deleted_fk():
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
1010 - def deleted_rb():
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
1022 - def created_on():
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
1035 - def modified_on():
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
1049 - def created_by(cls):
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
1065 - def modified_by(cls):
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
1083 - def approved_by(cls):
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
1099 - def owned_by_user(cls):
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
1115 - def owned_by_group():
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
1130 - def realm_entity():
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
1148 - def all_meta_fields(cls):
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
1172 - def sync_meta_fields(cls):
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
1190 - def owner_meta_fields(cls):
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
1204 - def timestamps(cls):
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
1217 - def _current_user():
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
1232 - def _represent_user():
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
1254 -def s3_all_meta_field_names():
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