diff --git a/.eslintrc b/.eslintrc index 44af7b458f..ade1623262 100644 --- a/.eslintrc +++ b/.eslintrc @@ -118,6 +118,8 @@ "getCookie": true, "getCookies": true, "get_url_arg": true, - "QUnit": true + "QUnit": true, + "Snap": true, + "mina": true } } diff --git a/.travis.yml b/.travis.yml index 2a60d20c6f..ab46e06c3d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,8 +15,7 @@ services: - mysql install: - - pip install flake8 # pytest - # stop the build if there are Python syntax errors or undefined names + - pip install flake8==3.3.0 - flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics - sudo rm /etc/apt/sources.list.d/docker.list - sudo apt-get purge -y mysql-common mysql-server mysql-client diff --git a/frappe/__init__.py b/frappe/__init__.py index 28fca585a7..cd5c0cd7f4 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -14,7 +14,7 @@ import os, sys, importlib, inspect, json from .exceptions import * from .utils.jinja import get_jenv, get_template, render_template, get_email_from_template -__version__ = '8.6.2' +__version__ = '8.6.3' __title__ = "Frappe Framework" local = Local() diff --git a/frappe/boot.py b/frappe/boot.py index d413d03fb6..1cc8202123 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -17,6 +17,7 @@ from frappe.utils.change_log import get_versions from frappe.translate import get_lang_dict from frappe.email.inbox import get_email_accounts from frappe.core.doctype.feedback_trigger.feedback_trigger import get_enabled_feedback_trigger +from frappe.core.doctype.user_permission.user_permission import get_user_permissions def get_bootinfo(): """build and return boot info""" @@ -30,6 +31,7 @@ def get_bootinfo(): # system info bootinfo.sysdefaults = frappe.defaults.get_defaults() + bootinfo.user_permissions = get_user_permissions() bootinfo.server_date = frappe.utils.nowdate() if frappe.session['user'] != 'Guest': diff --git a/frappe/change_log/v6/v6_16_4.md b/frappe/change_log/v6/v6_16_4.md index fc901c3248..63ae316f22 100644 --- a/frappe/change_log/v6/v6_16_4.md +++ b/frappe/change_log/v6/v6_16_4.md @@ -1,2 +1 @@ -- Developer Tutorial [Videos](http://frappe.github.io/frappe/user/videos/) - Increased uploaded file size limit upto 10MB \ No newline at end of file diff --git a/frappe/commands/docs.py b/frappe/commands/docs.py index 38d4a22bea..9556c21270 100644 --- a/frappe/commands/docs.py +++ b/frappe/commands/docs.py @@ -1,31 +1,9 @@ from __future__ import unicode_literals, absolute_import import click -import os +import os, shutil import frappe from frappe.commands import pass_context - -@click.command('write-docs') -@pass_context -@click.argument('app') -@click.option('--target', default=None) -@click.option('--local', default=False, is_flag=True, help='Run app locally') -def write_docs(context, app, target=None, local=False): - "Setup docs in target folder of target app" - from frappe.utils.setup_docs import setup_docs - - if not target: - target = os.path.abspath(os.path.join("..", "docs", app)) - - for site in context.sites: - try: - frappe.init(site=site) - frappe.connect() - make = setup_docs(app) - make.make_docs(target, local) - finally: - frappe.destroy() - @click.command('build-docs') @pass_context @click.argument('app') @@ -36,23 +14,26 @@ def write_docs(context, app, target=None, local=False): def build_docs(context, app, docs_version="current", target=None, local=False, watch=False): "Setup docs in target folder of target app" from frappe.utils import watch as start_watch - if not target: - target = os.path.abspath(os.path.join("..", "docs", app)) + from frappe.utils.setup_docs import add_breadcrumbs_tag for site in context.sites: _build_docs_once(site, app, docs_version, target, local) if watch: def trigger_make(source_path, event_type): - if "/templates/autodoc/" in source_path: - _build_docs_once(site, app, docs_version, target, local) + if "/docs/user/" in source_path: + # user file + target_path = frappe.get_app_path(target, 'www', 'docs', 'user', + os.path.relpath(source_path, start=frappe.get_app_path(app, 'docs', 'user'))) + shutil.copy(source_path, target_path) + add_breadcrumbs_tag(target_path) elif ("/docs.css" in source_path or "/docs/" in source_path or "docs.py" in source_path): _build_docs_once(site, app, docs_version, target, local, only_content_updated=True) - apps_path = frappe.get_app_path(app, "..", "..") + apps_path = frappe.get_app_path(app) start_watch(apps_path, handler=trigger_make) def _build_docs_once(site, app, docs_version, target, local, only_content_updated=False): @@ -62,17 +43,16 @@ def _build_docs_once(site, app, docs_version, target, local, only_content_update frappe.init(site=site) frappe.connect() - make = setup_docs(app) + make = setup_docs(app, target) if not only_content_updated: make.build(docs_version) - make.make_docs(target, local) + #make.make_docs(target, local) finally: frappe.destroy() commands = [ build_docs, - write_docs, ] diff --git a/frappe/config/docs.py b/frappe/config/docs.py index 029149d7c0..11c03cccac 100644 --- a/frappe/config/docs.py +++ b/frappe/config/docs.py @@ -2,12 +2,10 @@ from __future__ import unicode_literals -docs_version = "7.x.x" - source_link = "https://github.com/frappe/frappe" -docs_base_url = "https://frappe.github.io/frappe" -headline = "Superhero Web Framework" -sub_heading = "Build extensions to ERPNext or make your own app" +docs_base_url = "/docs" +headline = "Frappé Framework" +sub_heading = "Tutorials, API documentation and Model Reference" hide_install = True long_description = """Frappe is a full stack web application framework written in Python, Javascript, HTML/CSS with MySQL as the backend. It was built for ERPNext @@ -25,7 +23,7 @@ to ERPNext. Frappe Framework was designed to build [ERPNext](https://erpnext.com), open source ERP for managing small and medium sized businesses. -[Get started with the Tutorial](https://frappe.github.io/frappe/user/) +[Get started with the Tutorial](/docs/user/) """ google_analytics_id = 'UA-8911157-23' diff --git a/frappe/core/doctype/communication/feed.py b/frappe/core/doctype/communication/feed.py index ded84a469c..2d939447cd 100644 --- a/frappe/core/doctype/communication/feed.py +++ b/frappe/core/doctype/communication/feed.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals import frappe -import frappe.defaults import frappe.permissions from frappe.model.document import Document from frappe.utils import get_fullname @@ -68,7 +67,7 @@ def get_feed_match_conditions(user=None, force=True): conditions = ['`tabCommunication`.owner="{user}" or `tabCommunication`.reference_owner="{user}"'.format(user=frappe.db.escape(user))] - user_permissions = frappe.defaults.get_user_permissions(user) + user_permissions = frappe.permissions.get_user_permissions(user) can_read = frappe.get_user().get_can_read() can_read_doctypes = ['"{}"'.format(doctype) for doctype in diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 338974cc47..3f1ad1de3d 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -527,7 +527,7 @@ "bold": 0, "collapsible": 1, "columns": 0, - "fieldname": "security", + "fieldname": "permissions", "fieldtype": "Section Break", "hidden": 0, "ignore_user_permissions": 0, @@ -536,10 +536,11 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Security", + "label": "Permissions", "length": 0, "no_copy": 0, "permlevel": 0, + "precision": "", "print_hide": 0, "print_hide_if_no_value": 0, "read_only": 0, @@ -556,10 +557,9 @@ "bold": 0, "collapsible": 0, "columns": 0, - "default": "06:00", - "description": "Session Expiry in Hours e.g. 06:00", - "fieldname": "session_expiry", - "fieldtype": "Data", + "description": "If Apply User Permissions is checked for Report DocType but no User Permissions are defined for Report for a User, then all Reports are shown to that User", + "fieldname": "ignore_user_permissions_if_missing", + "fieldtype": "Check", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -567,11 +567,11 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Session Expiry", + "label": "Ignore User Permissions If Missing", "length": 0, "no_copy": 0, - "options": "", "permlevel": 0, + "precision": "", "print_hide": 0, "print_hide_if_no_value": 0, "read_only": 0, @@ -588,10 +588,10 @@ "bold": 0, "collapsible": 0, "columns": 0, - "default": "720:00", - "description": "In Hours", - "fieldname": "session_expiry_mobile", - "fieldtype": "Data", + "default": "0", + "description": "If Apply Strict User Permission is checked and User Permission is defined for a DocType for a User, then all the documents where value of the link is blank, will not be shown to that User", + "fieldname": "apply_strict_user_permissions", + "fieldtype": "Check", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -599,7 +599,7 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Session Expiry Mobile", + "label": "Apply Strict User Permissions", "length": 0, "no_copy": 0, "permlevel": 0, @@ -614,16 +614,45 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "columns": 0, + "fieldname": "security", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Security", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, - "default": "0", - "description": "If enabled, the password strength will be enforced based on the Minimum Password Score value. A value of 2 being medium strong and 4 being very strong.", - "fieldname": "enable_password_policy", - "fieldtype": "Check", + "default": "06:00", + "description": "Session Expiry in Hours e.g. 06:00", + "fieldname": "session_expiry", + "fieldtype": "Data", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -631,11 +660,11 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Enable Password Policy", + "label": "Session Expiry", "length": 0, "no_copy": 0, + "options": "", "permlevel": 0, - "precision": "", "print_hide": 0, "print_hide_if_no_value": 0, "read_only": 0, @@ -652,10 +681,10 @@ "bold": 0, "collapsible": 0, "columns": 0, - "default": "2", - "depends_on": "eval:doc.enable_password_policy==1", - "fieldname": "minimum_password_score", - "fieldtype": "Select", + "default": "720:00", + "description": "In Hours", + "fieldname": "session_expiry_mobile", + "fieldtype": "Data", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -663,10 +692,9 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Minimum Password Score", + "label": "Session Expiry Mobile", "length": 0, "no_copy": 0, - "options": "2\n4", "permlevel": 0, "precision": "", "print_hide": 0, @@ -823,6 +851,7 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, + "label": "Enable Password Policy", "length": 0, "no_copy": 0, "permlevel": 0, @@ -843,9 +872,10 @@ "bold": 0, "collapsible": 0, "columns": 0, - "description": "Note: Multiple sessions will be allowed in case of mobile device", - "fieldname": "deny_multiple_sessions", - "fieldtype": "Check", + "default": "2", + "depends_on": "eval:doc.enable_password_policy==1", + "fieldname": "minimum_password_score", + "fieldtype": "Select", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -853,9 +883,10 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Allow only one session per user", + "label": "Minimum Password Score", "length": 0, "no_copy": 0, + "options": "2\n4", "permlevel": 0, "precision": "", "print_hide": 0, @@ -874,9 +905,8 @@ "bold": 0, "collapsible": 0, "columns": 0, - "description": "If Apply User Permissions is checked for Report DocType but no User Permissions are defined for Report for a User, then all Reports are shown to that User", - "fieldname": "ignore_user_permissions_if_missing", - "fieldtype": "Check", + "fieldname": "column_break_13", + "fieldtype": "Column Break", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -884,7 +914,6 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Ignore User Permissions If Missing", "length": 0, "no_copy": 0, "permlevel": 0, @@ -905,9 +934,8 @@ "bold": 0, "collapsible": 0, "columns": 0, - "default": "0", - "description": "If Apply Strict User Permission is checked and User Permission is defined for a DocType for a User, then all the documents where value of the link is blank, will not be shown to that User", - "fieldname": "apply_strict_user_permissions", + "description": "Note: Multiple sessions will be allowed in case of mobile device", + "fieldname": "deny_multiple_sessions", "fieldtype": "Check", "hidden": 0, "ignore_user_permissions": 0, @@ -916,7 +944,7 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Apply Strict User Permissions", + "label": "Allow only one session per user", "length": 0, "no_copy": 0, "permlevel": 0, @@ -1157,8 +1185,8 @@ "issingle": 1, "istable": 0, "max_attachments": 0, - "modified": "2017-07-29 13:33:49.201189", - "modified_by": "chude.osiegbu@manqala.com", + "modified": "2017-07-20 22:57:56.466867", + "modified_by": "Administrator", "module": "Core", "name": "System Settings", "name_case": "", diff --git a/frappe/core/doctype/system_settings/system_settings.py b/frappe/core/doctype/system_settings/system_settings.py index 8f64feda9f..f7ecfc00bb 100644 --- a/frappe/core/doctype/system_settings/system_settings.py +++ b/frappe/core/doctype/system_settings/system_settings.py @@ -35,6 +35,7 @@ class SystemSettings(Document): frappe.cache().delete_value('system_settings') frappe.cache().delete_value('time_zone') + frappe.local.system_settings = {} @frappe.whitelist() def load(): diff --git a/frappe/core/doctype/test_runner/test_runner.js b/frappe/core/doctype/test_runner/test_runner.js index d5cac7f8a5..87ea09fab7 100644 --- a/frappe/core/doctype/test_runner/test_runner.js +++ b/frappe/core/doctype/test_runner/test_runner.js @@ -11,7 +11,8 @@ frappe.ui.form.on('Test Runner', { // all tests frappe.call({ - method: 'frappe.core.doctype.test_runner.test_runner.get_test_js' + method: 'frappe.core.doctype.test_runner.test_runner.get_test_js', + args: { test_path: frm.doc.module_path } }).always((data) => { $("
").appendTo(wrapper.empty()); frm.events.run_tests(frm, data.message); diff --git a/frappe/core/doctype/test_runner/test_runner.py b/frappe/core/doctype/test_runner/test_runner.py index c09d75ae4f..2961e9f38b 100644 --- a/frappe/core/doctype/test_runner/test_runner.py +++ b/frappe/core/doctype/test_runner/test_runner.py @@ -10,9 +10,10 @@ class TestRunner(Document): pass @frappe.whitelist() -def get_test_js(): +def get_test_js(test_path=None): '''Get test + data for app, example: app/tests/ui/test_name.js''' - test_path = frappe.db.get_single_value('Test Runner', 'module_path') + if not test_path: + test_path = frappe.db.get_single_value('Test Runner', 'module_path') test_js = [] # split diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js index d809d3a059..601c7d27ce 100644 --- a/frappe/core/doctype/user/user.js +++ b/frappe/core/doctype/user/user.js @@ -59,9 +59,13 @@ frappe.ui.form.on('User', { frappe.route_options = { "user": doc.name }; - frappe.set_route("user-permissions"); + frappe.set_route('List', 'User Permission'); }, null, "btn-default") + frm.add_custom_button(__('View Permitted Documents'), + () => frappe.set_route('query-report', 'Permitted Documents For User', + {user: frm.doc.name})); + frm.toggle_display(['sb1', 'sb3', 'modules_access'], true); } diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 13c3ac7f0f..8d72f8e0c1 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -871,7 +871,7 @@ def notify_admin_access_to_system_manager(login_manager=None): subject=_("Administrator Logged In"), template="administrator_logged_in", args={'access_message': access_message}, - header=[subject, 'orange'] + header=['Access Notification', 'orange'] ) def extract_mentions(txt): diff --git a/frappe/core/doctype/user_permission/__init__.py b/frappe/core/doctype/user_permission/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/user_permission/test_user_permission.js b/frappe/core/doctype/user_permission/test_user_permission.js new file mode 100644 index 0000000000..1770dddf81 --- /dev/null +++ b/frappe/core/doctype/user_permission/test_user_permission.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: User Permission", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially('User Permission', [ + // insert a new User Permission + () => frappe.tests.make([ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/frappe/core/doctype/user_permission/test_user_permission.py b/frappe/core/doctype/user_permission/test_user_permission.py new file mode 100644 index 0000000000..157fa44ae2 --- /dev/null +++ b/frappe/core/doctype/user_permission/test_user_permission.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +#import frappe +import unittest + +class TestUserPermission(unittest.TestCase): + pass diff --git a/frappe/core/doctype/user_permission/user_permission.js b/frappe/core/doctype/user_permission/user_permission.js new file mode 100644 index 0000000000..fbd6ed5616 --- /dev/null +++ b/frappe/core/doctype/user_permission/user_permission.js @@ -0,0 +1,10 @@ +// Copyright (c) 2017, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('User Permission', { + refresh: function(frm) { + frm.add_custom_button(__('View Permitted Documents'), + () => frappe.set_route('query-report', 'Permitted Documents For User', + {user: frm.doc.user})); + } +}); diff --git a/frappe/core/doctype/user_permission/user_permission.json b/frappe/core/doctype/user_permission/user_permission.json new file mode 100644 index 0000000000..2e67de5ce0 --- /dev/null +++ b/frappe/core/doctype/user_permission/user_permission.json @@ -0,0 +1,188 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2017-07-17 14:25:27.881871", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "user", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 1, + "label": "User", + "length": 0, + "no_copy": 0, + "options": "User", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "allow", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Allow", + "length": 0, + "no_copy": 0, + "options": "DocType", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "for_value", + "fieldtype": "Dynamic Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 1, + "label": "For Value", + "length": 0, + "no_copy": 0, + "options": "allow", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 1, + "collapsible": 0, + "columns": 0, + "default": "1", + "description": "If you un-check this, you will have to apply manually for each Role + Document Type combination", + "fieldname": "apply_for_all_roles", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Apply for all Roles for this User", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2017-07-27 22:55:58.647315", + "modified_by": "Administrator", + "module": "Core", + "name": "User Permission", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "user", + "track_changes": 1, + "track_seen": 0 +} \ No newline at end of file diff --git a/frappe/core/doctype/user_permission/user_permission.py b/frappe/core/doctype/user_permission/user_permission.py new file mode 100644 index 0000000000..e14a900e19 --- /dev/null +++ b/frappe/core/doctype/user_permission/user_permission.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe, json +from frappe.model.document import Document +from frappe.permissions import (get_valid_perms, update_permission_property) +from frappe import _ + +class UserPermission(Document): + def on_update(self): + frappe.cache().delete_value('user_permissions') + + if self.apply_for_all_roles: + self.apply_user_permissions_to_all_roles() + + def apply_user_permissions_to_all_roles(self): + # add apply user permissions for all roles that + # for this doctype + def show_progress(i, l): + if l > 2: + frappe.publish_realtime("progress", + dict(progress=[i, l], title=_('Updating...')), + user=frappe.session.user) + + + roles = frappe.get_roles(self.user) + linked = frappe.db.sql('''select distinct parent from tabDocField + where fieldtype="Link" and options=%s''', self.allow) + for i, link in enumerate(linked): + doctype = link[0] + for perm in get_valid_perms(doctype, self.user): + # if the role is applicable to the user + show_progress(i+1, len(linked)) + if perm.role in roles: + if not perm.apply_user_permissions: + update_permission_property(doctype, perm.role, 0, + 'apply_user_permissions', '1') + + try: + user_permission_doctypes = json.loads(perm.user_permission_doctypes or '[]') + except ValueError: + user_permission_doctypes = [] + + if self.allow not in user_permission_doctypes: + user_permission_doctypes.append(self.allow) + update_permission_property(doctype, perm.role, 0, + 'user_permission_doctypes', json.dumps(user_permission_doctypes), validate=False) + + show_progress(len(linked), len(linked)) + + def on_trash(self): # pylint: disable=no-self-use + frappe.cache().delete_value('user_permissions') + +def get_user_permissions(user=None): + '''Get all users permissions for the user as a dict of doctype''' + if not user: + user = frappe.session.user + + out = frappe.cache().hget("user_permissions", user) + + if not out: + out = {} + try: + for perm in frappe.get_all('User Permission', + fields=['allow', 'for_value'], filters=dict(user=user)): + out.setdefault(perm.allow, []).append(perm.for_value) + + # add profile match + if user not in out.get("User", []): + out.setdefault("User", []).append(user) + + frappe.cache().hset("user_permissions", user, out) + except frappe.SQLError, e: + if e.args[0]==1146: + # called from patch + pass + + return out \ No newline at end of file diff --git a/frappe/core/page/permission_manager/permission_manager.js b/frappe/core/page/permission_manager/permission_manager.js index 400043a5fd..020db1d16b 100644 --- a/frappe/core/page/permission_manager/permission_manager.js +++ b/frappe/core/page/permission_manager/permission_manager.js @@ -21,6 +21,7 @@ frappe.pages['permission-manager'].refresh = function(wrapper) { frappe.PermissionEngine = Class.extend({ init: function(wrapper) { this.wrapper = wrapper; + this.page = wrapper.page; this.body = $(this.wrapper).find(".perm-engine"); this.make(); this.refresh(); @@ -55,6 +56,10 @@ frappe.PermissionEngine = Class.extend({ .change(function() { me.refresh(); }); + + this.page.add_inner_button(__('Set User Permissions'), () => { + return frappe.set_route('List', 'User Permission'); + }); this.set_from_route(); }, set_from_route: function() { @@ -133,11 +138,11 @@ frappe.PermissionEngine = Class.extend({ refresh: function() { var me = this; if(!me.doctype_select) { - this.body.html("

" + __("Loading") + "..."); + this.body.html("

" + __("Loading") + "...

"); return; } if(!me.get_doctype() && !me.get_role()) { - this.body.html("

"+__("Select Document Type or Role to start.")+""); + this.body.html("

"+__("Select Document Type or Role to start.")+"

"); return; } // get permissions @@ -247,10 +252,13 @@ frappe.PermissionEngine = Class.extend({ setup_user_permissions: function(d, role_cell) { var me = this; - d.help = frappe.render('', {}); + d.help = ``; var checkbox = this.add_check(role_cell, d, "apply_user_permissions") .removeClass("col-md-4") @@ -336,8 +344,8 @@ frappe.PermissionEngine = Class.extend({ var me = this; this.body.on("click", ".show-user-permissions", function() { - frappe.route_options = { doctype: me.get_doctype() || "" }; - frappe.set_route("user-permissions"); + frappe.route_options = { allow: me.get_doctype() || "" }; + frappe.set_route('List', 'User Permission'); }); this.body.on("click", "input[type='checkbox']", function() { diff --git a/frappe/core/page/permission_manager/permission_manager.py b/frappe/core/page/permission_manager/permission_manager.py index 626bb1c20d..ae3a3971e6 100644 --- a/frappe/core/page/permission_manager/permission_manager.py +++ b/frappe/core/page/permission_manager/permission_manager.py @@ -7,7 +7,7 @@ import frappe.defaults from frappe.modules.import_file import get_file_path, read_doc_from_file from frappe.translate import send_translations from frappe.permissions import (reset_perms, get_linked_doctypes, get_all_perms, - setup_custom_perms, add_permission) + setup_custom_perms, add_permission, update_permission_property) from frappe.core.doctype.doctype.doctype import (clear_permissions_cache, validate_permissions_for_doctype) from frappe import _ @@ -68,18 +68,8 @@ def add(parent, role, permlevel): @frappe.whitelist() def update(doctype, role, permlevel, ptype, value=None): frappe.only_for("System Manager") - - out = None - if setup_custom_perms(doctype): - out = 'refresh' - - name = frappe.get_value('Custom DocPerm', dict(parent=doctype, role=role, permlevel=permlevel)) - - frappe.db.sql("""update `tabCustom DocPerm` set `%s`=%s where name=%s"""\ - % (frappe.db.escape(ptype), '%s', '%s'), (value, name)) - validate_permissions_for_doctype(doctype) - - return out + out = update_permission_property(doctype, role, permlevel, ptype, value) + return 'refresh' if out else None @frappe.whitelist() def remove(doctype, role, permlevel): diff --git a/frappe/core/page/user_permissions/README.md b/frappe/core/page/user_permissions/README.md deleted file mode 100644 index e4fc779c6c..0000000000 --- a/frappe/core/page/user_permissions/README.md +++ /dev/null @@ -1 +0,0 @@ -Interface to set user defaults (DefaultValue). \ No newline at end of file diff --git a/frappe/core/page/user_permissions/__init__.py b/frappe/core/page/user_permissions/__init__.py deleted file mode 100644 index 0e57cb68c3..0000000000 --- a/frappe/core/page/user_permissions/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - diff --git a/frappe/core/page/user_permissions/user_permissions.js b/frappe/core/page/user_permissions/user_permissions.js deleted file mode 100644 index 5cb81900eb..0000000000 --- a/frappe/core/page/user_permissions/user_permissions.js +++ /dev/null @@ -1,365 +0,0 @@ -frappe.pages['user-permissions'].on_page_load = function(wrapper) { - var page = frappe.ui.make_app_page({ - parent: wrapper, - title: __("User Permissions Manager"), - icon: "fa fa-shield", - single_column: true - }); - - frappe.breadcrumbs.add("Setup"); - - $("
\ -

\ - " + __("Edit Role Permissions") + "\ -


\ -

"+__("Help for User Permissions")+":

\ -
    \ -
  1. " - + __("Apart from Role based Permission Rules, you can apply User Permissions based on DocTypes.") - + "
  2. " - - + "
  3. " - + __("These permissions will apply for all transactions where the permitted record is linked. For example, if Company C is added to User Permissions of user X, user X will only be able to see transactions that has company C as a linked value.") - + "
  4. " - - + "
  5. " - + __("These will also be set as default values for those links, if only one such permission record is defined.") - + "
  6. " - - + "
  7. " - + __("A user can be permitted to multiple records of the same DocType.") - + "
  8. \ -
").appendTo(page.main); - wrapper.user_permissions = new frappe.UserPermissions(wrapper); -} - -frappe.pages['user-permissions'].refresh = function(wrapper) { - wrapper.user_permissions.set_from_route(); -} - -frappe.UserPermissions = Class.extend({ - init: function(wrapper) { - this.wrapper = wrapper; - this.body = $(this.wrapper).find(".user-settings"); - this.filters = {}; - this.make(); - this.refresh(); - }, - make: function() { - var me = this; - - $(this.wrapper).find(".view-role-permissions").on("click", function() { - frappe.route_options = { doctype: me.get_doctype() || "" }; - frappe.set_route("permission-manager"); - }) - - return frappe.call({ - module:"frappe.core", - page:"user_permissions", - method: "get_users_and_links", - callback: function(r) { - me.options = r.message; - - me.filters.user = me.wrapper.page.add_field({ - fieldname: "user", - label: __("User"), - fieldtype: "Select", - options: ([__("Select User") + "..."].concat(r.message.users)).join("\n") - }); - - me.filters.doctype = me.wrapper.page.add_field({ - fieldname: "doctype", - label: __("DocType"), - fieldtype: "Select", - options: ([__("Select DocType") + "..."].concat(me.get_link_names())).join("\n") - }); - - me.filters.user_permission = me.wrapper.page.add_field({ - fieldname: "user_permission", - label: __("Name"), - fieldtype: "Link", - options: "[Select]" - }); - - if(frappe.user_roles.includes("System Manager")) { - me.download = me.wrapper.page.add_field({ - fieldname: "download", - label: __("Download"), - fieldtype: "Button", - icon: "fa fa-download" - }); - - me.upload = me.wrapper.page.add_field({ - fieldname: "upload", - label: __("Upload"), - fieldtype: "Button", - icon: "fa fa-upload" - }); - } - - // bind change event - $.each(me.filters, function(k, f) { - f.$input.on("change", function() { - me.refresh(); - }); - }); - - // change options in user_permission link - me.filters.doctype.$input.on("change", function() { - me.filters.user_permission.df.options = me.get_doctype(); - }); - - me.set_from_route(); - me.setup_download_upload(); - } - }); - }, - setup_download_upload: function() { - var me = this; - me.download.$input.on("click", function() { - window.location.href = frappe.urllib.get_base_url() - + "/api/method/frappe.core.page.user_permissions.user_permissions.get_user_permissions_csv"; - }); - - me.upload.$input.on("click", function() { - var d = new frappe.ui.Dialog({ - title: __("Upload User Permissions"), - fields: [ - { - fieldtype:"HTML", - options: '

    '+ - "
  1. "+__("Upload CSV file containing all user permissions in the same format as Download.")+"
  2. "+ - "
  3. "+__("Any existing permission will be deleted / overwritten.")+"
  4. "+ - '

    ' - }, - { - fieldtype:"Attach", fieldname:"attach", - } - ], - primary_action_label: __("Upload and Sync"), - primary_action: function() { - var filedata = d.fields_dict.attach.get_value(); - if(!filedata) { - frappe.msgprint(__("Please attach a file")); - return; - } - frappe.call({ - method:"frappe.core.page.user_permissions.user_permissions.import_user_permissions", - args: { - filedata: filedata - }, - callback: function(r) { - if(!r.exc) { - frappe.msgprint(__("Permissions Updated")); - d.hide(); - } - } - }); - } - }); - d.show(); - }) - }, - get_link_names: function() { - return this.options.link_fields; - }, - set_from_route: function() { - var me = this; - if(frappe.route_options && this.filters && !$.isEmptyObject(this.filters)) { - $.each(frappe.route_options, function(key, value) { - if(me.filters[key] && frappe.route_options[key]!=null) - me.set_filter(key, value); - }); - frappe.route_options = null; - } - this.refresh(); - }, - set_filter: function(key, value) { - this.filters[key].$input.val(value); - }, - get_user: function() { - var user = this.filters.user.$input.val(); - return user== __("Select User") + "..." ? null : user; - }, - get_doctype: function() { - var doctype = this.filters.doctype.$input.val(); - return doctype== __("Select DocType") + "..." ? null : doctype; - }, - get_user_permission: function() { - // autosuggest hack! - var user_permission = this.filters.user_permission.$input.val(); - return (user_permission === "%") ? null : user_permission; - }, - render: function(prop_list) { - var me = this; - this.body.empty(); - this.prop_list = prop_list; - if(!prop_list || !prop_list.length) { - this.add_message(__("No User Restrictions found.")); - } else { - this.show_user_permissions_table(); - } - this.show_add_user_permission(); - if(this.get_user() && this.get_doctype()) { - $('').appendTo(this.body.find(".btn-area")).on("click", function() { - frappe.route_options = {doctype: me.get_doctype(), user:me.get_user() }; - frappe.set_route("query-report/Permitted Documents For User"); - }); - } - }, - add_message: function(txt) { - $('

    ' + txt + '

    ').appendTo(this.body); - }, - refresh: function() { - var me = this; - if(!me.filters.user) { - this.body.html("

    "+__("Loading")+"...

    "); - return; - } - if(!me.get_user() && !me.get_doctype()) { - this.body.html("

    "+__("Select User or DocType to start.")+"

    "); - return; - } - // get permissions - return frappe.call({ - module: "frappe.core", - page: "user_permissions", - method: "get_permissions", - args: { - parent: me.get_user(), - defkey: me.get_doctype(), - defvalue: me.get_user_permission() - }, - callback: function(r) { - me.render(r.message); - } - }); - }, - show_user_permissions_table: function() { - var me = this; - this.table = $("\ - \ - \ -
    ").appendTo(this.body); - - $('

    ' - +__("These restrictions will apply for Document Types where 'Apply User Permissions' is checked for the permission rule and a field with this value is present.") - +'

    ').appendTo(this.body); - - $.each([[__("Allow User"), 150], [__("If Document Type"), 150], [__("Is"),150], ["", 50]], - function(i, col) { - $("") - .html(col[0]) - .css("width", col[1]+"px") - .appendTo(me.table.find("thead tr")); - }); - - - $.each(this.prop_list, function(i, d) { - var row = $("").appendTo(me.table.find("tbody")); - - $("").html('' - +d.parent+'').appendTo(row); - $("").html(d.defkey).appendTo(row); - $("").html(d.defvalue).appendTo(row); - - me.add_delete_button(row, d); - }); - - }, - add_delete_button: function(row, d) { - var me = this; - $("") - .appendTo($("").appendTo(row)) - .attr("data-name", d.name) - .attr("data-user", d.parent) - .attr("data-defkey", d.defkey) - .attr("data-defvalue", d.defvalue) - .click(function() { - return frappe.call({ - module: "frappe.core", - page: "user_permissions", - method: "remove", - args: { - name: $(this).attr("data-name"), - user: $(this).attr("data-user"), - defkey: $(this).attr("data-defkey"), - defvalue: $(this).attr("data-defvalue") - }, - callback: function(r) { - if(r.exc) { - frappe.msgprint(__("Did not remove")); - } else { - me.refresh(); - } - } - }) - }); - }, - - show_add_user_permission: function() { - var me = this; - $("") - .appendTo($('

    ').appendTo(this.body)) - .click(function() { - var d = new frappe.ui.Dialog({ - title: __("Add A New Restriction"), - fields: [ - {fieldtype:"Select", label:__("Allow User"), - options:me.options.users, reqd:1, fieldname:"user"}, - {fieldtype:"Select", label: __("If Document Type"), fieldname:"defkey", - options:me.get_link_names(), reqd:1}, - {fieldtype:"Link", label:__("Is"), fieldname:"defvalue", - options:'[Select]', reqd:1}, - {fieldtype:"Button", label: __("Add"), fieldname:"add"}, - ] - }); - if(me.get_user()) { - d.set_value("user", me.get_user()); - d.get_input("user").prop("disabled", true); - } - if(me.get_doctype()) { - d.set_value("defkey", me.get_doctype()); - d.get_input("defkey").prop("disabled", true); - } - if(me.get_user_permission()) { - d.set_value("defvalue", me.get_user_permission()); - d.get_input("defvalue").prop("disabled", true); - } - - d.fields_dict["defvalue"].get_query = function(txt) { - if(!d.get_value("defkey")) { - frappe.throw(__("Please select Document Type")); - } - - return { - doctype: d.get_value("defkey") - } - }; - - d.get_input("add").click(function() { - var args = d.get_values(); - if(!args) { - return; - } - frappe.call({ - module: "frappe.core", - page: "user_permissions", - method: "add", - args: args, - callback: function(r) { - if(r.exc) { - frappe.msgprint(__("Did not add")); - } else { - me.refresh(); - } - } - }) - d.hide(); - }); - d.show(); - }); - } -}) diff --git a/frappe/core/page/user_permissions/user_permissions.json b/frappe/core/page/user_permissions/user_permissions.json deleted file mode 100644 index ab831e1919..0000000000 --- a/frappe/core/page/user_permissions/user_permissions.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "content": null, - "creation": "2013-01-01 18:50:55", - "docstatus": 0, - "doctype": "Page", - "icon": "fa fa-user", - "idx": 1, - "modified": "2014-05-28 16:53:43.103533", - "modified_by": "Administrator", - "module": "Core", - "name": "user-permissions", - "owner": "Administrator", - "page_name": "user-permissions", - "roles": [], - "script": null, - "standard": "Yes", - "style": null, - "title": "User Permissions Manager" -} \ No newline at end of file diff --git a/frappe/core/page/user_permissions/user_permissions.py b/frappe/core/page/user_permissions/user_permissions.py deleted file mode 100644 index 98d7e90095..0000000000 --- a/frappe/core/page/user_permissions/user_permissions.py +++ /dev/null @@ -1,109 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -from __future__ import unicode_literals -import frappe -from frappe import _ -import frappe.defaults -from frappe.permissions import (can_set_user_permissions, add_user_permission, - remove_user_permission, get_valid_perms) -from frappe.core.doctype.user.user import get_system_users -from frappe.utils.csvutils import UnicodeWriter, read_csv_content_from_uploaded_file -from frappe.defaults import clear_default - -@frappe.whitelist() -def get_users_and_links(): - return { - "users": get_system_users(), - "link_fields": get_doctypes_for_user_permissions() - } - -@frappe.whitelist() -def get_permissions(parent=None, defkey=None, defvalue=None): - if defkey and not can_set_user_permissions(defkey, defvalue): - raise frappe.PermissionError - - conditions, values = _build_conditions(locals()) - - permissions = frappe.db.sql("""select name, parent, defkey, defvalue - from tabDefaultValue - where parent not in ('__default', '__global') - and substr(defkey,1,1)!='_' - and parenttype='User Permission' - {conditions} - order by parent, defkey""".format(conditions=conditions), values, as_dict=True) - - if not defkey: - out = [] - doctypes = get_doctypes_for_user_permissions() - for p in permissions: - if p.defkey in doctypes: - out.append(p) - permissions = out - - return permissions - -def _build_conditions(filters): - conditions = [] - values = {} - for key, value in filters.items(): - if filters.get(key): - conditions.append("and `{key}`=%({key})s".format(key=key)) - values[key] = value - - return "\n".join(conditions), values - -@frappe.whitelist() -def remove(user, name, defkey, defvalue): - if not can_set_user_permissions(defkey, defvalue): - frappe.throw(_("Cannot remove permission for DocType: {0} and Name: {1}").format( - defkey, defvalue), frappe.PermissionError) - - remove_user_permission(defkey, defvalue, user, name) - -@frappe.whitelist() -def add(user, defkey, defvalue): - if not can_set_user_permissions(defkey, defvalue): - frappe.throw(_("Cannot set permission for DocType: {0} and Name: {1}").format( - defkey, defvalue), frappe.PermissionError) - - add_user_permission(defkey, defvalue, user, with_message=True) - -def get_doctypes_for_user_permissions(): - '''Get doctypes for the current user where user permissions are applicable''' - user_roles = frappe.get_roles() - - if "System Manager" in user_roles: - doctypes = set([p.parent for p in get_valid_perms()]) - else: - doctypes = set([p.parent for p in get_valid_perms() if p.set_user_permissions]) - - single_doctypes = set([d.name for d in frappe.get_all("DocType", {"issingle": 1})]) - - return sorted(doctypes.difference(single_doctypes)) - - -@frappe.whitelist() -def get_user_permissions_csv(): - out = [["User Permissions"], ["User", "Document Type", "Value"]] - out += [[a.parent, a.defkey, a.defvalue] for a in get_permissions()] - - csv = UnicodeWriter() - for row in out: - csv.writerow(row) - - frappe.response['result'] = str(csv.getvalue()) - frappe.response['type'] = 'csv' - frappe.response['doctype'] = "User Permissions" - -@frappe.whitelist() -def import_user_permissions(): - frappe.only_for("System Manager") - rows = read_csv_content_from_uploaded_file(ignore_encoding=True) - clear_default(parenttype="User Permission") - - if rows[0][0]!="User Permissions" and rows[1][0] != "User": - frappe.throw(frappe._("Please upload using the same template as download.")) - - for row in rows[2:]: - add_user_permission(row[1], row[2], row[0]) diff --git a/frappe/defaults.py b/frappe/defaults.py index b4c7ff6452..0e6e23ffdc 100644 --- a/frappe/defaults.py +++ b/frappe/defaults.py @@ -48,25 +48,10 @@ def is_a_user_permission_key(key): return ":" not in key and key != frappe.scrub(key) def get_user_permissions(user=None): - if not user: - user = frappe.session.user - - return build_user_permissions(user) - -def build_user_permissions(user): - out = frappe.cache().hget("user_permissions", user) - if out==None: - out = {} - for key, value in frappe.db.sql("""select defkey, ifnull(defvalue, '') as defvalue - from tabDefaultValue where parent=%s and parenttype='User Permission'""", (user,)): - out.setdefault(key, []).append(value) - - # add profile match - if user not in out.get("User", []): - out.setdefault("User", []).append(user) - - frappe.cache().hset("user_permissions", user, out) - return out + from frappe.core.doctype.user_permission.user_permission \ + import get_user_permissions as _get_user_permissions + '''Return frappe.core.doctype.user_permissions.user_permissions._get_user_permissions (kept for backward compatibility)''' + return _get_user_permissions(user) def get_defaults(user=None): globald = get_defaults_for() diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index 87e48b0450..ee7fffa242 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -70,7 +70,6 @@ def getdoctype(doctype, with_parent=False, cached_timestamp=None): if not docs: docs = get_meta_bundle(doctype) - frappe.response['user_permissions'] = get_user_permissions(docs) frappe.response['user_settings'] = get_user_settings(parent_dt or doctype) if cached_timestamp and docs[0].modified==cached_timestamp: @@ -102,16 +101,6 @@ def get_docinfo(doc=None, doctype=None, name=None): "rating": get_feedback_rating(doc.doctype, doc.name) } -def get_user_permissions(meta): - out = {} - all_user_permissions = frappe.defaults.get_user_permissions() - - for m in meta: - for df in m.get_fields_to_check_permissions(all_user_permissions): - out[df.options] = list(set(all_user_permissions[df.options])) - - return out - def get_attachments(dt, dn): return frappe.get_all("File", fields=["name", "file_name", "file_url", "is_private"], filters = {"attached_to_name": dn, "attached_to_doctype": dt}) diff --git a/frappe/docs/assets/img/desk/animated_line_graph.gif b/frappe/docs/assets/img/desk/animated_line_graph.gif new file mode 100644 index 0000000000..0b0e7b212c Binary files /dev/null and b/frappe/docs/assets/img/desk/animated_line_graph.gif differ diff --git a/frappe/docs/assets/img/desk/bar_graph.png b/frappe/docs/assets/img/desk/bar_graph.png index d25254af6d..b3bd89cc88 100644 Binary files a/frappe/docs/assets/img/desk/bar_graph.png and b/frappe/docs/assets/img/desk/bar_graph.png differ diff --git a/frappe/docs/assets/img/desk/line_graph.png b/frappe/docs/assets/img/desk/line_graph.png deleted file mode 100644 index 02c60c7c18..0000000000 Binary files a/frappe/docs/assets/img/desk/line_graph.png and /dev/null differ diff --git a/frappe/docs/assets/img/desk/line_graph_sales.png b/frappe/docs/assets/img/desk/line_graph_sales.png new file mode 100644 index 0000000000..0e70ae0031 Binary files /dev/null and b/frappe/docs/assets/img/desk/line_graph_sales.png differ diff --git a/frappe/docs/assets/img/desk/percentage_graph.png b/frappe/docs/assets/img/desk/percentage_graph.png new file mode 100644 index 0000000000..3a25d59479 Binary files /dev/null and b/frappe/docs/assets/img/desk/percentage_graph.png differ diff --git a/frappe/docs/contents.html b/frappe/docs/contents.html deleted file mode 100644 index 59d0b4459d..0000000000 --- a/frappe/docs/contents.html +++ /dev/null @@ -1,10 +0,0 @@ - - -

    Table of Contents

    -
    - -{% include "templates/includes/full_index.html" %} - - - - diff --git a/frappe/docs/contents.py b/frappe/docs/contents.py deleted file mode 100644 index c23737a3c0..0000000000 --- a/frappe/docs/contents.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -from __future__ import unicode_literals -import frappe -from frappe.website.utils import get_full_index - -def get_context(context): - context.full_index = get_full_index() diff --git a/frappe/docs/index.html b/frappe/docs/index.html deleted file mode 100644 index a70cba3109..0000000000 --- a/frappe/docs/index.html +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - -
    -
    -
    -
    -

    Superhero Web Framework

    -

    Build extensions to ERPNext or make your own app

    -
    -
    -
    - -
    -
    -
    -
    -
    - - - -
    -
    -
    -

    Frappe is a full stack web application framework written in Python, -Javascript, HTML/CSS with MySQL as the backend. It was built for ERPNext -but is pretty generic and can be used to build database driven apps.

    - -

    The key differece in Frappe compared to other frameworks is that Frappe -is that meta-data is also treated as data and is used to build front-ends -very easily. Frappe comes with a full blown admin UI called the Desk -that handles forms, navigation, lists, menus, permissions, file attachment -and much more out of the box.

    - -

    Frappe also has a plug-in architecture that can be used to build plugins -to ERPNext.

    - -

    Frappe Framework was designed to build ERPNext, open source -ERP for managing small and medium sized businesses.

    - -

    Get started with the Tutorial

    - -
    -
    -
    - - - - - - \ No newline at end of file diff --git a/frappe/docs/index.txt b/frappe/docs/index.txt deleted file mode 100644 index 4bdfdc4ac8..0000000000 --- a/frappe/docs/index.txt +++ /dev/null @@ -1,6 +0,0 @@ -assets -user -contents -current -install -license diff --git a/frappe/docs/install.md b/frappe/docs/install.md deleted file mode 100644 index 7350f8f4ab..0000000000 --- a/frappe/docs/install.md +++ /dev/null @@ -1,30 +0,0 @@ - - -# Installation - -Frappe Framework is based on the Frappe Framework, a full stack web framework based on Python, MariaDB, Redis, Node. - -To intall Frappe Framework, you will have to install the Frappe Bench, the command-line, package manager and site manager for Frappe Framework. For more details, read the Bench README. - -After you have installed Frappe Bench, go to you bench folder, which is `frappe.bench` by default and setup **frappe**. - - bench get-app frappe {{ source_link }} - -Then create a new site to install the app. - - bench new-site mysite - -This will create a new folder in your `/sites` directory and create a new database for this site. - -Next, install frappe in this site - - bench --site mysite install-app frappe - -To run this locally, run - - bench start - -Fire up your browser and go to http://localhost:8000 and you should see the login screen. Login as **Administrator** and **admin** (or the password you set at the time of `new-site`) and you are set. - - - \ No newline at end of file diff --git a/frappe/docs/license.html b/frappe/docs/license.html deleted file mode 100644 index 602685d65f..0000000000 --- a/frappe/docs/license.html +++ /dev/null @@ -1,16 +0,0 @@ - - -

    MIT

    - -

    The MIT License (MIT)

    - -

    Copyright (c) 2016 Frappe Technologies Pvt. Ltd.

    - -

    Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

    - -

    The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

    - -

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

    - - - \ No newline at end of file diff --git a/frappe/docs/user/en/guides/app-development/generating-docs.md b/frappe/docs/user/en/guides/app-development/generating-docs.md index 8494533d22..0775967494 100755 --- a/frappe/docs/user/en/guides/app-development/generating-docs.md +++ b/frappe/docs/user/en/guides/app-development/generating-docs.md @@ -1,18 +1,17 @@ # Generating Documentation Website for your App -Frappe version 6.7 onwards includes a full-blown documentation generator so that you can easily create a website for your app that has both user docs and developers docs (auto-generated). These pages are generated as static HTML pages so that you can add them as GitHub pages. +Frappe version 6.7 onwards includes a full-blown documentation generator so that you can easily create a website for your app that has both user docs and developers docs (auto-generated). + +Version 8.7 onwards, these will be generated in a target app. ## Writing Docs ### 1. Setting up docs -#### 1.1. Setup `docs.py` - The first step is to setup the docs folder. For that you must create a new file in your app `config/docs.py` if it is not auto-generated. In your `docs.py` file, add the following module properties. source_link = "https://github.com/[orgname]/[reponame]" - docs_base_url = "https://[orgname].github.io/[reponame]" headline = "This is what my app does" sub_heading = "Slightly more details with key features" long_description = """(long description in markdown)""" @@ -29,16 +28,6 @@ The first step is to setup the docs folder. For that you must create a new file pass -#### 1.2. Generate `/docs` - -To generate the docs for the `current` version, go to the command line and write - - bench --site [site] build-docs [appname] - -If you want to maintain versions of your docs, then you can add a version number instead of `current` - -This will create a `/docs` folder in your app. - ### 2. Add User Documentation To add user documentation, add folders and pages in your `/docs/user` folder in the same way you would build a website pages in the `www` folder. @@ -54,61 +43,28 @@ Some quick tips: While linking make sure you add `{{ docs_base_url }}` to all your links. - {% raw %}Link Description{% endraw %} + {% raw %}Link Description{% endraw %} ### 4. Adding Images You can add images in the `/docs/assets` folder. You can add links to the images as follows: - {% raw %}{% endraw %} - ---- - -## Setting up output docs - -The output docs are generated in your `docs/appname` folder using the `write-docs` command. - ---- - -## Viewing Locally - -To test your docs locally, add a `--local` option to the `write-docs` command. - - bench --site [site] write-docs [appname] --local - -Then it will build urls so that you can view these files locally. To view them locally in your browser, you can use the Python SimpleHTTPServer - -Run this from your `docs/myapp` folder: - - python -m SimpleHTTPServer 8080 + {% raw %}{% endraw %} --- -## Publishing to GitHub Pages -To publish your docs on GitHub pages, you will have to create an empty and orphan branch in your repository called `gh-pages` and push your documentation there. +## Building Docs -1. To easily publish your docs on gh-pages, commit and push your `apps/docs` folder on you master branch first. -2. The `/docs` generation will also generate a `/docs` folder in your bench, parallel to your `/sites` folder. e.g. `/frappe-bench/docs` -3. Generate you documentation using the `write-docs` command. -4. Go to your docs folder `cd docs/myapp` -5. Checkout the gh-pages branch `git checkout --orphan gh-pages` -6. Push your documentation to Github. +You must create a new app that will have the output of the docs, which is called the "target" app. For example, the docs for ERPNext are hosted at erpnext.org, which is based on the app "foundation". You can create a new app just to push docs of any other app. -Note > The branch name `gh-pages` is only if you are using GitHub. If you are hosting this on any other static file server, you can create any other orphan branch instead. +To output docs to another app, -Putting it all together: + bench --site [site] build-docs [app] --target [target_app] - # build the apps/docs folder and write the compiled docs at docs/appname - bench --site [site] build-docs [appname] +This will create a new folder `/docs` inside the `www` folder of the target app and generate automatic docs (from code), model references and copy user docs and assets. - # commit to the gh-pages branch (for GitHub Pages) - cd docs/appname - git checkout --orphan gh-pages - git remote add origin [remote git repository] - git add * - git commit -m "Documentation Initialization" - git push origin gh-pages +To view the docs, just go the the `/docs` url on your target app. Example: -To check your documentation online go to: https://[orgname].github.io/[reponame] + https://erpnext.org/docs diff --git a/frappe/docs/user/en/guides/desk/making_graphs.md b/frappe/docs/user/en/guides/desk/making_graphs.md index 9234fa58b4..720c9217bf 100644 --- a/frappe/docs/user/en/guides/desk/making_graphs.md +++ b/frappe/docs/user/en/guides/desk/making_graphs.md @@ -1,61 +1,100 @@ # Making Graphs -The Frappe UI **Graph** object enables you to render simple line and bar graphs for a discreet set of data points. You can also set special checkpoint values and summary stats. +The Frappe UI **Graph** object enables you to render simple line, bar or percentage graphs for single or multiple discreet sets of data points. You can also set special checkpoint values and summary stats. ### Example: Line graph -Here's is an example of a simple sales graph: - - render_graph: function() { - $('.form-graph').empty(); - - var months = ['Aug', 'Sep', 'Oct', 'Nov', 'Dec', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul']; - var values = [2410, 3100, 1700, 1200, 2700, 1600, 2740, 1000, 850, 1500, 400, 2013]; - - var goal = 2500; - var current_val = 2013; - - new frappe.ui.Graph({ - parent: $('.form-graph'), - width: 700, - height: 140, - mode: 'line-graph', - - title: 'Sales', - subtitle: 'Monthly', - y_values: values, - x_points: months, - - specific_values: [ - { - name: "Goal", - line_type: "dashed", // "dashed" or "solid" - value: goal - }, - ], - summary_values: [ - { - name: "This month", - color: 'green', // Indicator colors: 'grey', 'blue', 'red', - // 'green', 'orange', 'purple', 'darkgrey', - // 'black', 'yellow', 'lightblue' - value: '₹ ' + current_val - }, - { - name: "Goal", - color: 'blue', - value: '₹ ' + goal - }, - { - name: "Completed", - color: 'green', - value: (current_val/goal*100).toFixed(1) + "%" - } - ] - }); - }, - - - -Setting the mode to 'bar-graph': +Here's an example of a simple sales graph: + + // Data + let months = ['August, 2016', 'September, 2016', 'October, 2016', 'November, 2016', + 'December, 2016', 'January, 2017', 'February, 2017', 'March, 2017', 'April, 2017', + 'May, 2017', 'June, 2017', 'July, 2017']; + + let values1 = [24100, 31000, 17000, 12000, 27000, 16000, 27400, 11000, 8500, 15000, 4000, 20130]; + let values2 = [17890, 10400, 12350, 20400, 17050, 23000, 7100, 13800, 16000, 20400, 11000, 13000]; + let goal = 25000; + let current_val = 20130; + + let g = new frappe.ui.Graph({ + parent: $('.form-graph').empty(), + height: 200, // optional + mode: 'line', // 'line', 'bar' or 'percentage' + + title: 'Sales', + subtitle: 'Monthly', + + y: [ + { + title: 'Data 1', + values: values1, + formatted: values1.map(d => '$ ' + d), + color: 'green' // Indicator colors: 'grey', 'blue', 'red', + // 'green', 'light-green', 'orange', 'purple', 'darkgrey', + // 'black', 'yellow', 'lightblue' + }, + { + title: 'Data 2', + values: values2, + formatted: values2.map(d => '$ ' + d), + color: 'light-green' + } + ], + + x: { + values: months.map(d => d.substring(0, 3)), + formatted: months + }, + + specific_values: [ + { + name: 'Goal', + line_type: 'dashed', // 'dashed' or 'solid' + value: goal + }, + ], + + summary: [ + { + name: 'This month', + color: 'orange', + value: '$ ' + current_val + }, + { + name: 'Goal', + color: 'blue', + value: '$ ' + goal + }, + { + name: 'Completed', + color: 'green', + value: (current_val/goal*100).toFixed(1) + "%" + } + ] + }); + + + +`bar` mode yeilds: + +You can set the `colors` property of `x` to an array of color values for `percentage` mode: + + + +You can also change the values of an existing graph with a new set of `y` values: + + setTimeout(() => { + g.change_values([ + { + values: data[2], + formatted: data[2].map(d => d + 'L') + }, + { + values: data[3], + formatted: data[3].map(d => d + 'L') + } + ]); + }, 1000); + + diff --git a/frappe/docs/user/en/guides/reports-and-printing/how-to-make-query-report.md b/frappe/docs/user/en/guides/reports-and-printing/how-to-make-query-report.md index 9eb11b066a..1e22757d7e 100755 --- a/frappe/docs/user/en/guides/reports-and-printing/how-to-make-query-report.md +++ b/frappe/docs/user/en/guides/reports-and-printing/how-to-make-query-report.md @@ -39,7 +39,7 @@ You can define complex queries such as: ### 4. Advanced (adding filters) -If you are making a standard report, you can add filters in your query report just like [script reports](https://frappe.github.io/frappe/user/en/guides/reports-and-printing/how-to-make-script-reports) by adding a `.js` file in your query report folder. To include filters in your query, use `%(filter_key)s` where your filter value will be shown. +If you are making a standard report, you can add filters in your query report just like [script reports](https://frappe.io/docs/user/en/guides/reports-and-printing/how-to-make-script-reports) by adding a `.js` file in your query report folder. To include filters in your query, use `%(filter_key)s` where your filter value will be shown. For example diff --git a/frappe/docs/user/en/guides/reports-and-printing/how-to-make-script-reports.md b/frappe/docs/user/en/guides/reports-and-printing/how-to-make-script-reports.md index ab03c11051..74479a8b9c 100755 --- a/frappe/docs/user/en/guides/reports-and-printing/how-to-make-script-reports.md +++ b/frappe/docs/user/en/guides/reports-and-printing/how-to-make-script-reports.md @@ -4,9 +4,9 @@ You can create tabulated reports using server side scripts by creating a new Rep > Note: You will need Administrator Permissions for this. -Since these reports give you unrestricted access via Python scripts, they can only be created by Administrators. The script part of the report becomes a part of the repository of the application. If you have not created an app, [read this](https://frappe.github.io/frappe/user/en/guides/app-development/). +Since these reports give you unrestricted access via Python scripts, they can only be created by Administrators. The script part of the report becomes a part of the repository of the application. If you have not created an app, [read this](https://frappe.io/docs/user/en/guides/app-development/). -> Note: You must be in [Developer Mode](https://frappe.github.io/frappe/user/en/guides/app-development/how-enable-developer-mode-in-frappe) to do this +> Note: You must be in [Developer Mode](https://frappe.io/docs/user/en/guides/app-development/how-enable-developer-mode-in-frappe) to do this ### 1. Create a new Report diff --git a/frappe/docs/user/en/tutorial/before.md b/frappe/docs/user/en/tutorial/before.md index 80f34e01dd..426101e1cc 100755 --- a/frappe/docs/user/en/tutorial/before.md +++ b/frappe/docs/user/en/tutorial/before.md @@ -6,11 +6,12 @@ #### 1. Python -Frappe uses Python (v2.7) for server-side programming. It is highly recommended to learn Python before you start building apps with Frappe. +Frappe uses Python (v2.7) for server-side programming. It is highly recommended to learn Python before you start building apps with Frappe. To write quality server-side code, you must also include automated tests. Resources: + 1. [Codecademy Tutorial for Python](https://www.codecademy.com/learn/python) 1. [Official Python Tutorial](https://docs.python.org/2.7/tutorial/index.html) 1. [Basics of Test-driven development](http://code.tutsplus.com/tutorials/beginning-test-driven-development-in-python--net-30137) @@ -19,11 +20,12 @@ Resources: #### 2. MariaDB / MySQL -To create database-driven apps with Frappe, you must understand the basics of database management, like how to install, login, create new databases, and basic SQL queries. +To create database-driven apps with Frappe, you must understand the basics of database management, like how to install, login, create new databases, and basic SQL queries. Resources: + 1. [Codecademy Tutorial for SQL](https://www.codecademy.com/learn/learn-sql) - 1. [A basic MySQL tutorial by DigitalOcean](https://www.digitalocean.com/community/tutorials/a-basic-mysql-tutorial) + 1. [A basic MySQL tutorial by DigitalOcean](https://www.digitalocean.com/community/tutorials/a-basic-mysql-tutorial) 1. [Getting started with MariaDB](https://mariadb.com/kb/en/mariadb/documentation/getting-started/) --- @@ -33,6 +35,7 @@ Resources: If you want to build user interfaces using Frappe, you will need to learn basic HTML / CSS and the Boostrap CSS Framework. Resources: + 1. [Codecademy Tutorial for HTML/CSS](https://www.codecademy.com/learn/learn-html-css) 1. [Getting started with Bootstrap](https://getbootstrap.com/getting-started/) @@ -44,6 +47,7 @@ To customize forms and create rich user interfaces, you should learn JavaScript Resources: + 1. [Codecademy Tutorial for JavaScript](https://www.codecademy.com/learn/learn-javascript) 1. [Codecademy Tutorial for jQuery](https://www.codecademy.com/learn/jquery) --- @@ -53,6 +57,7 @@ Resources: If you are customizing Print templates or Web pages, you need to learn the Jinja Templating language. It is an easy way to create dynamic web pages (HTML). Resources: + 1. [Primer on Jinja Templating](https://realpython.com/blog/python/primer-on-jinja-templating/) 1. [Official Documentation](http://jinja.pocoo.org/) @@ -63,6 +68,7 @@ Resources: Learn how to contribute back to an open source project using Git and GitHub, two great tools to help you manage your code and share it with others. Resources: + 1. [Basic Git Tutorial](https://try.github.io) 2. [How to contribute to Open Source](https://opensource.guide/how-to-contribute/) diff --git a/frappe/model/create_new.py b/frappe/model/create_new.py index cc5c394d9f..846d8101e6 100644 --- a/frappe/model/create_new.py +++ b/frappe/model/create_new.py @@ -11,6 +11,7 @@ from frappe.utils import nowdate, nowtime, now_datetime import frappe.defaults from frappe.model.db_schema import type_map import copy +from frappe.core.doctype.user_permission.user_permission import get_user_permissions def get_new_doc(doctype, parent_doc = None, parentfield = None, as_dict=False): if doctype not in frappe.local.new_doc_templates: @@ -47,7 +48,7 @@ def make_new_doc(doctype): return doc def set_user_and_static_default_values(doc): - user_permissions = frappe.defaults.get_user_permissions() + user_permissions = get_user_permissions() defaults = frappe.defaults.get_defaults() for df in doc.meta.get("fields"): @@ -103,7 +104,7 @@ def get_static_default_value(df, user_permissions): def set_dynamic_default_values(doc, parent_doc, parentfield): # these values should not be cached - user_permissions = frappe.defaults.get_user_permissions() + user_permissions = get_user_permissions() for df in frappe.get_meta(doc["doctype"]).get("fields"): if df.get("default"): diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index eeee7f4bf7..56e4cc343d 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -388,7 +388,7 @@ class DatabaseQuery(object): # apply user permissions? if role_permissions.get("apply_user_permissions", {}).get("read"): # get user permissions - user_permissions = frappe.defaults.get_user_permissions(self.user) + user_permissions = frappe.permissions.get_user_permissions(self.user) self.add_user_permissions(user_permissions, user_permission_doctypes=role_permissions.get("user_permission_doctypes").get("read")) diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index 3bf49a5f6a..e242a48c88 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -11,7 +11,7 @@ from frappe.utils.file_manager import remove_all from frappe.utils.password import delete_all_passwords_for from frappe import _ from frappe.model.naming import revert_series_if_last -from frappe.utils.global_search import delete_for_document +from frappe.utils.global_search import delete_for_document def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reload=False, ignore_permissions=False, flags=None, ignore_on_trash=False): @@ -158,8 +158,13 @@ def update_flags(doc, flags=None, ignore_permissions=False): def check_permission_and_not_submitted(doc): # permission - if not doc.flags.ignore_permissions and frappe.session.user!="Administrator" and (not doc.has_permission("delete") or (doc.doctype=="DocType" and not doc.custom)): - frappe.msgprint(_("User not allowed to delete {0}: {1}").format(doc.doctype, doc.name), raise_exception=True) + if (not doc.flags.ignore_permissions + and frappe.session.user!="Administrator" + and ( + not doc.has_permission("delete") + or (doc.doctype=="DocType" and not doc.custom))): + frappe.msgprint(_("User not allowed to delete {0}: {1}") + .format(doc.doctype, doc.name), raise_exception=frappe.PermissionError) # check if submitted if doc.docstatus == 1: diff --git a/frappe/patches.txt b/frappe/patches.txt index 8adf636701..3347ae8287 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -189,3 +189,4 @@ frappe.patches.v8_1.enable_allow_error_traceback_in_system_settings frappe.patches.v8_1.update_format_options_in_auto_email_report frappe.patches.v8_1.delete_custom_docperm_if_doctype_not_exists frappe.patches.v8_5.delete_email_group_member_with_invalid_emails +frappe.patches.v8_x.update_user_permission diff --git a/frappe/patches/v8_x/__init__.py b/frappe/patches/v8_x/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/patches/v8_x/update_user_permission.py b/frappe/patches/v8_x/update_user_permission.py new file mode 100644 index 0000000000..4ceb26e945 --- /dev/null +++ b/frappe/patches/v8_x/update_user_permission.py @@ -0,0 +1,25 @@ +import frappe + +def execute(): + frappe.reload_doc('core', 'doctype', 'user_permission') + frappe.delete_doc('core', 'page', 'user-permissions') + for perm in frappe.db.sql(""" + select + name, parent, defkey, defvalue + from + tabDefaultValue + where + parent not in ('__default', '__global') + and + substr(defkey,1,1)!='_' + and + parenttype='User Permission' + """, as_dict=True): + frappe.get_doc(dict( + doctype='User Permission', + user=perm.parent, + allow=perm.defkey, + for_value=perm.defvalue + )).insert(ignore_permissions = True) + + frappe.db.sql('delete from tabDefaultValue where parenttype="User Permission"') diff --git a/frappe/permissions.py b/frappe/permissions.py index 13da62d366..29f223d08e 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -7,7 +7,6 @@ import frappe, copy, json from frappe import _, msgprint from frappe.utils import cint import frappe.share - rights = ("read", "write", "create", "delete", "submit", "cancel", "amend", "print", "email", "report", "import", "export", "set_user_permissions", "share") @@ -25,6 +24,9 @@ def has_permission(doctype, ptype="read", doc=None, verbose=False, user=None): """ if not user: user = frappe.session.user + if verbose: + print('--- Checking for {0} {1} ---'.format(doctype, doc.name if doc else '-')) + if frappe.is_table(doctype): if verbose: print("Table type, always true") return True @@ -40,7 +42,7 @@ def has_permission(doctype, ptype="read", doc=None, verbose=False, user=None): return False if user=="Administrator": - if verbose: print("Administrator") + if verbose: print("Allowing Administrator") return True def false_if_not_shared(): @@ -210,7 +212,10 @@ def get_role_permissions(meta, user=None, verbose=False): if p.user_permission_doctypes: # set user_permission_doctypes in perms - user_permission_doctypes = json.loads(p.user_permission_doctypes) + try: + user_permission_doctypes = json.loads(p.user_permission_doctypes) + except ValueError: + user_permission_doctypes = [] else: user_permission_doctypes = get_linked_doctypes(meta.name) @@ -247,8 +252,12 @@ def get_role_permissions(meta, user=None, verbose=False): return frappe.local.role_permissions[cache_key] +def get_user_permissions(user): + from frappe.core.doctype.user_permission.user_permission import get_user_permissions + return get_user_permissions(user) + def user_has_permission(doc, verbose=True, user=None, user_permission_doctypes=None): - from frappe.defaults import get_user_permissions + from frappe.core.doctype.user_permission.user_permission import get_user_permissions user_permissions = get_user_permissions(user) user_permission_doctypes = get_user_permission_doctypes(user_permission_doctypes, user_permissions) @@ -258,6 +267,10 @@ def user_has_permission(doc, verbose=True, user=None, user_permission_doctypes=N messages = {} + if not user_permission_doctypes: + # no doctypes restricted + end_result = True + # check multiple sets of user_permission_doctypes using OR condition for doctypes in user_permission_doctypes: result = True @@ -309,9 +322,9 @@ def has_controller_permissions(doc, ptype, user=None): def get_doctypes_with_read(): return list(set([p.parent for p in get_valid_perms()])) -def get_valid_perms(doctype=None): +def get_valid_perms(doctype=None, user=None): '''Get valid permissions for the current user from DocPerm and Custom DocPerm''' - roles = get_roles() + roles = get_roles(user) perms = get_perms_for(roles) custom_perms = get_perms_for(roles, 'Custom DocPerm') @@ -360,7 +373,8 @@ def get_roles(user=None, with_standard=True): def get_perms_for(roles, perm_doctype='DocPerm'): '''Get perms for given roles''' - return frappe.db.sql("""select * from `tab{doctype}` where docstatus=0 + return frappe.db.sql(""" + select * from `tab{doctype}` where docstatus=0 and ifnull(permlevel,0)=0 and role in ({roles})""".format(doctype = perm_doctype, roles=", ".join(["%s"]*len(roles))), tuple(roles), as_dict=1) @@ -386,22 +400,28 @@ def set_user_permission_if_allowed(doctype, name, user, with_message=False): if get_role_permissions(frappe.get_meta(doctype), user).set_user_permissions!=1: add_user_permission(doctype, name, user, with_message) -def add_user_permission(doctype, name, user, with_message=False): - '''Add user default''' - if name not in frappe.defaults.get_user_permissions(user).get(doctype, []): +def add_user_permission(doctype, name, user, apply=False): + '''Add user permission''' + from frappe.core.doctype.user_permission.user_permission import get_user_permissions + if name not in get_user_permissions(user).get(doctype, []): if not frappe.db.exists(doctype, name): frappe.throw(_("{0} {1} not found").format(_(doctype), name), frappe.DoesNotExistError) - frappe.defaults.add_default(doctype, name, user, "User Permission") - elif with_message: - msgprint(_("Permission already set")) + frappe.get_doc(dict( + doctype='User Permission', + user=user, + allow=doctype, + for_value=name, + apply_for_all_roles=apply + )).insert() -def remove_user_permission(doctype, name, user, default_value_name=None): - frappe.defaults.clear_default(key=doctype, value=name, parent=user, parenttype="User Permission", - name=default_value_name) +def remove_user_permission(doctype, name, user): + user_permission_name = frappe.db.get_value('User Permission', + dict(user=user, allow=doctype, for_value=name)) + frappe.delete_doc('User Permission', user_permission_name) def clear_user_permissions_for_doctype(doctype): - frappe.defaults.clear_default(parenttype="User Permission", key=doctype) + frappe.cache().delete_value('user_permissions') def can_import(doctype, raise_exception=False): if not ("System Manager" in frappe.get_roles() or has_permission(doctype, "import")): @@ -426,9 +446,10 @@ def apply_user_permissions(doctype, ptype, user=None): def get_user_permission_doctypes(user_permission_doctypes, user_permissions): """returns a list of list like [["User", "Blog Post"], ["User"]]""" - if cint(frappe.db.get_single_value("System Settings", "ignore_user_permissions_if_missing")): + if cint(frappe.get_system_settings('ignore_user_permissions_if_missing')): # select those user permission doctypes for which user permissions exist! - user_permission_doctypes = [list(set(doctypes).intersection(set(user_permissions.keys()))) + user_permission_doctypes = [ + list(set(doctypes).intersection(set(user_permissions.keys()))) for doctypes in user_permission_doctypes] if len(user_permission_doctypes) > 1: @@ -452,6 +473,22 @@ def get_user_permission_doctypes(user_permission_doctypes, user_permissions): return user_permission_doctypes +def update_permission_property(doctype, role, permlevel, ptype, value=None, validate=True): + '''Update a property in Custom Perm''' + from frappe.core.doctype.doctype.doctype import validate_permissions_for_doctype + out = setup_custom_perms(doctype) + + name = frappe.get_value('Custom DocPerm', dict(parent=doctype, role=role, + permlevel=permlevel)) + + frappe.db.sql(""" + update `tabCustom DocPerm` + set `{0}`=%s where name=%s""".format(ptype), (value, name)) + if validate: + validate_permissions_for_doctype(doctype) + + return out + def setup_custom_perms(parent): '''if custom permssions are not setup for the current doctype, set them up''' if not frappe.db.exists('Custom DocPerm', dict(parent=parent)): diff --git a/frappe/public/build.json b/frappe/public/build.json index b350c8151a..054421286e 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -52,7 +52,8 @@ "public/css/desktop.css", "public/css/form.css", "public/css/mobile.css", - "public/css/kanban.css" + "public/css/kanban.css", + "public/css/graphs.css" ], "css/frappe-rtl.css": [ "public/css/bootstrap-rtl.css", @@ -164,7 +165,7 @@ "public/js/frappe/query_string.js", "public/js/frappe/ui/charts.js", - "public/js/frappe/ui/graph.js", + "public/js/frappe/ui/graphs.js", "public/js/frappe/ui/comment.js", "public/js/frappe/misc/rating_icons.html", diff --git a/frappe/public/css/desk.css b/frappe/public/css/desk.css index bfce576e37..22ecdb993b 100644 --- a/frappe/public/css/desk.css +++ b/frappe/public/css/desk.css @@ -258,7 +258,7 @@ a[disabled="disabled"]:hover { } .link-btn { position: absolute; - top: 2px; + top: 3px; right: 4px; border-radius: 2px; padding: 3px; diff --git a/frappe/public/css/docs.css b/frappe/public/css/docs.css index 20b11d5cb1..2fdd0ae21d 100644 --- a/frappe/public/css/docs.css +++ b/frappe/public/css/docs.css @@ -597,9 +597,3 @@ a.edit:visited, .page-content-wrapper > .row .col-sm-4 { display: none; } -.screenshot { - border: 2px solid #d1d8dd; - box-shadow: 1px 1px 7px rgba(0, 0, 0, 0.15); - margin: 15px 0px; - max-width: 100%; -} diff --git a/frappe/public/css/form.css b/frappe/public/css/form.css index 0d21271862..c56811e892 100644 --- a/frappe/public/css/form.css +++ b/frappe/public/css/form.css @@ -678,80 +678,6 @@ select.form-control { padding: 10px; margin: 10px; } -.graph-container .graphics { - margin-top: 10px; - padding: 10px 0px; -} -.graph-container .stats-group { - display: flex; - justify-content: space-around; - flex: 1; -} -.graph-container .stats-container { - display: flex; - justify-content: space-around; -} -.graph-container .stats-container .stats { - padding-bottom: 15px; -} -.graph-container .stats-container .stats-title { - color: #8D99A6; -} -.graph-container .stats-container .stats-value { - font-size: 20px; - font-weight: 300; -} -.graph-container .stats-container .stats-description { - font-size: 12px; - color: #8D99A6; -} -.graph-container .stats-container .graph-data .stats-value { - color: #98d85b; -} -.bar-graph .axis, -.line-graph .axis { - font-size: 10px; - fill: #6a737d; -} -.bar-graph .axis line, -.line-graph .axis line { - stroke: rgba(27, 31, 35, 0.1); -} -.data-points circle { - fill: #28a745; - stroke: #fff; - stroke-width: 2; -} -.data-points g.mini { - fill: #98d85b; -} -.data-points path { - fill: none; - stroke: #28a745; - stroke-opacity: 1; - stroke-width: 2px; -} -.line-graph .path { - fill: none; - stroke: #28a745; - stroke-opacity: 1; - stroke-width: 2px; -} -line.dashed { - stroke-dasharray: 5,3; -} -.tick.x-axis-label { - display: block; -} -.tick .specific-value { - text-anchor: start; -} -.tick .y-value-text { - text-anchor: end; -} -.tick .x-value-text { - text-anchor: middle; -} body[data-route^="Form/Communication"] textarea[data-fieldname="subject"] { height: 80px !important; } diff --git a/frappe/public/css/graphs.css b/frappe/public/css/graphs.css new file mode 100644 index 0000000000..a9fdf62dc9 --- /dev/null +++ b/frappe/public/css/graphs.css @@ -0,0 +1,274 @@ +/* graphs */ +.graph-container .graph-focus-margin { + margin: 0px 5%; +} +.graph-container .graph-graphics { + margin-top: 10px; + padding: 10px 0px; + position: relative; +} +.graph-container .graph-stats-group { + display: flex; + justify-content: space-around; + flex: 1; +} +.graph-container .graph-stats-container { + display: flex; + justify-content: space-around; + padding-top: 10px; +} +.graph-container .graph-stats-container .stats { + padding-bottom: 15px; +} +.graph-container .graph-stats-container .stats-title { + color: #8D99A6; +} +.graph-container .graph-stats-container .stats-value { + font-size: 20px; + font-weight: 300; +} +.graph-container .graph-stats-container .stats-description { + font-size: 12px; + color: #8D99A6; +} +.graph-container .graph-stats-container .graph-data .stats-value { + color: #98d85b; +} +.graph-container .bar-graph .axis, +.graph-container .line-graph .axis { + font-size: 10px; + fill: #6a737d; +} +.graph-container .bar-graph .axis line, +.graph-container .line-graph .axis line { + stroke: rgba(27, 31, 35, 0.1); +} +.graph-container .percentage-graph { + margin-top: 35px; +} +.graph-container .percentage-graph .progress { + margin-bottom: 0px; +} +.graph-container .graph-data-points circle { + stroke: #fff; + stroke-width: 2; +} +.graph-container .graph-data-points path { + fill: none; + stroke-opacity: 1; + stroke-width: 2px; +} +.graph-container line.graph-dashed { + stroke-dasharray: 5,3; +} +.graph-container .tick.x-axis-label { + display: block; +} +.graph-container .tick .specific-value { + text-anchor: start; +} +.graph-container .tick .y-value-text { + text-anchor: end; +} +.graph-container .tick .x-value-text { + text-anchor: middle; +} +.graph-container .graph-svg-tip { + position: absolute; + z-index: 99999; + padding: 10px; + font-size: 12px; + color: #959da5; + text-align: center; + background: rgba(0, 0, 0, 0.8); + border-radius: 3px; +} +.graph-container .graph-svg-tip.comparison { + padding: 0; + text-align: left; + pointer-events: none; +} +.graph-container .graph-svg-tip.comparison .title { + display: block; + padding: 10px; + margin: 0; + font-weight: 600; + line-height: 1; + pointer-events: none; +} +.graph-container .graph-svg-tip.comparison ul { + margin: 0; + white-space: nowrap; + list-style: none; +} +.graph-container .graph-svg-tip.comparison li { + display: inline-block; + padding: 5px 10px; +} +.graph-container .graph-svg-tip ul, +.graph-container .graph-svg-tip ol { + padding-left: 0; + display: flex; +} +.graph-container .graph-svg-tip ul.data-point-list li { + min-width: 90px; + flex: 1; +} +.graph-container .graph-svg-tip strong { + color: #dfe2e5; +} +.graph-container .graph-svg-tip::after { + position: absolute; + bottom: -10px; + left: 50%; + width: 5px; + height: 5px; + margin: 0 0 0 -5px; + content: " "; + border: 5px solid transparent; + border-top-color: rgba(0, 0, 0, 0.8); +} +.graph-container .stroke.grey { + stroke: #F0F4F7; +} +.graph-container .stroke.blue { + stroke: #5e64ff; +} +.graph-container .stroke.red { + stroke: #ff5858; +} +.graph-container .stroke.light-green { + stroke: #98d85b; +} +.graph-container .stroke.green { + stroke: #28a745; +} +.graph-container .stroke.orange { + stroke: #ffa00a; +} +.graph-container .stroke.purple { + stroke: #743ee2; +} +.graph-container .stroke.darkgrey { + stroke: #b8c2cc; +} +.graph-container .stroke.black { + stroke: #36414C; +} +.graph-container .stroke.yellow { + stroke: #FEEF72; +} +.graph-container .stroke.light-blue { + stroke: #7CD6FD; +} +.graph-container .stroke.lightblue { + stroke: #7CD6FD; +} +.graph-container .fill.grey { + fill: #F0F4F7; +} +.graph-container .fill.blue { + fill: #5e64ff; +} +.graph-container .fill.red { + fill: #ff5858; +} +.graph-container .fill.light-green { + fill: #98d85b; +} +.graph-container .fill.green { + fill: #28a745; +} +.graph-container .fill.orange { + fill: #ffa00a; +} +.graph-container .fill.purple { + fill: #743ee2; +} +.graph-container .fill.darkgrey { + fill: #b8c2cc; +} +.graph-container .fill.black { + fill: #36414C; +} +.graph-container .fill.yellow { + fill: #FEEF72; +} +.graph-container .fill.light-blue { + fill: #7CD6FD; +} +.graph-container .fill.lightblue { + fill: #7CD6FD; +} +.graph-container .background.grey { + background: #F0F4F7; +} +.graph-container .background.blue { + background: #5e64ff; +} +.graph-container .background.red { + background: #ff5858; +} +.graph-container .background.light-green { + background: #98d85b; +} +.graph-container .background.green { + background: #28a745; +} +.graph-container .background.orange { + background: #ffa00a; +} +.graph-container .background.purple { + background: #743ee2; +} +.graph-container .background.darkgrey { + background: #b8c2cc; +} +.graph-container .background.black { + background: #36414C; +} +.graph-container .background.yellow { + background: #FEEF72; +} +.graph-container .background.light-blue { + background: #7CD6FD; +} +.graph-container .background.lightblue { + background: #7CD6FD; +} +.graph-container .border-top.grey { + border-top: 3px solid #F0F4F7; +} +.graph-container .border-top.blue { + border-top: 3px solid #5e64ff; +} +.graph-container .border-top.red { + border-top: 3px solid #ff5858; +} +.graph-container .border-top.light-green { + border-top: 3px solid #98d85b; +} +.graph-container .border-top.green { + border-top: 3px solid #28a745; +} +.graph-container .border-top.orange { + border-top: 3px solid #ffa00a; +} +.graph-container .border-top.purple { + border-top: 3px solid #743ee2; +} +.graph-container .border-top.darkgrey { + border-top: 3px solid #b8c2cc; +} +.graph-container .border-top.black { + border-top: 3px solid #36414C; +} +.graph-container .border-top.yellow { + border-top: 3px solid #FEEF72; +} +.graph-container .border-top.light-blue { + border-top: 3px solid #7CD6FD; +} +.graph-container .border-top.lightblue { + border-top: 3px solid #7CD6FD; +} diff --git a/frappe/public/css/website.css b/frappe/public/css/website.css index 9a92bccf38..b9b2d733bb 100644 --- a/frappe/public/css/website.css +++ b/frappe/public/css/website.css @@ -430,6 +430,9 @@ h6 a { color: inherit !important; text-decoration: none; } +li { + line-height: 1.7em; +} .navbar-brand { max-width: none; } @@ -503,6 +506,9 @@ h6 a { min-height: 140px; border-top: 1px solid #EBEFF2; } +.page_content { + padding-bottom: 30px; +} .carousel-control .icon { position: absolute; top: 50%; @@ -599,7 +605,7 @@ fieldset { } .web-sidebar .sidebar-item { margin: 0px; - padding: 12px 0px; + padding-bottom: 12px; border: none; color: #8D99A6; font-size: 12px; @@ -607,21 +613,14 @@ fieldset { .web-sidebar .sidebar-item .badge { font-weight: normal; } -.web-sidebar .sidebar-item:first-child { - padding-top: 10px; -} -.web-sidebar .sidebar-item:last-child { - padding-bottom: 10px; -} .web-sidebar .sidebar-item a { - color: #8D99A6; + color: #36414C !important; } .web-sidebar .sidebar-item a.active { color: #36414C !important; font-weight: 500 !important; } .web-sidebar .sidebar-items { - margin-top: -10px; margin-bottom: 30px; } .web-sidebar .sidebar-items .title { @@ -675,69 +674,25 @@ fieldset { .web-list-item:last-child { border-bottom: 0px; } -.blog-info { - text-align: center; - margin-top: 30px; -} -.post-description { - padding-bottom: 8px; -} -.post-description p { - margin-bottom: 8px; -} -.blog-footer { - padding: 5px 15px; - border-top: 1px solid #EBEFF2; - margin: 0px -15px -20px -15px; -} -.blog-list-content .website-list .result { +.website-list .result { border: 0px; } -.blog-list-content .web-list-item:hover { +.web-list-item:hover { background: transparent; } -.blog-category { - letter-spacing: 0.5px; - text-align: center; - margin-bottom: 30px; -} -.author { - letter-spacing: 0.5px; - border-bottom: 1px solid #EBEFF2; - padding-bottom: 30px; -} -.blogger { - padding-top: 0px; - padding-bottom: 50px; -} -.blog-dot:before { +.spacer-dot:before { padding-right: 8px; padding-left: 8px; content: "\2022"; } -.blog-list-item { - margin-top: 30px; - margin-bottom: 30px; -} -.blog-list-item .blog-header { - font-size: 1.6em; -} -.blog-header { - font-weight: 700; - font-size: 2em; -} .add-comment-section { padding-bottom: 30px; } -.blog-comments { - position: relative; - border-top: 1px solid #d1d8dd; -} -.blog-comment-row { +.comment-row { margin: 0px -15px; padding: 15px; } -.blog-comment-row:last-child { +.comment-row:last-child { margin-bottom: 30px; border-bottom: 0px; } @@ -837,7 +792,7 @@ a.active { } .sidebar-block, .page-content { - padding-top: 50px; + padding-top: 30px; padding-bottom: 50px; } .your-account-info { @@ -871,19 +826,6 @@ a.active { li.footer-child-item { margin: 15px 0px; } -.blog-info { - text-align: center; - margin-top: 30px; -} -.blog-text { - padding-top: 50px; - padding-bottom: 50px; - font-size: 18px; - line-height: 1.5; -} -.blog-text p { - margin-bottom: 30px; -} .comment-view { padding-bottom: 30px; } @@ -927,7 +869,7 @@ li.footer-child-item { overflow: hidden; } .vert-line > div + div { - border-left: 1px solid #EBEFF2; + border-left: 1px solid #d1d8dd; } .vert-line > div { padding-bottom: 2000px; @@ -994,3 +936,13 @@ li.footer-child-item { padding: 10px; border-radius: 4px; } +.docfields pre { + background-color: transparent; + border: none; +} +.screenshot { + border: 1px solid #d1d8dd; + box-shadow: 1px 1px 7px rgba(0, 0, 0, 0.15); + margin: 15px 0px; + max-width: 100%; +} diff --git a/frappe/public/js/frappe/defaults.js b/frappe/public/js/frappe/defaults.js index 24ba5e630e..570ae61e2a 100644 --- a/frappe/public/js/frappe/defaults.js +++ b/frappe/public/js/frappe/defaults.js @@ -77,10 +77,6 @@ frappe.defaults = { }, get_user_permissions: function() { - return frappe.defaults.user_permissions; + return frappe.boot.user_permissions; }, - set_user_permissions: function(user_permissions) { - if(!user_permissions) return; - frappe.defaults.user_permissions = $.extend(frappe.defaults.user_permissions || {}, user_permissions); - } } diff --git a/frappe/public/js/frappe/form/dashboard.js b/frappe/public/js/frappe/form/dashboard.js index d27aa619e0..ca2b4ab9bd 100644 --- a/frappe/public/js/frappe/form/dashboard.js +++ b/frappe/public/js/frappe/form/dashboard.js @@ -418,9 +418,8 @@ frappe.ui.form.Dashboard = Class.extend({ this.graph_area.empty().removeClass('hidden'); $.extend(args, { parent: me.graph_area, - width: 710, - height: 140, - mode: 'line-graph' + mode: 'line', + height: 140 }); new frappe.ui.Graph(args); diff --git a/frappe/public/js/frappe/list/list_renderer.js b/frappe/public/js/frappe/list/list_renderer.js index 1a6b6aa07c..1ec5b3e37e 100644 --- a/frappe/public/js/frappe/list/list_renderer.js +++ b/frappe/public/js/frappe/list/list_renderer.js @@ -271,7 +271,10 @@ frappe.views.ListRenderer = Class.extend({ setup_filterable: function () { var me = this; + + this.list_view.wrapper && this.list_view.wrapper.on('click', '.result-list .filterable', function (e) { + e.stopPropagation(); var filters = $(this).attr('data-filter').split('|'); var added = false; @@ -294,7 +297,9 @@ frappe.views.ListRenderer = Class.extend({ me.list_view.refresh(true); } }); - this.wrapper.on('click', '.list-item', function (e) { + + this.list_view.wrapper && + this.list_view.wrapper.on('click', '.list-item', function (e) { // don't open in case of checkbox, like, filterable if ($(e.target).hasClass('filterable') || $(e.target).hasClass('octicon-heart') diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 05facf346d..a7024a9fb7 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -599,9 +599,9 @@ frappe.views.ListView = frappe.ui.BaseList.extend({ }, true); } if (frappe.model.can_set_user_permissions(this.doctype)) { - this.page.add_menu_item(__('User Permissions Manager'), function () { - frappe.set_route('user-permissions', { - doctype: me.doctype + this.page.add_menu_item(__('User Permissions'), function () { + frappe.set_route('List', 'User Permission', { + allow: me.doctype }); }, true); } diff --git a/frappe/public/js/frappe/model/create_new.js b/frappe/public/js/frappe/model/create_new.js index 04496c6238..a25c3e70ca 100644 --- a/frappe/public/js/frappe/model/create_new.js +++ b/frappe/public/js/frappe/model/create_new.js @@ -127,8 +127,10 @@ $.extend(frappe.model, { var user_default = ""; var user_permissions = frappe.defaults.get_user_permissions(); var meta = frappe.get_meta(doc.doctype); - var has_user_permissions = (df.fieldtype==="Link" && user_permissions - && df.ignore_user_permissions != 1 && user_permissions[df.options]); + var has_user_permissions = (df.fieldtype==="Link" + && user_permissions + && df.ignore_user_permissions != 1 + && user_permissions[df.options]); // don't set defaults for "User" link field using User Permissions! if (df.fieldtype==="Link" && df.options!=="User") { diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js index adb2e845a6..7b22c5fd47 100644 --- a/frappe/public/js/frappe/model/model.js +++ b/frappe/public/js/frappe/model/model.js @@ -112,7 +112,6 @@ $.extend(frappe.model, { localStorage["_doctype:" + doctype] = JSON.stringify(r.docs); } frappe.model.init_doctype(doctype); - frappe.defaults.set_user_permissions(r.user_permissions); if(r.user_settings) { // remember filters and other settings from last view diff --git a/frappe/public/js/frappe/ui/filters/filters.js b/frappe/public/js/frappe/ui/filters/filters.js index b338d23f3d..0d74e61b84 100644 --- a/frappe/public/js/frappe/ui/filters/filters.js +++ b/frappe/public/js/frappe/ui/filters/filters.js @@ -87,6 +87,7 @@ frappe.ui.FilterList = Class.extend({ } var filter = this.push_new_filter(doctype, fieldname, condition, value); + if (!filter) return; if(this.wrapper.find('.clear-filters').hasClass("hide")) { this.wrapper.find('.clear-filters').removeClass("hide"); diff --git a/frappe/public/js/frappe/ui/graph.js b/frappe/public/js/frappe/ui/graph.js deleted file mode 100644 index 2347f13b0d..0000000000 --- a/frappe/public/js/frappe/ui/graph.js +++ /dev/null @@ -1,308 +0,0 @@ -// specific_values = [ -// { -// name: "Average", -// line_type: "dashed", // "dashed" or "solid" -// value: 10 -// }, - -// summary_values = [ -// { -// name: "Total", -// color: 'blue', // Indicator colors: 'grey', 'blue', 'red', 'green', 'orange', -// // 'purple', 'darkgrey', 'black', 'yellow', 'lightblue' -// value: 80 -// } -// ] - -frappe.ui.Graph = class Graph { - constructor({ - parent = null, - - width = 0, height = 0, - title = '', subtitle = '', - - y_values = [], - x_points = [], - - specific_values = [], - summary_values = [], - - color = '', - mode = '', - } = {}) { - - if(Object.getPrototypeOf(this) === frappe.ui.Graph.prototype) { - if(mode === 'line-graph') { - return new frappe.ui.LineGraph(arguments[0]); - } else if(mode === 'bar-graph') { - return new frappe.ui.BarGraph(arguments[0]); - } - } - - this.parent = parent; - - this.width = width; - this.height = height; - - this.title = title; - this.subtitle = subtitle; - - this.y_values = y_values; - this.x_points = x_points; - - this.specific_values = specific_values; - this.summary_values = summary_values; - - this.color = color; - this.mode = mode; - - this.$graph = null; - - frappe.require("assets/frappe/js/lib/snap.svg-min.js", this.setup.bind(this)); - } - - setup() { - this.setup_container(); - this.refresh(); - } - - refresh() { - this.setup_values(); - this.setup_components(); - this.make_y_axis(); - this.make_x_axis(); - this.make_units(); - if(this.specific_values.length > 0) { - this.show_specific_values(); - } - this.setup_group(); - - if(this.summary_values.length > 0) { - this.show_summary(); - } - } - - setup_container() { - this.container = $('
    ') - .addClass('graph-container') - .append($(`
    ${this.title}
    `)) - .append($(`
    ${this.subtitle}
    `)) - .append($(`
    `)) - .append($(`
    `)) - .appendTo(this.parent); - - let $graphics = this.container.find('.graphics'); - this.$stats_container = this.container.find('.stats-container'); - - this.$graph = $('
    ') - .addClass(this.mode) - .appendTo($graphics); - - this.$svg = $(``); - this.$graph.append(this.$svg); - - this.snap = new Snap(this.$svg[0]); - } - - setup_values() { - this.upper_graph_bound = this.get_upper_limit_and_parts(this.y_values)[0]; - this.y_axis = this.get_y_axis(this.y_values); - this.avg_unit_width = (this.width-100)/(this.x_points.length - 1); - } - - setup_components() { - this.y_axis_group = this.snap.g().attr({ - class: "y axis" - }); - - this.x_axis_group = this.snap.g().attr({ - class: "x axis" - }); - - this.graph_list = this.snap.g().attr({ - class: "data-points", - }); - - this.specific_y_lines = this.snap.g().attr({ - class: "specific axis", - }); - } - - setup_group() { - this.snap.g( - this.y_axis_group, - this.x_axis_group, - this.graph_list, - this.specific_y_lines - ).attr({ - transform: "translate(60, 10)" // default - }); - } - - show_specific_values() { - this.specific_values.map(d => { - this.specific_y_lines.add(this.snap.g( - this.snap.line(0, 0, this.width - 70, 0).attr({ - class: d.line_type === "dashed" ? "dashed": "" - }), - this.snap.text(this.width - 95, 0, d.name.toUpperCase()).attr({ - dy: ".32em", - class: "specific-value", - }) - ).attr({ - class: "tick", - transform: `translate(0, ${100 - 100/(this.upper_graph_bound/d.value) })` - })); - }); - } - - show_summary() { - this.summary_values.map(d => { - this.$stats_container.append($(`
    - ${d.name}: ${d.value} -
    `)); - }); - } - - // Helpers - get_upper_limit_and_parts(array) { - let specific_values = this.specific_values.map(d => d.value); - let max_val = parseInt(Math.max(...array, ...specific_values)); - if((max_val+"").length <= 1) { - return [10, 5]; - } else { - let multiplier = Math.pow(10, ((max_val+"").length - 1)); - let significant = Math.ceil(max_val/multiplier); - if(significant % 2 !== 0) significant++; - let parts = (significant < 5) ? significant : significant/2; - return [significant * multiplier, parts]; - } - } - - get_y_axis(array) { - let upper_limit, parts; - [upper_limit, parts] = this.get_upper_limit_and_parts(array); - let y_axis = []; - for(var i = 0; i <= parts; i++){ - y_axis.push(upper_limit / parts * i); - } - return y_axis; - } -}; - -frappe.ui.BarGraph = class BarGraph extends frappe.ui.Graph { - constructor(args = {}) { - super(args); - } - - setup_values() { - super.setup_values(); - this.avg_unit_width = (this.width-50)/(this.x_points.length + 2); - } - - make_y_axis() { - this.y_axis.map((point) => { - this.y_axis_group.add(this.snap.g( - this.snap.line(0, 0, this.width, 0), - this.snap.text(-3, 0, point+"").attr({ - dy: ".32em", - class: "y-value-text" - }) - ).attr({ - class: "tick", - transform: `translate(0, ${100 - (100/(this.y_axis.length-1) * this.y_axis.indexOf(point)) })` - })); - }); - } - - make_x_axis() { - this.x_axis_group.attr({ - transform: "translate(0,100)" - }); - this.x_points.map((point, i) => { - this.x_axis_group.add(this.snap.g( - this.snap.line(0, 0, 0, 6), - this.snap.text(0, 9, point).attr({ - dy: ".71em", - class: "x-value-text" - }) - ).attr({ - class: "tick x-axis-label", - transform: `translate(${ ((this.avg_unit_width - 5)*3/2) + i * (this.avg_unit_width + 5) }, 0)` - })); - }); - } - - make_units() { - this.y_values.map((value, i) => { - this.graph_list.add(this.snap.g( - this.snap.rect( - 0, - (100 - 100/(this.upper_graph_bound/value)), - this.avg_unit_width - 5, - 100/(this.upper_graph_bound/value) - ) - ).attr({ - class: "bar mini", - transform: `translate(${ (this.avg_unit_width - 5) + i * (this.avg_unit_width + 5) }, 0)`, - })); - }); - } -}; - -frappe.ui.LineGraph = class LineGraph extends frappe.ui.Graph { - constructor(args = {}) { - super(args); - } - - make_y_axis() { - this.y_axis.map((point) => { - this.y_axis_group.add(this.snap.g( - this.snap.line(0, 0, -6, 0), - this.snap.text(-9, 0, point+"").attr({ - dy: ".32em", - class: "y-value-text" - }) - ).attr({ - class: "tick", - transform: `translate(0, ${100 - (100/(this.y_axis.length-1) - * this.y_axis.indexOf(point)) })` - })); - }); - } - - make_x_axis() { - this.x_axis_group.attr({ - transform: "translate(0,-7)" - }); - this.x_points.map((point, i) => { - this.x_axis_group.add(this.snap.g( - this.snap.line(0, 0, 0, this.height - 25), - this.snap.text(0, this.height - 15, point).attr({ - dy: ".71em", - class: "x-value-text" - }) - ).attr({ - class: "tick", - transform: `translate(${ i * this.avg_unit_width }, 0)` - })); - }); - } - - make_units() { - let points_list = []; - this.y_values.map((value, i) => { - let x = i * this.avg_unit_width; - let y = (100 - 100/(this.upper_graph_bound/value)); - this.graph_list.add(this.snap.circle( x, y, 4)); - points_list.push(x+","+y); - }); - - this.make_path("M"+points_list.join("L")); - } - - make_path(path_str) { - this.graph_list.prepend(this.snap.path(path_str)); - } - -}; diff --git a/frappe/public/js/frappe/ui/graphs.js b/frappe/public/js/frappe/ui/graphs.js new file mode 100644 index 0000000000..f45ced98ba --- /dev/null +++ b/frappe/public/js/frappe/ui/graphs.js @@ -0,0 +1,569 @@ +// specific_values = [ +// { +// name: "Average", +// line_type: "dashed", // "dashed" or "solid" +// value: 10 +// }, + +// summary = [ +// { +// name: "Total", +// color: 'blue', // Indicator colors: 'grey', 'blue', 'red', 'green', 'orange', +// // 'purple', 'darkgrey', 'black', 'yellow', 'lightblue' +// value: 80 +// } +// ] + +// Graph: Abstract object +frappe.ui.Graph = class Graph { + constructor({ + parent = null, + height = 240, + + title = '', subtitle = '', + + y = [], + x = [], + + specific_values = [], + summary = [], + + color = 'blue', + mode = '', + }) { + + if(Object.getPrototypeOf(this) === frappe.ui.Graph.prototype) { + if(mode === 'line') { + return new frappe.ui.LineGraph(arguments[0]); + } else if(mode === 'bar') { + return new frappe.ui.BarGraph(arguments[0]); + } else if(mode === 'percentage') { + return new frappe.ui.PercentageGraph(arguments[0]); + } + } + + this.parent = parent; + this.base_height = height; + this.height = height - 40; + + this.translate_x = 60; + this.translate_y = 10; + + this.title = title; + this.subtitle = subtitle; + + this.y = y; + this.x = x; + + this.specific_values = specific_values; + this.summary = summary; + + this.color = color; + this.mode = mode; + + this.$graph = null; + + // Validate all arguments + + frappe.require("assets/frappe/js/lib/snap.svg-min.js", this.setup.bind(this)); + } + + setup() { + this.bind_window_event(); + this.refresh(); + } + + bind_window_event() { + $(window).on('resize orientationChange', () => { + this.refresh(); + }); + } + + refresh() { + + this.base_width = this.parent.width() - 20; + this.width = this.base_width - 100; + + this.setup_container(); + + this.setup_values(); + + this.setup_utils(); + + this.setup_components(); + this.make_graph_components(); + + this.make_tooltip(); + + if(this.summary.length > 0) { + this.show_custom_summary(); + } else { + this.show_summary(); + } + } + + setup_container() { + // Graph needs a dedicated parent element + this.parent.empty(); + + this.container = $('
    ') + .addClass('graph-container') + .append($(`
    ${this.title}
    `)) + .append($(`
    ${this.subtitle}
    `)) + .append($(`
    `)) + .append($(`
    `)) + .appendTo(this.parent); + + this.$graphics = this.container.find('.graph-graphics'); + this.$stats_container = this.container.find('.graph-stats-container'); + + this.$graph = $('
    ') + .addClass(this.mode + '-graph') + .appendTo(this.$graphics); + + this.$graph.append(this.make_graph_area()); + } + + make_graph_area() { + this.$svg = $(``); + this.snap = new Snap(this.$svg[0]); + return this.$svg; + } + + setup_values() { + // Multiplier + let all_values = this.specific_values.map(d => d.value); + this.y.map(d => { + all_values = all_values.concat(d.values); + }); + [this.upper_limit, this.parts] = this.get_upper_limit_and_parts(all_values); + this.multiplier = this.height / this.upper_limit; + + // Baselines + this.set_avg_unit_width_and_x_offset(); + + this.x_axis_values = this.x.values.map((d, i) => this.x_offset + i * this.avg_unit_width); + this.y_axis_values = this.get_y_axis_values(this.upper_limit, this.parts); + + // Data points + this.y.map(d => { + d.y_tops = d.values.map( val => this.height - val * this.multiplier ); + d.data_units = []; + }); + + this.calc_min_tops(); + } + + set_avg_unit_width_and_x_offset() { + this.avg_unit_width = this.width/(this.x.values.length - 1); + this.x_offset = 0; + } + + calc_min_tops() { + this.y_min_tops = new Array(this.x_axis_values.length).fill(9999); + this.y.map(d => { + d.y_tops.map( (y_top, i) => { + if(y_top < this.y_min_tops[i]) { + this.y_min_tops[i] = y_top; + } + }); + }); + } + + setup_components() { + this.y_axis_group = this.snap.g().attr({ class: "y axis" }); + this.x_axis_group = this.snap.g().attr({ class: "x axis" }); + this.data_units = this.snap.g().attr({ class: "graph-data-points" }); + this.specific_y_lines = this.snap.g().attr({ class: "specific axis" }); + } + + make_graph_components() { + this.make_y_axis(); + this.make_x_axis(); + + this.y.map((d, i) => { + this.make_units(d.y_tops, d.color, i); + this.make_path(d); + }); + + if(this.specific_values.length > 0) { + this.show_specific_values(); + } + this.setup_group(); + } + + setup_group() { + this.snap.g( + this.y_axis_group, + this.x_axis_group, + this.data_units, + this.specific_y_lines + ).attr({ + transform: `translate(${this.translate_x}, ${this.translate_y})` + }); + } + + // make HORIZONTAL lines for y values + make_y_axis() { + let width, text_end_at = -9, label_class = '', start_at = 0; + if(this.y_axis_mode === 'span') { // long spanning lines + width = this.width + 6; + start_at = -6; + } else if(this.y_axis_mode === 'tick'){ // short label lines + width = -6; + label_class = 'y-axis-label'; + } + + this.y_axis_values.map((point) => { + this.y_axis_group.add(this.snap.g( + this.snap.line(start_at, 0, width, 0), + this.snap.text(text_end_at, 0, point+"").attr({ + dy: ".32em", + class: "y-value-text" + }) + ).attr({ + class: `tick ${label_class}`, + transform: `translate(0, ${this.height - point * this.multiplier })` + })); + }); + } + + // make VERTICAL lines for x values + make_x_axis() { + let start_at, height, text_start_at, label_class = ''; + if(this.x_axis_mode === 'span') { // long spanning lines + start_at = -7; + height = this.height + 15; + text_start_at = this.height + 25; + } else if(this.x_axis_mode === 'tick'){ // short label lines + start_at = this.height; + height = 6; + text_start_at = 9; + label_class = 'x-axis-label'; + } + + this.x_axis_group.attr({ + transform: `translate(0,${start_at})` + }); + this.x.values.map((point, i) => { + this.x_axis_group.add(this.snap.g( + this.snap.line(0, 0, 0, height), + this.snap.text(0, text_start_at, point).attr({ + dy: ".71em", + class: "x-value-text" + }) + ).attr({ + class: `tick ${label_class}`, + transform: `translate(${ this.x_axis_values[i] }, 0)` + })); + }); + } + + make_units(y_values, color, dataset_index) { + let d = this.unit_args; + y_values.map((y, i) => { + let data_unit = this.draw[d.type](this.x_axis_values[i], + y, d.args, color, dataset_index); + this.data_units.add(data_unit); + this.y[dataset_index].data_units.push(data_unit); + }); + } + + make_path() { } + + make_tooltip() { + this.tip = $(`
    + +
      +
    +
    `).attr({ + style: `top: 0px; left: 0px; opacity: 0; pointer-events: none;` + }).appendTo(this.$graphics); + + this.tip_title = this.tip.find('.title'); + this.tip_data_point_list = this.tip.find('.data-point-list'); + + this.bind_tooltip(); + } + + bind_tooltip() { + this.$graphics.on('mousemove', (e) => { + let offset = $(this.$graphics).offset(); + var relX = e.pageX - offset.left - this.translate_x; + var relY = e.pageY - offset.top - this.translate_y; + + if(relY < this.height) { + for(var i=this.x_axis_values.length - 1; i >= 0 ; i--) { + let x_val = this.x_axis_values[i]; + if(relX > x_val - this.avg_unit_width/2) { + let x = x_val - this.tip.width()/2 + this.translate_x; + let y = this.y_min_tops[i] - this.tip.height() + this.translate_y; + + this.fill_tooltip(i); + + this.tip.attr({ + style: `top: ${y}px; left: ${x-0.5}px; opacity: 1; pointer-events: none;` + }); + break; + } + } + } else { + this.tip.attr({ + style: `top: 0px; left: 0px; opacity: 0; pointer-events: none;` + }); + } + }); + + this.$graphics.on('mouseleave', () => { + this.tip.attr({ + style: `top: 0px; left: 0px; opacity: 0; pointer-events: none;` + }); + }); + } + + fill_tooltip(i) { + this.tip_title.html(this.x.formatted && this.x.formatted.length>0 + ? this.x.formatted[i] : this.x.values[i]); + this.tip_data_point_list.empty(); + this.y.map(y_set => { + let $li = $(`
  5. + + ${y_set.formatted ? y_set.formatted[i] : y_set.values[i]} + + ${y_set.title ? y_set.title : '' } +
  6. `).addClass(`border-top ${y_set.color}`); + this.tip_data_point_list.append($li); + }); + } + + show_specific_values() { + this.specific_values.map(d => { + this.specific_y_lines.add(this.snap.g( + this.snap.line(0, 0, this.width, 0).attr({ + class: d.line_type === "dashed" ? "graph-dashed": "" + }), + this.snap.text(this.width + 5, 0, d.name.toUpperCase()).attr({ + dy: ".32em", + class: "specific-value", + }) + ).attr({ + class: "tick", + transform: `translate(0, ${this.height - d.value * this.multiplier })` + })); + }); + } + + show_summary() { } + + show_custom_summary() { + this.summary.map(d => { + this.$stats_container.append($(`
    + ${d.name}: ${d.value} +
    `)); + }); + } + + change_values(new_y) { + let u = this.unit_args; + this.y.map((d, i) => { + let new_d = new_y[i]; + new_d.y_tops = new_d.values.map(val => this.height - val * this.multiplier); + + // below is equal to this.y[i].data_units.. + d.data_units.map((unit, j) => { + let current_y_top = d.y_tops[j]; + let current_height = this.height - current_y_top; + + let new_y_top = new_d.y_tops[j]; + let new_height = current_height - (new_y_top - current_y_top); + + this.animate[u.type](unit, new_y_top, {new_height: new_height}); + }); + }); + + // Replace values and formatted and tops + this.y.map((d, i) => { + let new_d = new_y[i]; + [d.values, d.formatted, d.y_tops] = [new_d.values, new_d.formatted, new_d.y_tops]; + }); + + this.calc_min_tops(); + + // create new x,y pair string and animate path + if(this.y[0].path) { + new_y.map((e, i) => { + let new_points_list = e.y_tops.map((y, i) => (this.x_axis_values[i] + ',' + y)); + let new_path_str = "M"+new_points_list.join("L"); + this.y[i].path.animate({d:new_path_str}, 300, mina.easein); + }); + } + } + + // Helpers + get_strwidth(string) { + return string.length * 8; + } + + get_upper_limit_and_parts(array) { + let max_val = parseInt(Math.max(...array)); + if((max_val+"").length <= 1) { + return [10, 5]; + } else { + let multiplier = Math.pow(10, ((max_val+"").length - 1)); + let significant = Math.ceil(max_val/multiplier); + if(significant % 2 !== 0) significant++; + let parts = (significant < 5) ? significant : significant/2; + return [significant * multiplier, parts]; + } + } + + get_y_axis_values(upper_limit, parts) { + let y_axis = []; + for(var i = 0; i <= parts; i++){ + y_axis.push(upper_limit / parts * i); + } + return y_axis; + } + + // Objects + setup_utils() { + this.draw = { + 'bar': (x, y, args, color, index) => { + let total_width = this.avg_unit_width - args.space_width; + let start_x = x - total_width/2; + + let width = total_width / args.no_of_datasets; + let current_x = start_x + width * index; + return this.snap.rect(current_x, y, width, this.height - y).attr({ + class: `bar mini fill ${color}` + }); + }, + 'dot': (x, y, args, color) => { + return this.snap.circle(x, y, args.radius).attr({ + class: `fill ${color}` + }); + } + }; + + this.animate = { + 'bar': (bar, new_y, args) => { + bar.animate({height: args.new_height, y: new_y}, 300, mina.easein); + }, + 'dot': (dot, new_y) => { + dot.animate({cy: new_y}, 300, mina.easein); + } + }; + } +}; + +frappe.ui.BarGraph = class BarGraph extends frappe.ui.Graph { + constructor(args = {}) { + super(args); + } + + setup_values() { + var me = this; + super.setup_values(); + this.x_offset = this.avg_unit_width; + this.y_axis_mode = 'span'; + this.x_axis_mode = 'tick'; + this.unit_args = { + type: 'bar', + args: { + space_width: this.y.length > 1 ? + me.avg_unit_width/2 : me.avg_unit_width/8, + no_of_datasets: this.y.length + } + }; + } + + set_avg_unit_width_and_x_offset() { + this.avg_unit_width = this.width/(this.x.values.length + 1); + this.x_offset = this.avg_unit_width; + } +}; + +frappe.ui.LineGraph = class LineGraph extends frappe.ui.Graph { + constructor(args = {}) { + super(args); + } + + setup_values() { + super.setup_values(); + this.y_axis_mode = 'tick'; + this.x_axis_mode = 'span'; + this.unit_args = { + type: 'dot', + args: { radius: 4 } + }; + } + + make_path(d) { + let points_list = d.y_tops.map((y, i) => (this.x_axis_values[i] + ',' + y)); + let path_str = "M"+points_list.join("L"); + d.path = this.snap.path(path_str).attr({class: `stroke ${d.color}`}); + this.data_units.prepend(d.path); + } +}; + +frappe.ui.PercentageGraph = class PercentageGraph extends frappe.ui.Graph { + constructor(args = {}) { + super(args); + } + + make_graph_area() { + this.$graphics.addClass('graph-focus-margin'); + this.$stats_container.addClass('graph-focus-margin').attr({ + style: `padding-top: 0px; margin-bottom: 30px;` + }); + this.$div = $(`
    +
    +
    `); + this.$chart = this.$div.find('.progress-chart'); + return this.$div; + } + + setup_values() { + this.x.totals = this.x.values.map((d, i) => { + let total = 0; + this.y.map(e => { + total += e.values[i]; + }); + return total; + }); + + // Calculate x unit distances for tooltips + } + + setup_utils() { } + setup_components() { + this.$percentage_bar = $(`
    +
    `).appendTo(this.$chart); + } + + make_graph_components() { + let grand_total = this.x.totals.reduce((a, b) => a + b, 0); + this.x.units = []; + this.x.totals.map((total, i) => { + let $part = $(`
    `); + this.x.units.push($part); + this.$percentage_bar.append($part); + }); + } + + make_tooltip() { } + + show_summary() { + let values = this.x.formatted.length > 0 ? this.x.formatted : this.x.values; + this.x.totals.map((d, i) => { + this.$stats_container.append($(`
    + + ${values[i]}: + ${d} + +
    `)); + }); + } +}; diff --git a/frappe/public/js/frappe/ui/messages.js b/frappe/public/js/frappe/ui/messages.js index 35f3e93f8a..79c1345cfa 100644 --- a/frappe/public/js/frappe/ui/messages.js +++ b/frappe/public/js/frappe/ui/messages.js @@ -141,17 +141,17 @@ frappe.msgprint = function(msg, title) { msg = replace_newlines(data.message); } + var msg_exists = false; if(data.clear) { msg_dialog.msg_area.empty(); - var msg_exists = false; } else { - var msg_exists = msg_dialog.msg_area.html(); + msg_exists = msg_dialog.msg_area.html(); } if(data.title || !msg_exists) { // set title only if it is explicitly given // and no existing title exists - msg_dialog.set_title(data.title || __('Message')) + msg_dialog.set_title(data.title || __('Message')); } // show / hide indicator diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 3162cd5db1..7e0bc14a13 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -43,7 +43,7 @@ frappe.views.QueryReport = Class.extend({ this.wrapper = $("
    ").appendTo(this.page.main); $('\ \ -
    \ +
    \ - +
    -
    + App Name @@ -37,10 +37,10 @@

    Contents

    diff --git a/frappe/templates/autodoc/docs_home.html b/frappe/templates/autodoc/docs_home.html index 602c9fb5b0..9d38b0605b 100644 --- a/frappe/templates/autodoc/docs_home.html +++ b/frappe/templates/autodoc/docs_home.html @@ -5,57 +5,15 @@ {% if app.style %} {{ app.style }} {% endif %} - -
    -
    -
    -
    -

    {{ app.headline }}

    -

    {{ app.sub_heading }}

    -
    -
    -
    - -
    -
    -
    -
    -
    - - -{% if app.long_description %} -
    -
    -
    - {{ app.long_description|markdown }} -
    -
    -
    +{% if app.headline %} +

    {{ app.headline }}

    +{% endif %} +{% if app.sub_heading %} +

    {{ app.sub_heading }}

    {% endif %} -{% if not app.hide_install %} -
    -
    -
    -

    Install

    -

    From your site

    -

    To install this app, login to your site and click on "Installer". Search for {{ app.title }} and click on "Install"

    -

    Using Bench

    -

    Go to your bench folder and setup the new app

    -
    $ bench get-app {{app.name}} {{app.source_link}}
    -$ bench new-site testsite
    -$ bench --site testsite install-app {{app.name}}
    -

    Login to your site to configure the app.

    -

    Detailed Installation Steps

    -
    -
    -

    Author

    - -

    {{ app.publisher }} ({{ app.email }})

    -
    -
    -
    +{% if app.long_description %} +{{ app.long_description|markdown }} {% endif %} diff --git a/frappe/templates/autodoc/doctype.html b/frappe/templates/autodoc/doctype.html index 3236f0c994..b851fc52e8 100644 --- a/frappe/templates/autodoc/doctype.html +++ b/frappe/templates/autodoc/doctype.html @@ -1,10 +1,12 @@ + {% from "templates/autodoc/macros.html" import automodule, version, source_link, doctype_link %} {% set doc = frappe.get_doc("DocType", doctype) %} {% set controller = autodoc.get_controller(doctype) %} +

    {{ doctype }}

    {{ version(doctype) }} {{ source_link(app, app.name + "/" + scrub(doc.module) @@ -22,7 +24,7 @@

    Fields

    - +
    diff --git a/frappe/templates/autodoc/macros.html b/frappe/templates/autodoc/macros.html index 29e1d520b8..1401454967 100644 --- a/frappe/templates/autodoc/macros.html +++ b/frappe/templates/autodoc/macros.html @@ -59,7 +59,7 @@ {% macro doctype_link(app, doctype) %} {% set module = frappe.db.get_value("DocType", doctype, "module") %} {% if doctype and module %} -{{ doctype }} {% endif %} {% endmacro %} diff --git a/frappe/templates/autodoc/models_home.html b/frappe/templates/autodoc/models_home.html index c9f960d416..44c5057ca2 100644 --- a/frappe/templates/autodoc/models_home.html +++ b/frappe/templates/autodoc/models_home.html @@ -1,6 +1,9 @@ + {% from "templates/autodoc/macros.html" import source_link, version %} + +

    {{ app.title }}: Models (DocTypes)

    {{ version(app.name) }} {{ source_link(app, app.name, True) }} diff --git a/frappe/templates/autodoc/module_home.html b/frappe/templates/autodoc/module_home.html index 3e75f3aeca..c10994841b 100644 --- a/frappe/templates/autodoc/module_home.html +++ b/frappe/templates/autodoc/module_home.html @@ -1,6 +1,9 @@ + {% from "templates/autodoc/macros.html" import source_link, version %} + +

    Module {{ name }}

    {{ version(app.name) }} {{ source_link(app, app.name + "/" + scrub(name), True) }} diff --git a/frappe/templates/autodoc/package_index.html b/frappe/templates/autodoc/package_index.html index 0381e07af0..9107996dfa 100644 --- a/frappe/templates/autodoc/package_index.html +++ b/frappe/templates/autodoc/package_index.html @@ -1,6 +1,9 @@ + {% from "templates/autodoc/macros.html" import source_link, version %} + +

    {{ title }}

    {{ version(app.name) }} {{ source_link(app, title, True) }} diff --git a/frappe/templates/autodoc/pymodule.html b/frappe/templates/autodoc/pymodule.html index ab6301ae85..537b37ead0 100644 --- a/frappe/templates/autodoc/pymodule.html +++ b/frappe/templates/autodoc/pymodule.html @@ -1,7 +1,10 @@ + {%- from "templates/autodoc/macros.html" import automodule, source_link, version -%} + +

    {{ name }}

    {{ version(app.name) }} {{ source_link(app, full_module_name.replace(".", "/") + ".py") }} diff --git a/frappe/templates/includes/blog/blog.html b/frappe/templates/includes/blog/blog.html index 18617a3cf7..01e3e44d6b 100644 --- a/frappe/templates/includes/blog/blog.html +++ b/frappe/templates/includes/blog/blog.html @@ -5,6 +5,25 @@ {% block hero %}{% endblock %} {% block page_content %} +
    diff --git a/frappe/templates/includes/comments/comment.html b/frappe/templates/includes/comments/comment.html index 2d7e1603ed..e0f923b2e0 100644 --- a/frappe/templates/includes/comments/comment.html +++ b/frappe/templates/includes/comments/comment.html @@ -1,4 +1,4 @@ -
    +
    diff --git a/frappe/templates/includes/full_index.html b/frappe/templates/includes/full_index.html index 1f798c66ed..a7443c482a 100644 --- a/frappe/templates/includes/full_index.html +++ b/frappe/templates/includes/full_index.html @@ -2,10 +2,12 @@
      {% for item in children_map[route] %}
    1. - {{ item.title }} + {{ item.title }} + {# {% if children_map[item.route] %} {{ make_item_list(item.route, children_map) }} {% endif %} + #}
    2. {% endfor %}
    diff --git a/frappe/templates/includes/web_sidebar.html b/frappe/templates/includes/web_sidebar.html index 764ce4d277..d452e8af3f 100644 --- a/frappe/templates/includes/web_sidebar.html +++ b/frappe/templates/includes/web_sidebar.html @@ -22,7 +22,7 @@ {% endif %} {% for item in sidebar_items -%}
    diff --git a/frappe/test_runner.py b/frappe/test_runner.py index a50febabf3..c4fa2f6f47 100644 --- a/frappe/test_runner.py +++ b/frappe/test_runner.py @@ -266,12 +266,12 @@ def make_test_records_for_doctype(doctype, verbose=0, force=False): frappe.local.test_objects[doctype] += test_module._make_test_records(verbose) elif hasattr(test_module, "test_records"): - frappe.local.test_objects[doctype] += make_test_objects(doctype, test_module.test_records, verbose) + frappe.local.test_objects[doctype] += make_test_objects(doctype, test_module.test_records, verbose, force) else: test_records = frappe.get_test_records(doctype) if test_records: - frappe.local.test_objects[doctype] += make_test_objects(doctype, test_records, verbose) + frappe.local.test_objects[doctype] += make_test_objects(doctype, test_records, verbose, force) elif verbose: print_mandatory_fields(doctype) diff --git a/frappe/tests/test_goal.py b/frappe/tests/test_goal.py index 5fe490ab56..6e94858785 100644 --- a/frappe/tests/test_goal.py +++ b/frappe/tests/test_goal.py @@ -31,4 +31,4 @@ class TestGoal(unittest.TestCase): frappe.db.set_value('Event', docname, 'description', 1) data = get_monthly_goal_graph_data('Test', 'Event', docname, 'description', 'description', 'description', 'Event', '', 'description', 'creation', 'starts_on = "2014-01-01"', 'count') - self.assertEquals(float(data['y_values'][-1]), 1) + self.assertEquals(float(data['y'][0]['values'][-1]), 1) diff --git a/frappe/tests/test_permissions.py b/frappe/tests/test_permissions.py index 6ad0e84f07..d54c484eba 100644 --- a/frappe/tests/test_permissions.py +++ b/frappe/tests/test_permissions.py @@ -9,9 +9,11 @@ import frappe.defaults import unittest import json import frappe.model.meta -from frappe.core.page.user_permissions.user_permissions import add, remove, get_permissions -from frappe.permissions import clear_user_permissions_for_doctype, get_doc_permissions +from frappe.permissions import (add_user_permission, remove_user_permission, + clear_user_permissions_for_doctype, get_doc_permissions, add_permission, + get_valid_perms) from frappe.core.page.permission_manager.permission_manager import update, reset +from frappe.test_runner import make_test_records_for_doctype test_records = frappe.get_test_records('Blog Post') @@ -24,6 +26,7 @@ class TestPermissions(unittest.TestCase): user = frappe.get_doc("User", "test1@example.com") user.add_roles("Website Manager") + user.add_roles("System Manager") user = frappe.get_doc("User", "test2@example.com") user.add_roles("Blogger") @@ -36,6 +39,8 @@ class TestPermissions(unittest.TestCase): reset('Contact') reset('Salutation') + frappe.db.sql('delete from `tabUser Permission`') + self.set_ignore_user_permissions_if_missing(0) frappe.set_user("test1@example.com") @@ -78,7 +83,7 @@ class TestPermissions(unittest.TestCase): def test_user_permissions_in_doc(self): self.set_user_permission_doctypes(["Blog Category"]) - frappe.permissions.add_user_permission("Blog Category", "_Test Blog Category 1", + add_user_permission("Blog Category", "_Test Blog Category 1", "test2@example.com") frappe.set_user("test2@example.com") @@ -94,7 +99,7 @@ class TestPermissions(unittest.TestCase): def test_user_permissions_in_report(self): self.set_user_permission_doctypes(["Blog Category"]) - frappe.permissions.add_user_permission("Blog Category", "_Test Blog Category 1", "test2@example.com") + add_user_permission("Blog Category", "_Test Blog Category 1", "test2@example.com") frappe.set_user("test2@example.com") names = [d.name for d in frappe.get_list("Blog Post", fields=["name", "blog_category"])] @@ -103,7 +108,7 @@ class TestPermissions(unittest.TestCase): self.assertFalse("-test-blog-post" in names) def test_default_values(self): - frappe.permissions.add_user_permission("Blog Category", "_Test Blog Category 1", "test2@example.com") + add_user_permission("Blog Category", "_Test Blog Category 1", "test2@example.com") frappe.set_user("test2@example.com") doc = frappe.new_doc("Blog Post") @@ -139,14 +144,14 @@ class TestPermissions(unittest.TestCase): def test_set_user_permissions(self): frappe.set_user("test1@example.com") - add("test2@example.com", "Blog Post", "-test-blog-post") + add_user_permission("Blog Post", "-test-blog-post", "test2@example.com") def test_not_allowed_to_set_user_permissions(self): frappe.set_user("test2@example.com") # this user can't add user permissions - self.assertRaises(frappe.PermissionError, add, - "test2@example.com", "Blog Post", "-test-blog-post") + self.assertRaises(frappe.PermissionError, add_user_permission, + "Blog Post", "-test-blog-post", "test2@example.com") def test_read_if_explicit_user_permissions_are_set(self): self.set_user_permission_doctypes(["Blog Post"]) @@ -165,13 +170,12 @@ class TestPermissions(unittest.TestCase): def test_not_allowed_to_remove_user_permissions(self): self.test_set_user_permissions() - defname = get_permissions("test2@example.com", "Blog Post", "-test-blog-post")[0].name frappe.set_user("test2@example.com") # user cannot remove their own user permissions - self.assertRaises(frappe.PermissionError, remove, - "test2@example.com", defname, "Blog Post", "-test-blog-post") + self.assertRaises(frappe.PermissionError, remove_user_permission, + "Blog Post", "-test-blog-post", "test2@example.com") def test_user_permissions_based_on_blogger(self): frappe.set_user("test2@example.com") @@ -181,7 +185,7 @@ class TestPermissions(unittest.TestCase): self.set_user_permission_doctypes(["Blog Post"]) frappe.set_user("test1@example.com") - add("test2@example.com", "Blog Post", "-test-blog-post") + add_user_permission("Blog Post", "-test-blog-post", "test2@example.com") frappe.set_user("test2@example.com") doc = frappe.get_doc("Blog Post", "-test-blog-post-1") @@ -199,9 +203,9 @@ class TestPermissions(unittest.TestCase): blog_post.get_field("title").set_only_once = 0 def test_user_permission_doctypes(self): - frappe.permissions.add_user_permission("Blog Category", "_Test Blog Category 1", + add_user_permission("Blog Category", "_Test Blog Category 1", "test2@example.com") - frappe.permissions.add_user_permission("Blogger", "_Test Blogger 1", + add_user_permission("Blogger", "_Test Blogger 1", "test2@example.com") frappe.set_user("test2@example.com") @@ -221,9 +225,9 @@ class TestPermissions(unittest.TestCase): def if_owner_setup(self): update('Blog Post', 'Blogger', 0, 'if_owner', 1) - frappe.permissions.add_user_permission("Blog Category", "_Test Blog Category 1", + add_user_permission("Blog Category", "_Test Blog Category 1", "test2@example.com") - frappe.permissions.add_user_permission("Blogger", "_Test Blogger 1", + add_user_permission("Blogger", "_Test Blogger 1", "test2@example.com") update('Blog Post', 'Blogger', 0, 'user_permission_doctypes', json.dumps(["Blog Category"])) @@ -231,11 +235,13 @@ class TestPermissions(unittest.TestCase): frappe.model.meta.clear_cache("Blog Post") def set_user_permission_doctypes(self, user_permission_doctypes): - set_user_permission_doctypes(doctype="Blog Post", role="Blogger", + set_user_permission_doctypes(["Blog Post"], role="Blogger", apply_user_permissions=1, user_permission_doctypes=user_permission_doctypes) def test_insert_if_owner_with_user_permissions(self): """If `If Owner` is checked for a Role, check if that document is allowed to be read, updated, submitted, etc. except be created, even if the document is restricted based on User Permissions.""" + frappe.delete_doc('Blog Post', '-test-blog-post-title') + self.set_user_permission_doctypes(["Blog Category"]) self.if_owner_setup() @@ -252,7 +258,7 @@ class TestPermissions(unittest.TestCase): self.assertRaises(frappe.PermissionError, doc.insert) frappe.set_user("Administrator") - frappe.permissions.add_user_permission("Blog Category", "_Test Blog Category", + add_user_permission("Blog Category", "_Test Blog Category", "test2@example.com") frappe.set_user("test2@example.com") @@ -273,8 +279,8 @@ class TestPermissions(unittest.TestCase): self.set_user_permission_doctypes(['Blog Category', 'Blog Post', 'Blogger']) frappe.set_user("Administrator") - frappe.permissions.add_user_permission("Blog Category", "_Test Blog Category", - "test2@example.com") + # add_user_permission("Blog Category", "_Test Blog Category", + # "test2@example.com") frappe.set_user("test2@example.com") doc = frappe.get_doc({ @@ -294,33 +300,73 @@ class TestPermissions(unittest.TestCase): self.assertTrue(doc.has_permission("write")) def test_strict_user_permissions(self): - """If `Strict User Permissions` is checked in System Settings, show records even if User Permissions are missing for a linked doctype""" - set_user_permission_doctypes(doctype="Contact", role="Sales User", + """If `Strict User Permissions` is checked in System Settings, + show records even if User Permissions are missing for a linked + doctype""" + + frappe.set_user("Administrator") + frappe.db.sql('delete from tabContact') + make_test_records_for_doctype('Contact', force=True) + + set_user_permission_doctypes("Contact", role="Sales User", apply_user_permissions=1, user_permission_doctypes=['Salutation']) - set_user_permission_doctypes(doctype="Salutation", role="All", + set_user_permission_doctypes("Salutation", role="All", apply_user_permissions=1, user_permission_doctypes=['Salutation']) - frappe.set_user("Administrator") - frappe.permissions.add_user_permission("Salutation", "Mr", "test3@example.com") + add_user_permission("Salutation", "Mr", "test3@example.com") self.set_strict_user_permissions(0) frappe.set_user("test3@example.com") - self.assertEquals(len(frappe.get_list("Contact")),2) + self.assertEquals(len(frappe.get_list("Contact")), 2) frappe.set_user("Administrator") self.set_strict_user_permissions(1) frappe.set_user("test3@example.com") - self.assertTrue(len(frappe.get_list("Contact")),1) + self.assertTrue(len(frappe.get_list("Contact")), 1) frappe.set_user("Administrator") self.set_strict_user_permissions(0) + def test_automatic_apply_user_permissions(self): + '''Test user permissions are automatically applied when a user permission + is created''' + # create a user + frappe.get_doc(dict(doctype='User', email='test_user_perm@example.com', + first_name='tester')).insert(ignore_if_duplicate=True) + frappe.get_doc(dict(doctype='Role', role_name='Test Role User Perm') + ).insert(ignore_if_duplicate=True) + + # add a permission for event + add_permission('DocType', 'Test Role User Perm') + frappe.get_doc('User', 'test_user_perm@example.com').add_roles('Test Role User Perm') -def set_user_permission_doctypes(doctype, role, apply_user_permissions, user_permission_doctypes): + + # add user permission + add_user_permission('Module Def', 'Core', 'test_user_perm@example.com', True) + + # check if user permission is applied in the new role + _perm = None + for perm in get_valid_perms('DocType', 'test_user_perm@example.com'): + if perm.role == 'Test Role User Perm': + _perm = perm + + self.assertEqual(_perm.apply_user_permissions, 1) + + # restrict by module + self.assertTrue('Module Def' in json.loads(_perm.user_permission_doctypes)) + + +def set_user_permission_doctypes(doctypes, role, apply_user_permissions, + user_permission_doctypes): user_permission_doctypes = None if not user_permission_doctypes else json.dumps(user_permission_doctypes) - update(doctype, role, 0, 'apply_user_permissions', 1) - update(doctype, role, 0, 'user_permission_doctypes', user_permission_doctypes) + if isinstance(doctypes, basestring): + doctypes = [doctypes] + + for doctype in doctypes: + update(doctype, role, 0, 'apply_user_permissions', 1) + update(doctype, role, 0, 'user_permission_doctypes', + user_permission_doctypes) - frappe.clear_cache(doctype=doctype) + frappe.clear_cache(doctype=doctype) diff --git a/frappe/tests/ui/test_linked_with.js b/frappe/tests/ui/test_linked_with.js new file mode 100644 index 0000000000..aeaced2d19 --- /dev/null +++ b/frappe/tests/ui/test_linked_with.js @@ -0,0 +1,19 @@ +QUnit.module('form'); + +QUnit.test("Test Linked With", function(assert) { + assert.expect(2); + const done = assert.async(); + + frappe.run_serially([ + () => frappe.set_route('Form', 'Module Def', 'Contacts'), + () => frappe.tests.click_page_head_item('Menu'), + () => frappe.tests.click_dropdown_item('Links'), + () => frappe.timeout(4), + () => { + assert.equal(cur_dialog.title, 'Linked With', 'Linked with dialog is opened'); + const link_tables_count = cur_dialog.$wrapper.find('.list-item-table').length; + assert.equal(link_tables_count, 2, 'Two DocTypes are linked with Contacts'); + }, + done + ]); +}); \ No newline at end of file diff --git a/frappe/tests/ui/tests.txt b/frappe/tests/ui/tests.txt index aeb562f76e..3418aa6aed 100644 --- a/frappe/tests/ui/tests.txt +++ b/frappe/tests/ui/tests.txt @@ -7,4 +7,5 @@ frappe/tests/ui/test_kanban/test_kanban_creation.js frappe/tests/ui/test_kanban/test_kanban_view.js frappe/tests/ui/test_kanban/test_kanban_filters.js frappe/tests/ui/test_kanban/test_kanban_column.js -frappe/core/doctype/report/test_query_report.js \ No newline at end of file +frappe/core/doctype/report/test_query_report.js +frappe/tests/ui/test_linked_with.js \ No newline at end of file diff --git a/frappe/utils/goal.py b/frappe/utils/goal.py index bf1b9c345e..015c225ae4 100644 --- a/frappe/utils/goal.py +++ b/frappe/utils/goal.py @@ -76,15 +76,21 @@ def get_monthly_goal_graph_data(title, doctype, docname, goal_value_field, goal_ month_to_value_dict[current_month_year] = current_month_value months = [] + months_formatted = [] values = [] + values_formatted = [] for i in xrange(0, 12): month_value = formatdate(add_months(today(), -i), "MM-yyyy") month_word = getdate(month_value).strftime('%b') + month_year = getdate(month_value).strftime('%B') + ', ' + getdate(month_value).strftime('%Y') months.insert(0, month_word) + months_formatted.insert(0, month_year) if month_value in month_to_value_dict: - values.insert(0, month_to_value_dict[month_value]) + val = month_to_value_dict[month_value] else: - values.insert(0, 0) + val = 0 + values.insert(0, val) + values_formatted.insert(0, format_value(val, meta.get_field(goal_total_field), doc)) specific_values = [] summary_values = [ @@ -119,10 +125,20 @@ def get_monthly_goal_graph_data(title, doctype, docname, goal_value_field, goal_ data = { 'title': title, # 'subtitle': - 'y_values': values, - 'x_points': months, + 'y': [ + { + 'color': 'green', + 'values': values, + 'formatted': values_formatted + } + ], + 'x': { + 'values': months, + 'formatted': months_formatted + }, + 'specific_values': specific_values, - 'summary_values': summary_values + 'summary': summary_values } return data diff --git a/frappe/utils/jinja.py b/frappe/utils/jinja.py index 749a28479f..cccf4f079c 100644 --- a/frappe/utils/jinja.py +++ b/frappe/utils/jinja.py @@ -75,7 +75,7 @@ def get_allowed_functions_for_jenv(): import frappe.utils.data from frappe.utils.autodoc import automodule, get_version from frappe.model.document import get_controller - from frappe.website.utils import get_shade + from frappe.website.utils import (get_shade, get_toc, get_next_link) from frappe.modules import scrub import mimetypes from html2text import html2text @@ -128,11 +128,16 @@ def get_allowed_functions_for_jenv(): 'csrf_token': frappe.local.session.data.csrf_token if getattr(frappe.local, "session", None) else '' }, }, + 'style': { + 'border_color': '#d1d8dd' + }, "autodoc": { "get_version": get_version, "automodule": automodule, "get_controller": get_controller }, + 'get_toc': get_toc, + 'get_next_link': get_next_link, "_": frappe._, "get_shade": get_shade, "scrub": scrub, @@ -159,8 +164,10 @@ def get_jloader(): if frappe.local.flags.in_setup_help: apps = ['frappe'] else: - apps = frappe.local.flags.web_pages_apps or frappe.get_installed_apps(sort=True) - apps.reverse() + apps = frappe.get_hooks('template_apps') + if not apps: + apps = frappe.local.flags.web_pages_apps or frappe.get_installed_apps(sort=True) + apps.reverse() if not "frappe" in apps: apps.append('frappe') diff --git a/frappe/utils/setup_docs.py b/frappe/utils/setup_docs.py index 78f8a9602b..08092c472f 100644 --- a/frappe/utils/setup_docs.py +++ b/frappe/utils/setup_docs.py @@ -13,12 +13,12 @@ from frappe.utils import markdown from six import iteritems class setup_docs(object): - def __init__(self, app): + def __init__(self, app, target_app): """Generate source templates for models reference and module API and templates at `templates/autodoc` """ self.app = app - + self.target_app = target_app frappe.flags.web_pages_folders = ['docs',] frappe.flags.web_pages_apps = [self.app,] @@ -44,7 +44,6 @@ class setup_docs(object): "sub_heading": self.docs_config.sub_heading, "source_link": self.docs_config.source_link, "hide_install": getattr(self.docs_config, "hide_install", False), - "docs_base_url": self.docs_config.docs_base_url, "long_description": markdown(getattr(self.docs_config, "long_description", "")), "license": self.hooks.get("app_license")[0], "branch": getattr(self.docs_config, "branch", None) or "develop", @@ -59,7 +58,7 @@ class setup_docs(object): def build(self, docs_version): """Build templates for docs models and Python API""" - self.docs_path = frappe.get_app_path(self.app, "docs") + self.docs_path = frappe.get_app_path(self.target_app, 'www', "docs") self.path = os.path.join(self.docs_path, docs_version) self.app_context["app"]["docs_version"] = docs_version @@ -91,14 +90,50 @@ class setup_docs(object): #print parts module, doctype = parts[-3], parts[-1] - if doctype not in ("doctype", "boilerplate"): + if doctype != "boilerplate": self.write_model_file(basepath, module, doctype) # standard python module if self.is_py_module(basepath, folders, files): self.write_modules(basepath, folders, files) - self.build_user_docs() + #self.build_user_docs() + self.copy_user_assets() + self.add_sidebars() + self.add_breadcrumbs_for_user_pages() + + def add_breadcrumbs_for_user_pages(self): + for basepath, folders, files in os.walk(os.path.join(self.docs_path, + 'user')): # pylint: disable=unused-variable + for fname in files: + if fname.endswith('.md') or fname.endswith('.html'): + add_breadcrumbs_tag(os.path.join(basepath, fname)) + + def add_sidebars(self): + '''Add _sidebar.json in each folder in docs''' + for basepath, folders, files in os.walk(self.docs_path): # pylint: disable=unused-variable + with open(os.path.join(basepath, '_sidebar.json'), 'w') as sidebarfile: + sidebarfile.write(frappe.as_json([ + {"title": "Docs Home", "route": "/docs"}, + {"title": "User Guide", "route": "/docs/user"}, + {"title": "Server API", "route": "/docs/current/api"}, + {"title": "Models (Reference)", "route": "/docs/current/models"}, + {"title": "Improve Docs", "route": + "{0}/tree/develop/{1}/docs".format(self.docs_config.source_link, self.app)} + ])) + + + def copy_user_assets(self): + '''Copy docs/user and docs/assets to the target app''' + print('Copying docs/user and docs/assets...') + shutil.rmtree(os.path.join(self.docs_path, 'user'), + ignore_errors=True) + shutil.rmtree(os.path.join(self.docs_path, 'assets'), + ignore_errors=True) + shutil.copytree(os.path.join(self.app_path, 'docs', 'user'), + os.path.join(self.docs_path, 'user')) + shutil.copytree(os.path.join(self.app_path, 'docs', 'assets'), + frappe.get_app_path(self.target_app, 'www', 'docs', 'assets')) def make_home_pages(self): """Make standard home pages for docs, developer docs, api and models @@ -463,3 +498,9 @@ edit_link = '''
    ''' + +def add_breadcrumbs_tag(path): + with open(path, 'r') as f: + content = frappe.as_unicode(f.read()) + with open(path, 'w') as f: + f.write(('\n' + content).encode('utf-8')) diff --git a/frappe/website/context.py b/frappe/website/context.py index b800571aaa..99cb9371bc 100644 --- a/frappe/website/context.py +++ b/frappe/website/context.py @@ -16,15 +16,17 @@ def get_context(path, args=None): if args: context.update(args) - context = build_context(context) - if hasattr(frappe.local, 'request'): # for (remove leading slash) # path could be overriden in render.resolve_from_map - context["path"] = frappe.local.request.path[1:] + context["path"] = frappe.local.request.path.strip('/ ') else: context["path"] = path + context.route = context.path + + context = build_context(context) + # set using frappe.respond_as_web_page if hasattr(frappe.local, 'response') and frappe.local.response.get('context'): context.update(frappe.local.response.context) @@ -69,6 +71,9 @@ def build_context(context): if context.url_prefix and context.url_prefix[-1]!='/': context.url_prefix += '/' + # for backward compatibility + context.docs_base_url = '/docs' + context.update(get_website_settings()) context.update(frappe.local.conf.get("website_context") or {}) @@ -105,7 +110,21 @@ def build_context(context): update_controller_context(context, extension) add_metatags(context) + add_sidebar_and_breadcrumbs(context) + + # determine templates to be used + if not context.base_template_path: + app_base = frappe.get_hooks("base_template") + context.base_template_path = app_base[0] if app_base else "templates/base.html" + + if context.title_prefix and context.title and not context.title.startswith(context.title_prefix): + context.title = '{0} - {1}'.format(context.title_prefix, context.title) + return context + +def add_sidebar_and_breadcrumbs(context): + '''Add sidebar and breadcrumbs to context''' + from frappe.website.router import get_page_info_from_template if context.show_sidebar: context.no_cache = 1 add_sidebar_data(context) @@ -117,16 +136,12 @@ def build_context(context): context.sidebar_items = json.loads(sidebarfile.read()) context.show_sidebar = 1 - - # determine templates to be used - if not context.base_template_path: - app_base = frappe.get_hooks("base_template") - context.base_template_path = app_base[0] if app_base else "templates/base.html" - - if context.title_prefix and context.title and not context.title.startswith(context.title_prefix): - context.title = '{0} - {1}'.format(context.title_prefix, context.title) - - return context + if context.add_breadcrumbs and not context.parents: + if context.basepath: + parent_path = os.path.dirname(context.path).rstrip('/') + page_info = get_page_info_from_template(parent_path) + if page_info: + context.parents = [dict(route=parent_path, title=page_info.title)] def add_sidebar_data(context): from frappe.utils.user import get_fullname_and_avatar diff --git a/frappe/website/doctype/blog_post/templates/blog_post.html b/frappe/website/doctype/blog_post/templates/blog_post.html index 7ae1ed778e..24a128ddd6 100644 --- a/frappe/website/doctype/blog_post/templates/blog_post.html +++ b/frappe/website/doctype/blog_post/templates/blog_post.html @@ -1,6 +1,36 @@ {% extends "templates/web.html" %} {% block page_content %} +
    diff --git a/frappe/website/doctype/blog_post/templates/blog_post_row.html b/frappe/website/doctype/blog_post/templates/blog_post_row.html index 097feff8b9..42c1833920 100644 --- a/frappe/website/doctype/blog_post/templates/blog_post_row.html +++ b/frappe/website/doctype/blog_post/templates/blog_post_row.html @@ -12,11 +12,11 @@

    {{ _("By") }} {{ post.full_name }} - {{ frappe.format_date(post.published_on) }} - + {{ frappe.format_date(post.published_on) }} + {{ post.category.title }} - {{ post.comment_text }} + {{ post.comment_text }}

    diff --git a/frappe/website/purifycss.py b/frappe/website/purifycss.py new file mode 100644 index 0000000000..00ca06b0c4 --- /dev/null +++ b/frappe/website/purifycss.py @@ -0,0 +1,32 @@ +import frappe, re, os + +def purifycss(): + source = frappe.get_app_path('frappe_theme', 'public', 'less', 'frappe_theme.less') + target_apps = ['erpnext_com', 'frappe_io', 'translator', 'chart_of_accounts_builder', 'frappe_theme'] + with open(source, 'r') as f: + src = f.read() + + classes = [] + for line in src.splitlines(): + line = line.strip() + if not line: + continue + if line[0]=='@': + continue + classes.extend(re.findall('\.([^0-9][^ :&.{,(]*)', line)) + + classes = list(set(classes)) + + for app in target_apps: + for basepath, folders, files in os.walk(frappe.get_app_path(app)): + for fname in files: + if fname.endswith('.html') or fname.endswith('.md'): + #print 'checking {0}...'.format(fname) + with open(os.path.join(basepath, fname), 'r') as f: + src = f.read() + for c in classes: + if c in src: + classes.remove(c) + + for c in sorted(classes): + print c diff --git a/frappe/website/render.py b/frappe/website/render.py index a36013fa43..283862499e 100644 --- a/frappe/website/render.py +++ b/frappe/website/render.py @@ -6,14 +6,16 @@ import frappe from frappe import _ import frappe.sessions from frappe.utils import cstr -import mimetypes, json +import os, mimetypes, json from six import iteritems from werkzeug.wrappers import Response from werkzeug.routing import Map, Rule, NotFound +from werkzeug.wsgi import wrap_file from frappe.website.context import get_context -from frappe.website.utils import get_home_page, can_cache, delete_page_cache +from frappe.website.utils import (get_home_page, can_cache, delete_page_cache, + get_toc, get_next_link) from frappe.website.router import clear_sitemap from frappe.translate import guess_language @@ -21,14 +23,17 @@ class PageNotFoundError(Exception): pass def render(path=None, http_status_code=None): """render html page""" - path = resolve_path(path or frappe.local.request.path.strip('/ ')) + if not path: + path = frappe.local.request.path + path = resolve_path(path.strip('/ ')) data = None # if in list of already known 404s, send it if can_cache() and frappe.cache().hget('website_404', frappe.request.url): data = render_page('404') http_status_code = 404 - + elif is_static_file(path): + return get_static_file_reponse() else: try: data = render_page_by_language(path) @@ -71,6 +76,32 @@ def render(path=None, http_status_code=None): return build_response(path, data, http_status_code or 200) +def is_static_file(path): + if ('.' not in path): + return False + extn = path.rsplit('.', 1)[-1] + if extn in ('html', 'md', 'js', 'xml'): + return False + + for app in frappe.get_installed_apps(): + file_path = frappe.get_app_path(app, 'www') + '/' + path + if os.path.exists(file_path): + frappe.flags.file_path = file_path + return True + + return False + +def get_static_file_reponse(): + try: + f = open(frappe.flags.file_path, 'rb') + except IOError: + raise NotFound + + response = Response(wrap_file(frappe.local.request.environ, f), direct_passthrough=True) + response.mimetype = mimetypes.guess_type(frappe.flags.file_path)[0] or b'application/octet-stream' + return response + + def build_response(path, data, http_status_code, headers=None): # build response response = Response() @@ -143,6 +174,12 @@ def build_page(path): elif context.template: html = frappe.get_template(context.template).render(context) + if '{index}' in html: + html = html.replace('{index}', get_toc(context.route)) + + if '{next}' in html: + html = html.replace('{next}', get_next_link(context.route)) + # html = frappe.get_template(context.base_template_path).render(context) if can_cache(context.no_cache): @@ -221,7 +258,9 @@ def clear_cache(path=None): '''Clear website caches :param path: (optional) for the given path''' - frappe.cache().delete_value("website_generator_routes") + for key in ('website_generator_routes', 'website_pages', + 'website_full_index'): + frappe.cache().delete_value(key) delete_page_cache(path) frappe.cache().delete_value("website_404") if not path: diff --git a/frappe/website/router.py b/frappe/website/router.py index 4ce5655f78..38799b72ff 100644 --- a/frappe/website/router.py +++ b/frappe/website/router.py @@ -6,7 +6,6 @@ import frappe, os from frappe.website.utils import can_cache, delete_page_cache, extract_title from frappe.model.document import get_controller -from frappe import _ def resolve_route(path): """Returns the page route object based on searching in pages and generators. @@ -15,7 +14,7 @@ def resolve_route(path): The only exceptions are `/about` and `/contact` these will be searched in Web Pages first before checking the standard pages.""" if path not in ("about", "contact"): - context = get_page_context_from_template(path) + context = get_page_info_from_template(path) if context: return context return get_page_context_from_doctype(path) @@ -23,7 +22,7 @@ def resolve_route(path): context = get_page_context_from_doctype(path) if context: return context - return get_page_context_from_template(path) + return get_page_info_from_template(path) def get_page_context(path): page_context = None @@ -54,12 +53,12 @@ def make_page_context(path): return context -def get_page_context_from_template(path): +def get_page_info_from_template(path): '''Return page_info from path''' for app in frappe.get_installed_apps(frappe_last=True): app_path = frappe.get_app_path(app) - folders = frappe.local.flags.web_pages_folders or ('www', 'templates/pages') + folders = get_start_folders() for start in folders: search_path = os.path.join(app_path, start, path) @@ -68,14 +67,15 @@ def get_page_context_from_template(path): for o in options: option = frappe.as_unicode(o) if os.path.exists(option) and not os.path.isdir(option): - return get_page_info(option, app, app_path=app_path) + return get_page_info(option, app, start, app_path=app_path) return None def get_page_context_from_doctype(path): page_info = get_page_info_from_doctypes(path) if page_info: - return frappe.get_doc(page_info.get("doctype"), page_info.get("name")).get_page_info() + return frappe.get_doc(page_info.get("doctype"), + page_info.get("name")).get_page_info() def clear_sitemap(): delete_page_cache("*") @@ -120,57 +120,60 @@ def get_page_info_from_doctypes(path=None): def get_pages(app=None): '''Get all pages. Called for docs / sitemap''' - pages = {} - frappe.local.flags.in_get_all_pages = True - folders = frappe.local.flags.web_pages_folders or ('www', 'templates/pages') + def _build(app): + pages = {} - if app: - apps = [app] - else: - apps = frappe.local.flags.web_pages_apps or frappe.get_installed_apps() + if app: + apps = [app] + else: + apps = frappe.local.flags.web_pages_apps or frappe.get_installed_apps() - for app in apps: - app_path = frappe.get_app_path(app) + for app in apps: + app_path = frappe.get_app_path(app) - for start in folders: - path = os.path.join(app_path, start) - pages.update(get_pages_from_path(path, app, app_path)) - frappe.local.flags.in_get_all_pages = False + for start in get_start_folders(): + pages.update(get_pages_from_path(start, app, app_path)) - return pages + return pages + + return frappe.cache().get_value('website_pages', lambda: _build(app)) -def get_pages_from_path(path, app, app_path): +def get_pages_from_path(start, app, app_path): pages = {} - if os.path.exists(path): - for basepath, folders, files in os.walk(path): + start_path = os.path.join(app_path, start) + if os.path.exists(start_path): + for basepath, folders, files in os.walk(start_path): # add missing __init__.py if not '__init__.py' in files: open(os.path.join(basepath, '__init__.py'), 'a').close() for fname in files: fname = frappe.utils.cstr(fname) + if not '.' in fname: + continue page_name, extn = fname.rsplit(".", 1) if extn in ('js', 'css') and os.path.exists(os.path.join(basepath, fname + '.html')): # js, css is linked to html, skip continue if extn in ("html", "xml", "js", "css", "md"): - page_info = get_page_info(path, app, basepath, app_path, fname) + page_info = get_page_info(os.path.join(basepath, fname), + app, start, basepath, app_path, fname) pages[page_info.route] = page_info # print frappe.as_json(pages[-1]) return pages -def get_page_info(path, app, basepath=None, app_path=None, fname=None): +def get_page_info(path, app, start, basepath=None, app_path=None, fname=None): '''Load page info''' - if not fname: + if fname is None: fname = os.path.basename(path) - if not app_path: + if app_path is None: app_path = frappe.get_app_path(app) - if not basepath: + if basepath is None: basepath = os.path.dirname(path) page_name, extn = fname.rsplit(".", 1) @@ -187,9 +190,16 @@ def get_page_info(path, app, basepath=None, app_path=None, fname=None): if page_info.basename == 'index': page_info.basename = "" - page_info.route = page_info.name = page_info.page_name = os.path.join(os.path.relpath(basepath, path), - page_info.basename).strip('/.') + # get route from template name + page_info.route = page_info.template.replace(start, '').strip('/') + if os.path.basename(page_info.route) in ('index.html', 'index.md'): + page_info.route = os.path.dirname(page_info.route) + # remove the extension + if page_info.route.endswith('.md') or page_info.route.endswith('.html'): + page_info.route = page_info.route.rsplit('.', 1)[0] + + page_info.name = page_info.page_name = page_info.route # controller page_info.controller_path = os.path.join(basepath, page_name.replace("-", "_") + ".py") @@ -202,9 +212,11 @@ def get_page_info(path, app, basepath=None, app_path=None, fname=None): # get the source setup_source(page_info) - if page_info.only_content: - # extract properties from HTML comments - load_properties(page_info) + # extract properties from HTML comments + load_properties(page_info) + + # if not page_info.title: + # print('no-title-for', page_info.route) return page_info @@ -255,48 +267,14 @@ def setup_index(page_info): if os.path.exists(index_txt_path): page_info.index = open(index_txt_path, 'r').read().splitlines() -def make_toc(context, out, app=None): - '''Insert full index (table of contents) for {index} tag''' - from frappe.website.utils import get_full_index - if '{index}' in out: - html = frappe.get_template("templates/includes/full_index.html").render({ - "full_index": get_full_index(app=app), - "url_prefix": context.url_prefix or "/", - "route": context.route - }) - - out = out.replace('{index}', html) - - if '{next}' in out: - # insert next link - next_item = None - children_map = get_full_index(app=app) - parent_route = os.path.dirname(context.route) - children = children_map[parent_route] - - if parent_route and children: - for i, c in enumerate(children): - if c.route == context.route and i < (len(children) - 1): - next_item = children[i+1] - next_item.url_prefix = context.url_prefix or "/" - - if next_item: - if next_item.route and next_item.title: - html = ('

    '+_("Next")\ - +': {title}

    ').format(**next_item) - - out = out.replace('{next}', html) - - return out - - def load_properties(page_info): '''Load properties like no_cache, title from raw''' if not page_info.title: - page_info.title = extract_title(page_info.source, page_info.name) + page_info.title = extract_title(page_info.source, page_info.route) - if page_info.title and not '{% block title %}' in page_info.source: - page_info.source += '\n{% block title %}{{ title }}{% endblock %}' + # if page_info.title and not '{% block title %}' in page_info.source: + # if not page_info.only_content: + # page_info.source += '\n{% block title %}{{ title }}{% endblock %}' if "" in page_info.source: page_info.no_breadcrumbs = 1 @@ -304,13 +282,19 @@ def load_properties(page_info): if "" in page_info.source: page_info.show_sidebar = 1 + if "" in page_info.source: + page_info.add_breadcrumbs = 1 + if "" in page_info.source: page_info.no_header = 1 - else: - # every page needs a header - # add missing header if there is no

    tag - if (not '{% block header %}' in page_info.source) and (not ' tag + # if (not '{% block header %}' in page_info.source) and (not '" in page_info.source: page_info.no_cache = 1 @@ -346,7 +330,7 @@ def sync_global_search(): for app in frappe.get_installed_apps(frappe_last=True): app_path = frappe.get_app_path(app) - folders = frappe.local.flags.web_pages_folders or ('www', 'templates/pages') + folders = get_start_folders() for start in folders: for basepath, folders, files in os.walk(os.path.join(app_path, start)): @@ -374,3 +358,5 @@ def sync_global_search(): sync_global_search() +def get_start_folders(): + return frappe.local.flags.web_pages_folders or ('www', 'templates/pages') \ No newline at end of file diff --git a/frappe/website/utils.py b/frappe/website/utils.py index 31ff4acddc..9424ec722e 100644 --- a/frappe/website/utils.py +++ b/frappe/website/utils.py @@ -5,9 +5,9 @@ from __future__ import unicode_literals import frappe, re, os from six import iteritems - def delete_page_cache(path): cache = frappe.cache() + cache.delete_value('full_index') groups = ("website_page", "page_context") if path: for name in groups: @@ -184,38 +184,89 @@ def abs_url(path): path = "/" + path return path +def get_toc(route, url_prefix=None, app=None): + '''Insert full index (table of contents) for {index} tag''' + from frappe.website.utils import get_full_index + + full_index = get_full_index(app=app) + + return frappe.get_template("templates/includes/full_index.html").render({ + "full_index": full_index, + "url_prefix": url_prefix or "/", + "route": route.rstrip('/') + }) + +def get_next_link(route, url_prefix=None, app=None): + # insert next link + next_item = None + route = route.rstrip('/') + children_map = get_full_index(app=app) + parent_route = os.path.dirname(route) + children = children_map[parent_route] + + if parent_route and children: + for i, c in enumerate(children): + if c.route == route and i < (len(children) - 1): + next_item = children[i+1] + next_item.url_prefix = url_prefix or "/" + + if next_item: + if next_item.route and next_item.title: + html = ('

    ' + frappe._("Next")\ + +': {title}

    ').format(**next_item) + + return html + + return '' + def get_full_index(route=None, app=None): """Returns full index of the website for www upto the n-th level""" + from frappe.website.router import get_pages + if not frappe.local.flags.children_map: - from frappe.website.router import get_pages - children_map = {} - pages = get_pages(app=app) - - # make children map - for route, page_info in iteritems(pages): - parent_route = os.path.dirname(route) - children_map.setdefault(parent_route, []).append(page_info) - - if frappe.flags.local_docs: - page_info.extn = '.html' - - # order as per index if present - for route, children in children_map.items(): - page_info = pages[route] - if page_info.index: - new_children = [] - page_info.extn = '' - for name in page_info.index: - child_route = page_info.route + '/' + name - if child_route in pages: - new_children.append(pages[child_route]) - - # add remaining pages not in index.txt - for c in children: - if c not in new_children: - new_children.append(c) - - children_map[route] = new_children + def _build(): + children_map = {} + added = [] + pages = get_pages(app=app) + + # make children map + for route, page_info in iteritems(pages): + parent_route = os.path.dirname(route) + if parent_route not in added: + children_map.setdefault(parent_route, []).append(page_info) + + # order as per index if present + for route, children in children_map.items(): + if not route in pages: + # no parent (?) + continue + + page_info = pages[route] + if page_info.index or ('index' in page_info.template): + new_children = [] + page_info.extn = '' + for name in (page_info.index or []): + child_route = page_info.route + '/' + name + if child_route in pages: + if child_route not in added: + new_children.append(pages[child_route]) + added.append(child_route) + + # add remaining pages not in index.txt + _children = sorted(children, lambda a, b: cmp( + os.path.basename(a.route), os.path.basename(b.route))) + + for child_route in _children: + if child_route not in new_children: + if child_route not in added: + new_children.append(child_route) + added.append(child_route) + + children_map[route] = new_children + + return children_map + + children_map = frappe.cache().get_value('website_full_index', _build) frappe.local.flags.children_map = children_map @@ -223,12 +274,14 @@ def get_full_index(route=None, app=None): def extract_title(source, path): '''Returns title from `<!-- title -->` or <h1> or path''' + title = '' + if "', source)[0].strip() elif "

    " in source: match = re.findall('

    ([^<]*)', source) title = match[0].strip()[:300] - else: - title = os.path.basename(path).replace('_', ' ').replace('-', ' ').title() + if not title: + title = os.path.basename(path.rsplit('.', )[0].rstrip('/')).replace('_', ' ').replace('-', ' ').title() return title diff --git a/frappe/www/message.html b/frappe/www/message.html index 0eb183fcfd..4697534a2e 100644 --- a/frappe/www/message.html +++ b/frappe/www/message.html @@ -3,7 +3,19 @@ {% block title %}{{ title or _("Message") }}{% endblock %} {% block page_content %} - +
    @@ -18,17 +30,11 @@ {% if error_code %}

    {{ _("Status: {0}").format(error_code) }}

    {% endif %} - + {% endblock %} diff --git a/frappe/www/website_script.js b/frappe/www/website_script.js index 3b84bdd084..fd361f8986 100644 --- a/frappe/www/website_script.js +++ b/frappe/www/website_script.js @@ -1,7 +1,8 @@ +// website_script.js {% if javascript -%}{{ javascript }}{%- endif %} {% if google_analytics_id -%} - +// Google Analytics (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) @@ -9,5 +10,5 @@ m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) ga('create', '{{ google_analytics_id }}', 'auto'); ga('send', 'pageview'); - +// End Google Analytics {%- endif %} diff --git a/frappe/www/website_script.py b/frappe/www/website_script.py index 2f5ee0d654..0db00bc3d8 100644 --- a/frappe/www/website_script.py +++ b/frappe/www/website_script.py @@ -10,7 +10,8 @@ no_sitemap = 1 base_template_path = "templates/www/website_script.js" def get_context(context): - context.javascript = frappe.db.get_single_value('Website Script', 'javascript') or "" + context.javascript = frappe.db.get_single_value('Website Script', + 'javascript') or "" theme = get_active_theme() js = strip(theme and theme.js or "")

    Sr