├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── dash_integration ├── __init__.py ├── app.py ├── auth.py ├── config │ ├── __init__.py │ ├── desktop.py │ └── docs.py ├── dash_application.py ├── dash_integration │ ├── __init__.py │ ├── doctype │ │ ├── __init__.py │ │ └── dash_dashboard │ │ │ ├── __init__.py │ │ │ ├── dash_dashboard.js │ │ │ ├── dash_dashboard.json │ │ │ ├── dash_dashboard.py │ │ │ └── test_dash_dashboard.py │ └── page │ │ ├── __init__.py │ │ └── dash │ │ ├── __init__.py │ │ ├── dash.js │ │ └── dash.json ├── hooks.py ├── layout.py ├── modules.txt ├── patches.txt ├── public │ ├── css │ │ └── bulma_0-8-0.min.css │ └── js │ │ └── fontawesome_5-3-1.js ├── router.py ├── templates │ ├── __init__.py │ ├── dashboard.html │ ├── includes │ │ └── login │ │ │ └── dashboard.js │ └── pages │ │ └── __init__.py └── www │ ├── __init__.py │ ├── dashboard.html │ └── dashboard.py ├── readme_assets ├── authentication-min.gif ├── templating-min.gif └── user_permission-min.gif ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | *.egg-info 4 | *.swp 5 | tags 6 | dash_integration/docs/current -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 pipech 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include MANIFEST.in 2 | include requirements.txt 3 | include *.json 4 | include *.md 5 | include *.py 6 | include *.txt 7 | recursive-include dash_integration *.css 8 | recursive-include dash_integration *.csv 9 | recursive-include dash_integration *.html 10 | recursive-include dash_integration *.ico 11 | recursive-include dash_integration *.js 12 | recursive-include dash_integration *.json 13 | recursive-include dash_integration *.md 14 | recursive-include dash_integration *.png 15 | recursive-include dash_integration *.py 16 | recursive-include dash_integration *.svg 17 | recursive-include dash_integration *.txt 18 | recursive-exclude dash_integration *.pyc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## [Plotly Dash](https://github.com/plotly/dash) integration for [Frappe web framework](https://github.com/frappe/frappe). 2 | 3 | Plotly Dash is great dashboard tools which allow programmer to easily create interactive dashboard with endless possibilities but it lack of general functionality such as web-authentication and user permission. 4 | 5 | Frappe web framework is also a great web framework with come with alot of functionality. 6 | 7 | Integrate Plotly Dash with Frappe web framework will result in great dashboard tools with web-authentication, user permission, ability to easily performs CRUD operation and also the most important thing ERPNext. 8 | 9 | ### Feature 10 | 11 | - Access Frappe data from dash environment. 12 | 13 | - Dashboard template using CoreUI admin template. 14 |  15 | 16 | - Authentication from Frappe web framework. 17 |  18 | 19 | - Dashboard permission using user roles from Frappe. 20 |  21 | 22 | ### Description 23 | 24 | #### How it works 25 | 26 | It works by passing request from Frappe application to Dash application only if request url path start with `/dash`. 27 | 28 | Then we embedded Dash page with CoreUI template into Frappe page using iFrame. 29 | 30 | ### How to use 31 | 32 | Dash Integration app is design to use along with [Dash Dashboard](https://github.com/pipech/frappe-plotly-dash-dashboard) app. 33 | 34 | #### Installation 35 | 36 | bench get-app dash_integration https://github.com/pipech/frappe-plotly-dash.git 37 | bench get-app dash_dashboard https://github.com/pipech/frappe-plotly-dash-dashboard.git 38 | 39 | #### Adding dashboard 40 | 41 | 1. Adding dashboard layout to `dash_dashboard/dash_dashboard` folder 42 | 1. Set route in `dash_dashboard/router.py` file by adding route into `dashboard_route.route_wrapper` function 43 | 1. Set callback in `dash_dashboard/router.py` file by adding callback into `dashboard_callback.callback_wrapper` function 44 | 1. In Frappe web ui search for `Dash Dashboard` DocType, then add a new doc with matching name to `dashboard_route` 45 | 46 | ### Setup 47 | 48 | #### For production setup 49 | 50 | Change supervisor config 51 | - For [Normal setup](https://frappe.io/docs/user/en/bench/guides/setup-production.html) it's located on `/etc/supervisor/conf.d/frappe-bench.conf` 52 | - For [ERPNext Docker Debian setup](https://github.com/pipech/erpnext-docker-debian) it's locate on [`production_setup/conf/frappe-docker-conf/supervisor.conf`](https://github.com/pipech/erpnext-docker-debian/blob/master/production_setup/conf/frappe-docker-conf/supervisor.conf) 53 | 54 | From 55 | 56 | command=/home/frappe/bench/env/bin/gunicorn -b 0.0.0.0:8000 -w 4 -t 120 frappe.app:application --preload 57 | 58 | To 59 | 60 | command=/home/frappe/bench/env/bin/gunicorn -b 0.0.0.0:8000 -w 4 -t 120 dash_integration.app:application --preload 61 | 62 | #### For development setup 63 | 64 | bench execute dash_integration.app.serve 65 | 66 | ### Limitation 67 | 68 | - We use [ResizeObserver](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) for iFrame resizing, it compatible for most of the browser but not all. Check compatibility at [CanIUse](https://caniuse.com/#feat=resizeobserver). 69 | 70 | ### Attribution 71 | 72 | - [CoreUi Bootstrap Admin Template](https://github.com/coreui/coreui-free-bootstrap-admin-template/) - MIT License 73 | 74 | ### License 75 | 76 | This repository has been released under the MIT License. 77 | -------------------------------------------------------------------------------- /dash_integration/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | __version__ = '0.0.1' 5 | 6 | -------------------------------------------------------------------------------- /dash_integration/app.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from frappe.app import * 4 | 5 | 6 | local_manager = LocalManager([frappe.local]) 7 | 8 | 9 | @Request.application 10 | def application(request): 11 | response = None 12 | 13 | try: 14 | rollback = True 15 | 16 | init_request(request) 17 | if getattr(frappe.local, 'initialised', False): 18 | from dash_integration.dash_application import build_ajax, build_page 19 | 20 | frappe.recorder.record() 21 | 22 | if frappe.local.form_dict.cmd: 23 | response = frappe.handler.handle() 24 | 25 | # dash router 26 | # #################################################### 27 | elif frappe.request.path.startswith("/dash/_dash"): 28 | response = build_ajax(request) 29 | elif frappe.request.path.startswith("/dash/"): 30 | response = build_page(request) 31 | # #################################################### 32 | 33 | elif frappe.request.path.startswith("/api/"): 34 | response = frappe.api.handle() 35 | 36 | elif frappe.request.path.startswith('/backups'): 37 | response = frappe.utils.response.download_backup(request.path) 38 | 39 | elif frappe.request.path.startswith('/private/files/'): 40 | response = frappe.utils.response.download_private_file(request.path) 41 | 42 | elif frappe.local.request.method in ('GET', 'HEAD', 'POST'): 43 | response = frappe.website.render.render() 44 | 45 | else: 46 | raise NotFound 47 | 48 | except HTTPException as e: 49 | return e 50 | 51 | except frappe.SessionStopped as e: 52 | response = frappe.utils.response.handle_session_stopped() 53 | 54 | except frappe.exceptions.CSRFTokenError as e: 55 | # #################################################### 56 | # This is still needed because the first callback (href) 57 | # will still get frappe.exceptions.CSRFTokenError error 58 | if frappe.request.path.startswith("/dash/_dash"): 59 | if getattr(frappe.local, 'initialised', False): 60 | from dash_integration.dash_application import build_ajax 61 | response = build_ajax(request) 62 | # #################################################### 63 | else: 64 | response = handle_exception(e) 65 | 66 | except Exception as e: 67 | response = handle_exception(e) 68 | 69 | else: 70 | rollback = after_request(rollback) 71 | 72 | finally: 73 | if frappe.local.request.method in ("POST", "PUT") and frappe.db and rollback: 74 | frappe.db.rollback() 75 | 76 | # set cookies 77 | if response and hasattr(frappe.local, 'cookie_manager'): 78 | frappe.local.cookie_manager.flush_cookies(response=response) 79 | 80 | frappe.recorder.dump() 81 | 82 | frappe.destroy() 83 | 84 | return response 85 | 86 | 87 | application = local_manager.make_middleware(application) 88 | 89 | 90 | def serve(port=8000, profile=False, no_reload=False, no_threading=False, site=None, sites_path='.'): 91 | global application, _site, _sites_path 92 | _site = site 93 | _sites_path = sites_path 94 | 95 | from werkzeug.serving import run_simple 96 | 97 | if profile: 98 | application = ProfilerMiddleware( 99 | application, 100 | sort_by=('cumtime', 'calls'), 101 | ) 102 | 103 | if not os.environ.get('NO_STATICS'): 104 | application = SharedDataMiddleware( 105 | application, 106 | { 107 | str('/assets'): str(os.path.join(sites_path, 'assets')), 108 | }, 109 | ) 110 | 111 | application = StaticDataMiddleware( 112 | application, 113 | { 114 | str('/files'): str(os.path.abspath(sites_path)) 115 | }, 116 | ) 117 | 118 | application.debug = True 119 | application.config = { 120 | 'SERVER_NAME': 'localhost:8000' 121 | } 122 | 123 | in_test_env = os.environ.get('CI') 124 | if in_test_env: 125 | log = logging.getLogger('werkzeug') 126 | log.setLevel(logging.ERROR) 127 | 128 | run_simple('0.0.0.0', int(port), application, 129 | use_reloader=False if in_test_env else not no_reload, 130 | use_debugger=not in_test_env, 131 | use_evalex=not in_test_env, 132 | threaded=not no_threading) 133 | -------------------------------------------------------------------------------- /dash_integration/auth.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | 3 | 4 | def has_desk_permission(): 5 | user = frappe.session.user 6 | user_type = frappe.db.get_value('User', user, 'user_type') 7 | 8 | desk_permission = not ( 9 | (user == 'Guest') or 10 | (user_type == 'Website User') 11 | ) 12 | 13 | return desk_permission 14 | 15 | 16 | def has_dashboard_permission(dashboard): 17 | user = frappe.session.user 18 | 19 | # get allow role for dashboard 20 | allowed_roles_dict = frappe.get_all( 21 | 'Has Role', 22 | fields=['role'], 23 | filters={ 24 | 'parenttype': 'Dash Dashboard', 25 | 'parent': dashboard, 26 | } 27 | ) 28 | 29 | # convert list of role dict to role list 30 | allow_roles = [] 31 | for roles_dict in allowed_roles_dict: 32 | role = roles_dict.get('role') 33 | if role: 34 | allow_roles.append(role) 35 | 36 | # get user roles 37 | user_roles = frappe.permissions.get_roles(user) 38 | 39 | return frappe.utils.has_common(allow_roles, user_roles) 40 | -------------------------------------------------------------------------------- /dash_integration/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pipech/frappe-plotly-dash/9af1a81c81fdc6f79b7bde381415b407b7b5652f/dash_integration/config/__init__.py -------------------------------------------------------------------------------- /dash_integration/config/desktop.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | from frappe import _ 4 | 5 | def get_data(): 6 | return [ 7 | # { 8 | # 'module_name': 'Dash Integration', 9 | # 'color': 'grey', 10 | # 'icon': 'octicon octicon-graph', 11 | # 'type': 'module', 12 | # 'label': _('Dash Integration') 13 | # }, 14 | { 15 | 'module_name': 'dash', 16 | 'category': 'Places', 17 | 'label': _('Dash'), 18 | 'icon': 'octicon octicon-graph', 19 | 'type': 'link', 20 | 'link': '#dash', 21 | 'color': '#FF4136', 22 | 'standard': 1, 23 | 'idx': 11 24 | }, 25 | ] 26 | -------------------------------------------------------------------------------- /dash_integration/config/docs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Configuration for docs 3 | """ 4 | 5 | # source_link = "https://github.com/[org_name]/dash_integration" 6 | # docs_base_url = "https://[org_name].github.io/dash_integration" 7 | # headline = "App that does everything" 8 | # sub_heading = "Yes, you got that right the first time, everything" 9 | 10 | def get_context(context): 11 | context.brand_html = "Dash Integration" 12 | -------------------------------------------------------------------------------- /dash_integration/dash_application.py: -------------------------------------------------------------------------------- 1 | import dash 2 | import json 3 | 4 | from flask import Flask 5 | from werkzeug.wrappers import Response 6 | 7 | 8 | # load dash application 9 | server = Flask(__name__) 10 | dash_app = dash.Dash( 11 | __name__, 12 | server=server, 13 | url_base_pathname='/dash/' 14 | ) 15 | 16 | # config 17 | dash_app.config.suppress_callback_exceptions = True 18 | 19 | # load data 20 | try: 21 | from dash_dashboard.data import get_data 22 | df = get_data() 23 | except ImportError: 24 | pass 25 | 26 | # registered dash config 27 | with server.app_context(): 28 | # router 29 | from dash_integration.router import callback 30 | callback() 31 | 32 | # layout 33 | from dash_integration.layout import config_layout 34 | config_layout(dash_app) 35 | 36 | 37 | def dash_dispatcher(request): 38 | params = { 39 | 'data': request.data, 40 | 'method': request.method, 41 | 'content_type': request.content_type 42 | } 43 | 44 | # todo: check test_request_context function 45 | with server.test_request_context(request.path, **params): 46 | server.preprocess_request() 47 | try: 48 | response = server.full_dispatch_request() 49 | except Exception as e: 50 | response = server.make_response(server.handle_exception(e)) 51 | return response.get_data() 52 | 53 | 54 | def build_ajax(request): 55 | response = Response() 56 | 57 | data = dash_dispatcher(request) 58 | if data: 59 | if isinstance(data, dict): 60 | data = json.dumps(data) 61 | response.data = data 62 | response.status_code = 200 63 | response.mimetype = 'application/json' 64 | response.charset = 'utf-8' 65 | else: 66 | # incase of dash.exceptions.PreventUpdate or dash.no_update 67 | # data will == '' 68 | # response with 204 69 | response.status_code = 204 70 | 71 | return response 72 | 73 | 74 | def build_page(request): 75 | response = Response() 76 | 77 | data = dash_dispatcher(request) 78 | response.data = data 79 | 80 | response.status_code = 200 81 | response.mimetype = 'text/html' 82 | response.charset = 'utf-8' 83 | 84 | return response 85 | -------------------------------------------------------------------------------- /dash_integration/dash_integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pipech/frappe-plotly-dash/9af1a81c81fdc6f79b7bde381415b407b7b5652f/dash_integration/dash_integration/__init__.py -------------------------------------------------------------------------------- /dash_integration/dash_integration/doctype/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pipech/frappe-plotly-dash/9af1a81c81fdc6f79b7bde381415b407b7b5652f/dash_integration/dash_integration/doctype/__init__.py -------------------------------------------------------------------------------- /dash_integration/dash_integration/doctype/dash_dashboard/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pipech/frappe-plotly-dash/9af1a81c81fdc6f79b7bde381415b407b7b5652f/dash_integration/dash_integration/doctype/dash_dashboard/__init__.py -------------------------------------------------------------------------------- /dash_integration/dash_integration/doctype/dash_dashboard/dash_dashboard.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019, SpaceCode Co., Ltd. and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on('Dash Dashboard', { 5 | // refresh: function(frm) { 6 | 7 | // } 8 | }); 9 | -------------------------------------------------------------------------------- /dash_integration/dash_integration/doctype/dash_dashboard/dash_dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "allow_rename": 1, 3 | "autoname": "field:dashboard_name", 4 | "creation": "2019-11-04 16:48:42.605542", 5 | "doctype": "DocType", 6 | "editable_grid": 1, 7 | "engine": "InnoDB", 8 | "field_order": [ 9 | "dashboard_name", 10 | "is_active", 11 | "section_break_4", 12 | "roles" 13 | ], 14 | "fields": [ 15 | { 16 | "fieldname": "dashboard_name", 17 | "fieldtype": "Data", 18 | "in_list_view": 1, 19 | "label": "Name", 20 | "reqd": 1, 21 | "unique": 1 22 | }, 23 | { 24 | "default": "1", 25 | "depends_on": "eval: !doc.__islocal;", 26 | "fieldname": "is_active", 27 | "fieldtype": "Check", 28 | "label": "Is active" 29 | }, 30 | { 31 | "fieldname": "section_break_4", 32 | "fieldtype": "Section Break" 33 | }, 34 | { 35 | "description": "Dashboard can be view by user with these roles", 36 | "fieldname": "roles", 37 | "fieldtype": "Table", 38 | "label": "Roles", 39 | "options": "Has Role" 40 | } 41 | ], 42 | "modified": "2019-11-04 21:33:11.761171", 43 | "modified_by": "Administrator", 44 | "module": "Dash Integration", 45 | "name": "Dash Dashboard", 46 | "owner": "Administrator", 47 | "permissions": [ 48 | { 49 | "create": 1, 50 | "delete": 1, 51 | "email": 1, 52 | "export": 1, 53 | "print": 1, 54 | "read": 1, 55 | "report": 1, 56 | "role": "System Manager", 57 | "share": 1, 58 | "write": 1 59 | } 60 | ], 61 | "sort_field": "modified", 62 | "sort_order": "DESC", 63 | "track_changes": 1 64 | } -------------------------------------------------------------------------------- /dash_integration/dash_integration/doctype/dash_dashboard/dash_dashboard.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2019, SpaceCode Co., Ltd. and contributors 3 | # For license information, please see license.txt 4 | 5 | from __future__ import unicode_literals 6 | # import frappe 7 | from frappe.model.document import Document 8 | 9 | class DashDashboard(Document): 10 | pass 11 | -------------------------------------------------------------------------------- /dash_integration/dash_integration/doctype/dash_dashboard/test_dash_dashboard.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2019, SpaceCode Co., Ltd. and Contributors 3 | # See license.txt 4 | from __future__ import unicode_literals 5 | 6 | # import frappe 7 | import unittest 8 | 9 | class TestDashDashboard(unittest.TestCase): 10 | pass 11 | -------------------------------------------------------------------------------- /dash_integration/dash_integration/page/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pipech/frappe-plotly-dash/9af1a81c81fdc6f79b7bde381415b407b7b5652f/dash_integration/dash_integration/page/__init__.py -------------------------------------------------------------------------------- /dash_integration/dash_integration/page/dash/__init__.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | 3 | 4 | def check_permitted(dashboard_name, username): 5 | """Returns true if Has Role is not set or the user is allowed.""" 6 | allowed = False 7 | 8 | # get allow role for dashboard 9 | allowed_roles_dict = frappe.get_all( 10 | 'Has Role', 11 | fields=['role'], 12 | filters={ 13 | 'parenttype': 'Dash Dashboard', 14 | 'parent': dashboard_name, 15 | } 16 | ) 17 | 18 | # convert list of role dict to role list 19 | allow_roles = [] 20 | for roles_dict in allowed_roles_dict: 21 | role = roles_dict.get('role') 22 | if role: 23 | allow_roles.append(role) 24 | 25 | # get user roles 26 | user_roles = frappe.permissions.get_roles(username) 27 | 28 | # check 29 | if frappe.utils.has_common(allow_roles, user_roles): 30 | allowed = True 31 | 32 | return allowed 33 | -------------------------------------------------------------------------------- /dash_integration/dash_integration/page/dash/dash.js: -------------------------------------------------------------------------------- 1 | /* eslint require-jsdoc: 0 */ 2 | 3 | frappe.pages['dash'].on_page_load = (wrapper) => { 4 | // init page 5 | const page = frappe.ui.make_app_page({ 6 | 'parent': wrapper, 7 | 'title': 'Dashboard', 8 | 'single_column': true, 9 | }); 10 | 11 | new Dash(page, wrapper); 12 | }; 13 | 14 | 15 | class Dash { 16 | constructor(page, wrapper) { 17 | this.wrapper = wrapper; 18 | this.page = page; 19 | this.siteOrigin = window.location.origin; 20 | this.pageMain = $(page.main); 21 | this.pageAction = ( 22 | $(this.wrapper) 23 | .find('div.page-head div.page-actions') 24 | ); 25 | this.pageTitle = $(this.wrapper).find('div.title-text'); 26 | this.iframeHtml = ` 27 | 34 | `; 35 | this.init(); 36 | } 37 | 38 | init() { 39 | // attatch iframe 40 | this.$dashIframe = $(this.iframeHtml).appendTo(this.pageMain); 41 | // attatch iframe resizer 42 | this.resizer(); 43 | // attatch dashboard selector 44 | this.createSelectionField(); 45 | } 46 | 47 | resizer() { 48 | window.addEventListener('message', resize, false); 49 | function resize(event) { 50 | const dashIframe = document.getElementById('dash-iframe'); 51 | const frameHeight = event.data.frameHeight; 52 | dashIframe.style.height = `${frameHeight + 30}px`; 53 | } 54 | } 55 | 56 | createSelectionField() { 57 | // create dashboard selection field 58 | this.selectionField = frappe.ui.form.make_control({ 59 | 'parent': this.pageAction, 60 | 'df': { 61 | 'fieldname': 'Dashboard', 62 | 'fieldtype': 'Link', 63 | 'options': 'Dash Dashboard', 64 | 'onchange': () => { 65 | const dashboardName = this.selectionField.get_value(); 66 | if (dashboardName) { 67 | this.dashboardName = dashboardName; 68 | this.changeIframeUrl(); 69 | this.changeTitle(dashboardName); 70 | // clear input 71 | this.selectionField.set_input(''); 72 | } 73 | }, 74 | 'get_query': () => { 75 | return { 76 | 'filters': { 77 | 'is_active': 1, 78 | }, 79 | }; 80 | }, 81 | 'placeholder': 'Select Dashboard', 82 | }, 83 | 'render_input': true, 84 | }); 85 | 86 | // change css 87 | this.pageAction.removeClass('page-actions'); 88 | this.selectionField.$wrapper.css('text-align', 'left'); 89 | } 90 | 91 | changeIframeUrl() { 92 | this.iframeUrl = `${this.siteOrigin}/dash/dashboard?dash=${this.dashboardName}`; 93 | this.$dashIframe.attr('src', this.iframeUrl); 94 | } 95 | 96 | changeTitle() { 97 | this.pageTitle.text(`${this.dashboardName} Dashboard`); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /dash_integration/dash_integration/page/dash/dash.json: -------------------------------------------------------------------------------- 1 | { 2 | "content": null, 3 | "creation": "2019-10-30 12:54:29.234544", 4 | "docstatus": 0, 5 | "doctype": "Page", 6 | "idx": 0, 7 | "modified": "2019-11-01 09:51:52.138175", 8 | "modified_by": "Administrator", 9 | "module": "Dash Integration", 10 | "name": "dash", 11 | "owner": "Administrator", 12 | "page_name": "Dash", 13 | "roles": [ 14 | { 15 | "role": "System Manager" 16 | } 17 | ], 18 | "script": null, 19 | "standard": "Yes", 20 | "style": null, 21 | "system_page": 1, 22 | "title": "Dash" 23 | } -------------------------------------------------------------------------------- /dash_integration/hooks.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | from . import __version__ as app_version 4 | 5 | app_name = "dash_integration" 6 | app_title = "Dash Integration" 7 | app_publisher = "SpaceCode Co., Ltd." 8 | app_description = "Integration for Plotly Dash" 9 | app_icon = "octicon octicon-graph" 10 | app_color = "grey" 11 | app_email = "poranut@spacecode.co.th" 12 | app_license = "MIT License" 13 | -------------------------------------------------------------------------------- /dash_integration/layout.py: -------------------------------------------------------------------------------- 1 | import dash_core_components as dcc 2 | import dash_html_components as html 3 | 4 | from frappe import render_template 5 | from jinja2.exceptions import TemplateNotFound 6 | 7 | 8 | def config_layout(dash_app): 9 | # dash layout 10 | dash_app.layout = html.Div([ 11 | dcc.Location(id='url', refresh=False), 12 | dcc.Input( 13 | id='csrf_token', 14 | style={'display': 'none'}, 15 | ), 16 | html.Div(id='page-content'), 17 | ]) 18 | 19 | # get dashboard css from template 20 | context = {'dashboard_css': ''} 21 | try: 22 | context['dashboard_css'] = render_template( 23 | 'templates/dashboard_config.css', 24 | context={} 25 | ) 26 | except TemplateNotFound: 27 | pass 28 | 29 | # Override the underlying HTML template 30 | html_layout = dash_render_template( 31 | template_name='templates/dashboard.html', 32 | context=context, 33 | ) 34 | dash_app.index_string = html_layout 35 | 36 | 37 | def dash_render_template(template_name, context={}): 38 | # default render template by frappe 39 | template = render_template(template_name, context=context) 40 | 41 | # replacing custom context 42 | template = template.replace('[%', '{%') 43 | template = template.replace('%]', '%}') 44 | 45 | return template 46 | -------------------------------------------------------------------------------- /dash_integration/modules.txt: -------------------------------------------------------------------------------- 1 | Dash Integration -------------------------------------------------------------------------------- /dash_integration/patches.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pipech/frappe-plotly-dash/9af1a81c81fdc6f79b7bde381415b407b7b5652f/dash_integration/patches.txt -------------------------------------------------------------------------------- /dash_integration/router.py: -------------------------------------------------------------------------------- 1 | import urllib 2 | import dash_core_components as dcc 3 | import dash_html_components as html 4 | import urllib.parse as urlparse 5 | import frappe 6 | 7 | from dash_integration.dash_application import dash_app 8 | from dash_integration.auth import has_desk_permission 9 | from dash_integration.auth import has_dashboard_permission 10 | from dash_dashboard.router import dashboard_route 11 | from dash_dashboard.router import dashboard_callback 12 | from dash.dependencies import Input, Output 13 | 14 | 15 | @dashboard_callback 16 | def callback(): 17 | @dash_app.callback( 18 | [ 19 | Output('page-content', 'children'), 20 | Output('csrf_token', 'value'), 21 | ], 22 | [ 23 | Input('url', 'href'), 24 | ], 25 | ) 26 | def display_page(href): 27 | if href: 28 | # extract information from url 29 | parsed_uri = urllib.parse.urlparse(href) 30 | url_param = urllib.parse.parse_qs(parsed_uri.query) 31 | dashboard = url_param.get('dash', '')[0] 32 | 33 | csrf_token = frappe.local.session.data.csrf_token 34 | 35 | if has_desk_permission(): 36 | if has_dashboard_permission(dashboard): 37 | return [ 38 | dash_route(dashboard), 39 | csrf_token, 40 | ] 41 | else: 42 | return [ 43 | 'You are not permitted to access this dashboard', 44 | csrf_token, 45 | ] 46 | else: 47 | return [ 48 | get_not_permitted_layout(href), 49 | csrf_token, 50 | ] 51 | 52 | 53 | @dashboard_route 54 | def dash_route(dashboard): 55 | return '404' 56 | 57 | 58 | def get_not_permitted_layout(href): 59 | parsed = urlparse.urlparse(href) 60 | layout = html.Div( 61 | [ 62 | html.Div( 63 | className='modal-background', 64 | ), 65 | html.Div( 66 | [ 67 | html.Header( 68 | [ 69 | html.P( 70 | 'Not Permitted', 71 | className='modal-card-title', 72 | ), 73 | html.Img( 74 | src="/assets/dash_dashboard/images/eucerin_logo.svg", 75 | style={'height': '24px'}, 76 | ), 77 | ], 78 | className='modal-card-head', 79 | ), 80 | html.Section( 81 | [ 82 | html.P( 83 | 'You are not permitted to access this page', 84 | className='modal-card-title', 85 | ), 86 | ], 87 | className='modal-card-body', 88 | style={ 89 | 'border-bottom-right-radius': 0, 90 | 'border-bottom-left-radius': 0, 91 | }, 92 | ), 93 | html.Section( 94 | html.A( 95 | 'Login', 96 | href='/dashboard?{}'.format(parsed.query), 97 | className='button is-small is-info', 98 | style={ 99 | 'background-color': '#83868c', 100 | }, 101 | ), 102 | className='modal-card-foot', 103 | style={ 104 | 'padding': '12px 16px', 105 | }, 106 | ), 107 | ], 108 | className='modal-card', 109 | ) 110 | ], 111 | className='modal db-modal is-active', 112 | ) 113 | return layout 114 | -------------------------------------------------------------------------------- /dash_integration/templates/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pipech/frappe-plotly-dash/9af1a81c81fdc6f79b7bde381415b407b7b5652f/dash_integration/templates/__init__.py -------------------------------------------------------------------------------- /dash_integration/templates/dashboard.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 | 9 | [%metas%] 10 | [%favicon%] 11 | [%css%] 12 |