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

Source Code for Module s3.s3model

   1  # -*- coding: utf-8 -*- 
   2   
   3  """ S3 Data Model Extensions 
   4   
   5      @copyright: 2009-2019 (c) Sahana Software Foundation 
   6      @license: MIT 
   7   
   8      Permission is hereby granted, free of charge, to any person 
   9      obtaining a copy of this software and associated documentation 
  10      files (the "Software"), to deal in the Software without 
  11      restriction, including without limitation the rights to use, 
  12      copy, modify, merge, publish, distribute, sublicense, and/or sell 
  13      copies of the Software, and to permit persons to whom the 
  14      Software is furnished to do so, subject to the following 
  15      conditions: 
  16   
  17      The above copyright notice and this permission notice shall be 
  18      included in all copies or substantial portions of the Software. 
  19   
  20      THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
  21      EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 
  22      OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
  23      NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 
  24      HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 
  25      WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
  26      FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 
  27      OTHER DEALINGS IN THE SOFTWARE. 
  28  """ 
  29   
  30  __all__ = ("S3Model", 
  31             #"S3DynamicModel", 
  32             ) 
  33   
  34  from collections import OrderedDict 
  35   
  36  from gluon import current, IS_EMPTY_OR, IS_FLOAT_IN_RANGE, IS_INT_IN_RANGE, \ 
  37                    IS_IN_SET, IS_NOT_EMPTY, SQLFORM, TAG 
  38  from gluon.storage import Storage 
  39  from gluon.tools import callback 
  40   
  41  from s3dal import Table, Field, original_tablename 
  42  from s3navigation import S3ScriptItem 
  43  from s3resource import S3Resource 
  44  from s3validators import IS_ONE_OF 
  45  from s3widgets import s3_comments_widget, s3_richtext_widget 
  46   
  47  DYNAMIC_PREFIX = "s3dt" 
  48  DEFAULT = lambda: None 
  49   
  50  # Table options that are always JSON-serializable objects, 
  51  # and can thus be passed as-is from dynamic model "settings" 
  52  # to s3db.configure (& thence to mobile table.settings) 
  53  SERIALIZABLE_OPTS = ("autosync", 
  54                       "autototals", 
  55                       "card", 
  56                       "grids", 
  57                       "insertable", 
  58                       "show_hidden", 
  59                       "subheadings", 
  60                       ) 
  61   
  62  ogetattr = object.__getattribute__ 
