1
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
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
51
52
53 SERIALIZABLE_OPTS = ("autosync",
54 "autototals",
55 "card",
56 "grids",
57 "insertable",
58 "show_hidden",
59 "subheadings",
60 )
61
62 ogetattr = object.__getattribute__
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
75 """ Constructor """
76
77 self.cache = (current.cache.ram, 60)
78
79 self.context = None
80 self.classes = {}
81
82
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
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
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
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
178 """ Model auto-loader """
179
180 return self.table(name,
181 AttributeError("undefined table: %s" % name))
182
183
187
188
190 """
191 Mandatory objects defined by this model, regardless whether
192 enabled or disabled
193 """
194 return None
195
196
198 """
199 Defines all tables in this model, to be implemented by
200 subclasses
201 """
202 return None
203
204
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
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
260 s3db.classes[tablename] = (prefix, tablename)
261 found = s3models[tablename]
262 else:
263
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
365 """
366 Helper function to load all models
367 """
368
369 s3 = current.response.s3
370 if s3.all_models_loaded:
371
372 return
373 s3.load_all_models = True
374
375 models = current.models
376
377
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
384 from s3import import S3Importer, S3ImportJob
385 S3Importer.define_upload_table()
386 S3ImportJob.define_job_table()
387 S3ImportJob.define_item_table()
388
389
390 if current.deployment_settings.get_base_session_db():
391
392
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
404 s3.load_all_models = False
405 s3.all_models_loaded = True
406
407
408 @staticmethod
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
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
446 aliased._id = aliased[table._id.name]
447
448 return aliased
449
450
451
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
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
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
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
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
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
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
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
676
677 @classmethod
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
717
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
734
735
736 if alias in hooks and hooks[alias].tablename != tablename:
737 current.log.warning("Redeclaration of component (%s.%s)" %
738 (master, alias))
739
740
741
742
743
744
745
746 if linktable and alias == linktable.split("_", 1)[1]:
747
748
749 current.log.warning("Ambiguous link/component alias (%s.%s)" %
750 (master, alias))
751 if alias == master.split("_", 1)[1]:
752
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
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
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
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
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
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
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
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
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
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
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
1034 return None, None
1035
1036
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
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
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
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
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
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
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
1143 if cls.get_config(tablename, "dynamic_components"):
1144 cls.add_dynamic_components(tablename)
1145
1146
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
1156
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
1173 return False
1174
1175
1176 @classmethod
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
1231 """
1232 Get the alias of the component that represents the parent
1233 node in a hierarchy (for link-table based hierarchies)
1234
1235 @param tablename: the table name
1236
1237 @returns: the alias of the hierarchy parent component
1238 """
1239
1240 if not cls.table(tablename, db_only=True):
1241 return None
1242
1243 hierarchy_link = cls.get_config(tablename, "hierarchy_link")
1244 if not hierarchy_link:
1245
1246 hierarchy = cls.get_config(tablename, "hierarchy")
1247 if hierarchy and "." in hierarchy:
1248 alias = hierarchy.rsplit(".", 1)[0]
1249 if "__link" in alias:
1250 hierarchy_link = alias.rsplit("__link", 1)[0]
1251
1252 return hierarchy_link
1253
1254
1255
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
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
1390 - def super_link(cls,
1391 name,
1392 supertable,
1393 label=None,
1394 comment=None,
1395 represent=None,
1396 orderby=None,
1397 sort=True,
1398 filterby=None,
1399 filter_opts=None,
1400 not_filterby=None,
1401 not_filter_opts=None,
1402 instance_types=None,
1403 realms=None,
1404 updateable=False,
1405 groupby=None,
1406 script=None,
1407 widget=None,
1408 empty=True,
1409 default=DEFAULT,
1410 ondelete="CASCADE",
1411 readable=False,
1412 writable=False,
1413 ):
1414 """
1415 Get a foreign key field for a super-entity
1416
1417 @param supertable: the super-entity table
1418 @param label: label for the field
1419 @param comment: comment for the field
1420 @param readable: set the field readable
1421 @param represent: set a representation function for the field
1422 """
1423
1424 if isinstance(supertable, str):
1425 supertable = cls.table(supertable)
1426
1427 if supertable is None:
1428 if name is not None:
1429 return Field(name,
1430 "integer",
1431 readable = False,
1432 writable = False,
1433 )
1434 else:
1435 raise SyntaxError("Undefined super-entity")
1436
1437 try:
1438 key = supertable._id.name
1439 except AttributeError:
1440 raise SyntaxError("No id-type key found in %s" %
1441 supertable._tablename)
1442
1443 if name is not None and name != key:
1444 raise SyntaxError("Primary key %s not found in %s" %
1445 (name, supertable._tablename))
1446
1447 requires = IS_ONE_OF(current.db,
1448 "%s.%s" % (supertable._tablename, key),
1449 represent,
1450 orderby = orderby,
1451 sort = sort,
1452 groupby = groupby,
1453 filterby = filterby,
1454 filter_opts = filter_opts,
1455 instance_types = instance_types,
1456 realms = realms,
1457 updateable = updateable,
1458 not_filterby = not_filterby,
1459 not_filter_opts = not_filter_opts,
1460 )
1461 if empty:
1462 requires = IS_EMPTY_OR(requires)
1463
1464
1465 if script:
1466 if comment:
1467 comment = TAG[""](comment, S3ScriptItem(script=script))
1468 else:
1469 comment = S3ScriptItem(script=script)
1470
1471 return Field(key,
1472 supertable,
1473 default = default,
1474 requires = requires,
1475 readable = readable,
1476 writable = writable,
1477 label = label,
1478 comment = comment,
1479 represent = represent,
1480 widget = widget,
1481 ondelete = ondelete,
1482 )
1483
1484
1485 @classmethod
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
1497 tablename = original_tablename(table)
1498 supertables = get_config(tablename, "super_entity")
1499 if not supertables:
1500 return False
1501
1502
1503 record_id = record.get("id", None)
1504 if not record_id:
1505 return False
1506
1507
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
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
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
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
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
1588 if super_keys:
1589
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
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
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
1617 get_config = cls.get_config
1618 supertables = get_config(original_tablename(table), "super_entity")
1619
1620
1621 if not supertables:
1622 return True
1623 if not isinstance(supertables, (list, tuple)):
1624 supertables = [supertables]
1625
1626
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
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
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
1653 continue
1654
1655
1656 update_record(**{key: None})
1657
1658
1659 sresource = define_resource(sname, id=value)
1660 sresource.delete(cascade=True, log_errors=True)
1661
1662 if sresource.error:
1663
1664
1665
1666 update_record(**{key: value})
1667 return False
1668
1669 return True
1670
1671
1672 @classmethod
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
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
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
1737 """
1738 Class representing a dynamic table model
1739 """
1740
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
1757 """
1758 Instantiate a dynamic Table
1759
1760 @param tablename: the table name
1761
1762 @return: a Table instance
1763 """
1764
1765
1766 db = current.db
1767 redefine = tablename in db
1768
1769
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
1790 fields = []
1791 for row in rows:
1792 field = self._field(tablename, row)
1793 if field:
1794 fields.append(field)
1795
1796
1797 from s3fields import s3_meta_fields
1798 fields.extend(s3_meta_fields())
1799
1800
1801 if fields:
1802
1803
1804
1805 migrate_enabled = db._migrate_enabled
1806 db._migrate_enabled = True
1807
1808
1809 db.define_table(tablename,
1810 migrate = True,
1811 redefine = redefine,
1812 *fields)
1813
1814
1815
1816 table = db[tablename]
1817
1818
1819 db._migrate_enabled = migrate_enabled
1820
1821
1822 self._configure(tablename)
1823
1824 return table
1825 else:
1826 return None
1827
1828
1829 @staticmethod
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
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
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
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
1943 settings = row.settings
1944 if settings:
1945 field.s3_settings = settings
1946
1947 return field
1948
1949
1950 @staticmethod
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
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
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
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
2041 sort = settings.get("sort_options", False)
2042
2043 else:
2044 options_dict = options = {}
2045
2046
2047 default = row.default_value
2048 if default and s3_str(default) in (s3_str(k) for k in options_dict):
2049
2050
2051 zero = None if row.require_not_empty else ""
2052 else:
2053 default = None
2054
2055
2056
2057
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
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
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
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
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
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
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
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
2275
2276
2277
2278
2279
2280
2281
2282 widget = settings.get("widget")
2283 if widget == "radio":
2284
2285 T = current.T
2286 requires = [IS_IN_SET(OrderedDict([(True, T("Yes")),
2287 (False, T("No")),
2288 ]),
2289
2290 error_message = T("Please select a value"),
2291 ),
2292
2293
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
2300 requires = None
2301
2302
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
2318