├── .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 | ![Templating](readme_assets/templating-min.gif) 15 | 16 | - Authentication from Frappe web framework. 17 | ![Authentication](readme_assets/authentication-min.gif) 18 | 19 | - Dashboard permission using user roles from Frappe. 20 | ![User Permission](readme_assets/user_permission-min.gif) 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 | [%title%] 13 | 14 | 15 | 16 | 17 | {% if dashboard_css %} 18 | 21 | {% endif %} 22 | 23 | 24 | 25 |
26 |
27 | [%app_entry%] 28 |
29 |
30 | 31 | [%config%] 32 | [%scripts%] 33 | 34 | 53 | 54 | 55 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /dash_integration/templates/includes/login/dashboard.js: -------------------------------------------------------------------------------- 1 | // login.js 2 | // don't remove this line (used in test) 3 | 4 | window.disable_signup = {{ disable_signup and "true" or "false" }}; 5 | 6 | window.login = {}; 7 | 8 | window.verify = {}; 9 | 10 | login.bind_events = function() { 11 | $(window).on("hashchange", function() { 12 | login.route(); 13 | }); 14 | 15 | 16 | $(".form-login").on("submit", function(event) { 17 | event.preventDefault(); 18 | var args = {}; 19 | args.cmd = "login"; 20 | args.usr = frappe.utils.xss_sanitise(($("#login_email").val() || "").trim()); 21 | args.pwd = $("#login_password").val(); 22 | args.device = "desktop"; 23 | if(!args.usr || !args.pwd) { 24 | frappe.msgprint('{{ _("Both login and password required") }}'); 25 | return false; 26 | } 27 | login.call(args); 28 | return false; 29 | }); 30 | 31 | $(".form-signup").on("submit", function(event) { 32 | event.preventDefault(); 33 | var args = {}; 34 | args.cmd = "frappe.core.doctype.user.user.sign_up"; 35 | args.email = ($("#signup_email").val() || "").trim(); 36 | args.redirect_to = frappe.utils.get_url_arg("redirect-to") || ''; 37 | args.full_name = ($("#signup_fullname").val() || "").trim(); 38 | if(!args.email || !validate_email(args.email) || !args.full_name) { 39 | login.set_indicator('{{ _("Valid email and name required") }}', 'red'); 40 | return false; 41 | } 42 | login.call(args); 43 | return false; 44 | }); 45 | 46 | $(".form-forgot").on("submit", function(event) { 47 | event.preventDefault(); 48 | var args = {}; 49 | args.cmd = "frappe.core.doctype.user.user.reset_password"; 50 | args.user = ($("#forgot_email").val() || "").trim(); 51 | if(!args.user) { 52 | login.set_indicator('{{ _("Valid Login id required.") }}', 'red'); 53 | return false; 54 | } 55 | login.call(args); 56 | return false; 57 | }); 58 | 59 | $(".toggle-password").click(function() { 60 | $(this).toggleClass("fa-eye fa-eye-slash"); 61 | var input = $($(this).attr("toggle")); 62 | if (input.attr("type") == "password") { 63 | input.attr("type", "text"); 64 | } else { 65 | input.attr("type", "password"); 66 | } 67 | }); 68 | 69 | {% if ldap_settings.enabled %} 70 | $(".btn-ldap-login").on("click", function(){ 71 | var args = {}; 72 | args.cmd = "{{ ldap_settings.method }}"; 73 | args.usr = ($("#login_email").val() || "").trim(); 74 | args.pwd = $("#login_password").val(); 75 | args.device = "desktop"; 76 | if(!args.usr || !args.pwd) { 77 | login.set_indicator('{{ _("Both login and password required") }}', 'red'); 78 | return false; 79 | } 80 | login.call(args); 81 | return false; 82 | }); 83 | {% endif %} 84 | } 85 | 86 | 87 | login.route = function() { 88 | var route = window.location.hash.slice(1); 89 | if(!route) route = "login"; 90 | login[route](); 91 | } 92 | 93 | login.reset_sections = function(hide) { 94 | if(hide || hide===undefined) { 95 | $("section.for-login").toggle(false); 96 | $("section.for-forgot").toggle(false); 97 | $("section.for-signup").toggle(false); 98 | } 99 | $('section .indicator').each(function() { 100 | $(this).removeClass().addClass('indicator').addClass('blue') 101 | .text($(this).attr('data-text')); 102 | }); 103 | } 104 | 105 | login.login = function() { 106 | login.reset_sections(); 107 | $(".for-login").toggle(true); 108 | } 109 | 110 | login.steptwo = function() { 111 | login.reset_sections(); 112 | $(".for-login").toggle(true); 113 | } 114 | 115 | login.forgot = function() { 116 | login.reset_sections(); 117 | $(".for-forgot").toggle(true); 118 | } 119 | 120 | login.signup = function() { 121 | login.reset_sections(); 122 | $(".for-signup").toggle(true); 123 | } 124 | 125 | 126 | // Login 127 | login.call = function(args, callback) { 128 | login.set_indicator('{{ _("Verifying...") }}', 'blue'); 129 | 130 | return frappe.call({ 131 | type: "POST", 132 | args: args, 133 | callback: callback, 134 | freeze: true, 135 | statusCode: login.login_handlers 136 | }); 137 | } 138 | 139 | login.set_indicator = function(message, color) { 140 | $('section:visible .indicator') 141 | .removeClass().addClass('indicator').addClass(color).text(message) 142 | } 143 | 144 | login.login_handlers = (function() { 145 | const default_redirect = `dash/dashboard?dash=Executive summary`; 146 | 147 | var get_error_handler = function(default_message) { 148 | return function(xhr, data) { 149 | if(xhr.responseJSON) { 150 | data = xhr.responseJSON; 151 | } 152 | 153 | var message = default_message; 154 | if (data._server_messages) { 155 | message = ($.map(JSON.parse(data._server_messages || '[]'), function(v) { 156 | // temp fix for messages sent as dict 157 | try { 158 | return JSON.parse(v).message; 159 | } catch (e) { 160 | return v; 161 | } 162 | }) || []).join('
') || default_message; 163 | } 164 | 165 | if(message===default_message) { 166 | login.set_indicator(message, 'red'); 167 | } else { 168 | login.reset_sections(false); 169 | } 170 | 171 | }; 172 | } 173 | 174 | var login_handlers = { 175 | 200: function(data) { 176 | if(data.message == 'Logged In'){ 177 | login.set_indicator('{{ _("Success") }}', 'green'); 178 | const queryString = window.location.search; 179 | const urlParams = new URLSearchParams(queryString); 180 | const dashboard = urlParams.get('dash') 181 | 182 | if (dashboard) { 183 | window.location.href = `dash/dashboard?dash=${dashboard}`; 184 | } else { 185 | window.location.href = default_redirect; 186 | } 187 | 188 | } else if(data.message == 'Password Reset'){ 189 | window.location.href = data.redirect_to; 190 | } else if(data.message=="No App") { 191 | login.set_indicator("{{ _("Success") }}", 'green'); 192 | if(localStorage) { 193 | var last_visited = 194 | localStorage.getItem("last_visited") 195 | || frappe.utils.get_url_arg("redirect-to"); 196 | localStorage.removeItem("last_visited"); 197 | } 198 | 199 | if(data.redirect_to) { 200 | window.location.href = data.redirect_to; 201 | } 202 | 203 | if(last_visited && last_visited != "/login") { 204 | window.location.href = last_visited; 205 | } else { 206 | window.location.href = data.home_page; 207 | } 208 | } else if(window.location.hash === '#forgot') { 209 | if(data.message==='not found') { 210 | login.set_indicator('{{ _("Not a valid user") }}', 'red'); 211 | } else if (data.message=='not allowed') { 212 | login.set_indicator('{{ _("Not Allowed") }}', 'red'); 213 | } else if (data.message=='disabled') { 214 | login.set_indicator('{{ _("Not Allowed: Disabled User") }}', 'red'); 215 | } else { 216 | login.set_indicator('{{ _("Instructions Emailed") }}', 'green'); 217 | } 218 | 219 | 220 | } else if(window.location.hash === '#signup') { 221 | if(cint(data.message[0])==0) { 222 | login.set_indicator(data.message[1], 'red'); 223 | } else { 224 | login.set_indicator('{{ _("Success") }}', 'green'); 225 | frappe.msgprint(data.message[1]) 226 | } 227 | //login.set_indicator(__(data.message), 'green'); 228 | } 229 | 230 | //OTP verification 231 | if(data.verification && data.message != 'Logged In') { 232 | login.set_indicator('{{ _("Success") }}', 'green'); 233 | 234 | document.cookie = "tmp_id="+data.tmp_id; 235 | 236 | if (data.verification.method == 'OTP App'){ 237 | continue_otp_app(data.verification.setup, data.verification.qrcode); 238 | } else if (data.verification.method == 'SMS'){ 239 | continue_sms(data.verification.setup, data.verification.prompt); 240 | } else if (data.verification.method == 'Email'){ 241 | continue_email(data.verification.setup, data.verification.prompt); 242 | } 243 | } 244 | }, 245 | 401: get_error_handler('{{ _("Invalid Login. Try again.") }}'), 246 | 417: get_error_handler('{{ _("Oops! Something went wrong") }}') 247 | }; 248 | 249 | return login_handlers; 250 | } )(); 251 | 252 | frappe.ready(function() { 253 | 254 | login.bind_events(); 255 | 256 | if (!window.location.hash) { 257 | window.location.hash = "#login"; 258 | } else { 259 | $(window).trigger("hashchange"); 260 | } 261 | 262 | $(".form-signup, .form-forgot").removeClass("hide"); 263 | $(document).trigger('login_rendered'); 264 | }); 265 | 266 | var verify_token = function(event) { 267 | $(".form-verify").on("submit", function(eventx) { 268 | eventx.preventDefault(); 269 | var args = {}; 270 | args.cmd = "login"; 271 | args.otp = $("#login_token").val(); 272 | args.tmp_id = frappe.get_cookie('tmp_id'); 273 | if(!args.otp) { 274 | frappe.msgprint('{{ _("Login token required") }}'); 275 | return false; 276 | } 277 | login.call(args); 278 | return false; 279 | }); 280 | } 281 | 282 | var request_otp = function(r){ 283 | $('.login-content').empty().append($('
').attr({'id':'twofactor_div'}).html( 284 | '
\ 285 |
\ 286 | {{ _("Verification") }}\ 287 |
\ 288 |
\ 289 | \ 290 | \ 291 |
')); 292 | // add event handler for submit button 293 | verify_token(); 294 | } 295 | 296 | var continue_otp_app = function(setup, qrcode){ 297 | request_otp(); 298 | var qrcode_div = $('
'); 299 | 300 | if (setup){ 301 | direction = $('
').attr('id','qr_info').text('{{ _("Enter Code displayed in OTP App.") }}'); 302 | qrcode_div.append(direction); 303 | $('#otp_div').prepend(qrcode_div); 304 | } else { 305 | direction = $('
').attr('id','qr_info').text('{{ _("OTP setup using OTP App was not completed. Please contact Administrator.") }}'); 306 | qrcode_div.append(direction); 307 | $('#otp_div').prepend(qrcode_div); 308 | } 309 | } 310 | 311 | var continue_sms = function(setup, prompt){ 312 | request_otp(); 313 | var sms_div = $('
'); 314 | 315 | if (setup){ 316 | sms_div.append(prompt) 317 | $('#otp_div').prepend(sms_div); 318 | } else { 319 | direction = $('
').attr('id','qr_info').text(prompt || '{{ _("SMS was not sent. Please contact Administrator.") }}'); 320 | sms_div.append(direction); 321 | $('#otp_div').prepend(sms_div) 322 | } 323 | } 324 | 325 | var continue_email = function(setup, prompt){ 326 | request_otp(); 327 | var email_div = $('
'); 328 | 329 | if (setup){ 330 | email_div.append(prompt) 331 | $('#otp_div').prepend(email_div); 332 | } else { 333 | var direction = $('
').attr('id','qr_info').text(prompt || '{{ _("Verification code email not sent. Please contact Administrator.") }}'); 334 | email_div.append(direction); 335 | $('#otp_div').prepend(email_div); 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /dash_integration/templates/pages/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pipech/frappe-plotly-dash/9af1a81c81fdc6f79b7bde381415b407b7b5652f/dash_integration/templates/pages/__init__.py -------------------------------------------------------------------------------- /dash_integration/www/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pipech/frappe-plotly-dash/9af1a81c81fdc6f79b7bde381415b407b7b5652f/dash_integration/www/__init__.py -------------------------------------------------------------------------------- /dash_integration/www/dashboard.html: -------------------------------------------------------------------------------- 1 | {% extends "templates/web.html" %} 2 | 3 | {% block style %} 4 | 7 | {% endblock %} 8 | 9 | {% block page_content %} 10 | 11 |
12 | 41 |
42 | {% endblock %} 43 | 44 | {% block script %} 45 | 46 | {% endblock %} 47 | 48 | {% block sidebar %}{% endblock %} 49 | -------------------------------------------------------------------------------- /dash_integration/www/dashboard.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors 2 | # MIT License. See license.txt 3 | 4 | from __future__ import unicode_literals 5 | import frappe 6 | import frappe.utils 7 | from frappe.utils.oauth import get_oauth2_authorize_url, get_oauth_keys, login_via_oauth2, login_via_oauth2_id_token, login_oauth_user as _login_oauth_user, redirect_post_login, oauth_decoder 8 | import json 9 | from frappe import _ 10 | from frappe.auth import LoginManager 11 | from frappe.integrations.doctype.ldap_settings.ldap_settings import LDAPSettings 12 | from frappe.utils.password import get_decrypted_password 13 | from frappe.utils.html_utils import get_icon_html 14 | 15 | no_cache = True 16 | 17 | def get_context(context): 18 | if frappe.session.user != "Guest": 19 | frappe.local.flags.redirect_location = "/dash/dashboard?dash=Executive summary" 20 | raise frappe.Redirect 21 | 22 | # get settings from site config 23 | context.no_header = True 24 | context.for_test = 'login.html' 25 | context["title"] = "Login" 26 | context["provider_logins"] = [] 27 | context["disable_signup"] = frappe.utils.cint(frappe.db.get_value("Website Settings", "Website Settings", "disable_signup")) 28 | providers = [i.name for i in frappe.get_all("Social Login Key", filters={"enable_social_login":1})] 29 | for provider in providers: 30 | client_id, base_url = frappe.get_value("Social Login Key", provider, ["client_id", "base_url"]) 31 | client_secret = get_decrypted_password("Social Login Key", provider, "client_secret") 32 | icon = get_icon_html(frappe.get_value("Social Login Key", provider, "icon"), small=True) 33 | if (get_oauth_keys(provider) and client_secret and client_id and base_url): 34 | context.provider_logins.append({ 35 | "name": provider, 36 | "provider_name": frappe.get_value("Social Login Key", provider, "provider_name"), 37 | "auth_url": get_oauth2_authorize_url(provider), 38 | "icon": icon 39 | }) 40 | context["social_login"] = True 41 | ldap_settings = LDAPSettings.get_ldap_client_settings() 42 | context["ldap_settings"] = ldap_settings 43 | 44 | login_name_placeholder = [_("Email Address")] 45 | 46 | if frappe.utils.cint(frappe.get_system_settings("allow_login_using_mobile_number")): 47 | login_name_placeholder.append(_("Mobile number")) 48 | 49 | if frappe.utils.cint(frappe.get_system_settings("allow_login_using_user_name")): 50 | login_name_placeholder.append(_("Username")) 51 | 52 | context['login_name_placeholder'] = ' {0} '.format(_('or')).join(login_name_placeholder) 53 | 54 | return context 55 | 56 | @frappe.whitelist(allow_guest=True) 57 | def login_via_google(code, state): 58 | login_via_oauth2("google", code, state, decoder=oauth_decoder) 59 | 60 | @frappe.whitelist(allow_guest=True) 61 | def login_via_github(code, state): 62 | login_via_oauth2("github", code, state) 63 | 64 | @frappe.whitelist(allow_guest=True) 65 | def login_via_facebook(code, state): 66 | login_via_oauth2("facebook", code, state, decoder=oauth_decoder) 67 | 68 | @frappe.whitelist(allow_guest=True) 69 | def login_via_frappe(code, state): 70 | login_via_oauth2("frappe", code, state, decoder=oauth_decoder) 71 | 72 | @frappe.whitelist(allow_guest=True) 73 | def login_via_office365(code, state): 74 | login_via_oauth2_id_token("office_365", code, state, decoder=oauth_decoder) 75 | 76 | @frappe.whitelist(allow_guest=True) 77 | def login_oauth_user(data=None, provider=None, state=None, email_id=None, key=None, generate_login_token=False): 78 | if not ((data and provider and state) or (email_id and key)): 79 | frappe.respond_as_web_page(_("Invalid Request"), _("Missing parameters for login"), http_status_code=417) 80 | return 81 | 82 | _login_oauth_user(data, provider, state, email_id, key, generate_login_token) 83 | 84 | @frappe.whitelist(allow_guest=True) 85 | def login_via_token(login_token): 86 | sid = frappe.cache().get_value("login_token:{0}".format(login_token), expires=True) 87 | if not sid: 88 | frappe.respond_as_web_page(_("Invalid Request"), _("Invalid Login Token"), http_status_code=417) 89 | return 90 | 91 | frappe.local.form_dict.sid = sid 92 | frappe.local.login_manager = LoginManager() 93 | 94 | redirect_post_login(desk_user = frappe.db.get_value("User", frappe.session.user, "user_type")=="System User") 95 | -------------------------------------------------------------------------------- /readme_assets/authentication-min.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pipech/frappe-plotly-dash/9af1a81c81fdc6f79b7bde381415b407b7b5652f/readme_assets/authentication-min.gif -------------------------------------------------------------------------------- /readme_assets/templating-min.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pipech/frappe-plotly-dash/9af1a81c81fdc6f79b7bde381415b407b7b5652f/readme_assets/templating-min.gif -------------------------------------------------------------------------------- /readme_assets/user_permission-min.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pipech/frappe-plotly-dash/9af1a81c81fdc6f79b7bde381415b407b7b5652f/readme_assets/user_permission-min.gif -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | frappe 2 | dash==1.9.1 3 | dash-daq==0.5.0 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from setuptools import setup, find_packages 3 | 4 | with open('requirements.txt') as f: 5 | install_requires = f.read().strip().split('\n') 6 | 7 | # get version from __version__ variable in dash_integration/__init__.py 8 | from dash_integration import __version__ as version 9 | 10 | setup( 11 | name='dash_integration', 12 | version=version, 13 | description='Integration for Plotly Dash', 14 | author='SpaceCode Co., Ltd.', 15 | author_email='poranut@spacecode.co.th', 16 | packages=find_packages(), 17 | zip_safe=False, 18 | include_package_data=True, 19 | install_requires=install_requires 20 | ) 21 | --------------------------------------------------------------------------------