| Home | Trees | Indices | Help |
|
|---|
|
|
1 # -*- coding: utf-8 -*-
2
3 """ Dashboards
4
5 @copyright: 2016-2019 (c) Sahana Software Foundation
6 @license: MIT
7
8 @requires: U{B{I{gluon}} <http://web2py.com>}
9
10 Permission is hereby granted, free of charge, to any person
11 obtaining a copy of this software and associated documentation
12 files (the "Software"), to deal in the Software without
13 restriction, including without limitation the rights to use,
14 copy, modify, merge, publish, distribute, sublicense, and/or sell
15 copies of the Software, and to permit persons to whom the
16 Software is furnished to do so, subject to the following
17 conditions:
18
19 The above copyright notice and this permission notice shall be
20 included in all copies or substantial portions of the Software.
21
22 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
23 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
24 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
25 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
26 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
27 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
28 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
29 OTHER DEALINGS IN THE SOFTWARE.
30
31 @status: experimental, work in progress
32 """
33
34 __all__ = ("S3Dashboard",
35 "S3DashboardConfig",
36 "S3DashboardWidget",
37 )
38
39 import json
40 import uuid
41
42 from gluon import *
43
44 from s3utils import s3_get_extension, s3_str
45 from s3widgets import ICON
46 from s3validators import JSONERRORS
47
48 DEFAULT = lambda: None
49 DEFAULT_FORMAT = "html"
50
51 # =============================================================================
52 -class S3DashboardContext(object):
53 """ Context data for a dashboard """
54
56 """
57 Constructor
58
59 @param r: the request object (defaults to current.request)
60 @param dashboard: the dashboard (S3Dashboard)
61 """
62
63 # @todo: add request owner info (to select the active config)
64
65 # Context variables
66 self.dashboard = dashboard
67 self.shared = {}
68
69 # Global filters
70 # @todo: implement
71 self.filters = {}
72
73 # Parse request info
74 self._parse()
75
76 # -------------------------------------------------------------------------
78 """
79 Action upon error
80
81 @param status: HTTP status code
82 @param message: the error message
83 @param _next: destination URL for redirection upon error
84 (defaults to the index page of the module)
85 """
86
87 if self.representation == "html":
88
89 current.session.error = message
90
91 if _next is None:
92 _next = URL(f="index")
93 redirect(_next)
94
95 else:
96
97 current.log.error(message)
98
99 if self.representation == "popup":
100 # Display human-readable error message
101 headers = {}
102 body = DIV(message, _style="color:red")
103 else:
104 headers = {"Content-Type":"application/json"}
105 body = current.xml.json_message(success=False,
106 statuscode=status,
107 message=message,
108 )
109 raise HTTP(status, body=body, web2py_error=message, **headers)
110
111 # -------------------------------------------------------------------------
112 # Getters and setters for shared context variables
113 #
114 # Widgets and callbacks can share data in the context, e.g. when they
115 # use common object instances, or need to coordinate identifiers, like:
116 #
117 # - set the value for a variable:
118 # >>> context["key"] = value
119 #
120 # - get the value of a variable:
121 # >>> value = context["key"]
122 # >>> value = context.get("key", "some_default_value")
123 #
124 # - check for a variable:
125 # >>> if key in context:
126 #
127 # - remove a variable:
128 # >>> del context["key"]
129 #
133
137
141
145
149
150 # -------------------------------------------------------------------------
152 """
153 Called upon context.<key> - looks up the value for the <key>
154 attribute. Falls back to current.request if the attribute is
155 not defined in this context.
156
157 @param key: the key to lookup
158 """
159
160 if key in self.__dict__:
161 return self.__dict__[key]
162
163 sentinel = object()
164 value = getattr(current.request, key, sentinel)
165 if value is sentinel:
166 raise AttributeError
167 return value
168
169 # -------------------------------------------------------------------------
171 """
172 Parse the request info
173
174 @param r: the web2py Request, falls back to current.request
175 """
176
177 request = current.request if r is None else r
178
179 args = request.args
180 get_vars = request.get_vars
181
182 command = None
183 if len(args) > 0:
184 command = args[0]
185 if "." in command:
186 command = command.split(".", 1)[0]
187 if command:
188 self.command = command
189
190 # Dashboard controller can explicitly mark requests as bulk
191 # even with a single agent ID (e.g. if the dashboard happens
192 # to only have one widget) - either by appending a comma to
193 # the agent ID, or by specifying ?bulk=1
194 bulk = get_vars.get("bulk")
195 bulk = True if bulk and bulk.lower() in ("1", "true") else False
196
197 agent_id = request.get_vars.get("agent")
198 if agent_id:
199 if type(agent_id) is list:
200 agent_id = ",".join(agent_id)
201 if "," in agent_id:
202 bulk = True
203 agent_id = set([s.strip() for s in agent_id.split(",")])
204 agent_id.discard("")
205 agent_id = list(agent_id)
206 elif bulk:
207 agent_id = [agent_id]
208 self.agent = agent_id
209
210 self.bulk = bulk
211
212 self.http = current.request.env.request_method or "GET"
213 self.representation = s3_get_extension(request) or DEFAULT_FORMAT
214
215 # =============================================================================
216 -class S3DashboardConfig(object):
217 """
218 Dashboard Configuration
219 """
220
221 DEFAULT_LAYOUT = "boxes"
222
223 - def __init__(self,
224 layout,
225 widgets=None,
226 default=None,
227 configurable=False,
228 ):
229 """
230 Constructor
231
232 @param layout: the layout, or the config dict
233 @param widgets: the available widgets as dict {name: widget}
234 @param default: the default configuration (=list of widget configs)
235 @param configurable: whether this dashboard is user-configurable
236 """
237
238 if isinstance(layout, dict):
239 config = layout
240 title = config.get("title", current.T("Dashboard"))
241 layout = config.get("layout")
242 widgets = config.get("widgets", widgets)
243 default = config.get("default", default)
244 configurable = config.get("configurable", configurable)
245
246 # Configuration ID
247 # - None means the hardcoded default
248 self.config_id = None
249
250 # Page Title
251 self.title = title
252
253 # Layout
254 if layout is None:
255 layout = self.DEFAULT_LAYOUT
256 self.layout = layout
257
258 # Available Widgets
259 if not widgets:
260 widgets = {}
261 self.available_widgets = widgets
262
263 # Active Widgets
264 if not default:
265 default = []
266 self.active_widgets = default
267
268 self.version = None
269 self.next_id = 0
270
271 # Is this dashboard user-configurable?
272 self.configurable = configurable
273 self.loaded = True if not configurable else False
274
275 # -------------------------------------------------------------------------
277 """
278 Load the current active configuration for the context
279
280 @param context: the current S3DashboardContext
281 """
282
283 if not self.configurable:
284 return
285
286 table = current.s3db.s3_dashboard
287 query = (table.controller == context.controller) & \
288 (table.function == context.function) & \
289 (table.active == True) & \
290 (table.deleted != True)
291 row = current.db(query).select(table.id,
292 table.layout,
293 table.title,
294 table.version,
295 table.next_id,
296 table.widgets,
297 limitby = (0, 1),
298 ).first()
299
300 if row:
301 # Version key and next widget ID
302 self.version = row.version
303 if row.next_id:
304 self.next_id = row.next_id
305
306 # Layout and title
307 self.layout = row.layout
308 if row.title:
309 self.title = row.title
310
311 # Active widgets
312 widgets = row.widgets
313 if type(widgets) is list:
314 self.active_widgets = widgets
315
316 # Store record ID
317 self.config_id = row.id
318
319 self.loaded = True
320
321 # -------------------------------------------------------------------------
323 """
324 Save this configuration in the database
325
326 @param context: the current S3DashboardContext
327 @param update: widget configurations to update, as dict
328 {widget_id: {config-dict}}
329
330 @return: the new version key, or None if not successful
331 """
332
333 # Must be configurable and loaded
334 if not self.configurable or not self.loaded:
335 return None
336
337 db = current.db
338 table = current.s3db.s3_dashboard
339
340 # Collect the widget configs
341 widgets = self.active_widgets
342
343 # Updated widget configs
344 configs = []
345 for widget in widgets:
346 widget_id = widget.get("widget_id")
347 new_config = update.get(widget_id)
348 if new_config:
349 new_config["widget_id"] = widget_id
350 configs.append(new_config)
351 else:
352 configs.append(widget)
353
354 # Generate a new version key
355 version = uuid.uuid4().get_hex()
356
357 config_id = self.config_id
358 if not config_id:
359 # Create new record
360 data = {"controller": context.controller,
361 "function": context.function,
362 "version": version,
363 "next_id": self.next_id,
364 "active": True,
365 "widgets": configs,
366 }
367 config_id = table.insert(**data)
368 if not config_id:
369 version = None
370 # @todo: call onaccept?
371 else:
372 # Update existing record
373 data = {"version": version,
374 "next_id": self.next_id,
375 "widgets": configs,
376 }
377 success = db(table.id == config_id).update(**data)
378 if not success:
379 version = None
380 # @todo: call onaccept?
381
382 if version:
383 self.version = version
384
385 return version
386
387 # =============================================================================
388 -class S3DashboardChannel(object):
389 """
390 A dashboard channel
391 (=a section of the dashboard where widgets can be added)
392 """
393
400
401 # -------------------------------------------------------------------------
403 """
404 Add XML for a widget to this channel
405
406 @param widget: the widget XML (e.g. DIV instance)
407 @param position: the position of the widget in the channel,
408 if there are multiple widgets in the channel
409 """
410
411 widgets = self.widgets
412 if position not in widgets:
413 widgets[position] = [widget]
414 else:
415 widgets[position].append(widget)
416
417 # -------------------------------------------------------------------------
419 """
420 Iterate over the widgets in this channel, in order of their
421 positions; used in layouts to build the channel contents.
422
423 @note: Widgets without explicit position (=None), or multiple
424 widgets at the same position, will be returned in the
425 order in which they have been added to the channel.
426 """
427
428 widgets = self.widgets
429 positions = sorted(p for p in widgets if p is not None)
430
431 if None in widgets:
432 positions.append(None)
433
434 for position in positions:
435 widget_list = widgets[position]
436 for widget in widget_list:
437 yield(widget)
438
439 # -------------------------------------------------------------------------
441 """
442 Number of widgets in this channel, useful for sizing of
443 container elements in layouts:
444
445 - number_of_widgets = len(channel)
446 """
447
448 total = sum(len(widgets) for widgets in self.widgets.values())
449 return total
450
451 # =============================================================================
452 -class S3DashboardLayout(object):
453 """
454 Base class for dashboard layouts, can be subclassed
455 to implement custom layouts
456 """
457
458 # Tuple of available channels
459 # - leave at None in subclasses that dynamically generate channels
460 CHANNELS = None
461
462 # The default channel
463 DEFAULT_CHANNEL = None
464
465 # -------------------------------------------------------------------------
466 # Methods to be implemented in subclasses
467 # -------------------------------------------------------------------------
469 """
470 Build the layout with the contents added by agents
471 - to be implemented in subclasses
472
473 @param context: the current S3DashboardContext
474
475 @return: the dashboard contents, usually a TAG instance,
476 alternatively a dict for the view (for custom
477 views)
478
479 @note: can override current.response.view to use a
480 specific view template (default is dashboard.html)
481 """
482
483 return ""
484
485 # -------------------------------------------------------------------------
487 """
488 Add contents to layout,
489 - can be overwritten in subclasses (e.g. to dynamically
490 create channels)
491
492 @param contents: the contents to insert
493 @param channel: the channel where to insert the contents,
494 using the default channel if None
495 @param position: the position within the channel (numeric),
496 append to channel if None
497 """
498
499 # Fall back to default channel
500 if channel is DEFAULT:
501 channel = self.DEFAULT_CHANNEL
502
503 # Get the channel
504 # - subclasses may want to dynamically generate channels here
505 channel_ = self.channels.get(channel)
506
507 # Add widget to channel
508 if channel_ is not None:
509 channel_.add_widget(widget, position=position)
510
511 # -------------------------------------------------------------------------
512 # Helpers
513 # -------------------------------------------------------------------------
515 """
516 Build a single channel, usually called by build()
517
518 @param key: the channel key
519 @param attr: HTML attributes for the channel
520
521 @return: the XML for the channel, usually a DIV instance
522 """
523
524 widgets = default = XML(" ")
525
526 channel = self.channels.get(key)
527 if channel is not None:
528 widgets = [w for w in channel]
529 if not widgets:
530 widgets = default
531
532 return DIV(widgets, **attr)
533
534 # -------------------------------------------------------------------------
535 # Base class methods
536 # -------------------------------------------------------------------------
552
553 # =============================================================================
554 -class S3DashboardBoxesLayout(S3DashboardLayout):
555 """
556 Simple 5-boxes layout for dashboards
557
558 +--------------------+
559 | N |
560 +----+----------+----+
561 | W | C | E |
562 +----+----------+----+
563 | S |
564 +--------------------+
565 """
566
567 # Tuple of available channels
568 CHANNELS = ("N", "W", "C", "E", "S")
569
570 # The default channel
571 DEFAULT_CHANNEL = "C"
572
573 # -------------------------------------------------------------------------
575 """
576 Build the layout with the contents added by agents
577
578 @param context: the current S3DashboardContext
579
580 @return: the dashboard contents (TAG)
581 """
582
583 T = current.T
584
585 channel = self.build_channel
586
587 contents = TAG[""](
588 DIV(channel("N",
589 _class="small-12 columns db-box db-box-n",
590 ),
591 _class="row",
592 ),
593 DIV(channel("W",
594 _class="small-3 columns db-box db-box-w",
595 ),
596 channel("C",
597 _class="small-6 columns db-box db-box-c",
598 ),
599 channel("E",
600 _class="small-3 columns db-box db-box-e",
601 ),
602 _class="row",
603 ),
604 DIV(channel("S",
605 _class="small-12 columns db-box db-box-s",
606 ),
607 _class="row",
608 ),
609 )
610
611 return contents
612
613
614 # =============================================================================
615 -class S3DashboardColumnsLayout(S3DashboardLayout):
618
619 # =============================================================================
620 -class S3DashboardGridLayout(S3DashboardLayout):
623
624 # =============================================================================
625 -class S3Dashboard(object):
626 """
627 Class to build and manage dashboards
628
629 def my_controller():
630
631 config = {
632 # The default layout for the dashboard
633 "layout": "...",
634
635 # Available widgets
636 "widgets": {...},
637
638 # Default Widget Configurations
639 "default": [...],
640
641 # Allow the user to configure this dashboard
642 "configurable": True,
643 }
644
645 dashboard = S3Dashboard(config)
646 return dashboard()
647 """
648
649 # -------------------------------------------------------------------------
650 # Standard layouts
651 # - custom layouts/overrides can be specifed in constructor
652 #
653 layouts = {"boxes": ("Boxes", S3DashboardBoxesLayout),
654 "columns": ("Columns", S3DashboardColumnsLayout),
655 "grid": ("Grid", S3DashboardGridLayout),
656 }
657
658 # -------------------------------------------------------------------------
660 """
661 Initializes the dashboard
662
663 @param config: the default configuration for this dashboard
664 @param layouts: custom layouts to override/extend the available
665 layouts, as dict {name: (label, class)}
666 """
667
668 if not isinstance(config, S3DashboardConfig):
669 config = S3DashboardConfig(config)
670 self._config = config
671
672 self.context = S3DashboardContext(dashboard=self)
673
674 available_layouts = dict(self.layouts)
675 if layouts:
676 available_layouts.update(layouts)
677 self.available_layouts = available_layouts
678
679 self._agents = None
680
681 # -------------------------------------------------------------------------
682 @property
684 """
685 Lazy property to load the current configuration from the database
686
687 @return: the S3DashboardConfig
688 """
689
690 config = self._config
691 if not config.loaded:
692 config.load(self.context)
693
694 return config
695
696 # -------------------------------------------------------------------------
697 @property
699 """
700 Lazy property to instantiate the dashboard agents
701
702 @return: a dict {agent_id: agent}
703 """
704
705 agents = self._agents
706 if agents is None:
707
708 # The dashboard configuration
709 config = self.config
710
711 # The current context
712 context = self.context
713
714 # Config details
715 config_id = config.config_id
716 available_widgets = config.available_widgets
717 next_id = config.next_id
718
719 agents = self._agents = {}
720 for index, widget_config in enumerate(config.active_widgets):
721
722 # Get the widget type
723 name = widget_config.get("widget")
724 widget = available_widgets.get(name)
725 if not widget:
726 continue
727
728 # Get the widget ID
729 if config_id is None:
730 widget_id = next_id
731 else:
732 widget_id = widget_config.get("widget_id")
733 if widget_id is None:
734 widget_id = next_id
735 next_id = max(next_id, widget_id + 1)
736 widget_config["widget_id"] = widget_id
737
738 # Construct the agent ID
739 agent_id = "db-widget-%s" % widget_id
740 if not agent_id:
741 continue
742
743 # Instantiate the agent
744 agent = widget.create_agent(agent_id,
745 config = widget_config,
746 version = config.version,
747 context = context,
748 )
749
750 # Register the agent
751 agents[agent_id] = agent
752
753 config.next_id = next_id
754
755 return agents
756
757 # -------------------------------------------------------------------------
759 """
760 Dispatch requests - this method is called by the controller.
761
762 @param attr: keyword arguments from the controller
763
764 @keyword _id: the node ID for the dashboard (default: "dashboard")
765
766 @return: the output for the view
767 """
768
769 # @todo: handle the side menu (if any)
770
771 context = self.context
772
773 agent_id = context.agent
774 http = context.http
775 command = context.command
776
777 status, msg = None, None
778
779 # Verify that the requested version matches the current config
780 if agent_id or command or http != "GET":
781 request_version = context.get_vars.get("version")
782 if not request_version:
783 context.error(400,
784 current.T("Invalid Request URL (version key missing)"),
785 )
786 if request_version != self.config.version:
787 context.error(409,
788 current.T("Page configuration has changed, please reload the page"),
789 _next = URL(args=[], vars={}),
790 )
791
792 if not agent_id:
793 # Global request
794 if http == "GET":
795 if command:
796 # Global command
797 output, error = self.do(command, context)
798 if error:
799 status, msg = error
800 else:
801 # Build dashboard
802 if context.representation == "html":
803 output = self.build(**attr)
804 else:
805 status, msg = 415, current.ERROR.BAD_FORMAT
806 elif http == "POST":
807 if command:
808 # Global command
809 output, error = self.do(command, context)
810 if error:
811 status, msg = error
812 else:
813 # POST requires command
814 status, msg = 405, current.ERROR.BAD_METHOD
815 else:
816 status, msg = 405, current.ERROR.BAD_METHOD
817
818 elif context.bulk:
819 # Multi-agent request (bulk status check)
820 # @todo: implement
821 status, msg = 501, current.ERROR.NOT_IMPLEMENTED
822
823 else:
824 # Single-agent request
825 agent = self.agents.get(agent_id)
826 if agent:
827 # Call the agent
828 output, error = agent(self, context)
829 if error:
830 status, msg = error
831 else:
832 status, msg = 404, current.ERROR.BAD_RESOURCE
833
834 if status:
835 context.error(status, msg)
836 else:
837 return output
838
839 # -------------------------------------------------------------------------
841 """
842 Build the dashboard and all its contents
843
844 @param attr: keyword arguments from the controller
845
846 @return: the output dict for the view
847 """
848
849 config = self.config
850 context = self.context
851
852 dashboard_id = attr.get("_id", "dashboard")
853
854 # Switch for config mode
855 hide = " hide" if not config.configurable else ""
856 switch = SPAN(ICON("settings",
857 _class = "db-config-on%s" % hide,
858 ),
859 ICON("done",
860 _class = "db-config-off hide"),
861 _class = "db-config",
862 data = {"mode": "off"},
863 )
864
865 output = {"title": config.title,
866 "contents": "",
867 "dashboard_id": dashboard_id,
868 "switch": switch,
869 }
870
871 # Script Options
872 ajax_url = URL(args=[], vars={})
873
874 script_options = {"ajaxURL": ajax_url,
875 "version": config.version,
876 }
877
878 # Inject JavaScript
879 # - before building the widgets, so that widgets can subclass
880 self.inject_script(dashboard_id, options=script_options)
881
882 # Instantiate the layout for the active config
883 layout = self.get_active_layout(config)
884
885 # Build the widgets
886 for agent in self.agents.values():
887 agent.add_widget(layout, context)
888
889 # Set the view
890 current.response.view = "dashboard.html"
891
892 # Build the layout
893 contents = layout.build(context)
894
895 if isinstance(contents, dict):
896 output.update(contents)
897 else:
898 output["contents"] = contents
899
900 return output
901
902 # -------------------------------------------------------------------------
904 """
905 Execute a dashboard global command
906
907 @param command: the command
908 @param context: the current S3DashboardContext
909
910 @todo: implement global commands
911 """
912
913 output = None
914 error = (501, current.ERROR.NOT_IMPLEMENTED)
915
916 return output, error
917
918 # -------------------------------------------------------------------------
920 """
921 Get the active layout
922
923 @param config: the active dashboard configuration
924 @return: an instance of the active layout
925 """
926
927 layout = self.available_layouts.get(config.layout)
928
929 if layout is None:
930 layout = S3DashboardBoxesLayout
931 elif type(layout) is tuple:
932 # Can be a tuple to specify a label for the layout selector
933 layout = layout[-1]
934
935 return layout(config)
936
937 # -------------------------------------------------------------------------
938 @staticmethod
940 """
941 Inject the JS to instantiate the client-side widget controller
942
943 @param dashboard_id: the dashboard DOM node ID
944 @param options: JSON-serializable dict with script options
945 """
946
947 s3 = current.response.s3
948
949 scripts = s3.scripts
950 appname = current.request.application
951
952 # Inject UI widget script
953 if s3.debug:
954 script = "/%s/static/scripts/S3/s3.ui.dashboard.js" % appname
955 if script not in scripts:
956 scripts.append(script)
957 else:
958 script = "/%s/static/scripts/S3/s3.ui.dashboard.min.js" % appname
959 if script not in scripts:
960 scripts.append(script)
961
962 # Inject widget instantiation
963 if not options:
964 options = {}
965 script = """$("#%(dashboard_id)s").dashboardController(%(options)s)""" % \
966 {"dashboard_id": dashboard_id,
967 "options": json.dumps(options),
968 }
969 s3.jquery_ready.append(script)
970
971
972 # =============================================================================
973 -class delegated(object):
974 """
975 Decorator for widget methods that shall be exposed in the web API.
976
977 Delegated methods will be available as URL commands, so that
978 client-side scripts can send Ajax requests directly to their
979 agent:
980
981 /my/dashboard/[command]?agent=[agent-id]
982
983 Delegated methods will be executed in the context of the agent
984 rather than of the widget (therefore "delegated"), so that they
985 have access to the agent configuration.
986
987 Pattern:
988
989 @delegated
990 def example(agent, context):
991
992 # Accessing the agent config:
993 config = agent.config
994
995 # Accessing the widget context (=self):
996 widget = agent.widget
997
998 # Accessing other agents of the same widget:
999 agents = widget.agents
1000
1001 # do something with the context, return output
1002 return {"output": "something"}
1003 """
1004
1007
1011
1019
1020 # =============================================================================
1021 -class S3DashboardAgent(object):
1022 """
1023 Object serving a dashboard widget
1024
1025 - renders the widget according to the active configuration
1026 - dispatches Ajax requests to widget methods
1027 - manages the widget configuration
1028 """
1029
1031 """
1032 Initialize the agent
1033
1034 @param agent_id: the agent ID (string), a unique XML
1035 identifier for the widget configuration
1036 @param widget: the widget (S3DashboardWidget instance)
1037 @param config: the widget configuration (dict)
1038 @param version: the config version
1039 """
1040
1041 self.agent_id = agent_id
1042 self.widget = widget
1043
1044 self.config = config
1045 self.version = version
1046
1047 # -------------------------------------------------------------------------
1049 """
1050 Dispatch Ajax requests
1051
1052 @param dashboard: the calling S3Dashboard instance
1053 @param context: the current S3DashboardContext
1054
1055 @return: tuple (output, error), where:
1056 - "output" is the output of the command execution
1057 - "error" is a tuple (http_status, message), or None
1058 """
1059
1060 command = context.command
1061 representation = context.representation
1062
1063 output = None
1064 error = None
1065
1066 if command:
1067 if command == "config":
1068 if representation == "popup":
1069 output = self.configure(dashboard, context)
1070 else:
1071 error = (415, current.ERROR.BAD_FORMAT)
1072 elif command == "authorize":
1073 # Placeholder for agent command
1074 # @todo: implement authorize-action
1075 error = (501, current.ERROR.NOT_IMPLEMENTED)
1076 else:
1077 # Delegated command
1078 try:
1079 output = self.do(command, context)
1080 except NotImplementedError:
1081 error = (501, current.ERROR.NOT_IMPLEMENTED)
1082 else:
1083 if context.http == "GET":
1084 if representation in ("html", "iframe"):
1085 # Return the widget XML
1086 output = self.widget.widget(self.agent_id,
1087 self.config,
1088 context = context,
1089 )
1090 else:
1091 error = (415, current.ERROR.BAD_FORMAT)
1092 else:
1093 error = (405, current.ERROR.BAD_METHOD)
1094
1095 return output, error
1096
1097 # -------------------------------------------------------------------------
1099 """
1100 Execute a delegated widget method
1101
1102 @param command: the name of the delegated widget method
1103 @param context: the S3DashboardContext
1104 """
1105
1106 widget = self.widget
1107
1108 msg = "%s does not expose a '%s' method"
1109 exception = lambda: NotImplementedError(msg % (widget.__class__.__name__,
1110 command,
1111 ))
1112 try:
1113 method = getattr(widget, command)
1114 except AttributeError:
1115 raise exception()
1116 if type(method) is not delegated:
1117 raise exception()
1118
1119 return method.execute(self, context)
1120
1121 # -------------------------------------------------------------------------
1123 """
1124 Build the widget XML for the context, and add it to the layout
1125 """
1126
1127 config = self.config
1128 prototype = self.widget
1129
1130 # Produce the contents XML
1131 contents = prototype.widget(self.agent_id,
1132 config,
1133 version = self.version,
1134 context = context,
1135 )
1136
1137 # Get the config bar
1138 configbar = prototype.configbar()
1139
1140 # Construct the widget
1141 widget = DIV(configbar,
1142 contents,
1143 _class = "db-widget",
1144 _id = self.agent_id,
1145 )
1146
1147 # Add script file
1148 prototype._load_script()
1149
1150 # Determine channel and position from config
1151 channel = config.get("channel", DEFAULT)
1152 position = config.get("position", None)
1153
1154 # Add the widget to the layout
1155 layout.add_widget(widget,
1156 channel=channel,
1157 position=position,
1158 )
1159
1160 # -------------------------------------------------------------------------
1162 """
1163 Controller for the widget configuration dialog
1164
1165 @param dashboard: the calling S3Dashboard instance
1166 @param context: the S3DashboardContext
1167
1168 @return: output dict for the view
1169 """
1170
1171 response = current.response
1172 s3 = response.s3
1173
1174 # Get the form fields from the widget class
1175 prototype = self.widget
1176 formfields = prototype.configure(self)
1177
1178 # The current configuration as formdata
1179 formdata = dict(self.config)
1180 formdata["id"] = 0
1181
1182 # Form buttons
1183 T = current.T
1184 submit_btn = INPUT(_class = "tiny primary button submit-btn",
1185 _name = "submit",
1186 _type = "submit",
1187 _value = T("Submit"),
1188 )
1189 buttons = [submit_btn]
1190
1191 # Construct the form
1192 settings = s3.crud
1193 formstyle = settings.formstyle
1194 form = SQLFORM.factory(*formfields,
1195 record = formdata,
1196 showid = False,
1197 formstyle = formstyle,
1198 table_name = "config",
1199 upload = s3.download_url,
1200 separator = "",
1201 submit_button = settings.submit_button,
1202 buttons = buttons)
1203
1204 # Process the form
1205 formname = "config/%s" % self.agent_id
1206 if form.accepts(context.post_vars,
1207 current.session,
1208 onvalidation = prototype.validate_config,
1209 formname = formname,
1210 keepvalues = False,
1211 hideerror = False,
1212 ):
1213
1214 # Get an updated config dict from the widget
1215 widget_config = self.config
1216 widget_id = widget_config.get("widget_id")
1217
1218 new_config = prototype.accept_config(widget_config, form)
1219
1220 # Pass new config to client via s3.popup_data
1221 popup_data = {"c": new_config}
1222
1223 # Save the new config and add the new version key to the popup_data
1224 if dashboard:
1225 version = dashboard.config.save(context, {widget_id: new_config})
1226 if version:
1227 popup_data["v"] = version
1228
1229 # Using JSON serializer rather than raw json.dumps to catch T()'s
1230 from gluon.serializers import json as jsons
1231 s3.popup_data = jsons(popup_data)
1232
1233 # Send a confirmation so the popup gets closed
1234 # (layout.html diverts to layout_popup.html with
1235 # "popup" request format + response.confirmation)
1236 response.confirmation = T("Configuration updated")
1237
1238 # Set view (@todo: implement specific view?)
1239 response.view = "popup.html"
1240
1241 return {# Context needed for layout.html to determine
1242 # the representation format:
1243 "r": context,
1244 "form": form,
1245 }
1246
1247 # =============================================================================
1248 -class S3DashboardWidget(object):
1249 """
1250 Base class for dashboard widgets
1251 """
1252
1253 title = "Dashboard Widget"
1254
1255 # -------------------------------------------------------------------------
1256 # Methods to be implemented by subclasses
1257 # -------------------------------------------------------------------------
1259 """
1260 Construct the XML for this widget
1261
1262 @param agent_id: the agent ID (same as the DOM node ID of the
1263 outer wrapper DIV, to attach scripts)
1264 @param config: the active widget configuration
1265 @param version: the config version key
1266 @param context: the S3DashboardContext
1267
1268 @return: an XmlComponent with the widget contents,
1269 the outer DIV will be added by the agent
1270 """
1271
1272 # Base class just renders some static XML
1273 contents = config.get("xml", "")
1274
1275 # Inject the JavaScript components
1276 self.inject_script(agent_id, version=version)
1277
1278 return XML(contents)
1279
1280 # -------------------------------------------------------------------------
1282 """
1283 Get the path to the script file for this widget, can be
1284 implemented in subclasses to override the default.
1285
1286 @param debug: whether running in debug mode or not
1287
1288 @return: path relative to static/scripts,
1289 None if no separate script file required
1290 """
1291
1292 #if debug:
1293 # return "S3/mywidget.js"
1294 #else:
1295 # return "S3/mywidget.min.js"
1296
1297 # No separate script file required for base class
1298 return None
1299
1300 # -------------------------------------------------------------------------
1302 """
1303 Get widget-specific configuration form fields
1304
1305 @param agent: the agent
1306
1307 @return: a list of Fields for the form construction
1308 """
1309
1310 # Generic widget allows configuration of XML
1311 formfields = [Field("xml", "text",
1312 label = "XML",
1313 ),
1314 ]
1315
1316 return formfields
1317
1318 # -------------------------------------------------------------------------
1320 """
1321 Extract the new config settings from the form and
1322 update the config dict
1323
1324 @param config: the config dict
1325 @param form: the configuration form
1326
1327 @return: the updated config dict (can be a replacement)
1328
1329 NB config must remain JSON-serializable
1330 """
1331
1332 formvars = form.vars
1333
1334 xml = formvars.get("xml")
1335 if xml is not None:
1336 config["xml"] = xml
1337
1338 return config
1339
1340 # -------------------------------------------------------------------------
1342 """
1343 Validation function for configuration form
1344 """
1345
1346 # Generic widget has nothing to validate
1347 pass
1348
1349 # -------------------------------------------------------------------------
1350 # Helpers
1351 # -------------------------------------------------------------------------
1352 @classmethod
1353 - def inject_script(cls,
1354 agent_id,
1355 version=None,
1356 widget_class="dashboardWidget",
1357 options=None):
1358 """
1359 Helper method to inject the init script for a particular agent,
1360 usually called by widget() method.
1361
1362 @param agent_id: the agent ID
1363 @param version: the config version key
1364 @param widget_class: the widget class to instantiate
1365 @param options: JSON-serializable dict of options to pass
1366 to the widget instance
1367 """
1368
1369 s3 = current.response.s3
1370
1371 if not agent_id or not widget_class:
1372 return
1373 if not options:
1374 options = {}
1375
1376 # Add the widget title (for the configuration popup)
1377 title = cls.title
1378 if title:
1379 options["title"] = s3_str(current.T(title))
1380
1381 # Add the dashboard URL
1382 dashboard_url = URL(args=[], vars={})
1383 options["dashboardURL"] = dashboard_url
1384
1385 # Add the config version key
1386 options["version"] = version
1387
1388 script = """$("#%(agent_id)s").%(widget_class)s(%(options)s)""" % \
1389 {"agent_id": agent_id,
1390 "widget_class": widget_class,
1391 "options": json.dumps(options),
1392 }
1393 s3.jquery_ready.append(script)
1394
1395 # -------------------------------------------------------------------------
1396 @staticmethod
1398 """
1399 Build the widget configuration task bar
1400
1401 @return: the XML for the task bar
1402 """
1403
1404 return DIV(SPAN(ICON("move", _class="db-task-move"),
1405 _class="db-configbar-left",
1406 ),
1407 SPAN(ICON("delete", _class="db-task-delete"),
1408 ICON("settings", _class="db-task-config"),
1409 _class="db-configbar-right",
1410 ),
1411 _class = "db-configbar",
1412 )
1413
1414 # -------------------------------------------------------------------------
1415 # Base class methods
1416 # -------------------------------------------------------------------------
1417 - def __init__(self,
1418 label=None,
1419 defaults=None,
1420 on_create_agent=None,
1421 **options
1422 ):
1423 """
1424 Initialize the widget, called when configuring an available
1425 widget for the dashboard.
1426
1427 @param label: a label for this widget type, used in the widget
1428 selector in the configuration GUI; if left empty,
1429 then the widget type will not appear in the selector
1430 @param defaults: the default configuration for this widget
1431 @param on_create_agent: callback, invoked when an agent is created
1432 for this widget:
1433 - on_create_agent(agent, context)
1434 @param **options: type-specific options
1435 """
1436
1437 self.label = label
1438
1439 # The default configuration for this widget
1440 if defaults is None:
1441 defaults = {}
1442 self.defaults = defaults
1443
1444 # Widget-type specific options
1445 self.options = options
1446
1447 # Hooks
1448 self.on_create_agent = on_create_agent
1449
1450 self.agents = {}
1451 self.script_loaded = False
1452
1453 # -------------------------------------------------------------------------
1455 """
1456 Create an agent for this widget
1457
1458 @param agent_id: the agent ID
1459 @param config: the agent configuration dict
1460 @param version: the config version key
1461 @param context: the current S3DashboardContext
1462 """
1463
1464 # Add widget defaults to configuration
1465 agent_config = dict(self.defaults)
1466 if config:
1467 agent_config.update(config)
1468
1469 # Create or update agent for agent_id
1470 agent = self.agents.get(agent_id)
1471 if agent:
1472 # Update the agent configuration
1473 agent.config = agent_config
1474 agent.version = version
1475 else:
1476 # Create a new agent
1477 agent = S3DashboardAgent(agent_id,
1478 widget=self,
1479 config=agent_config,
1480 version=version,
1481 )
1482 self.agents[agent_id] = agent
1483
1484 # Callback
1485 on_create_agent = self.on_create_agent
1486 if on_create_agent:
1487 on_create_agent(agent, context)
1488
1489 return agent
1490
1491 # -------------------------------------------------------------------------
1493 """
1494 Add the script file to s3.scripts, called when an agent
1495 builds the widget
1496 """
1497
1498 if self.script_loaded:
1499 return
1500
1501 s3 = current.response.s3
1502 scripts = s3.scripts
1503
1504 path = self.get_script_path(debug=s3.debug)
1505 if path:
1506 appname = current.request.application
1507
1508 # Add script to s3.scripts
1509 script = "/%s/static/scripts/%s" % (appname, path)
1510 if script not in scripts:
1511 scripts.append(script)
1512
1513 self.script_loaded = True
1514
1515 # END =========================================================================
1516
| Home | Trees | Indices | Help |
|
|---|
| Generated by Epydoc 3.0.1 on Fri Mar 15 08:51:54 2019 | http://epydoc.sourceforge.net |