1
2
3 """ S3 Navigation Module
4
5 @copyright: 2011-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 @todo: - refine check_selected (e.g. must be False if link=False)
30 - implement collapse-flag (render only components in a TAG[""])
31 - implement convenience methods for breadcrumbs (+renderer))
32 - add site-map generator and renderer (easy)
33 - rewrite action buttons/links as S3NavigationItems
34 - ...and any todo's in the code
35 """
36
37 __all__ = ("S3NavigationItem",
38 "S3ScriptItem",
39 "S3ResourceHeader",
40 "s3_rheader_tabs",
41 "s3_rheader_resource",
42 )
43
44 from gluon import *
45 from gluon.storage import Storage
46 from s3utils import s3_str
50 """
51 Base class and API for navigation items.
52
53 Navigation items are GUI elements to navigate the application,
54 typically represented as hyperlinks. Besides form elements,
55 navigation items are most common type of GUI controls.
56
57 This base class can be used to implement both nagivation items
58 and navigation item containers (e.g. menus), each as subclasses.
59
60 Subclasses should implement the layout() method to render the item as
61 HTML (ideally using the web2py helpers). There is no default layout,
62 items will not be rendered at all unless the subclass implements a
63 layout method or the particular instance receives a renderer as
64 parameter.
65
66 Additionally, subclasses should implement the check_*() methods:
67
68 Method: Checks whether:
69
70 check_active this item belongs to the requested page
71 check_enabled this item is enabled
72 check_permission the user is permitted to access this item
73 check_selected the item has been selected to request the page
74
75 check_active is the first check run, and the only method that
76 actually deactivates the item completely, whereas the other
77 methods just set flags for the renderer. All of these methods
78 must return True or False for the respective condition.
79
80 There are default check_*() methods in this base class which support
81 a menu-alike behavior of the item - which may though not fit for all
82 navigation elements.
83
84 For more details, see the S3Navigation wiki page:
85 http://eden.sahanafoundation.org/wiki/S3/S3Navigation
86 """
87
88
89
90
91 - def __init__(self,
92 label=None,
93 c=None,
94 f=None,
95 args=None,
96 vars=None,
97 extension=None,
98 a=None,
99 r=None,
100 m=None,
101 p=None,
102 t=None,
103 url=None,
104 tags=None,
105 parent=None,
106 translate=True,
107 layout=None,
108 check=None,
109 restrict=None,
110 link=True,
111 mandatory=False,
112 ltr=False,
113 **attributes):
114 """
115 Constructor
116
117 @param label: the label
118
119 @param c: the controller
120 @param f: the function
121 @param args: the arguments list
122 @param vars: the variables Storage
123 @param extension: the request extension
124 @param a: the application (defaults to current.request.application)
125 @param r: the request to default to
126
127 @param m: the URL method (will be appended to args)
128 @param p: the method to check authorization for
129 (will not be appended to args)
130 @param t: the table concerned by this request
131 (overrides c_f for auth)
132
133 @param url: a URL to use instead of building one manually
134 - e.g. for external websites or mailto: links
135
136 @param tags: list of tags for this item
137 @param parent: the parent item
138
139 @param translate: translate the label
140 @param layout: the layout
141 @param check: a condition or list of conditions to automatically
142 enable/disable this item
143 @param restrict: restrict to roles (role UID or list of role UIDs)
144 @param link: item has its own URL
145 @param mandatory: item is always active
146 @param ltr: item is always rendered LTR
147
148 @param attributes: attributes to use in layout
149 """
150
151
152 if isinstance(label, basestring) and translate:
153 self.label = current.T(label)
154 else:
155 self.label = label
156
157
158 if tags:
159 if type(tags) is not list:
160 tags = [tags]
161 self.tags = tags
162 else:
163 self.tags = []
164
165
166 if r is not None:
167 self.r = r
168 if a is None:
169 a = r.application
170 if c is None:
171 c = r.controller
172 if f is None:
173 f = r.function
174 if args is None:
175 args = r.args
176 if vars is None:
177 vars = r.vars
178 else:
179 self.r = current.request
180
181
182 self.application = a
183
184 if isinstance(c, (list, tuple)) and len(c):
185 self.controller = c[0]
186 self.match_controller = c
187 else:
188 self.controller = c
189 self.match_controller = [c]
190
191 if isinstance(f, (list, tuple)) and len(f):
192 self.function = f[0]
193 self.match_function = f
194 else:
195 self.function = f
196 self.match_function = [f]
197
198 if args is None:
199 args = []
200 elif isinstance(args, str):
201 args = args.split("/")
202 self.args = args
203 if vars:
204 self.vars = vars
205 else:
206 self.vars = Storage()
207 self.extension = extension
208
209
210 self.tablename = t
211 self.method = m
212 if m is not None:
213 if not len(args):
214 self.args = [m]
215 elif args[-1] != m:
216 self.args.append(m)
217 if p is not None:
218 self.p = p
219 else:
220 self.p = m
221
222 self.override_url = url
223
224
225 attr = Storage()
226 opts = Storage()
227 for k, v in attributes.iteritems():
228 if k[0] == "_":
229 attr[k] = v
230 else:
231 opts[k] = v
232 self.attr = attr
233 self.opts = opts
234
235
236 self.parent = parent
237 self.components = []
238
239
240 self.enabled = True
241 self.selected = None
242 self.visible = None
243 self.link = link
244 self.mandatory = mandatory
245 self.ltr = ltr
246 self.authorized = None
247
248
249 self.restrict = restrict
250 if restrict is not None:
251 if not isinstance(restrict, (list, tuple)):
252 self.restrict = [restrict]
253
254
255 self.check = check
256
257
258 renderer = None
259 if layout is not None:
260
261 renderer = layout
262 elif hasattr(self.__class__, "OVERRIDE"):
263
264 renderer = self.get_layout(self.OVERRIDE)
265 if renderer is None and hasattr(self.__class__, "layout"):
266
267 renderer = self.layout
268 self.renderer = renderer
269
270
271 @staticmethod
273 """
274 Check whether the current theme has a custom layout for this
275 class, and if so, store it in current.layouts
276
277 @param: the name of the custom layout
278 @return: the layout or None if not present
279 """
280
281 if hasattr(current, "layouts"):
282 layouts = current.layouts
283 else:
284 layouts = {}
285 if layouts is False:
286 return None
287 if name in layouts:
288 return layouts[name]
289
290
291 application = current.request.application
292 theme = current.deployment_settings.get_theme_layouts().replace("/", ".")
293 package = "applications.%s.modules.templates.%s.layouts" % \
294 (application, theme)
295 try:
296 override = getattr(__import__(package, fromlist=[name]), name)
297 except ImportError:
298
299 current.layouts = False
300 return None
301 except AttributeError:
302 override = None
303
304 if override and \
305 hasattr(override, "layout") and \
306 type(override.layout) == type(lambda:None):
307 layout = override.layout
308 else:
309 layout = None
310
311 layouts[name] = layout
312 current.layouts = layouts
313
314 return layout
315
316
318 """ Clone this item and its components """
319
320 item = self.__class__()
321 item.label = self.label
322 item.tags = self.tags
323
324 item.r = self.r
325
326 item.application = self.application
327 item.controller = self.controller
328 item.function = self.function
329
330 item.match_controller = [c for c in self.match_controller]
331 item.match_function = [f for f in self.match_function]
332
333 item.args = [a for a in self.args]
334 item.vars = Storage(**self.vars)
335
336 item.extension = self.extension
337 item.tablename = self.tablename
338 item.method = self.method
339 item.p = self.p
340
341 item.override_url = self.override_url
342
343 item.attr = Storage(**self.attr)
344 item.opts = Storage(**self.opts)
345
346 item.parent = self.parent
347 item.components = [i.clone() for i in self.components]
348
349 item.enabled = self.enabled
350 item.selected = self.selected
351 item.visible = self.visible
352 item.link = self.link
353 item.mandatory = self.mandatory
354 if self.restrict is not None:
355 item.restrict = [r for r in self.restrict]
356 else:
357 item.restrict = None
358
359 item.check = self.check
360 item.renderer = self.renderer
361
362 return item
363
364
365
366
368 """
369 Check whether this item belongs to the requested page (request).
370
371 If this check returns False, then the item will be deactivated
372 entirely, i.e. no further checks will be run and the renderer
373 will never be called.
374
375 @param request: the request object (defaults to current.request)
376 """
377
378
379 c = self.get("controller")
380 if c:
381 return current.deployment_settings.has_module(c)
382
383
384 if request is None:
385 request = current.request
386
387 parent = self.parent
388 if parent is not None:
389
390 return parent.check_active(request)
391
392 elif self.mandatory:
393
394 return True
395
396 elif self.match(request):
397
398 return True
399
400 return False
401
402
404 """
405 Check whether this item is enabled.
406
407 This check does not directly disable the item, but rather
408 sets the enabled-flag in the item which can then be used
409 by the renderer.
410
411 This function is called as the very last action immediately
412 before rendering the item. If it returns True, then the
413 enabled-flag of the item remains unchanged, otherwise
414 it gets set to False (False-override).
415 """
416
417
418 return True
419
420
422 """
423 Check whether the user is permitted to access this item.
424
425 This check does not directly disable the item, but rather
426 sets the authorized-flag in the item which can then be used
427 by the renderer.
428 """
429
430 authorized = False
431
432 restrict = self.restrict
433 if restrict:
434 authorized = current.auth.s3_has_roles(restrict)
435 else:
436 authorized = True
437
438 if self.accessible_url() == False:
439 authorized = False
440 return authorized
441
442
444 """
445 Check whether this item is in the selected path (i.e. whether it
446 is or contains the item used to trigger the request).
447
448 This check doesn't change the processing of the item, but
449 rather sets the selected-flag which can then be used by the
450 renderer
451
452 If this is a top-level item, then this check sets the
453 selected-flags for the whole selected path down to the leaf
454 item that has triggered the request.
455
456 Note that this doesn't currently reset selected-flags, so this
457 check can be performed only once per subtree and request. If
458 it would be really neccessary to perform this check more than
459 once, then it should be easy to implement a reset_flags method.
460
461 @param request: the request object, defaults to current.request
462 """
463
464 if self.selected is not None:
465
466 return self.selected
467 if request is None:
468 request = current.request
469 if self.parent is None:
470
471 branch = self.branch(request)
472 if branch is not None:
473 branch.select()
474 if not self.selected:
475 self.selected = False
476 else:
477
478 root = self.get_root()
479 if root.selected is None:
480 root.check_selected(request)
481
482 return True if self.selected else False
483
484
486 """
487 Run hooked-in checks
488 """
489
490 cond = True
491 check = self.check
492 if check is not None:
493 if not isinstance(check, (list, tuple)):
494 check = [check]
495 for condition in check:
496 if callable(condition) and not condition(self):
497 cond = False
498 elif not condition:
499 cond = False
500 return cond
501
502
503
504
506 """ Check whether a tag is present in any item of the subtree """
507
508 components = self.components
509 for i in components:
510 if tag in i.tags or tag in i:
511 return 1
512 return 0
513
514
530
531
533 """
534 Enable items
535
536 @param tag: enable all items in the subtree with this tag
537 (no tag enables only this item)
538 """
539
540 if tag is not None:
541 items = self.findall(tag)
542 for item in items:
543 item.enable()
544 else:
545 self.enabled = True
546 return
547
548
550 """
551 Disable items
552
553 @param tag: disable all items in the subtree with this tag
554 (no tag disables only this item)
555 """
556
557 if tag is not None:
558 items = self.findall(tag)
559 for item in items:
560 item.disable()
561 else:
562 self.enabled = False
563 return
564
565
567 """
568 Select an item. If given a tag, this selects the first matching
569 descendant (depth-first search), otherwise selects this item.
570
571 Propagates the selection up the path to the root item (including
572 the root item)
573
574 @param tag: a string
575 """
576
577 selected = None
578 if tag is None:
579 parent = self.parent
580 if parent:
581 parent.select()
582 else:
583 self.deselect_all()
584 selected = True
585 else:
586 for item in self.components:
587 if not selected:
588 selected = item.select(tag=tag)
589 else:
590 item.deselect_all()
591 if not selected and tag in self.tags:
592 selected = True
593 self.selected = selected
594 return selected
595
596
598 """ De-select this item and all its descendants """
599
600 self.selected = None
601 for item in self.components:
602 item.deselect_all()
603 return
604
605
606 - def set_layout(self, layout, recursive=False, tag=None):
607 """
608 Alter the renderer for a tagged subset of items in the subtree.
609
610 @param layout: the layout (renderer)
611 @param recursive: set this layout recursively for the subtree
612 @param tag: set this layout only for items with this tag
613 """
614
615 if layout is not None:
616 if tag is None or tag in self.tags:
617 self.renderer = layout
618 if recursive:
619 for c in self.components:
620 if tag is None or tag in c.tags:
621 c.set_layout(layout, recursive=recursive, tag=tag)
622 return
623
624
625
626
627
628 - def get(self, name, default=None):
629 """
630 Get a Python-attribute of this item instance, falls back
631 to the same attribute in the parent item if not set in
632 this instance, used to inherit attributes to components
633
634 @param name: the attribute name
635 """
636
637 if name in self.__dict__:
638 value = self.__dict__[name]
639 else:
640 value = None
641 if value is not None:
642 return value
643 if name[:2] == "__":
644 raise AttributeError
645 parent = self.parent
646 if parent is not None:
647 return parent.get(name)
648 return default
649
650
651 - def match(self, request=None):
652 """
653 Match this item against request (uses GET vars)
654
655 @param request: the request object (defaults to current.request)
656
657 @return: the match level (integer):
658 0=no match
659 1=controller
660 2=controller+function
661 3=controller+function+args
662 4=controller+function+args+vars
663
664 @note: currently ignores numerical arguments in the request,
665 which is though subject to change (in order to support
666 numerical arguments in the item)
667 """
668
669 level = 0
670 args = self.args
671 link_vars = self.vars
672
673 if self.application is not None and \
674 self.application != request.application:
675
676 return 0
677
678 if self.opts.selectable is False:
679 return 0
680
681
682 check = self.check_hook()
683 if check:
684 enabled = self.check_enabled()
685 if not enabled:
686 check = False
687 if not check:
688
689 return 0
690
691 if request is None:
692 request = current.request
693
694 c = self.get("controller")
695 mc = self.get("match_controller")
696
697
698
699 if not c and self.parent is None:
700 return 1
701
702
703 rvars = request.get_vars
704 controller = request.controller
705 function = request.function
706
707
708
709
710
711
712
713 if "viewing" in rvars:
714 try:
715 tn = rvars["viewing"].split(".", 1)[0]
716 controller, function = tn.split("_", 1)
717 except (AttributeError, ValueError):
718 pass
719
720
721 if controller == c or controller in mc:
722 level = 1
723
724
725 if level == 1:
726 f = self.get("function")
727 mf = self.get("match_function")
728 if function == f or function in mf:
729 level = 2
730 elif f == "index" or "index" in mf:
731
732 return 1
733 elif f is not None:
734 return 0
735
736
737
738
739
740
741
742
743
744
745
746 if level == 2:
747 extra = 1
748 for k, v in link_vars.iteritems():
749 if k not in rvars or k in rvars and rvars[k] != s3_str(v):
750 extra = 0
751 break
752 else:
753 extra = 2
754 rargs = request.args
755 if rargs:
756 if args:
757 largs = [a for a in request.args if not a.isdigit()]
758 if len(args) == len(largs) and \
759 all([args[i] == largs[i] for i in xrange(len(args))]):
760 level = 5
761 else:
762 if len(rargs) >= len(args) > 0 and \
763 rargs[len(args)-1].isdigit() and \
764 not str(args[-1]).isdigit():
765 level = 1
766 else:
767 return 0
768 else:
769 level = 3
770 elif args:
771 return 0
772 else:
773 level = 5
774 level += extra
775
776 return level
777
778
779 - def branch(self, request=None):
780 """
781 Get the matching branch item for request
782
783 @param request: the request object (defaults to current.request)
784 """
785
786 if request is None:
787 request = current.request
788
789 leaf, level = self.__branch(request)
790 if level:
791 return leaf
792 else:
793 return None
794
795
797 """
798 Find the best match for request within the subtree, recursive
799 helper method for branch().
800
801 @param request: the request object
802 """
803
804 items = self.get_all(enabled=True)
805 l = self.match(request)
806 if not items:
807 return self, l
808 else:
809 match, maxlevel = None, l - 1
810 for i in items:
811 item, level = i.__branch(request)
812 if item is not None and level > maxlevel:
813 match = item
814 maxlevel = level
815 if match is not None:
816 return match, maxlevel
817 else:
818 return self, l
819
820
821
822
823
837
838
839 - def url(self, extension=None, **kwargs):
840 """
841 Return the target URL for this item, doesn't check permissions
842
843 @param extension: override the format extension
844 @param kwargs: override URL query vars
845 """
846
847 if not self.link:
848 return None
849
850 if self.override_url:
851 return self.override_url
852
853 args = self.args
854 if self.vars:
855 link_vars = Storage(self.vars)
856 link_vars.update(kwargs)
857 else:
858 link_vars = Storage(kwargs)
859 if extension is None:
860 extension = self.extension
861 a = self.get("application")
862 if a is None:
863 a = current.request.application
864 c = self.get("controller")
865 if c is None:
866 c = "default"
867 f = self.get("function")
868 if f is None:
869 f = "index"
870 f, args = self.__format(f, args, extension)
871 return URL(a=a, c=c, f=f, args=args, vars=link_vars)
872
873
875 """
876 Return the target URL for this item if accessible by the
877 current user, otherwise False
878
879 @param extension: override the format extension
880 @param kwargs: override URL query vars
881 """
882
883 aURL = current.auth.permission.accessible_url
884
885 if not self.link:
886 return None
887
888 args = self.args
889 if self.vars:
890 link_vars = Storage(self.vars)
891 link_vars.update(kwargs)
892 else:
893 link_vars = Storage(kwargs)
894 if extension is None:
895 extension = self.extension
896 a = self.get("application")
897 if a is None:
898 a = current.request.application
899 c = self.get("controller")
900 if c is None:
901 c = "default"
902 f = self.get("function")
903 if f is None:
904 f = "index"
905 f, args = self.__format(f, args, extension)
906 return aURL(c=c, f=f, p=self.p, a=a, t=self.tablename,
907 args=args, vars=link_vars)
908
909
910 @staticmethod
930
931
932 - def render(self, request=None):
933 """
934 Perform the checks and render this item.
935
936 @param request: the request object (defaults to current.request)
937 """
938
939 renderer = self.renderer
940 output = None
941
942 if request is None:
943 request = current.request
944
945 if self.check_active(request):
946
947
948 self.authorized = self.check_permission()
949
950
951 self.selected = self.check_selected()
952
953
954
955
956 cond = self.check_hook()
957
958 if cond:
959
960
961 enabled = self.check_enabled()
962 if not enabled:
963 self.enabled = False
964
965
966 if renderer is not None:
967 output = renderer(self)
968
969 return output
970
971
973 """
974 Render the components of this item and return the results as list
975 """
976
977 items = []
978 for c in self.components:
979 i = c.render()
980 if i is not None:
981 if type(i) is list:
982 items.extend(i)
983 else:
984 items.append(i)
985 return items
986
987
989 """
990 Invokes the renderer and serializes the output for the web2py
991 template parser, returns a string to be written to the response
992 body, uses the xml() method of the renderer output, if present.
993 """
994
995 output = self.render()
996 if output is None:
997 return ""
998 elif hasattr(output, "xml"):
999 return output.xml()
1000 else:
1001 return str(output)
1002
1003
1004
1005
1007 """
1008 Set a parent for this item, base method for tree construction
1009
1010 @param p: the parent
1011 @param i: the list index where to insert the item
1012 """
1013
1014 if p is None:
1015 p = self.parent
1016 if p is None:
1017 return
1018 parent = self.parent
1019 if parent is not None and parent !=p:
1020 while self in parent.components:
1021 parent.components.remove(self)
1022 if i is not None:
1023 p.component.insert(i, self)
1024 else:
1025 p.components.append(self)
1026 self.parent = p
1027 return
1028
1029
1030 - def append(self, item=None):
1031 """
1032 Append a component
1033
1034 @param item: the component
1035 """
1036
1037 if item is not None:
1038 if type(item) is list:
1039 for i in item:
1040 self.append(i)
1041 else:
1042 item.set_parent(self)
1043 return self
1044
1045
1046 - def insert(self, i, item=None):
1047 """
1048 Insert a component item at position i
1049
1050 @param i: the index position
1051 @param item: the component item
1052 """
1053
1054 if item is not None:
1055 item.set_parent(self, i=i)
1056 return self
1057
1058
1060 """
1061 Extend this item with a list of components
1062
1063 @param items: list of component items
1064 """
1065
1066 if items:
1067 for item in items:
1068 self.append(item)
1069 return self
1070
1071
1073 """
1074 Convenience shortcut for extend
1075
1076 @param components: list of components
1077 """
1078
1079 self.extend(components)
1080 return self
1081
1082
1084 """
1085 Append component items to this item
1086
1087 @param items: the items to append
1088 """
1089
1090 if isinstance(items, (list, tuple)):
1091 self.extend(items)
1092 else:
1093 self.append(items)
1094 return self
1095
1096
1097
1098
1100 """
1101 Get the component item at position i
1102
1103 @param i: the index of the component item
1104 """
1105
1106 return self.components.__getitem__(i)
1107
1108
1110 """
1111 Overwrite the component item at position i with item
1112
1113 @param i: the index within the component list
1114 @param item: the item
1115 """
1116
1117 self.components.__setitem__(i, item)
1118
1119
1120 - def pop(self, i=-1):
1121 """
1122 Return the component at index i and remove it from the list
1123
1124 @param i: the component index
1125 """
1126
1127 return self.components.pop(i)
1128
1129
1131 """
1132 Get the top level item of this navigation tree
1133 """
1134
1135 parent = self.parent
1136 if parent:
1137 return parent.get_root()
1138 else:
1139 return self
1140
1141
1142 - def path(self, sub=None):
1143 """
1144 Get the full path to this item (=a list of items from the root
1145 item down to this item).
1146 """
1147
1148 path = [self]
1149 if sub:
1150 path.extend(sub)
1151 if self.parent:
1152 return self.parent.path(sub=path)
1153 else:
1154 return path
1155
1156
1158 """
1159 Get all components with these flags
1160
1161 @param flags: dictionary of flags
1162 """
1163
1164 items = []
1165 for item in self.components:
1166 if not flags or \
1167 all([getattr(item, f) == flags[f] for f in flags]):
1168 items.append(item)
1169 return items
1170
1171
1173 """
1174 Get the first component item with these flags
1175
1176 @param flags: dictionary of flags
1177 """
1178
1179 for item in self.components:
1180 if not flags or \
1181 all([getattr(item, f) == flags[f] for f in flags]):
1182 return item
1183 return None
1184
1185
1187 """
1188 Get the first component item with these flags
1189
1190 @param flags: dictionary of flags
1191 """
1192
1193 components = list(self.components)
1194 components.reverse()
1195 for item in components:
1196 if not flags or \
1197 all([getattr(item, f) == flags[f] for f in flags]):
1198 return item
1199 return None
1200
1201
1202
1203
1205 """ The total number of components of this item """
1206
1207 return len(self.components)
1208
1209
1211 """
1212 To be used instead of __len__ to determine the boolean value
1213 if this item, should always return True for instances
1214 """
1215
1216 return self is not None
1217
1218
1220 """
1221 Get the index of a component item within the component list
1222
1223 @param item: the item
1224 """
1225
1226 return self.components.index(item)
1227
1228
1230 """
1231 Get the position of this item within the parent's component
1232 list, reverse method for index()
1233 """
1234
1235 if self.parent:
1236 return self.parent.index(self)
1237 else:
1238 return None
1239
1240
1242 """
1243 Check whether this is the first item within the parent's
1244 components list with these flags
1245
1246 @param flags: dictionary of flags
1247 """
1248
1249 if not flags:
1250 return len(self.preceding()) == 0
1251 if not all([getattr(self, f) == flags[f] for f in flags]):
1252 return False
1253 preceding = self.preceding()
1254 if preceding:
1255 for item in preceding:
1256 if all([getattr(item, f) == flags[f] for f in flags]):
1257 return False
1258 return True
1259
1260
1262 """
1263 Check whether this is the last item within the parent's
1264 components list with these flags
1265
1266 @param flags: dictionary of flags
1267 """
1268
1269 if not flags:
1270 return len(self.following()) == 0
1271 if not all([getattr(self, f) == flags[f] for f in flags]):
1272 return False
1273 following = self.following()
1274 if following:
1275 for item in following:
1276 if all([getattr(item, f) == flags[f] for f in flags]):
1277 return False
1278 return True
1279
1280
1290
1291
1304
1305
1307 """
1308 Get the previous item in the parent's component list with these
1309 flags
1310
1311 @param flags: dictionary of flags
1312 """
1313
1314 preceding = self.preceding()
1315 preceding.reverse()
1316 for item in preceding:
1317 if not flags or \
1318 all([getattr(item, f) == flags[f] for f in flags]):
1319 return item
1320 return None
1321
1322
1324 """
1325 Get the next item in the parent's component list with these flags
1326
1327 @param flags: dictionary of flags
1328 """
1329
1330 following = self.following()
1331 for item in following:
1332 if not flags or \
1333 all([getattr(item, f) == flags[f] for f in flags]):
1334 return item
1335 return None
1336
1339 """
1340 Identify the tablename and record ID for the rheader
1341
1342 @param r: the current S3Request
1343
1344 """
1345
1346 get_vars = r.get_vars
1347
1348 if "viewing" in get_vars:
1349 try:
1350 tablename, record_id = get_vars.viewing.rsplit(".", 1)
1351 except AttributeError:
1352 tablename = r.tablename
1353 record = r.record
1354 else:
1355 db = current.db
1356 record = db[tablename][record_id]
1357 else:
1358 tablename = r.tablename
1359 record = r.record
1360
1361 return (tablename, record)
1362
1365 """
1366 Constructs a DIV of component links for a S3RESTRequest
1367
1368 @param tabs: the tabs as list of tuples (title, component_name, vars),
1369 where vars is optional
1370 """
1371
1372 rheader_tabs = S3ComponentTabs(tabs)
1373 return rheader_tabs.render(r)
1374
1377 """ Class representing a row of component tabs """
1378
1380 """
1381 Constructor
1382
1383 @param tabs: the tabs configuration as list of names or tuples
1384 (label, name)
1385 """
1386
1387 if not tabs:
1388 self.tabs = []
1389 else:
1390 self.tabs = [S3ComponentTab(t) for t in tabs if t]
1391
1392
1394 """
1395 Render the tabs row
1396
1397 @param r: the S3Request
1398 """
1399
1400 rheader_tabs = []
1401
1402 if r.resource.get_config("dynamic_components"):
1403 self.dynamic_tabs(r.resource.tablename)
1404
1405 tabs = [t for t in self.tabs if t.active(r)]
1406
1407 mtab = False
1408 if r.component is None:
1409
1410 for t in tabs:
1411 if t.component == r.method:
1412 mtab = True
1413 break
1414
1415 record_id = r.id
1416 if not record_id and r.record:
1417 record_id = r.record[r.table._id]
1418
1419 for i, tab in enumerate(tabs):
1420
1421
1422 vars_match = tab.vars_match(r)
1423 if vars_match:
1424 _vars = Storage(r.get_vars)
1425 else:
1426 _vars = Storage(tab.vars)
1427 if "viewing" in r.get_vars:
1428 _vars.viewing = r.get_vars.viewing
1429
1430
1431 if tab.function is None:
1432
1433 if "viewing" in _vars:
1434 tablename, record_id = _vars.viewing.split(".", 1)
1435 function = tablename.split("_", 1)[1]
1436 else:
1437 function = r.function
1438 else:
1439
1440 function = tab.function
1441
1442
1443 component = tab.component
1444 here = False
1445 if function == r.name or function == r.function:
1446 here = r.method == component or not mtab
1447 if component:
1448 if r.component and \
1449 r.component.alias == component and \
1450 vars_match:
1451 here = True
1452 elif not r.component and r.method == component:
1453 here = True
1454 else:
1455 here = False
1456 else:
1457 if r.component or not vars_match:
1458 here = False
1459
1460
1461 if here:
1462 _class = "tab_here"
1463 elif i == len(tabs) - 1:
1464 _class = "tab_last"
1465 else:
1466 _class = "tab_other"
1467
1468
1469 if component:
1470 if record_id:
1471 args = [record_id, component]
1472 else:
1473 args = [component]
1474 if "viewing" in _vars:
1475 del _vars["viewing"]
1476 _href = URL(function, args=args, vars=_vars)
1477 _id = "rheader_tab_%s" % component
1478 else:
1479 args = []
1480 if function != r.name and not tab.native:
1481 if "viewing" not in _vars and r.id:
1482 _vars.update(viewing="%s.%s" % (r.tablename, r.id))
1483 elif not tab.component and not tab.function:
1484 if "viewing" in _vars:
1485 del _vars["viewing"]
1486 args = [record_id]
1487 else:
1488 if "viewing" not in _vars and record_id:
1489 args = [record_id]
1490 _href = URL(function, args=args, vars=_vars)
1491 _id = "rheader_tab_%s" % function
1492
1493
1494 rheader_tabs.append(SPAN(A(tab.title,
1495 _href=_href,
1496 _id=_id,
1497 ),
1498 _class=_class,
1499 ))
1500
1501
1502 if rheader_tabs:
1503 rheader_tabs = DIV(rheader_tabs, _class="tabs")
1504 else:
1505 rheader_tabs = ""
1506 return rheader_tabs
1507
1508
1510 """
1511 Add dynamic tabs
1512
1513 @param master: the name of the master table
1514 """
1515
1516 T = current.T
1517 s3db = current.s3db
1518
1519 tabs = self.tabs
1520 if not tabs:
1521 return
1522
1523 ftable = s3db.s3_field
1524 query = (ftable.component_key == True) & \
1525 (ftable.component_tab == True) & \
1526 (ftable.master == master) & \
1527 (ftable.deleted == False)
1528 rows = current.db(query).select(ftable.component_alias,
1529 ftable.settings,
1530 )
1531 for row in rows:
1532 alias = row.component_alias
1533 if not alias:
1534 continue
1535
1536 static_tab = False
1537 for tab in tabs:
1538 if tab.component == alias:
1539 static_tab = True
1540 break
1541
1542 if not static_tab:
1543
1544 label = None
1545 position = None
1546
1547 settings = row.settings
1548 if settings:
1549
1550 label = settings.get("tab_label")
1551
1552 position = settings.get("tab_position")
1553 if position is not None:
1554 try:
1555 position = int(position)
1556 except ValueError:
1557 position = None
1558 if position < 1 or position >= len(tabs):
1559 position = None
1560
1561 if not label:
1562
1563 label = T(" ".join(s.capitalize()
1564 for s in alias.split("_")
1565 ))
1566 tab = S3ComponentTab((label, alias))
1567 if not position:
1568 tabs.append(tab)
1569 else:
1570 tabs.insert(position, tab)
1571
1574 """ Class representing a single Component Tab """
1575
1577 """
1578 Constructor
1579
1580 @param tab: the component tab configuration as tuple
1581 (label, component_alias, {get_vars}), where the
1582 get_vars dict is optional.
1583 """
1584
1585
1586
1587 title, component = tab[:2]
1588 if component and component.find("/") > 0:
1589 function, component = component.split("/", 1)
1590 else:
1591 function = None
1592
1593 self.title = title
1594
1595 self.native = False
1596
1597 if function:
1598 self.function = function
1599 else:
1600 self.function = None
1601
1602 if component:
1603 self.component = component
1604 else:
1605 self.component = None
1606
1607 if len(tab) > 2:
1608 tab_vars = self.vars = Storage(tab[2])
1609 if "native" in tab_vars:
1610 self.native = True if tab_vars["native"] else False
1611 del tab_vars["native"]
1612 else:
1613 self.vars = None
1614
1615
1617 """
1618 Check whether the this tab is active
1619
1620 @param r: the S3Request
1621 """
1622
1623 s3db = current.s3db
1624
1625 get_components = s3db.get_components
1626 get_method = s3db.get_method
1627 get_vars = r.get_vars
1628 tablename = None
1629 if "viewing" in get_vars:
1630 try:
1631 tablename = get_vars["viewing"].split(".", 1)[0]
1632 except AttributeError:
1633 pass
1634
1635 resource = r.resource
1636 component = self.component
1637 function = self.function
1638 if component:
1639 clist = get_components(resource.table, names=[component])
1640 is_component = False
1641 if component in clist:
1642 is_component = True
1643 elif tablename:
1644 clist = get_components(tablename, names=[component])
1645 if component in clist:
1646 is_component = True
1647 if is_component:
1648 return self.authorised(clist[component])
1649 handler = get_method(resource.prefix,
1650 resource.name,
1651 method=component)
1652 if handler is None and tablename:
1653 prefix, name = tablename.split("_", 1)
1654 handler = get_method(prefix, name,
1655 method=component)
1656 if handler is None:
1657 handler = r.get_handler(component)
1658 if handler is None:
1659 return component in ("create", "read", "update", "delete")
1660
1661 elif function:
1662 return current.auth.permission.has_permission("read", f=function)
1663
1664 return True
1665
1666
1668 """
1669 Check permissions for component tabs (in order to deactivate
1670 tabs the user is not permitted to access)
1671
1672 @param hook: the component hook
1673 """
1674
1675 READ = "read"
1676 has_permission = current.auth.s3_has_permission
1677
1678
1679 if hook.linktable and not has_permission(READ, hook.linktable):
1680 return False
1681
1682
1683 if has_permission(READ, hook.tablename):
1684 return True
1685
1686 return False
1687
1688
1690 """
1691 Check whether the request GET vars match the GET vars in
1692 the URL of this tab
1693
1694 @param r: the S3Request
1695 """
1696
1697 if self.vars is None:
1698 return True
1699 get_vars = r.get_vars
1700 for k, v in self.vars.iteritems():
1701 if v is None:
1702 continue
1703 if k not in get_vars or \
1704 k in get_vars and get_vars[k] != v:
1705 return False
1706 return True
1707
1710 """
1711 Simple Navigation Item just for injecting scripts into HTML forms
1712 """
1713
1714
1715 - def __init__(self,
1716 script=None,
1717 **attributes):
1718 """
1719 @param script: script to inject into jquery_ready when rendered
1720 """
1721
1722 super(S3ScriptItem, self).__init__(attributes)
1723 self.script = script
1724
1725
1727 """
1728 Injects associated script into jquery_ready.
1729 """
1730
1731 if self.script:
1732 current.response.s3.jquery_ready.append(self.script)
1733 return ""
1734
1735
1736 @staticmethod
1738 """
1739 Present to ensure that script injected even in inline forms
1740 """
1741
1742 return ""
1743
1746 """ Simple Generic Resource Header for tabbed component views """
1747
1749 """
1750 Constructor
1751
1752 @param fields: the fields to display, list of lists of
1753 fieldnames, Field instances or callables
1754 @param tabs: the tabs
1755 @param title: the title fieldname, Field or callable
1756
1757 Fields are specified in order rows->cols, i.e. if written
1758 like:
1759
1760 [
1761 ["fieldA", "fieldF", "fieldX"],
1762 ["fieldB", None, "fieldY"]
1763 ]
1764
1765 then that's exactly the screen order. Row or column spans are
1766 not supported - empty fields will be rendered as empty fields.
1767 If you need to construct more complex rheaders, you should
1768 implement a custom method.
1769
1770 Fields can be specified by field names, Field instances or
1771 as callables. Where a field specifier is a callable, it will
1772 be invoked with the record as parameter and is respected to
1773 return the representation value.
1774
1775 Where a field specifier is a tuple of two items, the first
1776 item is taken for the label (overriding the field label, if
1777 any), like in:
1778
1779
1780 [
1781 [(T("Name"), s3_fullname)],
1782 ...
1783 ]
1784
1785 Where the second item is a callable, it maybe necessary to
1786 specify a label.
1787
1788 If you don't want any fields, specify this explicitly as:
1789
1790 rheader = S3ResourceHeader(fields=[])
1791
1792 Where you don't specify any fields and the table contains a
1793 "name" field, the rheader defaults to: [["name"]].
1794 """
1795
1796 self.fields = fields
1797 self.tabs = tabs
1798 self.title = title
1799
1800
1802 """
1803 Return the HTML representation of this rheader
1804
1805 @param r: the S3Request instance to render the header for
1806 @param tabs: the tabs (overrides the original tabs definition)
1807 @param table: override r.table
1808 @param record: override r.record
1809 @param as_div: True: will return the rheader_fields and the
1810 rheader_tabs together as a DIV
1811 False will return the rheader_fields and the
1812 rheader_tabs as a tuple
1813 (rheader_fields, rheader_tabs)
1814 """
1815
1816 if table is None:
1817 table = r.table
1818 if record is None:
1819 record = r.record
1820
1821 if tabs is None:
1822 tabs = self.tabs
1823
1824 if self.fields is None and "name" in table.fields:
1825 fields = [["name"]]
1826 else:
1827 fields = self.fields
1828
1829 if record:
1830
1831 if tabs is not None:
1832 rheader_tabs = s3_rheader_tabs(r, tabs)
1833 else:
1834 rheader_tabs = ""
1835
1836 trs = []
1837 for row in fields:
1838 tr = TR()
1839 for col in row:
1840 if col is None:
1841 continue
1842 label, value, colspan = self.render_field(table, record, col)
1843 if value is False:
1844 continue
1845 if label is not None:
1846 tr.append(TH(("%s: " % label) if label else ""))
1847 tr.append(TD(value, _colspan=colspan) if colspan else TD(value))
1848 trs.append(tr)
1849
1850 title = self.title
1851 if title:
1852 title = self.render_field(table, record, title)[1]
1853
1854 if title:
1855 content = DIV(H6(title, _class="rheader-title"),
1856 TABLE(trs),
1857 _class="rheader-content",
1858 )
1859 else:
1860 content = TABLE(trs, _class="rheader-content")
1861
1862 rheader = (content, rheader_tabs)
1863 if as_div:
1864 rheader = DIV(*rheader)
1865
1866 else:
1867 rheader = None
1868
1869 return rheader
1870
1871
1873 """
1874 Render an rheader field
1875
1876 @param table: the table
1877 @param record: the record
1878 @param col: the column spec (field name or tuple (label, fieldname))
1879
1880 @returns: tuple (label, value)
1881 """
1882
1883 field = None
1884 label = True
1885 value = ""
1886 colspan = None
1887
1888
1889
1890
1891
1892
1893
1894 if isinstance(col, (tuple, list)):
1895 if len(col) == 2:
1896 label, f = col
1897 elif len(col) > 2:
1898 label, f, colspan = col
1899 else:
1900 f = col[0]
1901 else:
1902 f = col
1903
1904
1905
1906
1907 if callable(f):
1908 try:
1909 value = f(record)
1910 except:
1911 pass
1912 else:
1913 if isinstance(f, str):
1914 fn = f
1915 if "." in fn:
1916 fn = f.split(".", 1)[1]
1917 if fn not in record or fn not in table:
1918 return None, False, None
1919 field = table[fn]
1920 value = record[fn]
1921
1922 if callable(value):
1923 value = value()
1924 elif isinstance(f, Field) and f.name in record:
1925 field = f
1926 value = record[f.name]
1927 if hasattr(field, "represent") and field.represent is not None:
1928 value = field.represent(value)
1929
1930
1931 if label is True:
1932 label = field.label if field is not None else False
1933
1934
1935 if not isinstance(value, basestring) and \
1936 not isinstance(value, DIV):
1937 value = s3_str(value)
1938
1939 return label, value, colspan
1940
1941
1942