63 64 # ============================================================================= 65 -class S3Model(object):
66 """ Base class for S3 models """ 67 68 _s3model = True 69 70 LOCK = "s3_model_lock" 71 LOAD = "s3_model_load" 72 DELETED = "deleted" 73
74 - def __init__(self, module=None):
75 """ Constructor """ 76 77 self.cache = (current.cache.ram, 60) 78 79 self.context = None 80 self.classes = {} 81 82 # Initialize current.model 83 if not hasattr(current, "model"): 84 current.model = {"config": {}, 85 "components": {}, 86 "methods": {}, 87 "cmethods": {}, 88 "hierarchies": {}, 89 } 90 91 response = current.response 92 if "s3" not in response: 93 response.s3 = Storage() 94 self.prefix = module 95 96 mandatory_models = ("auth", 97 "sync", 98 "s3", 99 "gis", 100 "pr", 101 "sit", 102 "org", 103 ) 104 105 if module is not None: 106 if self.__loaded(): 107 return 108 self.__lock() 109 try: 110 env = self.mandatory() 111 except Exception: 112 self.__unlock() 113 raise 114 else: 115 if isinstance(env, dict): 116 response.s3.update(env) 117 if module in mandatory_models or \ 118 current.deployment_settings.has_module(module): 119 try: 120 env = self.model() 121 except Exception: 122 self.__unlock() 123 raise 124 else: 125 try: 126 env = self.defaults() 127 except Exception: 128 self.__unlock() 129 raise 130 if isinstance(env, dict): 131 response.s3.update(env) 132 self.__loaded(True) 133 self.__unlock()
134 135 # -------------------------------------------------------------------------
136 - def __loaded(self, loaded=None):
137 138 LOAD = self.LOAD 139 name = self.__class__.__name__ 140 response = current.response 141 if LOAD not in response: 142 response[LOAD] = [] 143 if name in response[LOAD]: 144 return True 145 elif loaded: 146 response[LOAD].append(name) 147 return loaded
148 149 # -------------------------------------------------------------------------
150 - def __lock(self):
151 152 LOCK = self.LOCK 153 name = self.__class__.__name__ 154 response = current.response 155 if LOCK not in response: 156 response[LOCK] = {} 157 if name in response[LOCK]: 158 raise RuntimeError("circular model reference deadlock in %s" % name) 159 else: 160 response[LOCK][name] = True 161 return
162 163 # -------------------------------------------------------------------------
164 - def __unlock(self):
165 166 LOCK = self.LOCK 167 name = self.__class__.__name__ 168 response = current.response 169 if LOCK in response: 170 if name in response[LOCK]: 171 response[LOCK].pop(name, None) 172 if not response[LOCK]: 173 del response[LOCK] 174 return
175 176 # -------------------------------------------------------------------------
177 - def __getattr__(self, name):
178 """ Model auto-loader """ 179 180 return self.table(name, 181 AttributeError("undefined table: %s" % name))
182 183 # -------------------------------------------------------------------------
184 - def __getitem__(self, key):
185 186 return self.__getattr__(str(key))
187 188 # -------------------------------------------------------------------------
189 - def mandatory(self):
190 """ 191 Mandatory objects defined by this model, regardless whether 192 enabled or disabled 193 """ 194 return None
195 196 # -------------------------------------------------------------------------
197 - def model(self):
198 """ 199 Defines all tables in this model, to be implemented by 200 subclasses 201 """ 202 return None
203 204 # -------------------------------------------------------------------------
205 - def defaults(self):
206 """ 207 Definitions of model globals (response.s3.*) if the model 208 has been disabled in deployment settings, to be implemented 209 by subclasses 210 """ 211 return None
212 213 # ------------------------------------------------------------------------- 214 @classmethod
215 - def table(cls, tablename, default=None, db_only=False):
216 """ 217 Helper function to load a table definition by its name 218 """ 219 220 s3 = current.response.s3 221 if s3 is None: 222 s3 = current.response.s3 = Storage() 223 224 s3db = current.s3db 225 models = current.models 226 227 if not db_only: 228 if tablename in s3: 229 return s3[tablename] 230 elif s3db is not None and tablename in s3db.classes: 231 prefix, name = s3db.classes[tablename] 232 return models.__dict__[prefix].__dict__[name] 233 234 db = current.db 235 236 # Table already defined? 237 if hasattr(db, tablename): 238 return ogetattr(db, tablename) 239 elif ogetattr(db, "_lazy_tables") and \ 240 tablename in ogetattr(db, "_LAZY_TABLES"): 241 return ogetattr(db, tablename) 242 243 found = None 244 245 prefix, name = tablename.split("_", 1) 246 if prefix == DYNAMIC_PREFIX: 247 try: 248 found = S3DynamicModel(tablename).table 249 except AttributeError: 250 pass 251 252 elif hasattr(models, prefix): 253 module = models.__dict__[prefix] 254 255 names = module.__all__ 256 s3models = module.__dict__ 257 258 if not db_only and tablename in names: 259 # A name defined at module level (e.g. a class) 260 s3db.classes[tablename] = (prefix, tablename) 261 found = s3models[tablename] 262 else: 263 # A name defined in an S3Model 264 generic = [] 265 loaded = False 266 for n in names: 267 model = s3models[n] 268 if hasattr(model, "_s3model"): 269 if hasattr(model, "names"): 270 if tablename in model.names: 271 model(prefix) 272 loaded = True 273 break 274 else: 275 generic.append(n) 276 if not loaded: 277 for n in generic: 278 s3models[n](prefix) 279 280 if found: 281 return found 282 283 if not db_only and tablename in s3: 284 return s3[tablename] 285 elif hasattr(db, tablename): 286 return ogetattr(db, tablename) 287 elif ogetattr(db, "_lazy_tables") and \ 288 tablename in ogetattr(db, "_LAZY_TABLES"): 289 return ogetattr(db, tablename) 290 elif isinstance(default, Exception): 291 raise default 292 else: 293 return default
294 295 # ------------------------------------------------------------------------- 296 @classmethod
297 - def get(cls, name, default=None):
298 """ 299 Helper function to load a response.s3 variable from models 300 """ 301 302 s3 = current.response.s3 303 if s3 is None: 304 s3 = current.response.s3 = Storage() 305 306 if name in s3: 307 return s3[name] 308 elif "_" in name: 309 prefix = name.split("_", 1)[0] 310 models = current.models 311 if hasattr(models, prefix): 312 module = models.__dict__[prefix] 313 loaded = False 314 generic = [] 315 for n in module.__all__: 316 model = module.__dict__[n] 317 if type(model).__name__ == "type": 318 if loaded: 319 continue 320 if hasattr(model, "names"): 321 if name in model.names: 322 model(prefix) 323 loaded = True 324 generic = [] 325 else: 326 continue 327 else: 328 generic.append(n) 329 elif n.startswith("%s_" % prefix): 330 s3[n] = model 331 [module.__dict__[n](prefix) for n in generic] 332 if name in s3: 333 return s3[name] 334 elif isinstance(default, Exception): 335 raise default 336 else: 337 return default
338 339 # ------------------------------------------------------------------------- 340 @classmethod
341 - def load(cls, name):
342 """ 343 Helper function to load a model by its name (=prefix) 344 """ 345 346 s3 = current.response.s3 347 if s3 is None: 348 s3 = current.response.s3 = Storage() 349 models = current.models 350 351 if models is not None and hasattr(models, name): 352 module = models.__dict__[name] 353 for n in module.__all__: 354 model = module.__dict__[n] 355 if type(model).__name__ == "type" and \ 356 issubclass(model, S3Model): 357 model(name) 358 elif n.startswith("%s_" % name): 359 s3[n] = model 360 return
361 362 # ------------------------------------------------------------------------- 363 @classmethod
364 - def load_all_models(cls):
365 """ 366 Helper function to load all models 367 """ 368 369 s3 = current.response.s3 370 if s3.all_models_loaded: 371 # Already loaded 372 return 373 s3.load_all_models = True 374 375 models = current.models 376 377 # Load models 378 if models is not None: 379 for name in models.__dict__: 380 if type(models.__dict__[name]).__name__ == "module": 381 cls.load(name) 382 383 # Define importer tables 384 from s3import import S3Importer, S3ImportJob 385 S3Importer.define_upload_table() 386 S3ImportJob.define_job_table() 387 S3ImportJob.define_item_table() 388 389 # Define sessions table 390 if current.deployment_settings.get_base_session_db(): 391 # Copied from https://github.com/web2py/web2py/blob/master/gluon/globals.py#L895 392 # Not DRY, but no easy way to make it so 393 current.db.define_table("web2py_session", 394 Field("locked", "boolean", default=False), 395 Field("client_ip", length=64), 396 Field("created_datetime", "datetime", 397 default=current.request.now), 398 Field("modified_datetime", "datetime"), 399 Field("unique_key", length=64), 400 Field("session_data", "blob"), 401 ) 402 403 # Don't do this again within the current request cycle 404 s3.load_all_models = False 405 s3.all_models_loaded = True
406 407 # ------------------------------------------------------------------------- 408 @staticmethod
409 - def define_table(tablename, *fields, **args):
410 """ 411 Same as db.define_table except that it does not repeat 412 a table definition if the table is already defined. 413 """ 414 415 db = current.db 416 if hasattr(db, tablename): 417 table = ogetattr(db, tablename) 418 else: 419 table = db.define_table(tablename, *fields, **args) 420 return table
421 422 # ------------------------------------------------------------------------- 423 @staticmethod
424 - def get_aliased(table, alias):
425 """ 426 Helper method to get a Table instance with alias; prevents 427 re-instantiation of an already existing alias for the same 428 table (which can otherwise lead to name collisions in PyDAL). 429 430 @param table: the original table 431 @param alias: the alias 432 433 @return: the aliased Table instance 434 """ 435 436 db = current.db 437 438 if hasattr(db, alias): 439 aliased = ogetattr(db, alias) 440 if original_tablename(aliased) == original_tablename(table): 441 return aliased 442 443 aliased = table.with_alias(alias) 444 if aliased._id.table != aliased: 445 # Older PyDAL not setting _id attribute correctly 446 aliased._id = aliased[table._id.name] 447 448 return aliased
449 450 # ------------------------------------------------------------------------- 451 # Resource configuration 452 # ------------------------------------------------------------------------- 453 @staticmethod
454 - def resource(tablename, *args, **kwargs):
455 """ 456 Wrapper for the S3Resource constructor to realize 457 the global s3db.resource() method 458 """ 459 460 return S3Resource(tablename, *args, **kwargs)
461 462 # ------------------------------------------------------------------------- 463 @classmethod
464 - def configure(cls, tablename, **attr):
465 """ 466 Update the extra configuration of a table 467 468 @param tablename: the name of the table 469 @param attr: dict of attributes to update 470 """ 471 472 config = current.model["config"] 473 474 tn = tablename._tablename if type(tablename) is Table else tablename 475 if tn not in config: 476 config[tn] = {} 477 config[tn].update(attr) 478 return
479 480 # ------------------------------------------------------------------------- 481 @classmethod
482 - def get_config(cls, tablename, key, default=None):
483 """ 484 Reads a configuration attribute of a resource 485 486 @param tablename: the name of the resource DB table 487 @param key: the key (name) of the attribute 488 """ 489 490 config = current.model["config"] 491 492 tn = tablename._tablename if type(tablename) is Table else tablename 493 if tn in config: 494 return config[tn].get(key, default) 495 else: 496 return default
497 498 # ------------------------------------------------------------------------- 499 @classmethod
500 - def clear_config(cls, tablename, *keys):
501 """ 502 Removes configuration attributes of a resource 503 504 @param table: the resource DB table 505 @param keys: keys of attributes to remove (maybe multiple) 506 """ 507 508 config = current.model["config"] 509 510 tn = tablename._tablename if type(tablename) is Table else tablename 511 if tn in config: 512 if not keys: 513 del config[tn] 514 else: 515 [config[tn].pop(k, None) for k in keys] 516 return
517 518 # ------------------------------------------------------------------------- 519 @classmethod
520 - def add_custom_callback(cls, tablename, hook, cb, method=None):
521 """ 522 Generic method to append a custom onvalidation|onaccept 523 callback to the originally configured callback chain, 524 for use in customise_* in templates 525 526 @param tablename: the table name 527 @param hook: the main hook ("onvalidation"|"onaccept") 528 @param cb: the custom callback function 529 @param method: the sub-hook ("create"|"update"|None) 530 531 @example: 532 # Add a create-onvalidation callback for the pr_person 533 # table, while retaining any existing onvalidation: 534 s3db.add_custom_callback("pr_person", 535 "onvalidation", 536 my_create_onvalidation, 537 method = "create", 538 ) 539 """ 540 541 def extend(this, new): 542 if isinstance(this, (tuple, list)): 543 this = list(this) 544 elif this is not None: 545 this = [this] 546 else: 547 this = [] 548 if new not in this: 549 this.append(new) 550 return this
551 552 callbacks = {} 553 for m in ("create", "update", None): 554 key = "%s_%s" % (m, hook) if m else hook 555 callbacks[m] = cls.get_config(tablename, key) 556 557 if method is None: 558 generic_cb = callbacks[None] 559 if generic_cb: 560 callbacks[None] = extend(generic_cb, cb) 561 else: 562 callbacks[None] = cb 563 for m in ("create", "update"): 564 current_cb = callbacks[m] 565 if current_cb: 566 callbacks[m] = extend(current_cb, cb) 567 else: 568 current_cb = callbacks[method] 569 if current_cb: 570 callbacks[method] = extend(current_cb, cb) 571 else: 572 callbacks[method] = extend(callbacks[None], cb) 573 574 settings = {} 575 for m, setting in callbacks.items(): 576 if setting: 577 key = "%s_%s" % (m, hook) if m else hook 578 settings[key] = setting 579 cls.configure(tablename, **settings)
580 581 # ------------------------------------------------------------------------- 582 @classmethod
583 - def virtual_reference(cls, field):
584 """ 585 Reverse-lookup of virtual references which are declared for 586 the respective lookup-table as: 587 588 configure(tablename, 589 referenced_by = [(tablename, fieldname), ...], 590 ) 591 592 & in the table with the fields(auth_user only current example) as: 593 594 configure(tablename, 595 references = {fieldname: tablename, 596 ... 597 }, 598 ) 599 600 @param field: the Field 601 602 @returns: the name of the referenced table 603 """ 604 605 if str(field.type) == "integer": 606 607 config = current.model["config"] 608 tablename, fieldname = str(field).split(".") 609 610 # 1st try this table's references 611 this_config = config.get(tablename) 612 if this_config: 613 references = this_config.get("references") 614 if references is not None and fieldname in references: 615 return references[fieldname] 616 617 # Then try other tables' referenced_by 618 key = (tablename, fieldname) 619 for tn in config: 620 referenced_by = config[tn].get("referenced_by") 621 if referenced_by is not None and key in referenced_by: 622 return tn 623 624 return None
625 626 # ------------------------------------------------------------------------- 627 @classmethod
628 - def onaccept(cls, table, record, method="create"):
629 """ 630 Helper to run the onvalidation routine for a record 631 632 @param table: the Table 633 @param record: the FORM or the Row to validate 634 @param method: the method 635 """ 636 637 if hasattr(table, "_tablename"): 638 tablename = table._tablename 639 else: 640 tablename = table 641 642 onaccept = cls.get_config(tablename, "%s_onaccept" % method, 643 cls.get_config(tablename, "onaccept")) 644 if "vars" not in record: 645 record = Storage(vars=Storage(record), errors=Storage()) 646 if onaccept: 647 callback(onaccept, record, tablename=tablename) 648 return
649 650 # ------------------------------------------------------------------------- 651 @classmethod
652 - def onvalidation(cls, table, record, method="create"):
653 """ 654 Helper to run the onvalidation routine for a record 655 656 @param table: the Table 657 @param record: the FORM or the Row to validate 658 @param method: the method 659 """ 660 661 if hasattr(table, "_tablename"): 662 tablename = table._tablename 663 else: 664 tablename = table 665 666 onvalidation = cls.get_config(tablename, "%s_onvalidation" % method, 667 cls.get_config(tablename, "onvalidation")) 668 if "vars" not in record: 669 record = Storage(vars=Storage(record), errors=Storage()) 670 if onvalidation: 671 callback(onvalidation, record, tablename=tablename) 672 return record.errors
673 674 # ------------------------------------------------------------------------- 675 # Resource components 676 #-------------------------------------------------------------------------- 677 @classmethod
678 - def add_components(cls, master, **links):
679 """ 680 Configure component links for a master table. 681 682 @param master: the name of the master table 683 @param links: component link configurations 684 """ 685 686 components = current.model["components"] 687 load_all_models = current.response.s3.load_all_models 688 689 master = master._tablename if type(master) is Table else master 690 691 hooks = components.get(master) 692 if hooks is None: 693 hooks = {} 694 for tablename, ll in links.items(): 695 696 name = tablename.split("_", 1)[1] 697 if not isinstance(ll, (tuple, list)): 698 ll = [ll] 699 700 for link in ll: 701 702 if isinstance(link, str): 703 alias = name 704 705 pkey = None 706 fkey = link 707 linktable = None 708 lkey = None 709 rkey = None 710 actuate = None 711 autodelete = False 712 autocomplete = None 713 defaults = None 714 multiple = True 715 filterby = None 716 # @ToDo: use these as fallback for RHeader Tabs on Web App 717 # (see S3ComponentTab.__init__) 718 label = None 719 plural = None 720 721 elif isinstance(link, dict): 722 alias = link.get("name", name) 723 724 joinby = link.get("joinby") 725 if not joinby: 726 continue 727 728 linktable = link.get("link") 729 linktable = linktable._tablename \ 730 if type(linktable) is Table else linktable 731 732 if load_all_models: 733 # Warn for redeclaration of components (different table 734 # under the same alias) - this is wrong most of the time, 735 # even though it would produce valid+consistent results: 736 if alias in hooks and hooks[alias].tablename != tablename: 737 current.log.warning("Redeclaration of component (%s.%s)" % 738 (master, alias)) 739 740 # Ambiguous aliases can cause accidental deletions and 741 # other serious integrity problems, so we warn for ambiguous 742 # aliases (not raising exceptions just yet because there 743 # are a number of legacy cases), 744 # Currently only logging during load_all_models to not 745 # completely submerge other important log messages 746 if linktable and alias == linktable.split("_", 1)[1]: 747 # @todo: fix legacy cases (e.g. renaming the link tables) 748 # @todo: raise Exception once all legacy cases are fixed 749 current.log.warning("Ambiguous link/component alias (%s.%s)" % 750 (master, alias)) 751 if alias == master.split("_", 1)[1]: 752 # No legacy cases, so crash to prevent introduction of any 753 raise SyntaxError("Ambiguous master/component alias (%s.%s)" % 754 (master, alias)) 755 756 pkey = link.get("pkey") 757 if linktable is None: 758 lkey = None 759 rkey = None 760 fkey = joinby 761 else: 762 lkey = joinby 763 rkey = link.get("key") 764 if not rkey: 765 continue 766 fkey = link.get("fkey") 767 768 actuate = link.get("actuate") 769 autodelete = link.get("autodelete", False) 770 autocomplete = link.get("autocomplete") 771 defaults = link.get("defaults") 772 multiple = link.get("multiple", True) 773 filterby = link.get("filterby") 774 label = link.get("label") 775 plural = link.get("plural") 776 777 else: 778 continue 779 780 component = Storage(tablename = tablename, 781 pkey = pkey, 782 fkey = fkey, 783 linktable = linktable, 784 lkey = lkey, 785 rkey = rkey, 786 actuate = actuate, 787 autodelete = autodelete, 788 autocomplete = autocomplete, 789 defaults = defaults, 790 multiple = multiple, 791 filterby = filterby, 792 label = label, 793 plural = plural, 794 ) 795 hooks[alias] = component 796 797 components[master] = hooks
798 799 # ------------------------------------------------------------------------- 800 @classmethod
801 - def add_dynamic_components(cls, tablename, exclude=None):
802 """ 803 Helper function to look up and declare dynamic components 804 for a table; called by get_components if dynamic_components 805 is configured for the table 806 807 @param tablename: the table name 808 @param exclude: names to exclude (static components) 809 """ 810 811 mtable = cls.table(tablename) 812 if mtable is None: 813 return 814 815 if cls.get_config(tablename, "dynamic_components_loaded"): 816 # Already loaded 817 return 818 819 ttable = cls.table("s3_table") 820 ftable = cls.table("s3_field") 821 822 join = ttable.on(ttable.id == ftable.table_id) 823 query = (ftable.master == tablename) & \ 824 (ftable.component_key == True) & \ 825 (ftable.deleted != True) 826 rows = current.db(query).select(ftable.name, 827 ftable.field_type, 828 ftable.component_alias, 829 ftable.settings, 830 ttable.name, 831 join = join, 832 ) 833 834 # Don't do this again during the same request cycle 835 cls.configure(tablename, dynamic_components_loaded=True) 836 837 components = {} 838 for row in rows: 839 840 hook = {} 841 842 ctable = row["s3_table"] 843 ctablename = ctable.name 844 default_alias = ctablename.split("_", 1)[-1] 845 846 field = row["s3_field"] 847 alias = field.component_alias 848 849 if not alias: 850 alias = default_alias 851 if exclude and alias in exclude: 852 continue 853 854 if alias != default_alias: 855 hook["name"] = alias 856 857 hook["joinby"] = field.name 858 859 settings = field.settings 860 if settings: 861 multiple = settings.get("component_multiple", DEFAULT) 862 if multiple is not DEFAULT: 863 hook["multiple"] = multiple 864 865 # Get the primary key 866 field_type = field.field_type 867 if field_type[:10] == "reference ": 868 ktablename = field_type.split(" ", 1)[1] 869 if "." in ktablename: 870 ktablename, pkey = ktablename.split(".", 1)[1] 871 if pkey and pkey != mtable._id.name: 872 hook["pkey"] = pkey 873 874 components[ctablename] = hook 875 876 if components: 877 cls.add_components(tablename, **components)
878 879 # ------------------------------------------------------------------------- 880 @classmethod
881 - def get_component(cls, table, alias):
882 """ 883 Get a component description for a component alias 884 885 @param table: the master table 886 @param alias: the component alias 887 888 @returns: the component description (Storage) 889 """ 890 return cls.parse_hook(table, alias)
891 892 # ------------------------------------------------------------------------- 893 @classmethod
894 - def get_components(cls, table, names=None):
895 """ 896 Finds components of a table 897 898 @param table: the table or table name 899 @param names: a list of components names to limit the search to, 900 None for all available components 901 902 @returns: the component descriptions (Storage {alias: description}) 903 """ 904 905 table, hooks = cls.get_hooks(table, names=names) 906 907 # Build component-objects for each hook 908 components = Storage() 909 if table and hooks: 910 for alias in hooks: 911 component = cls.parse_hook(table, alias, hook=hooks[alias]) 912 if component: 913 components[alias] = component 914 915 return components
916 917 # ------------------------------------------------------------------------- 918 @classmethod
919 - def parse_hook(cls, table, alias, hook=None):
920 """ 921 Parse a component configuration, loading all necessary table 922 models and applying defaults 923 924 @param table: the master table 925 @param alias: the component alias 926 @param hook: the component configuration (if already known) 927 928 @returns: the component description (Storage {key: value}) 929 """ 930 931 load = cls.table 932 933 if hook is None: 934 table, hooks = cls.get_hooks(table, names=[alias]) 935 if hooks and alias in hooks: 936 hook = hooks[alias] 937 else: 938 return None 939 940 tn = hook.tablename 941 lt = hook.linktable 942 943 ctable = load(tn) 944 if ctable is None: 945 return None 946 947 if lt: 948 ltable = load(lt) 949 if ltable is None: 950 return None 951 else: 952 ltable = None 953 954 prefix, name = tn.split("_", 1) 955 component = Storage(defaults=hook.defaults, 956 multiple=hook.multiple, 957 tablename=tn, 958 table=ctable, 959 prefix=prefix, 960 name=name, 961 alias=alias, 962 label=hook.label, 963 plural=hook.plural, 964 ) 965 966 if hook.supertable is not None: 967 joinby = hook.supertable._id.name 968 else: 969 joinby = hook.fkey 970 971 if hook.pkey is None: 972 if hook.supertable is not None: 973 component.pkey = joinby 974 else: 975 component.pkey = table._id.name 976 else: 977 component.pkey = hook.pkey 978 979 if ltable is not None: 980 981 if hook.actuate: 982 component.actuate = hook.actuate 983 else: 984 component.actuate = "link" 985 component.linktable = ltable 986 987 if hook.fkey is None: 988 component.fkey = ctable._id.name 989 else: 990 component.fkey = hook.fkey 991 992 component.lkey = hook.lkey 993 component.rkey = hook.rkey 994 component.autocomplete = hook.autocomplete 995 component.autodelete = hook.autodelete 996 997 else: 998 component.linktable = None 999 component.fkey = hook.fkey 1000 component.lkey = component.rkey = None 1001 component.actuate = None 1002 component.autocomplete = None 1003 component.autodelete = None 1004 1005 if hook.filterby is not None: 1006 component.filterby = hook.filterby 1007 1008 return component
1009 1010 # ------------------------------------------------------------------------- 1011 @classmethod
1012 - def get_hooks(cls, table, names=None):
1013 """ 1014 Find applicable component configurations (hooks) for a table 1015 1016 @param table: the master table (or table name) 1017 @param names: component aliases to find (default: all configured 1018 components for the master table) 1019 1020 @returns: tuple (table, {alias: hook, ...}) 1021 """ 1022 1023 components = current.model["components"] 1024 load = cls.table 1025 1026 # Get tablename and table 1027 if type(table) is Table: 1028 tablename = original_tablename(table) 1029 else: 1030 tablename = table 1031 table = load(tablename) 1032 if table is None: 1033 # Primary table not defined 1034 return None, None 1035 1036 # Single alias? 1037 if isinstance(names, str): 1038 names = set([names]) 1039 elif names is not None: 1040 names = set(names) 1041 1042 hooks = {} 1043 get_hooks = cls.__filter_hooks 1044 supertables = None 1045 1046 # Get hooks for direct components 1047 direct_components = components.get(tablename) 1048 if direct_components: 1049 names = get_hooks(hooks, direct_components, names=names) 1050 1051 if names is None or names: 1052 # Add hooks for super-components 1053 supertables = cls.get_config(tablename, "super_entity") 1054 if supertables: 1055 if not isinstance(supertables, (list, tuple)): 1056 supertables = [supertables] 1057 for s in supertables: 1058 if isinstance(s, str): 1059 s = load(s) 1060 if s is None: 1061 continue 1062 super_components = components.get(s._tablename) 1063 if super_components: 1064 names = get_hooks(hooks, super_components, 1065 names = names, 1066 supertable = s, 1067 ) 1068 1069 dynamic_components = cls.get_config(tablename, "dynamic_components") 1070 if dynamic_components: 1071 1072 if names is None or names: 1073 # Add hooks for dynamic components 1074 cls.add_dynamic_components(tablename, exclude=hooks) 1075 direct_components = components.get(tablename) 1076 if direct_components: 1077 names = get_hooks(hooks, direct_components, names=names) 1078 1079 if supertables and (names is None or names): 1080 # Add hooks for dynamic super-components 1081 for s in supertables: 1082 if isinstance(s, str): 1083 s = load(s) 1084 if s is None: 1085 continue 1086 cls.add_dynamic_components(s._tablename, exclude=hooks) 1087 super_components = components.get(s._tablename) 1088 if super_components: 1089 names = get_hooks(hooks, super_components, 1090 names = names, 1091 supertable = s, 1092 ) 1093 1094 return table, hooks
1095 1096 # ------------------------------------------------------------------------- 1097 @classmethod
1098 - def __filter_hooks(cls, components, hooks, names=None, supertable=None):
1099 """ 1100 DRY Helper method to filter component hooks 1101 1102 @param components: components already found, dict {alias: component} 1103 @param hooks: component hooks to filter, dict {alias: hook} 1104 @param names: the names (=aliases) to include 1105 @param supertable: the super-table name to set for the component 1106 1107 @returns: set of names that could not be found, 1108 or None if names was None 1109 """ 1110 1111 for alias in hooks: 1112 if alias in components or \ 1113 names is not None and alias not in names: 1114 continue 1115 hook = hooks[alias] 1116 hook["supertable"] = supertable 1117 components[alias] = hook 1118 1119 return set(names) - set(hooks) if names is not None else None
1120 1121 # ------------------------------------------------------------------------- 1122 @classmethod
1123 - def has_components(cls, table):
1124 """ 1125 Checks whether there are components defined for a table 1126 1127 @param table: the table or table name 1128 """ 1129 1130 components = current.model["components"] 1131 load = cls.table 1132 1133 # Get tablename and table 1134 if type(table) is Table: 1135 tablename = table._tablename 1136 else: 1137 tablename = table 1138 table = load(tablename) 1139 if table is None: 1140 return False 1141 1142 # Attach dynamic components 1143 if cls.get_config(tablename, "dynamic_components"): 1144 cls.add_dynamic_components(tablename) 1145 1146 # Get table hooks 1147 hooks = {} 1148 filter_hooks = cls.__filter_hooks 1149 h = components.get(tablename, None) 1150 if h: 1151 filter_hooks(hooks, h) 1152 if len(hooks): 1153 return True 1154 1155 # Check for super-components 1156 # FIXME: add dynamic components for super-table? 1157 supertables = cls.get_config(tablename, "super_entity") 1158 if supertables: 1159 if not isinstance(supertables, (list, tuple)): 1160 supertables = [supertables] 1161 for s in supertables: 1162 if isinstance(s, str): 1163 s = load(s) 1164 if s is None: 1165 continue 1166 h = components.get(s._tablename, None) 1167 if h: 1168 filter_hooks(hooks, h, supertable=s) 1169 if len(hooks): 1170 return True 1171 1172 # No components found 1173 return False
1174 1175 # ------------------------------------------------------------------------- 1176 @classmethod
1177 - def get_alias(cls, tablename, link):
1178 """ 1179 Find a component alias from the link table alias. 1180 1181 @param tablename: the name of the master table 1182 @param link: the alias of the link table 1183 """ 1184 1185 components = current.model["components"] 1186 1187 table = cls.table(tablename) 1188 if not table: 1189 return None 1190 1191 def get_alias(hooks, link): 1192 1193 if link[-6:] == "__link": 1194 alias = link.rsplit("__link", 1)[0] 1195 hook = hooks.get(alias) 1196 if hook: 1197 return alias 1198 else: 1199 for alias in hooks: 1200 hook = hooks[alias] 1201 if hook.linktable: 1202 name = hook.linktable.split("_", 1)[1] 1203 if name == link: 1204 return alias 1205 return None
1206 1207 hooks = components.get(tablename) 1208 if hooks: 1209 alias = get_alias(hooks, link) 1210 if alias: 1211 return alias 1212 1213 supertables = cls.get_config(tablename, "super_entity") 1214 if supertables: 1215 if not isinstance(supertables, (list, tuple)): 1216 supertables = [supertables] 1217 for s in supertables: 1218 table = cls.table(s) 1219 if table is None: 1220 continue 1221 hooks = components.get(table._tablename) 1222 if hooks: 1223 alias = get_alias(hooks, link) 1224 if alias: 1225 return alias 1226 return None 1227 1228 # ------------------------------------------------------------------------- 1229 @classmethod 1253 1254 # ------------------------------------------------------------------------- 1255 # Resource Methods 1256 # ------------------------------------------------------------------------- 1257 @classmethod
1258 - def set_method(cls, prefix, name, 1259 component_name=None, 1260 method=None, 1261 action=None):
1262 """ 1263 Adds a custom method for a resource or component 1264 1265 @param prefix: prefix of the resource name (=module name) 1266 @param name: name of the resource (=without prefix) 1267 @param component_name: name of the component 1268 @param method: name of the method 1269 @param action: function to invoke for this method 1270 """ 1271 1272 methods = current.model["methods"] 1273 cmethods = current.model["cmethods"] 1274 1275 if not method: 1276 raise SyntaxError("No method specified") 1277 1278 tablename = "%s_%s" % (prefix, name) 1279 1280 if not component_name: 1281 if method not in methods: 1282 methods[method] = {} 1283 methods[method][tablename] = action 1284 else: 1285 if method not in cmethods: 1286 cmethods[method] = {} 1287 if component_name not in cmethods[method]: 1288 cmethods[method][component_name] = {} 1289 cmethods[method][component_name][tablename] = action
1290 1291 # ------------------------------------------------------------------------- 1292 @classmethod
1293 - def get_method(cls, prefix, name, 1294 component_name=None, 1295 method=None):
1296 """ 1297 Retrieves a custom method for a resource or component 1298 1299 @param prefix: prefix of the resource name (=module name) 1300 @param name: name of the resource (=without prefix) 1301 @param component_name: name of the component 1302 @param method: name of the method 1303 """ 1304 1305 methods = current.model["methods"] 1306 cmethods = current.model["cmethods"] 1307 1308 if not method: 1309 return None 1310 1311 tablename = "%s_%s" % (prefix, name) 1312 1313 if not component_name: 1314 if method in methods and tablename in methods[method]: 1315 return methods[method][tablename] 1316 else: 1317 return None 1318 else: 1319 if method in cmethods and \ 1320 component_name in cmethods[method] and \ 1321 tablename in cmethods[method][component_name]: 1322 return cmethods[method][component_name][tablename] 1323 else: 1324 return None
1325 1326 # ------------------------------------------------------------------------- 1327 # Super-Entity API 1328 # ------------------------------------------------------------------------- 1329 @classmethod
1330 - def super_entity(cls, tablename, key, types, *fields, **args):
1331 """ 1332 Define a super-entity table 1333 1334 @param tablename: the tablename 1335 @param key: name of the primary key 1336 @param types: a dictionary of instance types 1337 @param fields: any shared fields 1338 @param args: table arguments (e.g. migrate) 1339 """ 1340 1341 db = current.db 1342 if db._dbname == "postgres": 1343 sequence_name = "%s_%s_seq" % (tablename, key) 1344 else: 1345 sequence_name = None 1346 1347 table = db.define_table(tablename, 1348 Field(key, "id", 1349 readable=False, 1350 writable=False), 1351 Field("deleted", "boolean", 1352 readable=False, 1353 writable=False, 1354 default=False), 1355 Field("instance_type", 1356 represent = lambda opt: \ 1357 types.get(opt, opt) or \ 1358 current.messages["NONE"], 1359 readable=False, 1360 writable=False), 1361 Field("uuid", length=128, 1362 readable=False, 1363 writable=False), 1364 sequence_name=sequence_name, 1365 *fields, **args) 1366 1367 return table
1368 1369 # ------------------------------------------------------------------------- 1370 @classmethod
1371 - def super_key(cls, supertable, default=None):
1372 """ 1373 Get the name of the key for a super-entity 1374 1375 @param supertable: the super-entity table 1376 """ 1377 1378 if supertable is None and default: 1379 return default 1380 if isinstance(supertable, str): 1381 supertable = cls.table(supertable) 1382 try: 1383 return supertable._id.name 1384 except AttributeError: 1385 pass 1386 raise SyntaxError("No id-type key found in %s" % supertable._tablename)
1387 1388 # ------------------------------------------------------------------------- 1389 @classmethod 1483 1484 # ------------------------------------------------------------------------- 1485 @classmethod
1486 - def update_super(cls, table, record):
1487 """ 1488 Updates the super-entity links of an instance record 1489 1490 @param table: the instance table 1491 @param record: the instance record 1492 """ 1493 1494 get_config = cls.get_config 1495 1496 # Get all super-entities of this table 1497 tablename = original_tablename(table) 1498 supertables = get_config(tablename, "super_entity") 1499 if not supertables: 1500 return False 1501 1502 # Get the record 1503 record_id = record.get("id", None) 1504 if not record_id: 1505 return False 1506 1507 # Find all super-tables, super-keys and shared fields 1508 if not isinstance(supertables, (list, tuple)): 1509 supertables = [supertables] 1510 updates = [] 1511 fields = [] 1512 has_deleted = "deleted" in table.fields 1513 has_uuid = "uuid" in table.fields 1514 for s in supertables: 1515 if type(s) is not Table: 1516 s = cls.table(s) 1517 if s is None: 1518 continue 1519 tn = s._tablename 1520 key = cls.super_key(s) 1521 shared = get_config(tablename, "%s_fields" % tn) 1522 if not shared: 1523 shared = dict((fn, fn) 1524 for fn in s.fields 1525 if fn != key and fn in table.fields) 1526 else: 1527 shared = dict((fn, shared[fn]) 1528 for fn in shared 1529 if fn != key and \ 1530 fn in s.fields and \ 1531 shared[fn] in table.fields) 1532 fields.extend(shared.values()) 1533 fields.append(key) 1534 updates.append((tn, s, key, shared)) 1535 1536 # Get the record data 1537 db = current.db 1538 if has_deleted: 1539 fields.append("deleted") 1540 if has_uuid: 1541 fields.append("uuid") 1542 fields = [ogetattr(table, fn) for fn in list(set(fields))] 1543 _record = db(table.id == record_id).select(limitby=(0, 1), 1544 *fields).first() 1545 if not _record: 1546 return False 1547 1548 super_keys = {} 1549 for tn, s, key, shared in updates: 1550 data = Storage([(fn, _record[shared[fn]]) for fn in shared]) 1551 data.instance_type = tablename 1552 if has_deleted: 1553 data.deleted = _record.get("deleted", False) 1554 if has_uuid: 1555 data.uuid = _record.get("uuid", None) 1556 1557 # Do we already have a super-record? 1558 skey = ogetattr(_record, key) 1559 if skey: 1560 query = (s[key] == skey) 1561 row = db(query).select(s._id, limitby=(0, 1)).first() 1562 else: 1563 row = None 1564 1565 if row: 1566 # Update the super-entity record 1567 db(s._id == skey).update(**data) 1568 super_keys[key] = skey 1569 data[key] = skey 1570 form = Storage(vars=data) 1571 onaccept = get_config(tn, "update_onaccept", 1572 get_config(tn, "onaccept", None)) 1573 if onaccept: 1574 onaccept(form) 1575 else: 1576 # Insert a new super-entity record 1577 k = s.insert(**data) 1578 if k: 1579 super_keys[key] = k 1580 data[key] = k 1581 onaccept = get_config(tn, "create_onaccept", 1582 get_config(tn, "onaccept", None)) 1583 if onaccept: 1584 form = Storage(vars=data) 1585 onaccept(form) 1586 1587 # Update the super_keys in the record 1588 if super_keys: 1589 # System update => don't update modified_by/on 1590 if "modified_on" in table.fields: 1591 super_keys["modified_by"] = table.modified_by 1592 super_keys["modified_on"] = table.modified_on 1593 db(table.id == record_id).update(**super_keys) 1594 1595 record.update(super_keys) 1596 return True
1597 1598 # ------------------------------------------------------------------------- 1599 @classmethod
1600 - def delete_super(cls, table, record):
1601 """ 1602 Removes the super-entity links of an instance record 1603 1604 @param table: the instance table 1605 @param record: the instance record 1606 1607 @return: True if successful, otherwise False (caller must 1608 roll back the transaction if False is returned!) 1609 """ 1610 1611 # Must have a record ID 1612 record_id = record.get(table._id.name, None) 1613 if not record_id: 1614 raise RuntimeError("Record ID required for delete_super") 1615 1616 # Get all super-tables 1617 get_config = cls.get_config 1618 supertables = get_config(original_tablename(table), "super_entity") 1619 1620 # None? Ok - done! 1621 if not supertables: 1622 return True 1623 if not isinstance(supertables, (list, tuple)): 1624 supertables = [supertables] 1625 1626 # Get the keys for all super-tables 1627 keys = {} 1628 load = {} 1629 for sname in supertables: 1630 stable = cls.table(sname) if isinstance(sname, str) else sname 1631 if stable is None: 1632 continue 1633 key = stable._id.name 1634 if key in record: 1635 keys[stable._tablename] = (key, record[key]) 1636 else: 1637 load[stable._tablename] = key 1638 1639 # If necessary, load missing keys 1640 if load: 1641 row = current.db(table._id == record_id).select( 1642 table._id, *load.values(), limitby=(0, 1)).first() 1643 for sname, key in load.items(): 1644 keys[sname] = (key, row[key]) 1645 1646 # Delete super-records 1647 define_resource = current.s3db.resource 1648 update_record = record.update_record 1649 for sname in keys: 1650 key, value = keys[sname] 1651 if not value: 1652 # Skip if we don't have a super-key 1653 continue 1654 1655 # Remove the super key 1656 update_record(**{key: None}) 1657 1658 # Delete the super record 1659 sresource = define_resource(sname, id=value) 1660 sresource.delete(cascade=True, log_errors=True) 1661 1662 if sresource.error: 1663 # Restore the super key 1664 # @todo: is this really necessary? => caller must roll back 1665 # anyway in this case, which would automatically restore 1666 update_record(**{key: value}) 1667 return False 1668 1669 return True
1670 1671 # ------------------------------------------------------------------------- 1672 @classmethod
1673 - def get_super_keys(cls, table):
1674 """ 1675 Get the super-keys in an instance table 1676 1677 @param table: the instance table 1678 @returns: list of field names 1679 """ 1680 1681 tablename = original_tablename(table) 1682 1683 supertables = cls.get_config(tablename, "super_entity") 1684 if not supertables: 1685 return [] 1686 if not isinstance(supertables, (list, tuple)): 1687 supertables = [supertables] 1688 1689 keys = [] 1690 append = keys.append 1691 for s in supertables: 1692 if type(s) is not Table: 1693 s = cls.table(s) 1694 if s is None: 1695 continue 1696 key = s._id.name 1697 if key in table.fields: 1698 append(key) 1699 1700 return keys
1701 1702 # ------------------------------------------------------------------------- 1703 @classmethod
1704 - def get_instance(cls, supertable, superid):
1705 """ 1706 Get prefix, name and ID of an instance record 1707 1708 @param supertable: the super-entity table 1709 @param superid: the super-entity record ID 1710 @return: a tuple (prefix, name, ID) of the instance 1711 record (if it exists) 1712 """ 1713 1714 if not hasattr(supertable, "_tablename"): 1715 # tablename passed instead of Table 1716 supertable = cls.table(supertable) 1717 if supertable is None: 1718 return (None, None, None) 1719 db = current.db 1720 query = (supertable._id == superid) 1721 entry = db(query).select(supertable.instance_type, 1722 supertable.uuid, 1723 limitby=(0, 1)).first() 1724 if entry: 1725 instance_type = entry.instance_type 1726 prefix, name = instance_type.split("_", 1) 1727 instancetable = current.s3db[entry.instance_type] 1728 query = instancetable.uuid == entry.uuid 1729 record = db(query).select(instancetable.id, 1730 limitby=(0, 1)).first() 1731 if record: 1732 return (prefix, name, record.id) 1733 return (None, None, None)
1734
1735 # ============================================================================= 1736 -class S3DynamicModel(object):
1737 """ 1738 Class representing a dynamic table model 1739 """ 1740
1741 - def __init__(self, tablename):
1742 """ 1743 Constructor 1744 1745 @param tablename: the table name 1746 """ 1747 1748 self.tablename = tablename 1749 table = self.define_table(tablename) 1750 if table: 1751 self.table = table 1752 else: 1753 raise AttributeError("Undefined dynamic model: %s" % tablename)
1754 1755 # -------------------------------------------------------------------------
1756 - def define_table(self, tablename):
1757 """ 1758 Instantiate a dynamic Table 1759 1760 @param tablename: the table name 1761 1762 @return: a Table instance 1763 """ 1764 1765 # Is the table already defined? 1766 db = current.db 1767 redefine = tablename in db 1768 1769 # Load the table model 1770 s3db = current.s3db 1771 ttable = s3db.s3_table 1772 ftable = s3db.s3_field 1773 query = (ttable.name == tablename) & \ 1774 (ttable.deleted != True) & \ 1775 (ftable.table_id == ttable.id) 1776 rows = db(query).select(ftable.name, 1777 ftable.field_type, 1778 ftable.label, 1779 ftable.require_unique, 1780 ftable.require_not_empty, 1781 ftable.options, 1782 ftable.default_value, 1783 ftable.settings, 1784 ftable.comments, 1785 ) 1786 if not rows: 1787 return None 1788 1789 # Instantiate the fields 1790 fields = [] 1791 for row in rows: 1792 field = self._field(tablename, row) 1793 if field: 1794 fields.append(field) 1795 1796 # Automatically add standard meta-fields 1797 from s3fields import s3_meta_fields 1798 fields.extend(s3_meta_fields()) 1799 1800 # Define the table 1801 if fields: 1802 # Enable migrate 1803 # => is globally disabled when settings.base.migrate 1804 # is False, overriding the table parameter 1805 migrate_enabled = db._migrate_enabled 1806 db._migrate_enabled = True 1807 1808 # Define the table 1809 db.define_table(tablename, 1810 migrate = True, 1811 redefine = redefine, 1812 *fields) 1813 1814 # Instantiate table 1815 # => otherwise lazy_tables may prevent it 1816 table = db[tablename] 1817 1818 # Restore global migrate_enabled 1819 db._migrate_enabled = migrate_enabled 1820 1821 # Configure the table 1822 self._configure(tablename) 1823 1824 return table 1825 else: 1826 return None
1827 1828 # ------------------------------------------------------------------------- 1829 @staticmethod
1830 - def _configure(tablename):
1831 """ 1832 Configure the table (e.g. CRUD strings) 1833 """ 1834 1835 s3db = current.s3db 1836 1837 # Load table configuration settings 1838 ttable = s3db.s3_table 1839 query = (ttable.name == tablename) & \ 1840 (ttable.deleted != True) 1841 row = current.db(query).select(ttable.title, 1842 ttable.settings, 1843 limitby = (0, 1), 1844 ).first() 1845 if row: 1846 # Configure CRUD strings 1847 title = row.title 1848 if title: 1849 current.response.s3.crud_strings[tablename] = Storage( 1850 title_list = current.T(title), 1851 ) 1852 1853 # Table Configuration 1854 settings = row.settings 1855 if settings: 1856 1857 config = {} 1858 1859 # CRUD Form 1860 crud_fields = settings.get("form") 1861 if crud_fields: 1862 from s3forms import S3SQLCustomForm 1863 try: 1864 crud_form = S3SQLCustomForm(**crud_fields) 1865 except: 1866 pass 1867 else: 1868 config["crud_form"] = crud_form 1869 1870 # JSON-serializable config options can be configured 1871 # without pre-processing 1872 for key in SERIALIZABLE_OPTS: 1873 setting = settings.get(key) 1874 if setting: 1875 config[key] = setting 1876 1877 # Apply config 1878 if config: 1879 s3db.configure(tablename, **config)
1880 1881 # ------------------------------------------------------------------------- 1882 @classmethod
1883 - def _field(cls, tablename, row):
1884 """ 1885 Convert a s3_field Row into a Field instance 1886 1887 @param tablename: the table name 1888 @param row: the s3_field Row 1889 1890 @return: a Field instance 1891 """ 1892 1893 field = None 1894 1895 if row: 1896 1897 # Type-specific field constructor 1898 fieldtype = row.field_type 1899 if row.options: 1900 construct = cls._options_field 1901 elif fieldtype == "date": 1902 construct = cls._date_field 1903 elif fieldtype == "datetime": 1904 construct = cls._datetime_field 1905 elif fieldtype[:9] == "reference": 1906 construct = cls._reference_field 1907 elif fieldtype == "boolean": 1908 construct = cls._boolean_field 1909 elif fieldtype in ("integer", "double"): 1910 construct = cls._numeric_field 1911 else: 1912 construct = cls._generic_field 1913 1914 field = construct(tablename, row) 1915 if not field: 1916 return None 1917 1918 requires = field.requires 1919 1920 # Handle require_not_empty 1921 if fieldtype != "boolean": 1922 if row.require_not_empty: 1923 if not requires: 1924 requires = IS_NOT_EMPTY() 1925 elif requires: 1926 requires = IS_EMPTY_OR(requires) 1927 1928 field.requires = requires 1929 1930 # Field label and comment 1931 T = current.T 1932 label = row.label 1933 if not label: 1934 fieldname = row.name 1935 label = " ".join(s.capitalize() for s in fieldname.split("_")) 1936 if label: 1937 field.label = T(label) 1938 comments = row.comments 1939 if comments: 1940 field.comment = T(comments) 1941 1942 # Field settings 1943 settings = row.settings 1944 if settings: 1945 field.s3_settings = settings 1946 1947 return field
1948 1949 # ------------------------------------------------------------------------- 1950 @staticmethod
1951 - def _generic_field(tablename, row):
1952 """ 1953 Generic field constructor 1954 1955 @param tablename: the table name 1956 @param row: the s3_field Row 1957 1958 @return: the Field instance 1959 """ 1960 1961 fieldname = row.name 1962 fieldtype = row.field_type 1963 1964 if row.require_unique: 1965 from s3validators import IS_NOT_ONE_OF 1966 requires = IS_NOT_ONE_OF(current.db, "%s.%s" % (tablename, 1967 fieldname, 1968 ), 1969 ) 1970 else: 1971 requires = None 1972 1973 if fieldtype in ("string", "text"): 1974 default = row.default_value 1975 settings = row.settings or {} 1976 widget = settings.get("widget") 1977 if widget == "richtext": 1978 widget = s3_richtext_widget 1979 elif widget == "comments": 1980 widget = s3_comments_widget 1981 else: 1982 widget = None 1983 else: 1984 default = None 1985 widget = None 1986 1987 field = Field(fieldname, fieldtype, 1988 default = default, 1989 requires = requires, 1990 widget = widget, 1991 ) 1992 return field
1993 1994 # ------------------------------------------------------------------------- 1995 @staticmethod
1996 - def _options_field(tablename, row):
1997 """ 1998 Options-field constructor 1999 2000 @param tablename: the table name 2001 @param row: the s3_field Row 2002 2003 @return: the Field instance 2004 """ 2005 2006 fieldname = row.name 2007 fieldtype = row.field_type 2008 fieldopts = row.options 2009 2010 settings = row.settings or {} 2011 2012 # Always translate options unless translate_options is False 2013 translate = settings.get("translate_options", True) 2014 T = current.T 2015 2016 from s3utils import s3_str 2017 2018 sort = False 2019 zero = "" 2020 2021 if isinstance(fieldopts, dict): 2022 options = fieldopts 2023 if translate: 2024 options = dict((k, T(v)) for k, v in options.items()) 2025 options_dict = options 2026 # Sort options unless sort_options is False (=default True) 2027 sort = settings.get("sort_options", True) 2028 2029 elif isinstance(fieldopts, list): 2030 options = [] 2031 for opt in fieldopts: 2032 if isinstance(opt, (tuple, list)) and len(opt) >= 2: 2033 k, v = opt[:2] 2034 else: 2035 k, v = opt, s3_str(opt) 2036 if translate: 2037 v = T(v) 2038 options.append((k, v)) 2039 options_dict = dict(options) 2040 # Retain list order unless sort_options is True (=default False) 2041 sort = settings.get("sort_options", False) 2042 2043 else: 2044 options_dict = options = {} 2045 2046 # Apply default value (if it is a valid option) 2047 default = row.default_value 2048 if default and s3_str(default) in (s3_str(k) for k in options_dict): 2049 # No zero-option if we have a default value and 2050 # the field must not be empty: 2051 zero = None if row.require_not_empty else "" 2052 else: 2053 default = None 2054 2055 # Widget? 2056 #widget = settings.get("widget") 2057 #if widget == "radio": 2058 len_options = len(options) 2059 if len_options < 4: 2060 widget = lambda field, value: SQLFORM.widgets.radio.widget(field, value, cols=len_options) 2061 else: 2062 widget = None 2063 2064 from s3fields import S3Represent 2065 field = Field(fieldname, fieldtype, 2066 default = default, 2067 represent = S3Represent(options = options_dict, 2068 translate = translate, 2069 ), 2070 requires = IS_IN_SET(options, 2071 sort = sort, 2072 zero = zero, 2073 ), 2074 widget = widget, 2075 ) 2076 return field
2077 2078 # ------------------------------------------------------------------------- 2079 @staticmethod
2080 - def _date_field(tablename, row):
2081 """ 2082 Date field constructor 2083 2084 @param tablename: the table name 2085 @param row: the s3_field Row 2086 2087 @return: the Field instance 2088 """ 2089 2090 fieldname = row.name 2091 settings = row.settings or {} 2092 2093 attr = {} 2094 for keyword in ("past", "future"): 2095 setting = settings.get(keyword, DEFAULT) 2096 if setting is not DEFAULT: 2097 attr[keyword] = setting 2098 attr["empty"] = False 2099 2100 default = row.default_value 2101 if default: 2102 if default == "now": 2103 attr["default"] = default 2104 else: 2105 from s3datetime import s3_decode_iso_datetime 2106 try: 2107 dt = s3_decode_iso_datetime(default) 2108 except ValueError: 2109 # Ignore 2110 pass 2111 else: 2112 attr["default"] = dt.date() 2113 2114 from s3fields import s3_date 2115 field = s3_date(fieldname, **attr) 2116 2117 return field
2118 2119 # ------------------------------------------------------------------------- 2120 @staticmethod
2121 - def _datetime_field(tablename, row):
2122 """ 2123 DateTime field constructor 2124 2125 @param tablename: the table name 2126 @param row: the s3_field Row 2127 2128 @return: the Field instance 2129 """ 2130 2131 fieldname = row.name 2132 settings = row.settings or {} 2133 2134 attr = {} 2135 for keyword in ("past", "future"): 2136 setting = settings.get(keyword, DEFAULT) 2137 if setting is not DEFAULT: 2138 attr[keyword] = setting 2139 attr["empty"] = False 2140 2141 default = row.default_value 2142 if default: 2143 if default == "now": 2144 attr["default"] = default 2145 else: 2146 from s3datetime import s3_decode_iso_datetime 2147 try: 2148 dt = s3_decode_iso_datetime(default) 2149 except ValueError: 2150 # Ignore 2151 pass 2152 else: 2153 attr["default"] = dt 2154 2155 from s3fields import s3_datetime 2156 field = s3_datetime(fieldname, **attr) 2157 2158 return field
2159 2160 # ------------------------------------------------------------------------- 2161 @staticmethod
2162 - def _reference_field(tablename, row):
2163 """ 2164 Reference field constructor 2165 2166 @param tablename: the table name 2167 @param row: the s3_field Row 2168 2169 @return: the Field instance 2170 """ 2171 2172 fieldname = row.name 2173 fieldtype = row.field_type 2174 2175 ktablename = fieldtype.split(" ", 1)[1].split(".", 1)[0] 2176 ktable = current.s3db.table(ktablename) 2177 if ktable: 2178 from s3fields import S3Represent 2179 if "name" in ktable.fields: 2180 represent = S3Represent(lookup = ktablename, 2181 translate = True, 2182 ) 2183 else: 2184 represent = None 2185 requires = IS_ONE_OF(current.db, str(ktable._id), 2186 represent, 2187 ) 2188 field = Field(fieldname, fieldtype, 2189 represent = represent, 2190 requires = requires, 2191 ) 2192 else: 2193 field = None 2194 2195 return field
2196 2197 # ------------------------------------------------------------------------- 2198 @staticmethod
2199 - def _numeric_field(tablename, row):
2200 """ 2201 Numeric field constructor 2202 2203 @param tablename: the table name 2204 @param row: the s3_field Row 2205 2206 @return: the Field instance 2207 """ 2208 2209 fieldname = row.name 2210 fieldtype = row.field_type 2211 2212 settings = row.settings or {} 2213 minimum = settings.get("min") 2214 maximum = settings.get("max") 2215 2216 if fieldtype == "integer": 2217 parse = int 2218 requires = IS_INT_IN_RANGE(minimum=minimum, 2219 maximum=maximum, 2220 ) 2221 elif fieldtype == "double": 2222 parse = float 2223 requires = IS_FLOAT_IN_RANGE(minimum=minimum, 2224 maximum=maximum, 2225 ) 2226 else: 2227 parse = None 2228 requires = None 2229 2230 default = row.default_value 2231 if default and parse is not None: 2232 try: 2233 default = parse(default) 2234 except ValueError: 2235 default = None 2236 else: 2237 default = None 2238 2239 field = Field(fieldname, fieldtype, 2240 default = default, 2241 requires = requires, 2242 ) 2243 return field
2244 2245 # ------------------------------------------------------------------------- 2246 @staticmethod
2247 - def _boolean_field(tablename, row):
2248 """ 2249 Boolean field constructor 2250 2251 @param tablename: the table name 2252 @param row: the s3_field Row 2253 2254 @return: the Field instance 2255 """ 2256 2257 fieldname = row.name 2258 fieldtype = row.field_type 2259 2260 default = row.default_value 2261 if default: 2262 default = default.lower() 2263 if default == "true": 2264 default = True 2265 elif default == "none": 2266 default = None 2267 else: 2268 default = False 2269 else: 2270 default = False 2271 2272 settings = row.settings or {} 2273 2274 # NB no IS_EMPTY_OR for boolean-fields: 2275 # => NULL values in SQL are neither True nor False, so always 2276 # require special handling; to prevent that, we remove the 2277 # default IS_EMPTY_OR and always set a default 2278 # => DAL converts everything that isn't True to False anyway, 2279 # so accepting an empty selection would create an 2280 # implicit default with no visible feedback (poor UX) 2281 2282 widget = settings.get("widget") 2283 if widget == "radio": 2284 # Render two radio-buttons Yes|No 2285 T = current.T 2286 requires = [IS_IN_SET(OrderedDict([(True, T("Yes")), 2287 (False, T("No")), 2288 ]), 2289 # better than "Value not allowed" 2290 error_message = T("Please select a value"), 2291 ), 2292 # Form option comes in as str 2293 # => convert to boolean 2294 lambda v: (str(v) == "True", None), 2295 ] 2296 widget = lambda field, value: \ 2297 SQLFORM.widgets.radio.widget(field, value, cols=2) 2298 else: 2299 # Remove default IS_EMPTY_OR 2300 requires = None 2301 2302 # Default single checkbox widget 2303 widget = None 2304 2305 from s3utils import s3_yes_no_represent 2306 field = Field(fieldname, fieldtype, 2307 default = default, 2308 represent = s3_yes_no_represent, 2309 requires = requires, 2310 ) 2311 2312 if widget: 2313 field.widget = widget 2314 2315 return field
2316 2317 # END ========================================================================= 2318