diff --git a/.eslintrc b/.eslintrc index 7e469f7672..69c731b079 100644 --- a/.eslintrc +++ b/.eslintrc @@ -5,7 +5,7 @@ "es6": true }, "parserOptions": { - "ecmaVersion": 8, + "ecmaVersion": 9, "sourceType": "module" }, "extends": "eslint:recommended", @@ -78,6 +78,8 @@ "has_common": true, "has_words": true, "validate_email": true, + "validate_name": true, + "validate_phone": true, "get_number_format": true, "format_number": true, "format_currency": true, diff --git a/.github/frappe_linter/translation.py b/.github/frappe_linter/translation.py new file mode 100644 index 0000000000..bb81e848f1 --- /dev/null +++ b/.github/frappe_linter/translation.py @@ -0,0 +1,28 @@ +import re +import sys + +errors_encounter = 0 +pattern = re.compile(r"_\(([\"']{,3})(?P((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P((?!\5).)*)\5)*(\s*,\s*(.)*?\s*(,\s*([\"'])(?P((?!\11).)*)\11)*)*\)") +start_pattern = re.compile(r"_{1,2}\([\"']{1,3}") + +# skip first argument +files = sys.argv[1:] +for _file in files: + if not _file.endswith(('.py', '.js')): + continue + with open(_file, 'r') as f: + print(f'Checking: {_file}') + for num, line in enumerate(f, 1): + all_matches = start_pattern.finditer(line) + if all_matches: + for match in all_matches: + verify = pattern.search(line) + if not verify: + errors_encounter += 1 + print(f'A syntax error has been discovered at line number: {num}') + print(f'Syntax error occurred with: {line}') +if errors_encounter > 0: + print('You can visit "https://frappe.io/docs/user/en/translations" to resolve this error.') + assert 1+1 == 3 +else: + print('Good To Go!') diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml deleted file mode 100644 index 26801ebbe8..0000000000 --- a/.github/workflows/backport.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Backport -on: - pull_request: - types: - - closed - - labeled - -jobs: - backport: - runs-on: ubuntu-18.04 - name: Backport - steps: - - name: Backport - uses: tibdex/backport@v1 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/translation_linter.yml b/.github/workflows/translation_linter.yml new file mode 100644 index 0000000000..ca7b152921 --- /dev/null +++ b/.github/workflows/translation_linter.yml @@ -0,0 +1,22 @@ +name: Frappe Linter +on: + pull_request: + branches: + - develop + - version-12-hotfix + - version-11-hotfix +jobs: + check_translation: + name: Translation Syntax Check + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v2 + - name: Setup python3 + uses: actions/setup-python@v1 + with: + python-version: 3.6 + - name: Validating Translation Syntax + run: | + git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q + files=$(git diff --name-only $GITHUB_BASE_REF) + python $GITHUB_WORKSPACE/.github/frappe_linter/translation.py $files \ No newline at end of file diff --git a/.mergify.yml b/.mergify.yml index d810898eee..b145834cc4 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -1,7 +1,7 @@ pull_request_rules: - name: Automatic merge on CI success and review conditions: - - status-success=Codacy/PR Quality Review + - status-success=Sider - status-success=Semantic Pull Request - status-success=Travis CI - Pull Request - status-success=security/snyk - package.json (frappe) @@ -14,7 +14,7 @@ pull_request_rules: method: merge - name: Automatic squash on CI success and review conditions: - - status-success=Codacy/PR Quality Review + - status-success=Sider - status-success=Semantic Pull Request - status-success=Travis CI - Pull Request - status-success=security/snyk - package.json (frappe) diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000000..8e6f8eb5e9 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,18 @@ +# Each line is a file pattern followed by one or more owners. + +# These owners will be the default owners for everything in +# the repo. Unless a later match takes precedence, + +* @frappe/frappe-review-team +website/ @scmmishra +web_form/ @scmmishra +templates/ @scmmishra +www/ @scmmishra +integrations/ @Mangesh-Khairnar +patches/ @sahil28297 +dashboard/ @prssanna +email/ @Thunderbottom +event_streaming/ @ruchamahabal +data_import* @netchampfaris +core/ @surajshetty3416 +requirements.txt @gavindsouza diff --git a/README.md b/README.md index 2a8fba0af4..860958087e 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ Full-stack web application framework that uses Python and MariaDB on the server ### Table of Contents * [Installation](#installation) +* [Documentation](https://frappe.io/docs) * [License](#license) ### Installation diff --git a/cypress.json b/cypress.json index 7d853271b9..ae0c45c3ae 100644 --- a/cypress.json +++ b/cypress.json @@ -1,5 +1,7 @@ { "baseUrl": "http://test_site_ui:8000", "projectId": "92odwv", - "adminPassword": "admin" + "adminPassword": "admin", + "defaultCommandTimeout": 10000, + "pageLoadTimeout": 15000 } diff --git a/frappe/__init__.py b/frappe/__init__.py index 97ac94afc6..b912011a61 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -587,6 +587,7 @@ def clear_cache(user=None, doctype=None): else: # everything from frappe import translate frappe.cache_manager.clear_user_cache() + frappe.cache_manager.clear_domain_cache() translate.clear_cache() reset_metadata_version() local.cache = {} @@ -832,6 +833,8 @@ def rename_doc(*args, **kwargs): """ kwargs.pop('cmd', None) kwargs.pop('ignore_permissions', None) + kwargs.pop('cmd', None) + from frappe.model.rename_doc import rename_doc return rename_doc(*args, **kwargs) diff --git a/frappe/api.py b/frappe/api.py index 6655ebc4d8..b73ee4c128 100644 --- a/frappe/api.py +++ b/frappe/api.py @@ -2,14 +2,17 @@ # MIT License. See license.txt from __future__ import unicode_literals -import json -import frappe -import frappe.handler -import frappe.client -from frappe.utils.response import build_response -from frappe import _ -from six.moves.urllib.parse import urlparse, urlencode import base64 +import binascii +import json + +from six.moves.urllib.parse import urlencode, urlparse + +import frappe +import frappe.client +import frappe.handler +from frappe import _ +from frappe.utils.response import build_response def handle(): """ @@ -35,8 +38,8 @@ def handle(): `/api/resource/{doctype}/{name}?run_method={method}` will run a whitelisted controller method """ - validate_oauth() - validate_auth_via_api_keys() + + validate_auth() parts = frappe.request.path[1:].split("/",3) call = doctype = name = None @@ -145,48 +148,82 @@ def get_request_form_data(): return frappe.parse_json(data) -def validate_oauth(): - """ authentication using oauth """ +def validate_auth(): + if frappe.get_request_header("Authorization") is None: + return + + VALID_AUTH_PREFIX_TYPES = ['basic', 'bearer', 'token'] + VALID_AUTH_PREFIX_STRING = ", ".join(VALID_AUTH_PREFIX_TYPES).title() + + authorization_header = frappe.get_request_header("Authorization", str()).split(" ") + authorization_type = authorization_header[0].lower() + + if len(authorization_header) == 1: + frappe.throw(_('Invalid Authorization headers, add a token with a prefix from one of the following: {0}.'.format(VALID_AUTH_PREFIX_STRING)), frappe.InvalidAuthorizationHeader) + + if authorization_type == "bearer": + validate_oauth(authorization_header) + elif authorization_type in VALID_AUTH_PREFIX_TYPES: + validate_auth_via_api_keys(authorization_header) + else: + frappe.throw(_('Invalid Authorization Type {0}, must be one of {1}.'.format(authorization_type, VALID_AUTH_PREFIX_STRING)), frappe.InvalidAuthorizationPrefix) + + +def validate_oauth(authorization_header): + """ + Authenticate request using OAuth and set session user + + Args: + authorization_header (list of str): The 'Authorization' header containing the prefix and token + """ + from frappe.oauth import get_url_delimiter + from frappe.integrations.oauth2 import get_oauth_server + form_dict = frappe.local.form_dict - authorization_header = frappe.get_request_header("Authorization", "").split(" ") - if authorization_header and authorization_header[0].lower() == "bearer": - from frappe.integrations.oauth2 import get_oauth_server - token = authorization_header[1] - req = frappe.request - parsed_url = urlparse(req.url) - access_token = {"access_token": token} - uri = parsed_url.scheme + "://" + parsed_url.netloc + parsed_url.path + "?" + urlencode(access_token) - http_method = req.method - body = req.get_data() - headers = req.headers + token = authorization_header[1] + req = frappe.request + parsed_url = urlparse(req.url) + access_token = {"access_token": token} + uri = parsed_url.scheme + "://" + parsed_url.netloc + parsed_url.path + "?" + urlencode(access_token) + http_method = req.method + body = req.get_data() + headers = req.headers - required_scopes = frappe.db.get_value("OAuth Bearer Token", token, "scopes").split(get_url_delimiter()) - - valid, _oauthlib_request = get_oauth_server().verify_request(uri, http_method, body, headers, required_scopes) - - if valid: - frappe.set_user(frappe.db.get_value("OAuth Bearer Token", token, "user")) - frappe.local.form_dict = form_dict - - -def validate_auth_via_api_keys(): - """ - authentication using api key and api secret - - set user - """ try: - authorization_header = frappe.get_request_header("Authorization", "").split(" ") + required_scopes = frappe.db.get_value("OAuth Bearer Token", token, "scopes").split(get_url_delimiter()) + except AttributeError: + frappe.throw(_("Invalid Bearer token, please provide a valid access token with prefix 'Bearer'."), frappe.InvalidAuthorizationToken) + + valid, oauthlib_request = get_oauth_server().verify_request(uri, http_method, body, headers, required_scopes) + + if valid: + frappe.set_user(frappe.db.get_value("OAuth Bearer Token", token, "user")) + frappe.local.form_dict = form_dict + + +def validate_auth_via_api_keys(authorization_header): + """ + Authenticate request using API keys and set session user + + Args: + authorization_header (list of str): The 'Authorization' header containing the prefix and token + """ + + try: + auth_type, auth_token = authorization_header authorization_source = frappe.get_request_header("Frappe-Authorization-Source") - if authorization_header and authorization_header[0] == 'Basic': - token = frappe.safe_decode(base64.b64decode(authorization_header[1])).split(":") - validate_api_key_secret(token[0], token[1], authorization_source) - elif authorization_header and authorization_header[0] == 'token': - token = authorization_header[1].split(":") - validate_api_key_secret(token[0], token[1], authorization_source) - except Exception as e: - raise e + if auth_type.lower() == 'basic': + api_key, api_secret = frappe.safe_decode(base64.b64decode(auth_token)).split(":") + validate_api_key_secret(api_key, api_secret, authorization_source) + elif auth_type.lower() == 'token': + api_key, api_secret = auth_token.split(":") + validate_api_key_secret(api_key, api_secret, authorization_source) + except binascii.Error: + frappe.throw(_("Failed to decode token, please provide a valid base64-encoded token."), frappe.InvalidAuthorizationToken) + except (AttributeError, TypeError, ValueError): + frappe.throw(_("Invalid token, please provide a valid token with prefix 'Basic' or 'Token'."), frappe.InvalidAuthorizationToken) + def validate_api_key_secret(api_key, api_secret, frappe_authorization_source=None): diff --git a/frappe/app.py b/frappe/app.py index 24ce35b514..41798b0bc4 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -19,7 +19,7 @@ import frappe.auth import frappe.api import frappe.utils.response import frappe.website.render -from frappe.utils import get_site_name +from frappe.utils import get_site_name, sanitize_html from frappe.middlewares import StaticDataMiddleware from frappe.utils.error import make_error_snapshot from frappe.core.doctype.comment.comment import update_comments_in_parent_after_request @@ -172,7 +172,7 @@ def handle_exception(e): return_as_message = True else: - traceback = "
"+frappe.get_traceback()+"
" + traceback = "
" + sanitize_html(frappe.get_traceback()) + "
" if frappe.local.flags.disable_traceback: traceback = "" diff --git a/frappe/automation/desk_page/tools/tools.json b/frappe/automation/desk_page/tools/tools.json index 3cfaa0bd97..2164a4ce38 100644 --- a/frappe/automation/desk_page/tools/tools.json +++ b/frappe/automation/desk_page/tools/tools.json @@ -1,22 +1,24 @@ { "cards": [ { - "icon": "octicon octicon-briefcase", - "links": "[\n {\n \"description\": \"Documents assigned to you and by you.\",\n \"label\": \"To Do\",\n \"name\": \"ToDo\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Event and other calendars.\",\n \"label\": \"Calendar\",\n \"link\": \"List/Event/Calendar\",\n \"name\": \"Event\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Private and public Notes.\",\n \"label\": \"Note\",\n \"name\": \"Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Files\",\n \"name\": \"File\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Activity log of all users.\",\n \"label\": \"Activity\",\n \"name\": \"activity\",\n \"type\": \"page\"\n }\n]", - "title": "Tools" + "hidden": 0, + "label": "Tools", + "links": "[\n {\n \"description\": \"Documents assigned to you and by you.\",\n \"label\": \"To Do\",\n \"name\": \"ToDo\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Event and other calendars.\",\n \"label\": \"Calendar\",\n \"link\": \"List/Event/Calendar\",\n \"name\": \"Event\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Private and public Notes.\",\n \"label\": \"Note\",\n \"name\": \"Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Files\",\n \"name\": \"File\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Video\",\n \"name\": \"Video\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Activity log of all users.\",\n \"label\": \"Activity\",\n \"name\": \"activity\",\n \"type\": \"page\"\n }\n]" }, { - "links": "[\n {\n \"description\": \"Newsletters to contacts, leads.\",\n \"label\": \"Newsletter\",\n \"name\": \"Newsletter\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Email Group List\",\n \"label\": \"Email Group\",\n \"name\": \"Email Group\",\n \"type\": \"doctype\"\n }\n]", - "title": "Email" + "hidden": 0, + "label": "Email", + "links": "[\n {\n \"description\": \"Newsletters to contacts, leads.\",\n \"label\": \"Newsletter\",\n \"name\": \"Newsletter\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Email Group List\",\n \"label\": \"Email Group\",\n \"name\": \"Email Group\",\n \"type\": \"doctype\"\n }\n]" }, { - "icon": "fa fa-cog", - "links": "[\n {\n \"type\": \"doctype\",\n \"name\": \"Assignment Rule\",\n \"description\": \"Set up rules for user assignments.\",\n \"label\": \"Assignment Rule\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Milestone\",\n \"description\": \"Tracks milestones on the lifecycle of a document if it undergoes multiple stages.\",\n \"label\": \"Milestone\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Auto Repeat\",\n \"description\": \"Automatically generates recurring documents.\",\n \"label\": \"Auto Repeat\"\n }\n]", - "title": "Automation" + "hidden": 0, + "label": "Automation", + "links": "[\n {\n \"type\": \"doctype\",\n \"name\": \"Assignment Rule\",\n \"description\": \"Set up rules for user assignments.\",\n \"label\": \"Assignment Rule\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Milestone\",\n \"description\": \"Tracks milestones on the lifecycle of a document if it undergoes multiple stages.\",\n \"label\": \"Milestone\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Auto Repeat\",\n \"description\": \"Automatically generates recurring documents.\",\n \"label\": \"Auto Repeat\"\n }\n]" }, { - "links": "[\n {\n \"type\": \"doctype\",\n \"name\": \"Event Producer\",\n \"description\": \"The site you want to subscribe to for consuming events.\",\n \"label\": \"Event Producer\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Event Consumer\",\n \"description\": \"The site which is consuming your events.\",\n \"label\": \"Event Consumer\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Event Update Log\",\n \"description\": \"Maintains a Log of all inserts, updates and deletions on Event Producer site for documents that have consumers.\",\n \"label\": \"Event Update Log\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Event Sync Log\",\n \"description\": \"Maintains a log of every event consumed along with the status of the sync and a Resync button in case sync fails.\",\n \"label\": \"Event Sync Log\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Document Type Mapping\",\n \"description\": \"The mapping configuration between two doctypes.\",\n \"label\": \"Document Type Mapping\"\n }\n]", - "title": "Event Streaming" + "hidden": 0, + "label": "Event Streaming", + "links": "[\n {\n \"type\": \"doctype\",\n \"name\": \"Event Producer\",\n \"description\": \"The site you want to subscribe to for consuming events.\",\n \"label\": \"Event Producer\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Event Consumer\",\n \"description\": \"The site which is consuming your events.\",\n \"label\": \"Event Consumer\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Event Update Log\",\n \"description\": \"Maintains a Log of all inserts, updates and deletions on Event Producer site for documents that have consumers.\",\n \"label\": \"Event Update Log\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Event Sync Log\",\n \"description\": \"Maintains a log of every event consumed along with the status of the sync and a Resync button in case sync fails.\",\n \"label\": \"Event Sync Log\"\n },\n {\n \"type\": \"doctype\",\n \"name\": \"Document Type Mapping\",\n \"description\": \"The mapping configuration between two doctypes.\",\n \"label\": \"Document Type Mapping\"\n }\n]" } ], "category": "Administration", @@ -30,7 +32,7 @@ "idx": 0, "is_standard": 1, "label": "Tools", - "modified": "2020-03-12 16:30:41.841895", + "modified": "2020-04-20 18:21:14.152537", "modified_by": "Administrator", "module": "Automation", "name": "Tools", @@ -39,27 +41,27 @@ "pin_to_top": 0, "shortcuts": [ { - "is_query_report": 0, + "label": "ToDo", "link_to": "ToDo", "type": "DocType" }, { - "is_query_report": 0, + "label": "Note", "link_to": "Note", "type": "DocType" }, { - "is_query_report": 0, + "label": "File", "link_to": "File", "type": "DocType" }, { - "is_query_report": 0, + "label": "Assignment Rule", "link_to": "Assignment Rule", "type": "DocType" }, { - "is_query_report": 0, + "label": "Auto Repeat", "link_to": "Auto Repeat", "type": "DocType" } diff --git a/frappe/boot.py b/frappe/boot.py index 7fc071b6b8..9d5dbe1909 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.social.doctype.energy_point_settings.energy_point_settings import is_energy_point_enabled +from frappe.website.doctype.web_page_view.web_page_view import is_tracking_enabled from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_points from frappe.social.doctype.post.post import frequently_visited_links @@ -41,7 +42,7 @@ def get_bootinfo(): bootinfo.modules = {} bootinfo.module_list = [] - load_desktop_icons(bootinfo) + load_desktop_data(bootinfo) bootinfo.letter_heads = get_letter_heads() bootinfo.active_domains = frappe.get_active_domains() bootinfo.all_domains = [d.get("name") for d in frappe.get_all("Domain")] @@ -79,6 +80,7 @@ def get_bootinfo(): bootinfo.success_action = get_success_action() bootinfo.update(get_email_accounts(user=frappe.session.user)) bootinfo.energy_points_enabled = is_energy_point_enabled() + bootinfo.website_tracking_enabled = is_tracking_enabled() bootinfo.points = get_energy_points(frappe.session.user) bootinfo.frequently_visited_links = frequently_visited_links() bootinfo.link_preview_doctypes = get_link_preview_doctypes() @@ -99,9 +101,11 @@ def load_conf_settings(bootinfo): for key in ('developer_mode', 'socketio_port', 'file_watcher_port'): if key in conf: bootinfo[key] = conf.get(key) -def load_desktop_icons(bootinfo): +def load_desktop_data(bootinfo): from frappe.config import get_modules_from_all_apps_for_user + from frappe.desk.desktop import get_desk_sidebar_items bootinfo.allowed_modules = get_modules_from_all_apps_for_user() + bootinfo.allowed_workspaces = get_desk_sidebar_items(True) def get_allowed_pages(): return get_user_pages_or_reports('Page') @@ -266,4 +270,18 @@ def get_success_action(): return frappe.get_all("Success Action", fields=["*"]) def get_link_preview_doctypes(): - return [d.name for d in frappe.db.get_all('DocType', {'show_preview_popup': 1})] \ No newline at end of file + from frappe.utils import cint + + link_preview_doctypes = [d.name for d in frappe.db.get_all('DocType', {'show_preview_popup': 1})] + customizations = frappe.get_all("Property Setter", + fields=['doc_type', 'value'], + filters={'property': 'show_preview_popup'} + ) + + for custom in customizations: + if not cint(custom.value) and custom.doc_type in link_preview_doctypes: + link_preview_doctypes.remove(custom.doc_type) + else: + link_preview_doctypes.append(custom.doc_type) + + return link_preview_doctypes diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py index 9959ba97bb..78f452db21 100644 --- a/frappe/cache_manager.py +++ b/frappe/cache_manager.py @@ -14,7 +14,9 @@ common_default_keys = ["__default", "__global"] global_cache_keys = ("app_hooks", "installed_apps", "app_modules", "module_app", "system_settings", 'scheduler_events', 'time_zone', 'webhooks', 'active_domains', - 'active_modules', 'assignment_rule', 'server_script_map', 'wkhtmltopdf_version') + 'active_modules', 'assignment_rule', 'server_script_map', 'wkhtmltopdf_version', + 'domain_restricted_doctypes', 'domain_restricted_pages', 'information_schema:counts', + 'sitemap_routes') user_cache_keys = ("bootinfo", "user_recent", "roles", "user_doc", "lang", "defaults", "user_permissions", "home_page", "linked_with", @@ -23,10 +25,6 @@ user_cache_keys = ("bootinfo", "user_recent", "roles", "user_doc", "lang", doctype_cache_keys = ("meta", "form_meta", "table_columns", "last_modified", "linked_doctypes", 'notifications', 'workflow' ,'energy_point_rule_map') -count_cache_blacklist = ["Version", "Tag", "ToDo", "List Filter", "Note Seen By", "Notification Log", - "Document Follow", "Communication", "Email Queue", "Deleted Document", "File", "Email Queue Recipient" - "Comment", "Has Role", "Attendance", "Route History"] - def clear_user_cache(user=None): cache = frappe.cache() @@ -46,6 +44,11 @@ def clear_user_cache(user=None): clear_defaults_cache() clear_global_cache() +def clear_domain_cache(user=None): + cache = frappe.cache() + domain_cache_keys = ('domain_restricted_doctypes', 'domain_restricted_pages') + cache.delete_value(domain_cache_keys) + def clear_global_cache(): from frappe.website.render import clear_cache as clear_website_cache @@ -121,7 +124,7 @@ def clear_doctype_map(doctype, name): cache_key = frappe.scrub(doctype) + '_map' frappe.cache().hdel(cache_key, name) -def build_table_count_cache(doc=None, method=None, *args, **kwargs): +def build_table_count_cache(): if (frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_migrate @@ -129,15 +132,6 @@ def build_table_count_cache(doc=None, method=None, *args, **kwargs): or frappe.flags.in_setup_wizard): return - if doc and isinstance(doc, Document): - doctype = doc.doctype - - if doc.meta.istable: - return - - if doctype in count_cache_blacklist: - return - _cache = frappe.cache() data = frappe.db.multisql({ "mariadb": """ diff --git a/frappe/commands/site.py b/frappe/commands/site.py index fd43cc8cf3..2b4923281c 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -1,13 +1,23 @@ -from __future__ import unicode_literals, absolute_import, print_function +# imports - standard imports +import atexit +import compileall +import hashlib +import os +import re +import shutil +import sys + +# imports - third party imports import click -import hashlib, os, sys, compileall, re + +# imports - module imports import frappe from frappe import _ -from frappe.commands import pass_context, get_site +from frappe.commands import get_site, pass_context from frappe.commands.scheduler import _is_scheduler_enabled from frappe.installer import update_site_config -from frappe.utils import touch_file, get_site_path -from six import text_type +from frappe.utils import get_site_path, touch_file + @click.command('new-site') @click.argument('site') @@ -68,32 +78,33 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N make_site_dirs() - installing = None - try: - installing = touch_file(get_site_path('locks', 'installing.lock')) + installing = touch_file(get_site_path('locks', 'installing.lock')) + atexit.register(_new_site_cleanup, site, mariadb_root_username, mariadb_root_password) - install_db(root_login=mariadb_root_username, root_password=mariadb_root_password, - db_name=db_name, admin_password=admin_password, verbose=verbose, - source_sql=source_sql, force=force, reinstall=reinstall, db_password=db_password, db_type=db_type, db_host=db_host, db_port=db_port, no_mariadb_socket=no_mariadb_socket) + install_db(root_login=mariadb_root_username, root_password=mariadb_root_password, db_name=db_name, + admin_password=admin_password, verbose=verbose, source_sql=source_sql, force=force, reinstall=reinstall, + db_password=db_password, db_type=db_type, db_host=db_host, db_port=db_port, no_mariadb_socket=no_mariadb_socket) + apps_to_install = ['frappe'] + (frappe.conf.get("install_apps") or []) + (list(install_apps) or []) + for app in apps_to_install: + _install_app(app, verbose=verbose, set_as_patched=not source_sql) - apps_to_install = ['frappe'] + (frappe.conf.get("install_apps") or []) + (list(install_apps) or []) - for app in apps_to_install: - _install_app(app, verbose=verbose, set_as_patched=not source_sql) + os.remove(installing) - frappe.utils.scheduler.toggle_scheduler(enable_scheduler) - frappe.db.commit() + frappe.utils.scheduler.toggle_scheduler(enable_scheduler) + frappe.db.commit() - scheduler_status = "disabled" if frappe.utils.scheduler.is_scheduler_disabled() else "enabled" - print("*** Scheduler is", scheduler_status, "***") + scheduler_status = "disabled" if frappe.utils.scheduler.is_scheduler_disabled() else "enabled" + print("*** Scheduler is", scheduler_status, "***") - except frappe.exceptions.ImproperDBConfigurationError: - _drop_site(site, mariadb_root_username, mariadb_root_password, force=True) +def _new_site_cleanup(site, mariadb_root_username, mariadb_root_password): + installing = get_site_path('locks', 'installing.lock') - finally: - if installing and os.path.exists(installing): - os.remove(installing) + if installing and os.path.exists(installing): + if mariadb_root_password: + _drop_site(site, mariadb_root_username, mariadb_root_password, force=True) + shutil.rmtree(site) - frappe.destroy() + frappe.destroy() @click.command('restore') @click.argument('sql-file-path') @@ -317,10 +328,18 @@ def backup(context, with_files=False, backup_path_db=None, backup_path_files=Non "Backup" from frappe.utils.backups import scheduled_backup verbose = context.verbose + exit_code = 0 for site in context.sites: - frappe.init(site=site) - frappe.connect() - odb = scheduled_backup(ignore_files=not with_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, backup_path_private_files=backup_path_private_files, force=True) + try: + frappe.init(site=site) + frappe.connect() + odb = scheduled_backup(ignore_files=not with_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, backup_path_private_files=backup_path_private_files, force=True) + except Exception as e: + if verbose: + print("Backup failed for {0}. Database or site_config.json may be corrupted".format(site)) + exit_code = 1 + continue + if verbose: from frappe.utils import now print("database backup taken -", odb.backup_path_db, "- on", now()) @@ -329,6 +348,7 @@ def backup(context, with_files=False, backup_path_db=None, backup_path_files=Non print("private files backup taken -", odb.backup_path_private_files, "- on", now()) frappe.destroy() + sys.exit(exit_code) @click.command('remove-from-installed-apps') @click.argument('app') diff --git a/frappe/contacts/doctype/contact/contact.json b/frappe/contacts/doctype/contact/contact.json index 7dd5aad4ce..2e2fb6df67 100644 --- a/frappe/contacts/doctype/contact/contact.json +++ b/frappe/contacts/doctype/contact/contact.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_events_in_timeline": 1, "allow_import": 1, "allow_rename": 1, @@ -115,6 +116,7 @@ "label": "Phone", "oldfieldname": "contact_no", "oldfieldtype": "Data", + "options": "Phone", "read_only": 1 }, { @@ -200,6 +202,7 @@ "fieldname": "mobile_no", "fieldtype": "Data", "label": "Mobile No", + "options": "Phone", "read_only": 1 }, { @@ -245,7 +248,8 @@ "icon": "fa fa-user", "idx": 1, "image_field": "image", - "modified": "2019-10-10 22:04:41.070479", + "links": [], + "modified": "2020-04-06 18:25:28.223693", "modified_by": "Administrator", "module": "Contacts", "name": "Contact", diff --git a/frappe/contacts/doctype/contact/contact.py b/frappe/contacts/doctype/contact/contact.py index b9239dc1f6..82311b19d4 100644 --- a/frappe/contacts/doctype/contact/contact.py +++ b/frappe/contacts/doctype/contact/contact.py @@ -69,23 +69,25 @@ class Contact(Document): return True def add_email(self, email_id, is_primary=0, autosave=False): - self.append("email_ids", { - "email_id": email_id, - "is_primary": is_primary - }) + if not frappe.db.exists("Contact Email", {"email_id": email_id, "parent": self.name}): + self.append("email_ids", { + "email_id": email_id, + "is_primary": is_primary + }) - if autosave: - self.save(ignore_permissions=True) + if autosave: + self.save(ignore_permissions=True) def add_phone(self, phone, is_primary_phone=0, is_primary_mobile_no=0, autosave=False): - self.append("phone_nos", { - "phone": phone, - "is_primary_phone": is_primary_phone, - "is_primary_mobile_no": is_primary_mobile_no - }) + if not frappe.db.exists("Contact Phone", {"phone": phone, "parent": self.name}): + self.append("phone_nos", { + "phone": phone, + "is_primary_phone": is_primary_phone, + "is_primary_mobile_no": is_primary_mobile_no + }) - if autosave: - self.save(ignore_permissions=True) + if autosave: + self.save(ignore_permissions=True) def set_primary_email(self): if not self.email_ids: diff --git a/frappe/contacts/doctype/contact_phone/contact_phone.json b/frappe/contacts/doctype/contact_phone/contact_phone.json index 3fb203ed69..5412e4a1b7 100644 --- a/frappe/contacts/doctype/contact_phone/contact_phone.json +++ b/frappe/contacts/doctype/contact_phone/contact_phone.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2019-08-02 13:10:37.890214", "doctype": "DocType", "editable_grid": 1, @@ -14,6 +15,7 @@ "fieldtype": "Data", "in_list_view": 1, "label": "Number", + "options": "Phone", "reqd": 1 }, { @@ -34,7 +36,8 @@ } ], "istable": 1, - "modified": "2019-09-24 17:47:50.375326", + "links": [], + "modified": "2020-04-06 18:28:10.486220", "modified_by": "Administrator", "module": "Contacts", "name": "Contact Phone", diff --git a/frappe/core/desk_page/settings/settings.json b/frappe/core/desk_page/settings/settings.json index 41d1765684..6569b2fb20 100644 --- a/frappe/core/desk_page/settings/settings.json +++ b/frappe/core/desk_page/settings/settings.json @@ -1,37 +1,37 @@ { "cards": [ { - "icon": "fa fa-th", - "links": "[\n {\n \"description\": \"Import Data from CSV / Excel files.\",\n \"icon\": \"octicon octicon-cloud-upload\",\n \"label\": \"Import Data\",\n \"name\": \"Data Import\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Export Data in CSV / Excel format.\",\n \"icon\": \"octicon octicon-cloud-upload\",\n \"label\": \"Export Data\",\n \"name\": \"Data Export\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Update many values at one time.\",\n \"hide_count\": true,\n \"label\": \"Bulk Update\",\n \"name\": \"Bulk Update\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"List of backups available for download\",\n \"icon\": \"fa fa-download\",\n \"label\": \"Download Backups\",\n \"name\": \"backups\",\n \"type\": \"page\"\n },\n {\n \"description\": \"Restore or permanently delete a document.\",\n \"label\": \"Deleted Documents\",\n \"name\": \"Deleted Document\",\n \"type\": \"doctype\"\n }\n]", - "title": "Data" + "hidden": 0, + "label": "Data", + "links": "[\n {\n \"description\": \"Import Data from CSV / Excel files.\",\n \"icon\": \"octicon octicon-cloud-upload\",\n \"label\": \"Import Data\",\n \"name\": \"Data Import\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Export Data in CSV / Excel format.\",\n \"icon\": \"octicon octicon-cloud-upload\",\n \"label\": \"Export Data\",\n \"name\": \"Data Export\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Update many values at one time.\",\n \"hide_count\": true,\n \"label\": \"Bulk Update\",\n \"name\": \"Bulk Update\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"List of backups available for download\",\n \"icon\": \"fa fa-download\",\n \"label\": \"Download Backups\",\n \"name\": \"backups\",\n \"type\": \"page\"\n },\n {\n \"description\": \"Restore or permanently delete a document.\",\n \"label\": \"Deleted Documents\",\n \"name\": \"Deleted Document\",\n \"type\": \"doctype\"\n }\n]" }, { - "icon": "fa fa-envelope", - "links": "[\n {\n \"description\": \"Add / Manage Email Accounts.\",\n \"label\": \"Email Account\",\n \"name\": \"Email Account\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Add / Manage Email Domains.\",\n \"label\": \"Email Domain\",\n \"name\": \"Email Domain\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Setup Notifications based on various criteria.\",\n \"label\": \"Notification\",\n \"name\": \"Notification\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Email Templates for common queries.\",\n \"label\": \"Email Template\",\n \"name\": \"Email Template\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Setup Reports to be emailed at regular intervals\",\n \"label\": \"Auto Email Report\",\n \"name\": \"Auto Email Report\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Create and manage newsletter\",\n \"label\": \"Newsletter\",\n \"name\": \"Newsletter\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Configure notifications for mentions, assignments, energy points and more.\",\n \"label\": \"Notification Settings\",\n \"name\": \"Notification Settings\",\n \"route\": \"Form/Notification Settings/Administrator\",\n \"type\": \"doctype\"\n }\n]", - "title": "Email / Notifications" + "hidden": 0, + "label": "Email / Notifications", + "links": "[\n {\n \"description\": \"Add / Manage Email Accounts.\",\n \"label\": \"Email Account\",\n \"name\": \"Email Account\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Add / Manage Email Domains.\",\n \"label\": \"Email Domain\",\n \"name\": \"Email Domain\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Setup Notifications based on various criteria.\",\n \"label\": \"Notification\",\n \"name\": \"Notification\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Email Templates for common queries.\",\n \"label\": \"Email Template\",\n \"name\": \"Email Template\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Setup Reports to be emailed at regular intervals\",\n \"label\": \"Auto Email Report\",\n \"name\": \"Auto Email Report\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Create and manage newsletter\",\n \"label\": \"Newsletter\",\n \"name\": \"Newsletter\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Configure notifications for mentions, assignments, energy points and more.\",\n \"label\": \"Notification Settings\",\n \"name\": \"Notification Settings\",\n \"route\": \"Form/Notification Settings/Administrator\",\n \"type\": \"doctype\"\n }\n]" }, { - "icon": "fa fa-globe", - "links": "[\n {\n \"description\": \"Setup of top navigation bar, footer and logo.\",\n \"label\": \"Website Settings\",\n \"name\": \"Website Settings\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"List of themes for Website.\",\n \"label\": \"Website Theme\",\n \"name\": \"Website Theme\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Javascript to append to the head section of the page.\",\n \"label\": \"Website Script\",\n \"name\": \"Website Script\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Settings for About Us Page.\",\n \"label\": \"About Us Settings\",\n \"name\": \"About Us Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Settings for Contact Us Page.\",\n \"label\": \"Contact Us Settings\",\n \"name\": \"Contact Us Settings\",\n \"type\": \"doctype\"\n }\n]", - "title": "Website" + "hidden": 0, + "label": "Website", + "links": "[\n {\n \"description\": \"Setup of top navigation bar, footer and logo.\",\n \"label\": \"Website Settings\",\n \"name\": \"Website Settings\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"List of themes for Website.\",\n \"label\": \"Website Theme\",\n \"name\": \"Website Theme\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Javascript to append to the head section of the page.\",\n \"label\": \"Website Script\",\n \"name\": \"Website Script\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Settings for About Us Page.\",\n \"label\": \"About Us Settings\",\n \"name\": \"About Us Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Settings for Contact Us Page.\",\n \"label\": \"Contact Us Settings\",\n \"name\": \"Contact Us Settings\",\n \"type\": \"doctype\"\n }\n]" }, { - "icon": "fa fa-wrench", - "links": "[\n {\n \"description\": \"Language, Date and Time settings\",\n \"hide_count\": true,\n \"label\": \"System Settings\",\n \"name\": \"System Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Log of error on automated events (scheduler).\",\n \"label\": \"Error Log\",\n \"name\": \"Error Log\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Log of error during requests.\",\n \"label\": \"Error Snapshot\",\n \"name\": \"Error Snapshot\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Enable / Disable Domains\",\n \"hide_count\": true,\n \"label\": \"Domain Settings\",\n \"name\": \"Domain Settings\",\n \"type\": \"doctype\"\n }\n]", - "title": "Core" + "hidden": 0, + "label": "Core", + "links": "[\n {\n \"description\": \"Language, Date and Time settings\",\n \"hide_count\": true,\n \"label\": \"System Settings\",\n \"name\": \"System Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Log of error on automated events (scheduler).\",\n \"label\": \"Error Log\",\n \"name\": \"Error Log\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Log of error during requests.\",\n \"label\": \"Error Snapshot\",\n \"name\": \"Error Snapshot\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Enable / Disable Domains\",\n \"hide_count\": true,\n \"label\": \"Domain Settings\",\n \"name\": \"Domain Settings\",\n \"type\": \"doctype\"\n }\n]" }, { - "icon": "fa fa-print", - "links": "[\n {\n \"description\": \"Drag and Drop tool to build and customize Print Formats.\",\n \"label\": \"Print Format Builder\",\n \"name\": \"print-format-builder\",\n \"type\": \"page\"\n },\n {\n \"description\": \"Set default format, page size, print style etc.\",\n \"label\": \"Print Settings\",\n \"name\": \"Print Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Customized HTML Templates for printing transactions.\",\n \"label\": \"Print Format\",\n \"name\": \"Print Format\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Stylesheets for Print Formats\",\n \"label\": \"Print Style\",\n \"name\": \"Print Style\",\n \"type\": \"doctype\"\n }\n]", - "title": "Printing" + "hidden": 0, + "label": "Printing", + "links": "[\n {\n \"description\": \"Drag and Drop tool to build and customize Print Formats.\",\n \"label\": \"Print Format Builder\",\n \"name\": \"print-format-builder\",\n \"type\": \"page\"\n },\n {\n \"description\": \"Set default format, page size, print style etc.\",\n \"label\": \"Print Settings\",\n \"name\": \"Print Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Customized HTML Templates for printing transactions.\",\n \"label\": \"Print Format\",\n \"name\": \"Print Format\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Stylesheets for Print Formats\",\n \"label\": \"Print Style\",\n \"name\": \"Print Style\",\n \"type\": \"doctype\"\n }\n]" }, { - "icon": "fa fa-random", - "links": "[\n {\n \"description\": \"Define workflows for forms.\",\n \"label\": \"Workflow\",\n \"name\": \"Workflow\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"States for workflow (e.g. Draft, Approved, Cancelled).\",\n \"label\": \"Workflow State\",\n \"name\": \"Workflow State\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Actions for workflow (e.g. Approve, Cancel).\",\n \"label\": \"Workflow Action\",\n \"name\": \"Workflow Action\",\n \"type\": \"doctype\"\n }\n]", - "title": "Workflow" + "hidden": 0, + "label": "Workflow", + "links": "[\n {\n \"description\": \"Define workflows for forms.\",\n \"label\": \"Workflow\",\n \"name\": \"Workflow\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"States for workflow (e.g. Draft, Approved, Cancelled).\",\n \"label\": \"Workflow State\",\n \"name\": \"Workflow State\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Actions for workflow (e.g. Approve, Cancel).\",\n \"label\": \"Workflow Action\",\n \"name\": \"Workflow Action\",\n \"type\": \"doctype\"\n }\n]" } ], - "category": "Administration", + "category": "Modules", "charts": [], "creation": "2020-03-02 15:09:40.527211", "developer_mode_only": 0, @@ -42,29 +42,29 @@ "idx": 0, "is_standard": 1, "label": "Settings", - "modified": "2020-03-12 16:30:43.510434", + "modified": "2020-04-01 11:24:40.636747", "modified_by": "Administrator", "module": "Core", "name": "Settings", "owner": "Administrator", - "pin_to_bottom": 0, - "pin_to_top": 1, + "pin_to_bottom": 1, + "pin_to_top": 0, "shortcuts": [ { "icon": "octicon octicon-settings", - "is_query_report": 0, + "label": "System Settings", "link_to": "System Settings", "type": "DocType" }, { "icon": "fa fa-print", - "is_query_report": 0, + "label": "Print Settings", "link_to": "Print Settings", "type": "DocType" }, { "icon": "fa fa-globe", - "is_query_report": 0, + "label": "Website Settings", "link_to": "Website Settings", "type": "DocType" } diff --git a/frappe/core/desk_page/users/users.json b/frappe/core/desk_page/users/users.json index dc9619314e..30455b86e6 100644 --- a/frappe/core/desk_page/users/users.json +++ b/frappe/core/desk_page/users/users.json @@ -1,19 +1,19 @@ { "cards": [ { - "icon": "fa fa-group", - "links": "[\n {\n \"description\": \"System and Website Users\",\n \"label\": \"User\",\n \"name\": \"User\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"User Roles\",\n \"label\": \"Role\",\n \"name\": \"Role\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Role Profile\",\n \"label\": \"Role Profile\",\n \"name\": \"Role Profile\",\n \"type\": \"doctype\"\n }\n]", - "title": "Users" + "hidden": 0, + "label": "Users", + "links": "[\n {\n \"description\": \"System and Website Users\",\n \"label\": \"User\",\n \"name\": \"User\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"User Roles\",\n \"label\": \"Role\",\n \"name\": \"Role\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Role Profile\",\n \"label\": \"Role Profile\",\n \"name\": \"Role Profile\",\n \"type\": \"doctype\"\n }\n]" }, { - "icon": "fa fa-group", - "links": "[\n {\n \"description\": \"Activity Log by \",\n \"label\": \"Activity Log\",\n \"name\": \"Activity Log\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"View Log of all print, download and export events\",\n \"label\": \"Access Log\",\n \"name\": \"Access Log\",\n \"type\": \"doctype\"\n }\n]", - "title": "Logs" + "hidden": 0, + "label": "Logs", + "links": "[\n {\n \"description\": \"Activity Log by \",\n \"label\": \"Activity Log\",\n \"name\": \"Activity Log\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"View Log of all print, download and export events\",\n \"label\": \"Access Log\",\n \"name\": \"Access Log\",\n \"type\": \"doctype\"\n }\n]" }, { - "icon": "fa fa-lock", - "links": "[\n {\n \"description\": \"Set Permissions on Document Types and Roles\",\n \"icon\": \"fa fa-lock\",\n \"label\": \"Role Permissions Manager\",\n \"name\": \"permission-manager\",\n \"type\": \"page\"\n },\n {\n \"description\": \"Restrict user for specific document\",\n \"icon\": \"fa fa-lock\",\n \"label\": \"User Permissions\",\n \"name\": \"User Permission\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Set custom roles for page and report\",\n \"label\": \"Role Permission for Page and Report\",\n \"name\": \"Role Permission for Page and Report\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"User\"\n ],\n \"description\": \"Check which Documents are readable by a User\",\n \"doctype\": \"User\",\n \"icon\": \"fa fa-eye-open\",\n \"is_query_report\": true,\n \"label\": \"Permitted Documents For User\",\n \"name\": \"Permitted Documents For User\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"DocShare\"\n ],\n \"description\": \"Report of all document shares\",\n \"doctype\": \"DocShare\",\n \"icon\": \"fa fa-share\",\n \"label\": \"Document Share Report\",\n \"name\": \"Document Share Report\",\n \"type\": \"report\"\n }\n]", - "title": "Permissions" + "hidden": 0, + "label": "Permissions", + "links": "[\n {\n \"description\": \"Set Permissions on Document Types and Roles\",\n \"icon\": \"fa fa-lock\",\n \"label\": \"Role Permissions Manager\",\n \"name\": \"permission-manager\",\n \"type\": \"page\"\n },\n {\n \"description\": \"Restrict user for specific document\",\n \"icon\": \"fa fa-lock\",\n \"label\": \"User Permissions\",\n \"name\": \"User Permission\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Set custom roles for page and report\",\n \"label\": \"Role Permission for Page and Report\",\n \"name\": \"Role Permission for Page and Report\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"User\"\n ],\n \"description\": \"Check which Documents are readable by a User\",\n \"doctype\": \"User\",\n \"icon\": \"fa fa-eye-open\",\n \"is_query_report\": true,\n \"label\": \"Permitted Documents For User\",\n \"name\": \"Permitted Documents For User\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"DocShare\"\n ],\n \"description\": \"Report of all document shares\",\n \"doctype\": \"DocShare\",\n \"icon\": \"fa fa-share\",\n \"label\": \"Document Share Report\",\n \"name\": \"Document Share Report\",\n \"type\": \"report\"\n }\n]" } ], "category": "Administration", @@ -27,7 +27,7 @@ "idx": 0, "is_standard": 1, "label": "Users", - "modified": "2020-03-12 16:30:42.483376", + "modified": "2020-04-01 11:24:40.767676", "modified_by": "Administrator", "module": "Core", "name": "Users", @@ -36,22 +36,22 @@ "pin_to_top": 0, "shortcuts": [ { - "is_query_report": 0, + "label": "User", "link_to": "User", "type": "DocType" }, { - "is_query_report": 0, + "label": "Role", "link_to": "Role", "type": "DocType" }, { - "is_query_report": 0, + "label": "permission-manager", "link_to": "permission-manager", "type": "Page" }, { - "is_query_report": 0, + "label": "user-profile", "link_to": "user-profile", "type": "Page" } diff --git a/frappe/core/doctype/communication/communication.json b/frappe/core/doctype/communication/communication.json index 5e34804b93..ae6fb164ec 100644 --- a/frappe/core/doctype/communication/communication.json +++ b/frappe/core/doctype/communication/communication.json @@ -1,9 +1,11 @@ { + "actions": [], "allow_import": 1, "creation": "2013-01-29 10:47:14", "description": "Keeps track of all communications", "doctype": "DocType", "document_type": "Setup", + "email_append_to": 1, "engine": "InnoDB", "field_order": [ "subject", @@ -383,7 +385,8 @@ ], "icon": "fa fa-comment", "idx": 1, - "modified": "2019-10-09 14:22:27.664645", + "links": [], + "modified": "2019-12-27 14:44:04.880373", "modified_by": "Administrator", "module": "Core", "name": "Communication", @@ -430,8 +433,10 @@ } ], "search_fields": "subject", + "sender_field": "sender", "sort_field": "modified", "sort_order": "DESC", + "subject_field": "subject", "title_field": "subject", "track_changes": 1, "track_seen": 1 diff --git a/frappe/core/doctype/data_import/test_exporter_new.py b/frappe/core/doctype/data_import/test_exporter_new.py index 7464d6edc5..eabf371b07 100644 --- a/frappe/core/doctype/data_import/test_exporter_new.py +++ b/frappe/core/doctype/data_import/test_exporter_new.py @@ -20,7 +20,7 @@ class TestExporter(unittest.TestCase): e = Exporter('Web Page', export_fields='All') csv_array = e.get_csv_array() header = csv_array[0] - self.assertEqual(len(header), 24) + self.assertEqual(len(header), 28) def test_exports_selected_fields(self): diff --git a/frappe/core/doctype/docfield/docfield.json b/frappe/core/doctype/docfield/docfield.json index 9b04ebb7ad..6d8ee41a5a 100644 --- a/frappe/core/doctype/docfield/docfield.json +++ b/frappe/core/doctype/docfield/docfield.json @@ -11,9 +11,9 @@ "label", "fieldtype", "fieldname", - "reqd", "precision", "length", + "reqd", "search_index", "in_list_view", "in_standard_filter", @@ -102,6 +102,7 @@ }, { "default": "0", + "depends_on": "eval:!in_list([\"Section Break\", \"Column Break\", \"Button\", \"HTML\"], doc.fieldtype)", "fieldname": "reqd", "fieldtype": "Check", "in_list_view": 1, @@ -452,7 +453,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-03-16 14:49:49.672099", + "modified": "2020-04-19 21:54:13.783908", "modified_by": "Administrator", "module": "Core", "name": "DocField", diff --git a/frappe/core/doctype/doctype/doctype.js b/frappe/core/doctype/doctype/doctype.js index 9a19185cfc..b3469abf29 100644 --- a/frappe/core/doctype/doctype/doctype.js +++ b/frappe/core/doctype/doctype/doctype.js @@ -53,7 +53,7 @@ frappe.ui.form.on('DocType', { frm.events.autoname(frm); }, - autoname(frm) { + autoname: function(frm) { frm.set_df_property('fields', 'reqd', frm.doc.autoname !== 'Prompt'); } }) diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json index 4e3f2fd84a..379ea227cb 100644 --- a/frappe/core/doctype/doctype/doctype.json +++ b/frappe/core/doctype/doctype/doctype.json @@ -54,6 +54,10 @@ "color", "show_preview_popup", "show_name_in_global_search", + "email_settings_sb", + "email_append_to", + "sender_field", + "subject_field", "sb2", "permissions", "restrict_to_domain", @@ -488,11 +492,37 @@ "fieldtype": "Table", "label": "Links", "options": "DocType Link" + }, + { + "depends_on": "email_append_to", + "fieldname": "subject_field", + "fieldtype": "Data", + "label": "Subject Field" + }, + { + "depends_on": "email_append_to", + "fieldname": "sender_field", + "fieldtype": "Data", + "label": "Sender Field", + "mandatory_depends_on": "email_append_to" + }, + { + "default": "0", + "fieldname": "email_append_to", + "fieldtype": "Check", + "label": "Allow document creation via Email" + }, + { + "collapsible": 1, + "fieldname": "email_settings_sb", + "fieldtype": "Section Break", + "label": "Email Settings" } ], "icon": "fa fa-bolt", "idx": 6, - "modified": "2019-11-25 17:24:03.690192", + "links": [], + "modified": "2020-03-27 14:51:44.581128", "modified_by": "Administrator", "module": "Core", "name": "DocType", diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 2c8cd240ee..d922cfe166 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -15,7 +15,7 @@ import frappe import frappe.website.render from frappe import _ from frappe.utils import now, cint -from frappe.model import no_value_fields, default_fields, data_fieldtypes, table_fields +from frappe.model import no_value_fields, default_fields, data_fieldtypes, table_fields, data_field_options from frappe.model.document import Document from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.custom.doctype.custom_field.custom_field import create_custom_field @@ -94,10 +94,11 @@ class DocType(Document): if not self.is_new(): self.setup_fields_to_fetch() + check_email_append_to(self) + if self.default_print_format and not self.custom: frappe.throw(_('Standard DocType cannot have default print format, use Customize Form')) - def set_default_in_list_view(self): '''Set default in-list-view for first 4 mandatory fields''' if not [d.fieldname for d in self.fields if d.in_list_view]: @@ -108,14 +109,12 @@ class DocType(Document): cnt += 1 if cnt == 4: break - def set_default_translatable(self): '''Ensure that non-translatable never will be translatable''' for d in self.fields: if d.translatable and not supports_translation(d.fieldtype): d.translatable = 0 - def check_developer_mode(self): """Throw exception if not developer mode or via patch""" if frappe.flags.in_patch or frappe.flags.in_test: @@ -124,7 +123,6 @@ class DocType(Document): if not frappe.conf.get("developer_mode") and not self.custom: frappe.throw(_("Not in Developer Mode! Set in site_config.json or make 'Custom' DocType."), CannotCreateStandardDoctypeError) - def setup_fields_to_fetch(self): '''Setup query to update values for newly set fetch values''' try: @@ -169,21 +167,18 @@ class DocType(Document): ) ) - def update_fields_to_fetch(self): '''Update fetch values based on queries setup''' if self.flags.update_fields_to_fetch_queries and not self.issingle: for query in self.flags.update_fields_to_fetch_queries: frappe.db.sql(query) - def validate_document_type(self): if self.document_type=="Transaction": self.document_type = "Document" if self.document_type=="Master": self.document_type = "Setup" - def validate_website(self): """Ensure that website generator has field 'route'""" if self.has_web_view: @@ -194,7 +189,6 @@ class DocType(Document): # clear website cache frappe.website.render.clear_cache() - def change_modified_of_parent(self): """Change the timestamp of parent DocType if the current one is a child to clear caches.""" if frappe.flags.in_import: @@ -204,7 +198,6 @@ class DocType(Document): for p in parent_list: frappe.db.sql('UPDATE `tabDocType` SET modified=%s WHERE `name`=%s', (now(), p.parent)) - def scrub_field_names(self): """Sluggify fieldnames if not set from Label.""" restricted = ('name','parent','creation','modified','modified_by', @@ -213,7 +206,7 @@ class DocType(Document): if d.fieldtype: if (not getattr(d, "fieldname", None)): if d.label: - d.fieldname = d.label.strip().lower().replace(' ','_') + d.fieldname = d.label.strip().lower().replace(' ','_').strip('?') if d.fieldname in restricted: d.fieldname = d.fieldname + '1' if d.fieldtype=='Section Break': @@ -234,7 +227,6 @@ class DocType(Document): # unique is automatically an index if d.unique: d.search_index = 0 - def validate_series(self, autoname=None, name=None): """Validate if `autoname` property is correctly set.""" if not autoname: autoname = self.autoname @@ -271,7 +263,6 @@ class DocType(Document): if used_in: frappe.throw(_("Series {0} already used in {1}").format(prefix, used_in[0][0])) - def on_update(self): """Update database schema, make controller templates if `custom` is not set and clear cache.""" self.delete_duplicate_custom_fields() @@ -325,7 +316,6 @@ class DocType(Document): dt = {0} and fieldname in ({1}) '''.format('%s', ', '.join(['%s'] * len(fields))), tuple([self.name] + fields), as_dict=True) - def sync_global_search(self): '''If global search settings are changed, rebuild search properties for this table''' global_search_fields_before_update = [d.fieldname for d in @@ -343,7 +333,6 @@ class DocType(Document): frappe.enqueue('frappe.utils.global_search.rebuild_for_doctype', now=now, doctype=self.name) - def set_base_class_for_controller(self): '''Updates the controller class to subclass from `WebsiteGenertor`, if it is a subclass of `Document`''' @@ -363,14 +352,12 @@ class DocType(Document): with open(controller_path, 'w') as f: f.write(code) - def run_module_method(self, method): from frappe.modules import load_doctype_module module = load_doctype_module(self.name, self.module) if hasattr(module, method): getattr(module, method)() - def before_rename(self, old, new, merge=False): """Throw exception if merge. DocTypes cannot be merged.""" if not self.custom and frappe.session.user != "Administrator": @@ -386,7 +373,6 @@ class DocType(Document): if not self.custom and not frappe.flags.in_test and not frappe.flags.in_patch: self.rename_files_and_folders(old, new) - def after_rename(self, old, new, merge=False): """Change table name using `RENAME TABLE` if table exists. Or update `doctype` property for Single type.""" @@ -397,7 +383,6 @@ class DocType(Document): else: frappe.db.sql("rename table `tab%s` to `tab%s`" % (old, new)) - def rename_files_and_folders(self, old, new): # move files new_path = get_doc_path(self.module, 'doctype', new) @@ -414,7 +399,6 @@ class DocType(Document): self.rename_inside_controller(new, old, new_path) frappe.msgprint(_('Renamed files and replaced code in controllers, please check!')) - def rename_inside_controller(self, new, old, new_path): for fname in ('{}.js', '{}.py', '{}_list.js', '{}_calendar.js', 'test_{}.py', 'test_{}.js'): fname = os.path.join(new_path, fname.format(frappe.scrub(new))) @@ -440,7 +424,6 @@ class DocType(Document): if not (self.issingle and self.istable): self.preserve_naming_series_options_in_property_setter() - def preserve_naming_series_options_in_property_setter(self): """Preserve naming_series as property setter if it does not exist""" naming_series = self.get("fields", {"fieldname": "naming_series"}) @@ -460,7 +443,6 @@ class DocType(Document): if naming_series[0].default: make_property_setter(self.name, "naming_series", "default", naming_series[0].default, "Text", validate_fields_for_doctype=False) - def before_export(self, docdict): # remove null and empty fields def remove_null_fields(o): @@ -495,7 +477,8 @@ class DocType(Document): field_dict = list(filter(lambda d: d['fieldname'] == fieldname, docdict['fields'])) if field_dict: new_field_dicts.append(field_dict[0]) - remaining_field_names.remove(fieldname) + if fieldname in remaining_field_names: + remaining_field_names.remove(fieldname) for fieldname in remaining_field_names: field_dict = list(filter(lambda d: d['fieldname'] == fieldname, docdict['fields'])) @@ -505,7 +488,6 @@ class DocType(Document): except ValueError: pass - @staticmethod def prepare_for_import(docdict): # set order of fields from field_order @@ -517,7 +499,8 @@ class DocType(Document): field_dict = list(filter(lambda d: d['fieldname'] == fieldname, docdict.get('fields', []))) if field_dict: new_field_dicts.append(field_dict[0]) - remaining_field_names.remove(fieldname) + if fieldname in remaining_field_names: + remaining_field_names.remove(fieldname) for fieldname in remaining_field_names: field_dict = list(filter(lambda d: d['fieldname'] == fieldname, docdict.get('fields', []))) @@ -528,19 +511,16 @@ class DocType(Document): if "field_order" in docdict: del docdict["field_order"] - def export_doc(self): """Export to standard folder `[module]/doctype/[name]/[name].json`.""" from frappe.modules.export_file import export_to_files export_to_files(record_list=[['DocType', self.name]], create_init=True) - def import_doc(self): """Import from standard folder `[module]/doctype/[name]/[name].json`.""" from frappe.modules.import_module import import_from_files import_from_files(record_list=[[self.module, 'doctype', self.name]]) - def make_controller_template(self): """Make boilerplate controller template.""" make_boilerplate("controller._py", self) @@ -557,7 +537,6 @@ class DocType(Document): make_boilerplate('templates/controller.html', self.as_dict()) make_boilerplate('templates/controller_row.html', self.as_dict()) - def make_amendable(self): """If is_submittable is set, add amended_from docfields.""" if self.is_submittable: @@ -573,7 +552,6 @@ class DocType(Document): "no_copy": 1 }) - def make_repeatable(self): """If allow_auto_repeat is set, add auto_repeat custom field.""" if self.allow_auto_repeat: @@ -642,14 +620,12 @@ class DocType(Document): }) self.nsm_parent_field = parent_field_name - def get_max_idx(self): """Returns the highest `idx`""" max_idx = frappe.db.sql("""select max(idx) from `tabDocField` where parent = %s""", self.name) return max_idx and max_idx[0][0] or 0 - def validate_name(self, name=None): if not name: name = self.name @@ -669,7 +645,6 @@ def validate_fields_for_doctype(doctype): doc.delete_duplicate_custom_fields() validate_fields(frappe.get_meta(doctype, cached=False)) - # this is separate because it is also called via custom field def validate_fields(meta): """Validate doctype fields. Checks @@ -693,29 +668,24 @@ def validate_fields(meta): def check_illegal_characters(fieldname): validate_column_name(fieldname) - def check_invalid_fieldnames(docname, fieldname): invalid_fields = ('doctype',) if fieldname in invalid_fields: frappe.throw(_("{0}: Fieldname cannot be one of {1}") .format(docname, ", ".join([frappe.bold(d) for d in invalid_fields]))) - def check_unique_fieldname(docname, fieldname): duplicates = list(filter(None, map(lambda df: df.fieldname==fieldname and str(df.idx) or None, fields))) if len(duplicates) > 1: frappe.throw(_("{0}: Fieldname {1} appears multiple times in rows {2}").format(docname, fieldname, ", ".join(duplicates)), UniqueFieldnameError) - def check_fieldname_length(fieldname): validate_column_length(fieldname) - def check_illegal_mandatory(docname, d): if (d.fieldtype in no_value_fields) and d.fieldtype not in table_fields and d.reqd: frappe.throw(_("{0}: Field {1} of type {2} cannot be mandatory").format(docname, d.label, d.fieldtype), IllegalMandatoryError) - def check_link_table_options(docname, d): if frappe.flags.in_patch: return if d.fieldtype in ("Link",) + table_fields: @@ -734,28 +704,23 @@ def validate_fields(meta): # fix case d.options = options - def check_hidden_and_mandatory(docname, d): if d.hidden and d.reqd and not d.default: frappe.throw(_("{0}: Field {1} in row {2} cannot be hidden and mandatory without default").format(docname, d.label, d.idx), HiddenAndMandatoryWithoutDefaultError) - def check_width(d): if d.fieldtype == "Currency" and cint(d.width) < 100: frappe.throw(_("Max width for type Currency is 100px in row {0}").format(d.idx)) - def check_in_list_view(d): if d.in_list_view and (d.fieldtype in not_allowed_in_list_view): frappe.throw(_("'In List View' not allowed for type {0} in row {1}").format(d.fieldtype, d.idx)) - def check_in_global_search(d): if d.in_global_search and d.fieldtype in no_value_fields: frappe.throw(_("'In Global Search' not allowed for type {0} in row {1}") .format(d.fieldtype, d.idx)) - def check_dynamic_link_options(d): if d.fieldtype=="Dynamic Link": doctype_pointer = list(filter(lambda df: df.fieldname==d.options, fields)) @@ -763,7 +728,6 @@ def validate_fields(meta): or (doctype_pointer[0].fieldtype=="Link" and doctype_pointer[0].options!="DocType"): frappe.throw(_("Options 'Dynamic Link' type of field must point to another Link Field with options as 'DocType'")) - def check_illegal_default(d): if d.fieldtype == "Check" and not d.default: d.default = '0' @@ -772,12 +736,10 @@ def validate_fields(meta): if d.fieldtype == "Select" and d.default and (d.default not in d.options.split("\n")): frappe.throw(_("Default for {0} must be an option").format(d.fieldname)) - def check_precision(d): if d.fieldtype in ("Currency", "Float", "Percent") and d.precision is not None and not (1 <= cint(d.precision) <= 6): frappe.throw(_("Precision should be between 1 and 6")) - def check_unique_and_text(docname, d): if meta.issingle: d.unique = 0 @@ -799,7 +761,6 @@ def validate_fields(meta): if d.search_index and d.fieldtype in ("Text", "Long Text", "Small Text", "Code", "Text Editor"): frappe.throw(_("{0}:Fieldtype {1} for {2} cannot be indexed").format(docname, d.fieldtype, d.label), CannotIndexedError) - def check_fold(fields): fold_exists = False for i, f in enumerate(fields): @@ -814,7 +775,6 @@ def validate_fields(meta): else: frappe.throw(_("Fold can not be at the end of the form")) - def check_search_fields(meta, fields): """Throw exception if `search_fields` don't contain valid fields.""" if not meta.search_fields: @@ -831,7 +791,6 @@ def validate_fields(meta): (fieldname not in fieldname_list): frappe.throw(_("Search field {0} is not valid").format(fieldname)) - def check_title_field(meta): """Throw exception if `title_field` isn't a valid fieldname.""" if not meta.get("title_field"): @@ -858,7 +817,6 @@ def validate_fields(meta): _validate_title_field_pattern(df.options) _validate_title_field_pattern(df.default) - def check_image_field(meta): '''check image_field exists and is of type "Attach Image"''' if not meta.image_field: @@ -870,7 +828,6 @@ def validate_fields(meta): if df[0].fieldtype != 'Attach Image': frappe.throw(_("Image field must be of type Attach Image"), InvalidFieldNameError) - def check_is_published_field(meta): if not meta.is_published_field: return @@ -878,7 +835,6 @@ def validate_fields(meta): if meta.is_published_field not in fieldname_list: frappe.throw(_("Is Published Field must be a valid fieldname"), InvalidFieldNameError) - def check_timeline_field(meta): if not meta.timeline_field: return @@ -890,7 +846,6 @@ def validate_fields(meta): if df.fieldtype not in ("Link", "Dynamic Link"): frappe.throw(_("Timeline field must be a Link or Dynamic Link"), InvalidFieldNameError) - def check_sort_field(meta): '''Validate that sort_field(s) is a valid field''' if meta.sort_field: @@ -903,7 +858,6 @@ def validate_fields(meta): frappe.throw(_("Sort field {0} must be a valid fieldname").format(fieldname), InvalidFieldNameError) - def check_illegal_depends_on_conditions(docfield): ''' assignment operation should not be allowed in the depends on condition.''' depends_on_fields = ["depends_on", "collapsible_depends_on", "mandatory_depends_on", "read_only_depends_on"] @@ -913,7 +867,6 @@ def validate_fields(meta): re.match("""[\w\.:_]+\s*={1}\s*[\w\.@'"]+""", depends_on): frappe.throw(_("Invalid {0} condition").format(frappe.unscrub(field)), frappe.ValidationError) - def check_table_multiselect_option(docfield): '''check if the doctype provided in Option has atleast 1 Link field''' if not docfield.fieldtype == 'Table MultiSelect': return @@ -926,7 +879,6 @@ def validate_fields(meta): frappe.throw(_('DocType {0} provided for the field {1} must have atleast one Link field') .format(doctype, docfield.fieldname), frappe.ValidationError) - def scrub_options_in_select(field): """Strip options for whitespaces""" @@ -938,11 +890,20 @@ def validate_fields(meta): options_list.append(_option) field.options = '\n'.join(options_list) - def scrub_fetch_from(field): if hasattr(field, 'fetch_from') and getattr(field, 'fetch_from'): field.fetch_from = field.fetch_from.strip('\n').strip() + def validate_data_field_type(docfield): + if docfield.fieldtype == "Data" and not (docfield.oldfieldtype and docfield.oldfieldtype != "Data"): + if docfield.options and (docfield.options not in data_field_options): + df_str = frappe.bold(_(docfield.label)) + text_str = _("{0} is an invalid Data field.").format(df_str) + "
" * 2 + _("Only Options allowed for Data field are:") + "
" + df_options_str = "
  • " + "
  • ".join([_(x) for x in data_field_options]) + "
" + + frappe.msgprint(text_str + df_options_str, title="Invalid Data Field", raise_exception=True) + + fields = meta.get("fields") fieldname_list = [d.fieldname for d in fields] @@ -955,7 +916,7 @@ def validate_fields(meta): if not d.permlevel: d.permlevel = 0 if d.fieldtype not in table_fields: d.allow_bulk_edit = 0 if not d.fieldname: - d.fieldname = d.fieldname.lower() + d.fieldname = d.fieldname.lower().strip('?') check_illegal_characters(d.fieldname) check_invalid_fieldnames(meta.get("name"), d.fieldname) @@ -973,6 +934,7 @@ def validate_fields(meta): check_table_multiselect_option(d) scrub_options_in_select(d) scrub_fetch_from(d) + validate_data_field_type(d) check_fold(fields) check_search_fields(meta, fields) @@ -982,7 +944,6 @@ def validate_fields(meta): check_sort_field(meta) check_image_field(meta) - def validate_permissions_for_doctype(doctype, for_remove=False): """Validates if permissions are set correctly.""" doctype = frappe.get_doc("DocType", doctype) @@ -994,7 +955,6 @@ def validate_permissions_for_doctype(doctype, for_remove=False): clear_permissions_cache(doctype.name) - def clear_permissions_cache(doctype): frappe.clear_cache(doctype=doctype) delete_notification_count_for(doctype) @@ -1009,7 +969,6 @@ def clear_permissions_cache(doctype): """, doctype): frappe.clear_cache(user=user) - def validate_permissions(doctype, for_remove=False): permissions = doctype.get("permissions") if not permissions: @@ -1103,7 +1062,6 @@ def validate_permissions(doctype, for_remove=False): check_level_zero_is_set(d) remove_rights_for_single(d) - def make_module_and_roles(doc, perm_fieldname="permissions"): """Make `Module Def` and `Role` records if already not made. Called while installing.""" try: @@ -1134,7 +1092,6 @@ def make_module_and_roles(doc, perm_fieldname="permissions"): else: raise - def check_if_fieldname_conflicts_with_methods(doctype, fieldname): doc = frappe.get_doc({"doctype": doctype}) method_list = [method for method in dir(doc) if isinstance(method, str) and callable(getattr(doc, method))] @@ -1142,6 +1099,38 @@ def check_if_fieldname_conflicts_with_methods(doctype, fieldname): if fieldname in method_list: frappe.throw(_("Fieldname {0} conflicting with meta object").format(fieldname)) - def clear_linked_doctype_cache(): frappe.cache().delete_value('linked_doctypes_without_ignore_user_permissions_enabled') + +def check_email_append_to(doc): + if not hasattr(doc, "email_append_to") or not doc.email_append_to: + return + + # Subject Field + doc.subject_field = doc.subject_field.strip() if doc.subject_field else None + subject_field = get_field(doc, doc.subject_field) + + if doc.subject_field and not subject_field: + frappe.throw(_("Select a valid Subject field for creating documents from Email")) + + if subject_field and subject_field.fieldtype not in ["Data", "Text", "Long Text", "Small Text", "Text Editor"]: + frappe.throw(_("Subject Field type should be Data, Text, Long Text, Small Text, Text Editor")) + + # Sender Field is mandatory + doc.sender_field = doc.sender_field.strip() if doc.sender_field else None + sender_field = get_field(doc, doc.sender_field) + + if doc.sender_field and not sender_field: + frappe.throw(_("Select a valid Sender Field for creating documents from Email")) + + if not sender_field.options == "Email": + frappe.throw(_("Sender Field should have Email in options")) + + +def get_field(doc, fieldname): + if not (doc or fieldname): + return + + for field in doc.fields: + if field.fieldname == fieldname: + return field diff --git a/frappe/core/doctype/doctype/test_doctype.py b/frappe/core/doctype/doctype/test_doctype.py index 969a71ab7d..fe9f88b9b3 100644 --- a/frappe/core/doctype/doctype/test_doctype.py +++ b/frappe/core/doctype/doctype/test_doctype.py @@ -113,6 +113,32 @@ class TestDocType(unittest.TestCase): if condition: self.assertFalse(re.match(pattern, condition)) + def test_data_field_options(self): + doctype_name = "Test Data Fields" + valid_data_field_options = frappe.model.data_field_options + ("",) + invalid_data_field_options = ("Invalid Option 1", frappe.utils.random_string(5)) + + for field_option in (valid_data_field_options + invalid_data_field_options): + test_doctype = frappe.get_doc({ + "doctype": "DocType", + "name": doctype_name, + "module": "Core", + "custom": 1, + "fields": [{ + "fieldname": "{0}_field".format(field_option), + "fieldtype": "Data", + "options": field_option + }] + }) + + if field_option in invalid_data_field_options: + # assert that only data options in frappe.model.data_field_options are valid + self.assertRaises(frappe.ValidationError, test_doctype.insert) + else: + test_doctype.insert() + self.assertEqual(test_doctype.name, doctype_name) + test_doctype.delete() + def test_sync_field_order(self): from frappe.modules.import_file import get_file_path import os @@ -349,4 +375,4 @@ class TestDocType(unittest.TestCase): # delete doctype link_doc.delete() doc.delete() - frappe.db.commit() \ No newline at end of file + frappe.db.commit() diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index 6633884bb3..8741101976 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -517,7 +517,7 @@ class File(Document): delete_file(self.thumbnail_url) def is_downloadable(self): - return self.is_private and has_permission(self, 'read') + return has_permission(self, 'read') def get_extension(self): '''returns split filename and extension''' @@ -608,8 +608,7 @@ def get_local_image(file_url): try: image = Image.open(file_path) except IOError: - frappe.msgprint(_("Unable to read file format for {0}").format(file_url)) - raise + frappe.msgprint(_("Unable to read file format for {0}").format(file_url), raise_exception=True) content = None @@ -712,7 +711,11 @@ def remove_all(dt, dn, from_delete=False): def has_permission(doc, ptype=None, user=None): - permission = True + has_access = False + user = user or frappe.session.user + + if not doc.is_private or doc.owner == user or user == 'Administrator': + has_access = True if doc.attached_to_doctype and doc.attached_to_name: attached_to_doctype = doc.attached_to_doctype @@ -722,20 +725,20 @@ def has_permission(doc, ptype=None, user=None): ref_doc = frappe.get_doc(attached_to_doctype, attached_to_name) if ptype in ['write', 'create', 'delete']: - permission = ref_doc.has_permission('write') + has_access = ref_doc.has_permission('write') - if ptype == 'delete' and permission == False: + if ptype == 'delete' and not has_access: frappe.throw(_("Cannot delete file as it belongs to {0} {1} for which you do not have permissions").format( doc.attached_to_doctype, doc.attached_to_name), frappe.PermissionError) else: - permission = ref_doc.has_permission('read') + has_access = ref_doc.has_permission('read') except frappe.DoesNotExistError: # if parent doc is not created before file is created - # we cannot check its permission so allow the file - permission = True + # we cannot check its permission so we will use file's permission + pass - return permission + return has_access def remove_file_by_url(file_url, doctype=None, name=None): diff --git a/frappe/core/doctype/language/language.json b/frappe/core/doctype/language/language.json index 099b383980..eed29883c1 100644 --- a/frappe/core/doctype/language/language.json +++ b/frappe/core/doctype/language/language.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_rename": 1, "autoname": "field:language_code", "creation": "2014-08-22 16:12:17.249590", @@ -41,7 +42,9 @@ } ], "icon": "fa fa-globe", - "modified": "2019-07-19 16:32:12.652550", + "in_create": 1, + "links": [], + "modified": "2020-04-16 22:11:33.066852", "modified_by": "Administrator", "module": "Core", "name": "Language", diff --git a/frappe/core/doctype/prepared_report/prepared_report.json b/frappe/core/doctype/prepared_report/prepared_report.json index ab6650d9e3..4663dcb463 100644 --- a/frappe/core/doctype/prepared_report/prepared_report.json +++ b/frappe/core/doctype/prepared_report/prepared_report.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "REP.#####", "creation": "2018-06-25 18:39:11.152960", "doctype": "DocType", @@ -101,7 +102,8 @@ } ], "in_create": 1, - "modified": "2019-09-18 04:00:55.644257", + "links": [], + "modified": "2020-03-05 10:52:56.598365", "modified_by": "Administrator", "module": "Core", "name": "Prepared Report", @@ -118,6 +120,15 @@ "role": "System Manager", "share": 1, "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Prepared Report User", + "share": 1 } ], "quick_entry": 1, diff --git a/frappe/core/doctype/prepared_report/prepared_report.py b/frappe/core/doctype/prepared_report/prepared_report.py index bc9a1fcdcd..4e87bb92dd 100644 --- a/frappe/core/doctype/prepared_report/prepared_report.py +++ b/frappe/core/doctype/prepared_report/prepared_report.py @@ -98,3 +98,34 @@ def download_attachment(dn): attached_file = frappe.get_doc("File", attachment.name) frappe.local.response.filecontent = gzip_decompress(attached_file.get_content()) frappe.local.response.type = "binary" + + +def get_permission_query_condition(user): + if not user: user = frappe.session.user + if user == "Administrator": + return None + + from frappe.utils.user import UserPermissions + user = UserPermissions(user) + + if "System Manager" in user.roles: + return None + + reports = [frappe.db.escape(report) for report in user.get_all_reports().keys()] + + return """`tabPrepared Report`.ref_report_doctype in ({reports})"""\ + .format(reports=','.join(reports)) + + +def has_permission(doc, user): + if not user: user = frappe.session.user + if user == "Administrator": + return True + + from frappe.utils.user import UserPermissions + user = UserPermissions(user) + + if "System Manager" in user.roles: + return True + + return doc.ref_report_doctype in user.get_all_reports().keys() diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py index 2d49915f59..967b28b8b2 100644 --- a/frappe/core/doctype/report/report.py +++ b/frappe/core/doctype/report/report.py @@ -6,7 +6,7 @@ import frappe import json, datetime from frappe import _, scrub import frappe.desk.query_report -from frappe.utils import cint +from frappe.utils import cint, cstr from frappe.model.document import Document from frappe.modules.export_file import export_to_files from frappe.modules import make_boilerplate @@ -92,6 +92,18 @@ class Report(Document): make_boilerplate("controller.py", self, {"name": self.name}) make_boilerplate("controller.js", self, {"name": self.name}) + def execute_query_report(self, filters): + if not self.query: + frappe.throw(_("Must specify a Query to run"), title=_('Report Document Error')) + + if not self.query.lower().startswith("select"): + frappe.throw(_("Query must be a SELECT"), title=_('Report Document Error')) + + result = [list(t) for t in frappe.db.sql(self.query, filters)] + columns = [cstr(c[0]) for c in frappe.db.get_description()] + + return [columns, result] + def execute_script_report(self, filters): # save the timestamp to automatically set to prepared threshold = 30 diff --git a/frappe/core/doctype/role_profile/role_profile.js b/frappe/core/doctype/role_profile/role_profile.js index 09aead670a..d31618cc4a 100644 --- a/frappe/core/doctype/role_profile/role_profile.js +++ b/frappe/core/doctype/role_profile/role_profile.js @@ -2,7 +2,7 @@ // For license information, please see license.txt frappe.ui.form.on('Role Profile', { - setup: function(frm) { + refresh: function(frm) { if(has_common(frappe.user_roles, ["Administrator", "System Manager"])) { if(!frm.roles_editor) { var role_area = $('
') diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json index e2ec921679..2a9c1a4573 100644 --- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.json @@ -13,6 +13,7 @@ "field_order": [ "stopped", "method", + "server_script", "frequency", "cron_format", "last_execution", @@ -63,6 +64,14 @@ "options": "All\nHourly\nHourly Long\nDaily\nDaily Long\nWeekly\nWeekly Long\nMonthly\nMonthly Long\nCron\nYearly\nAnnual", "read_only": 1, "reqd": 1 + }, + { + "fieldname": "server_script", + "fieldtype": "Link", + "label": "Server Script", + "options": "Server Script", + "read_only": 1, + "search_index": 1 } ], "in_create": 1, @@ -72,7 +81,7 @@ "link_fieldname": "scheduled_job_type" } ], - "modified": "2019-12-09 11:10:21.259929", + "modified": "2020-04-05 17:27:33.480562", "modified_by": "Administrator", "module": "Core", "name": "Scheduled Job Type", diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py index 3cd994ebfa..c179054550 100644 --- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py @@ -70,7 +70,12 @@ class ScheduledJobType(Document): self.scheduler_log = None try: self.log_status('Start') - frappe.get_attr(self.method)() + if self.server_script: + script_name = frappe.db.get_value("Server Script", self.server_script) + if script_name: + frappe.get_doc('Server Script', script_name).execute_scheduled_method() + else: + frappe.get_attr(self.method)() frappe.db.commit() self.log_status('Complete') except Exception: diff --git a/frappe/core/doctype/server_script/server_script.js b/frappe/core/doctype/server_script/server_script.js index eea8558456..d7f4c3e536 100644 --- a/frappe/core/doctype/server_script/server_script.js +++ b/frappe/core/doctype/server_script/server_script.js @@ -2,7 +2,45 @@ // For license information, please see license.txt frappe.ui.form.on('Server Script', { - // refresh: function(frm) { + refresh: function(frm) { + if(frm.doc.script_type === 'Scheduler Event' && !frm.doc.disabled){ + frm.add_custom_button('Schedule Script', function() { + var d = new frappe.ui.Dialog({ + title: "Schedule Script Execution", + fields: [ + { + fieldname: "event_type", + label: __('Select Event Type'), + fieldtype: "Select", + options: "All\nHourly\nDaily\nWeekly\nMonthly\nYearly\nHourly Long\nDaily Long\nWeekly Long\nMonthly Long" + }, + ], + primary_action_label: __('Schedule Script'), + primary_action: () => { + d.get_primary_btn().attr('disabled', true); + var data = d.get_values(); + d.hide(); + if(data) { + frm.events.schedule_script(frm, data); + } + + } + }); + + d.show(); + + }); + } + }, + + schedule_script(frm, data){ + frm.call({ + method: "frappe.core.doctype.server_script.server_script.setup_scheduler_events", + args: { + 'script_name': frm.doc.name, + 'frequency': data.event_type + } + }) + } - // } }); diff --git a/frappe/core/doctype/server_script/server_script.json b/frappe/core/doctype/server_script/server_script.json index 36c297cc26..bef3dfc60c 100644 --- a/frappe/core/doctype/server_script/server_script.json +++ b/frappe/core/doctype/server_script/server_script.json @@ -22,7 +22,7 @@ "fieldtype": "Select", "in_list_view": 1, "label": "Script Type", - "options": "DocType Event\nAPI", + "options": "DocType Event\nScheduler Event\nAPI", "reqd": 1 }, { @@ -75,7 +75,7 @@ } ], "links": [], - "modified": "2019-12-17 12:55:07.389775", + "modified": "2020-04-06 11:24:38.161555", "modified_by": "Administrator", "module": "Core", "name": "Server Script", diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py index e2c6d3b7b0..9522b77b4b 100644 --- a/frappe/core/doctype/server_script/server_script.py +++ b/frappe/core/doctype/server_script/server_script.py @@ -7,6 +7,8 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document from frappe.utils.safe_exec import safe_exec +from frappe import _ + class ServerScript(Document): @staticmethod @@ -31,3 +33,39 @@ class ServerScript(Document): # execute event safe_exec(self.script, None, dict(doc = doc)) + def execute_scheduled_method(self): + if self.script_type == 'Scheduler Event': + safe_exec(self.script) + else: + # wrong report type! + raise frappe.DoesNotExistError + +@frappe.whitelist() +def setup_scheduler_events(script_name, frequency): + method = frappe.scrub(script_name) + '_' + frequency.lower() + scheduled_script = frappe.db.get_value('Scheduled Job Type', + dict(method=method)) + + if not scheduled_script: + doc = frappe.get_doc(dict( + doctype = 'Scheduled Job Type', + method = method, + frequency = frequency, + server_script = script_name + )) + + doc.insert() + + frappe.msgprint(_('Enabled scheduled execution for script {0}').format(script_name)) + + else: + doc = frappe.get_doc('Scheduled Job Type', scheduled_script) + doc.update(dict( + doctype = 'Scheduled Job Type', + method = method, + frequency = frequency, + server_script = script_name + )) + doc.save() + + frappe.msgprint(_('Scheduled execution for script {0} has updated').format(script_name)) diff --git a/frappe/core/doctype/server_script/server_script_utils.py b/frappe/core/doctype/server_script/server_script_utils.py index 2e1a5ae8bb..e03504f30b 100644 --- a/frappe/core/doctype/server_script/server_script_utils.py +++ b/frappe/core/doctype/server_script/server_script_utils.py @@ -66,6 +66,7 @@ def get_server_script_map(): script_map.setdefault(script.reference_doctype, {}).setdefault(script.doctype_event, []).append(script.name) else: script_map.setdefault('_api', {})[script.api_method] = script.name + frappe.cache().set_value('server_script_map', script_map) - return script_map + return script_map \ No newline at end of file diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js index c4873ee40e..b8e16bfe25 100644 --- a/frappe/core/doctype/user/user.js +++ b/frappe/core/doctype/user/user.js @@ -97,6 +97,50 @@ frappe.ui.form.on('User', { }); }, __("Password")); + if (frappe.user.has_role("System Manager")) { + frappe.db.get_single_value("LDAP Settings", "enabled").then((value) => { + if (value === 1 && frm.doc.name != "Administrator") { + frm.add_custom_button(__("Reset LDAP Password"), function() { + const d = new frappe.ui.Dialog({ + title: __("Reset LDAP Password"), + fields: [ + { + label: __("New Password"), + fieldtype: "Password", + fieldname: "new_password", + reqd: 1 + }, + { + label: __("Confirm New Password"), + fieldtype: "Password", + fieldname: "confirm_password", + reqd: 1 + }, + { + label: __("Logout All Sessions"), + fieldtype: "Check", + fieldname: "logout_sessions" + } + ], + primary_action: (values) => { + d.hide(); + if (values.new_password !== values.confirm_password) { + frappe.throw(__("Passwords do not match!")); + } + frappe.call( + "frappe.integrations.doctype.ldap_settings.ldap_settings.reset_password", { + user: frm.doc.email, + password: values.new_password, + logout: values.logout_sessions + }); + } + }); + d.show(); + }, __("Password")); + } + }); + } + frm.add_custom_button(__("Reset OTP Secret"), function() { frappe.call({ method: "frappe.core.doctype.user.user.reset_otp_secret", diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json index 5ebde7e7bd..7ed14e094c 100644 --- a/frappe/core/doctype/user/user.json +++ b/frappe/core/doctype/user/user.json @@ -238,12 +238,14 @@ { "fieldname": "phone", "fieldtype": "Data", - "label": "Phone" + "label": "Phone", + "options": "Phone" }, { "fieldname": "mobile_no", "fieldtype": "Data", "label": "Mobile No", + "options": "Phone", "unique": 1 }, { @@ -588,7 +590,7 @@ "image_field": "user_image", "links": [], "max_attachments": 5, - "modified": "2020-03-23 22:59:26.154985", + "modified": "2020-04-08 12:27:36.716490", "modified_by": "Administrator", "module": "Core", "name": "User", diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index ddad3a91fb..8370af6808 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -205,7 +205,7 @@ class User(Document): _update_password(user=self.name, pwd=new_password, logout_all_sessions=self.logout_all_sessions) - if not self.flags.no_welcome_mail and self.send_welcome_email: + if not self.flags.no_welcome_mail and cint(self.send_welcome_email): self.send_welcome_mail_to_user() self.flags.email_sent = 1 if frappe.session.user != 'Guest': @@ -551,6 +551,7 @@ def update_password(new_password, logout_all_sessions=0, key=None, old_password= res = _get_user_for_update_password(key, old_password) if res.get('message'): + frappe.local.response.http_status_code = 410 return res['message'] else: user = res['user'] @@ -577,7 +578,7 @@ def update_password(new_password, logout_all_sessions=0, key=None, old_password= return redirect_url if redirect_url else "/" @frappe.whitelist(allow_guest=True) -def test_password_strength(new_password, key=None, old_password=None, user_data=[]): +def test_password_strength(new_password, key=None, old_password=None, user_data=None): from frappe.utils.password_strength import test_password_strength as _test_password_strength password_policy = frappe.db.get_value("System Settings", None, @@ -718,7 +719,7 @@ def _get_user_for_update_password(key, old_password): user = frappe.db.get_value("User", {"reset_password_key": key}) if not user: return { - 'message': _("Cannot Update: Incorrect / Expired Link.") + 'message': _("The Link specified has either been used before or Invalid") } elif old_password: diff --git a/frappe/core/doctype/user_email/user_email.json b/frappe/core/doctype/user_email/user_email.json index 16e6b5a24e..b106ed4a19 100644 --- a/frappe/core/doctype/user_email/user_email.json +++ b/frappe/core/doctype/user_email/user_email.json @@ -1,201 +1,63 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2016-03-30 10:04:25.828742", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, + "actions": [], + "creation": "2016-03-30 10:04:25.828742", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "email_account", + "email_id", + "column_break_3", + "awaiting_password", + "enable_outgoing" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "email_account", - "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": 0, - "label": "Email Account", - "length": 0, - "no_copy": 0, - "options": "Email Account", - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "email_account", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Email Account", + "options": "Email Account", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fetch_from": "email_account.email_id", - "fieldname": "email_id", - "fieldtype": "Data", - "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": "Email ID", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "email_id", + "fieldtype": "Data", + "label": "Email ID", + "options": "Email", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_3", - "fieldtype": "Column 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, - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "email_account.awaiting_password", - "fieldname": "awaiting_password", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Awaiting Password", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fetch_from": "email_account.awaiting_password", + "fieldname": "awaiting_password", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Awaiting Password", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "email_account.enable_outgoing", - "fieldname": "enable_outgoing", - "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": "Enable Outgoing", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "default": "0", + "fetch_from": "email_account.enable_outgoing", + "fieldname": "enable_outgoing", + "fieldtype": "Check", + "label": "Enable Outgoing", + "read_only": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-05-25 22:43:34.045787", - "modified_by": "Administrator", - "module": "Core", - "name": "User Email", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0 + ], + "istable": 1, + "links": [], + "modified": "2020-04-06 19:19:12.130246", + "modified_by": "Administrator", + "module": "Core", + "name": "User Email", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC" } \ No newline at end of file diff --git a/frappe/core/doctype/video/__init__.py b/frappe/core/doctype/video/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/video/test_video.py b/frappe/core/doctype/video/test_video.py new file mode 100644 index 0000000000..0bed1e98d6 --- /dev/null +++ b/frappe/core/doctype/video/test_video.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestVideo(unittest.TestCase): + pass diff --git a/frappe/core/doctype/video/video.js b/frappe/core/doctype/video/video.js new file mode 100644 index 0000000000..36ea240a36 --- /dev/null +++ b/frappe/core/doctype/video/video.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Video', { + // refresh: function(frm) { + + // } +}); diff --git a/frappe/core/doctype/video/video.json b/frappe/core/doctype/video/video.json new file mode 100644 index 0000000000..26a407c05c --- /dev/null +++ b/frappe/core/doctype/video/video.json @@ -0,0 +1,106 @@ +{ + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "field:title", + "creation": "2018-10-17 05:47:13.087395", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "title", + "provider", + "url", + "column_break_4", + "publish_date", + "duration", + "section_break_7", + "description" + ], + "fields": [ + { + "fieldname": "title", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Title", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "provider", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Provider", + "options": "YouTube\nVimeo", + "reqd": 1 + }, + { + "fieldname": "url", + "fieldtype": "Data", + "in_list_view": 1, + "label": "URL", + "reqd": 1 + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "publish_date", + "fieldtype": "Date", + "label": "Publish Date" + }, + { + "fieldname": "duration", + "fieldtype": "Data", + "label": "Duration" + }, + { + "fieldname": "section_break_7", + "fieldtype": "Section Break" + }, + { + "fieldname": "description", + "fieldtype": "Text Editor", + "in_list_view": 1, + "label": "Description", + "reqd": 1 + } + ], + "links": [], + "modified": "2020-04-22 12:09:49.057403", + "modified_by": "Administrator", + "module": "Core", + "name": "Video", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/core/doctype/video/video.py b/frappe/core/doctype/video/video.py new file mode 100644 index 0000000000..fdbd3a1abe --- /dev/null +++ b/frappe/core/doctype/video/video.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class Video(Document): + pass diff --git a/frappe/core/page/background_jobs/background_jobs.py b/frappe/core/page/background_jobs/background_jobs.py index c8a2352968..4a94de4ace 100644 --- a/frappe/core/page/background_jobs/background_jobs.py +++ b/frappe/core/page/background_jobs/background_jobs.py @@ -28,6 +28,7 @@ def get_info(show_failed=False): if j.kwargs.get('site')==frappe.local.site: jobs.append({ 'job_name': j.kwargs.get('kwargs', {}).get('playbook_method') \ + or j.kwargs.get('kwargs', {}).get('job_type') \ or str(j.kwargs.get('job_name')), 'status': j.get_status(), 'queue': name, 'creation': format_datetime(convert_utc_to_user_timezone(j.created_at)), diff --git a/frappe/core/page/dashboard/dashboard.js b/frappe/core/page/dashboard/dashboard.js index 511aac7010..ad65b05894 100644 --- a/frappe/core/page/dashboard/dashboard.js +++ b/frappe/core/page/dashboard/dashboard.js @@ -59,7 +59,7 @@ class Dashboard { } show_dashboard(current_dashboard_name) { - if(this.dashboard_name !== current_dashboard_name) { + if (this.dashboard_name !== current_dashboard_name) { this.dashboard_name = current_dashboard_name; let title = this.dashboard_name; if (!this.dashboard_name.toLowerCase().includes(__('dashboard'))) { @@ -76,30 +76,48 @@ class Dashboard { } refresh() { - this.get_dashboard_doc().then((doc) => { - this.dashboard_doc = doc; - this.charts = this.dashboard_doc.charts - .map(chart => { - return { - chart_name: chart.chart, - label: chart.chart, - ...chart - } - }); + this.get_permitted_dashboard_charts().then(charts => { + if (!charts.length) { + frappe.msgprint(__('No Permitted Charts on this Dashboard'), __('No Permitted Charts')) + } - this.chart_group = new frappe.widget.WidgetGroup({ - title: null, - container: this.container, - type: "chart", - columns: 2, - allow_sorting: false, - widgets: this.charts, - }); + frappe.dashboard_utils.get_dashboard_settings().then((settings) => { + let chart_config = settings.chart_config? JSON.parse(settings.chart_config): {}; + this.charts = + charts.map(chart => { + return { + chart_name: chart.chart, + label: chart.chart, + chart_settings: chart_config[chart.chart] || {}, + ...chart + } + }); + this.chart_group = new frappe.widget.WidgetGroup({ + title: null, + container: this.container, + type: "chart", + columns: 2, + options: { + allow_sorting: false, + allow_create: false, + allow_delete: false, + allow_hiding: false, + allow_edit: false, + }, + widgets: this.charts, + }); + }) }); } - get_dashboard_doc() { - return frappe.model.with_doc('Dashboard', this.dashboard_name); + get_permitted_dashboard_charts() { + return frappe.xcall( + 'frappe.desk.doctype.dashboard.dashboard.get_permitted_charts', + { + dashboard_name: this.dashboard_name + }).then(charts => { + return charts; + }); } set_dropdown() { diff --git a/frappe/core/page/dashboard/dashboard.json b/frappe/core/page/dashboard/dashboard.json index 891dcb26f8..58fda5a34c 100644 --- a/frappe/core/page/dashboard/dashboard.json +++ b/frappe/core/page/dashboard/dashboard.json @@ -4,7 +4,7 @@ "docstatus": 0, "doctype": "Page", "idx": 0, - "modified": "2019-01-08 19:19:48.073410", + "modified": "2020-03-26 13:30:44.603948", "modified_by": "Administrator", "module": "Core", "name": "dashboard", diff --git a/frappe/core/page/permission_manager/permission_manager.js b/frappe/core/page/permission_manager/permission_manager.js index 710bb51680..ed3b0d17db 100644 --- a/frappe/core/page/permission_manager/permission_manager.js +++ b/frappe/core/page/permission_manager/permission_manager.js @@ -217,6 +217,7 @@ frappe.PermissionEngine = Class.extend({ me.rights.forEach(r => { if (!d.is_submittable && ['submit', 'cancel', 'amend'].includes(r)) return; + if (d.in_create && ['create', 'write', 'delete'].includes(r)) return; me.add_check(perm_container, d, r); }); diff --git a/frappe/core/page/permission_manager/permission_manager.py b/frappe/core/page/permission_manager/permission_manager.py index 1afd7bb423..637b526d5c 100644 --- a/frappe/core/page/permission_manager/permission_manager.py +++ b/frappe/core/page/permission_manager/permission_manager.py @@ -66,6 +66,7 @@ def get_permissions(doctype=None, role=None): meta = frappe.get_meta(d.parent) if meta: d.is_submittable = meta.is_submittable + d.in_create = meta.in_create return out diff --git a/frappe/core/utils.py b/frappe/core/utils.py index 55767ffe34..55cfbc34d7 100644 --- a/frappe/core/utils.py +++ b/frappe/core/utils.py @@ -67,3 +67,19 @@ def find_all(list_of_dict, match_function): if match_function(entry): found.append(entry) return found + +def ljust_list(_list, length, fill_word=None): + """ + Similar to ljust but for list. + + Usage: + $ ljust_list([1, 2, 3], 5) + > [1, 2, 3, None, None] + """ + # make a copy to avoid mutation of passed list + _list = list(_list) + fill_length = length - len(_list) + if fill_length > 0: + _list.extend([fill_word] * fill_length) + + return _list diff --git a/frappe/custom/desk_page/customization/customization.json b/frappe/custom/desk_page/customization/customization.json index dedfcaeeec..29f4cb745f 100644 --- a/frappe/custom/desk_page/customization/customization.json +++ b/frappe/custom/desk_page/customization/customization.json @@ -1,17 +1,19 @@ { "cards": [ { - "links": "[\n {\n \"label\": \"Dashboard\",\n \"name\": \"Dashboard\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Dashboard Chart\",\n \"name\": \"Dashboard Chart\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Dashboard Chart Source\",\n \"name\": \"Dashboard Chart Source\",\n \"type\": \"doctype\"\n }\n]", - "title": "Dashboards" + "hidden": 0, + "label": "Dashboards", + "links": "[\n {\n \"label\": \"Dashboard\",\n \"name\": \"Dashboard\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Dashboard Chart\",\n \"name\": \"Dashboard Chart\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Dashboard Chart Source\",\n \"name\": \"Dashboard Chart Source\",\n \"type\": \"doctype\"\n }\n]" }, { - "icon": "fa fa-glass", - "links": "[\n {\n \"description\": \"Change field properties (hide, readonly, permission etc.)\",\n \"label\": \"Customize Form\",\n \"name\": \"Customize Form\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Add fields to forms.\",\n \"label\": \"Custom Field\",\n \"name\": \"Custom Field\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Add custom javascript to forms.\",\n \"label\": \"Custom Script\",\n \"name\": \"Custom Script\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Add custom forms.\",\n \"label\": \"DocType\",\n \"name\": \"DocType\",\n \"type\": \"doctype\"\n }\n]", - "title": "Form Customization" + "hidden": 0, + "label": "Form Customization", + "links": "[\n {\n \"description\": \"Change field properties (hide, readonly, permission etc.)\",\n \"label\": \"Customize Form\",\n \"name\": \"Customize Form\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Add fields to forms.\",\n \"label\": \"Custom Field\",\n \"name\": \"Custom Field\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Add custom javascript to forms.\",\n \"label\": \"Custom Script\",\n \"name\": \"Custom Script\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Add custom forms.\",\n \"label\": \"DocType\",\n \"name\": \"DocType\",\n \"type\": \"doctype\"\n }\n]" }, { - "links": "[\n {\n \"description\": \"Add your own translations\",\n \"label\": \"Custom Translations\",\n \"name\": \"Translation\",\n \"type\": \"doctype\"\n }\n]", - "title": "Other" + "hidden": 0, + "label": "Other", + "links": "[\n {\n \"description\": \"Add your own translations\",\n \"label\": \"Custom Translations\",\n \"name\": \"Translation\",\n \"type\": \"doctype\"\n }\n]" } ], "category": "Administration", @@ -25,7 +27,7 @@ "idx": 0, "is_standard": 1, "label": "Customization", - "modified": "2020-03-12 16:30:42.155206", + "modified": "2020-04-01 11:24:40.787109", "modified_by": "Administrator", "module": "Custom", "name": "Customization", @@ -34,17 +36,17 @@ "pin_to_top": 0, "shortcuts": [ { - "is_query_report": 0, + "label": "Customize Form", "link_to": "Customize Form", "type": "DocType" }, { - "is_query_report": 0, + "label": "Custom Role", "link_to": "Custom Role", "type": "DocType" }, { - "is_query_report": 0, + "label": "Custom Script", "link_to": "Custom Script", "type": "DocType" } diff --git a/frappe/custom/doctype/custom_field/custom_field.json b/frappe/custom/doctype/custom_field/custom_field.json index b274033f80..394f38b56c 100644 --- a/frappe/custom/doctype/custom_field/custom_field.json +++ b/frappe/custom/doctype/custom_field/custom_field.json @@ -41,6 +41,7 @@ "in_list_view", "in_standard_filter", "in_global_search", + "in_preview", "bold", "report_hide", "search_index", @@ -371,12 +372,18 @@ "fieldname": "allow_in_quick_entry", "fieldtype": "Check", "label": "Allow in Quick Entry" + }, + { + "default": "0", + "fieldname": "in_preview", + "fieldtype": "Check", + "label": "In Preview" } ], "icon": "fa fa-glass", "idx": 1, "links": [], - "modified": "2020-03-16 14:52:43.954709", + "modified": "2020-04-10 11:57:10.392218", "modified_by": "Administrator", "module": "Custom", "name": "Custom Field", diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js index 5c9fa37ef5..b1743a96a5 100644 --- a/frappe/custom/doctype/customize_form/customize_form.js +++ b/frappe/custom/doctype/customize_form/customize_form.js @@ -143,8 +143,7 @@ frappe.ui.form.on("Customize Form", { }, 1000); } - }, - + } }); frappe.ui.form.on("Customize Form Field", { diff --git a/frappe/custom/doctype/customize_form/customize_form.json b/frappe/custom/doctype/customize_form/customize_form.json index 0b1df62f9d..cd57aa23fe 100644 --- a/frappe/custom/doctype/customize_form/customize_form.json +++ b/frappe/custom/doctype/customize_form/customize_form.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "DL.####", "creation": "2013-01-29 17:55:08", "doctype": "DocType", @@ -19,6 +20,7 @@ "track_views", "allow_auto_repeat", "allow_import", + "show_preview_popup", "image_view", "column_break_5", "title_field", @@ -28,6 +30,10 @@ "sort_field", "column_break_10", "sort_order", + "section_break_23", + "email_append_to", + "sender_field", + "subject_field", "fields_section_break", "fields" ], @@ -174,13 +180,44 @@ "fieldname": "allow_import", "fieldtype": "Check", "label": "Allow Import (via Data Import Tool)" + }, + { + "depends_on": "email_append_to", + "fieldname": "subject_field", + "fieldtype": "Data", + "label": "Subject Field" + }, + { + "depends_on": "email_append_to", + "fieldname": "sender_field", + "fieldtype": "Data", + "label": "Sender Field", + "mandatory_depends_on": "email_append_to" + }, + { + "default": "0", + "fieldname": "email_append_to", + "fieldtype": "Check", + "label": "Allow document creation via Email" + }, + { + "depends_on": "doc_type", + "fieldname": "section_break_23", + "fieldtype": "Section Break" + }, + { + "default": "0", + "fieldname": "show_preview_popup", + "fieldtype": "Check", + "label": "Show Preview Popup" } ], "hide_toolbar": 1, "icon": "fa fa-glass", "idx": 1, "issingle": 1, - "modified": "2019-10-08 11:16:36.698006", + "links": [], + "modified": "2020-04-10 12:16:01.320411", "modified_by": "Administrator", "module": "Custom", "name": "Customize Form", diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index 3259085781..9efa555152 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -12,7 +12,7 @@ from frappe import _ from frappe.utils import cint from frappe.model.document import Document from frappe.model import no_value_fields, core_doctypes_list -from frappe.core.doctype.doctype.doctype import validate_fields_for_doctype +from frappe.core.doctype.doctype.doctype import validate_fields_for_doctype, check_email_append_to from frappe.custom.doctype.custom_field.custom_field import create_custom_field from frappe.model.docfield import supports_translation @@ -31,7 +31,11 @@ doctype_properties = { 'track_changes': 'Check', 'track_views': 'Check', 'allow_auto_repeat': 'Check', - 'allow_import': 'Check' + 'allow_import': 'Check', + 'show_preview_popup': 'Check', + 'email_append_to': 'Check', + 'subject_field': 'Data', + 'sender_field': 'Data' } docfield_properties = { @@ -50,6 +54,7 @@ docfield_properties = { 'in_list_view': 'Check', 'in_standard_filter': 'Check', 'in_global_search': 'Check', + 'in_preview': 'Check', 'bold': 'Check', 'hidden': 'Check', 'collapsible': 'Check', @@ -170,6 +175,7 @@ class CustomizeForm(Document): self.update_custom_fields() self.set_name_translation() validate_fields_for_doctype(self.doc_type) + check_email_append_to(self) if self.flags.update_db: frappe.db.updatedb(self.doc_type) @@ -204,9 +210,11 @@ class CustomizeForm(Document): self.validate_fieldtype_change(df, meta_df[0].get(property), df.get(property)) elif property == "allow_on_submit" and df.get(property): - frappe.msgprint(_("Row {0}: Not allowed to enable Allow on Submit for standard fields")\ - .format(df.idx)) - continue + if not frappe.db.get_value("DocField", + {"parent": self.doc_type, "fieldname": df.fieldname}, "allow_on_submit"): + frappe.msgprint(_("Row {0}: Not allowed to enable Allow on Submit for standard fields")\ + .format(df.idx)) + continue elif property == "reqd" and \ ((frappe.db.get_value("DocField", @@ -365,7 +373,12 @@ class CustomizeForm(Document): for allowed_changes in allowed_fieldtype_change: if (old_value in allowed_changes and new_value in allowed_changes): allowed = True - if frappe.db.type_map.get(old_value)[1] > frappe.db.type_map.get(new_value)[1]: + old_value_length = cint(frappe.db.type_map.get(old_value)[1]) + new_value_length = cint(frappe.db.type_map.get(new_value)[1]) + + # Ignore fieldtype check validation if new field type has unspecified maxlength + # Changes like DATA to TEXT, where new_value_lenth equals 0 will not be validated + if new_value_length and (old_value_length > new_value_length): self.check_length_for_fieldtypes.append({'df': df, 'old_value': old_value}) self.validate_fieldtype_length() else: @@ -377,7 +390,7 @@ class CustomizeForm(Document): def validate_fieldtype_length(self): for field in self.check_length_for_fieldtypes: df = field.get('df') - max_length = frappe.db.type_map.get(df.fieldtype)[1] + max_length = cint(frappe.db.type_map.get(df.fieldtype)[1]) fieldname = df.fieldname docs = frappe.db.sql(''' SELECT name, {fieldname}, LENGTH({fieldname}) AS len diff --git a/frappe/custom/doctype/customize_form/test_customize_form.py b/frappe/custom/doctype/customize_form/test_customize_form.py index 1cd71ea05d..cace25a03d 100644 --- a/frappe/custom/doctype/customize_form/test_customize_form.py +++ b/frappe/custom/doctype/customize_form/test_customize_form.py @@ -46,7 +46,7 @@ class TestCustomizeForm(unittest.TestCase): d = self.get_customize_form("Event") self.assertEquals(d.doc_type, "Event") - self.assertEquals(len(d.get("fields")), 35) + self.assertEquals(len(d.get("fields")), 36) d = self.get_customize_form("Event") self.assertEquals(d.doc_type, "Event") diff --git a/frappe/custom/doctype/customize_form_field/customize_form_field.json b/frappe/custom/doctype/customize_form_field/customize_form_field.json index 57b4cec23b..d7887cf8bd 100644 --- a/frappe/custom/doctype/customize_form_field/customize_form_field.json +++ b/frappe/custom/doctype/customize_form_field/customize_form_field.json @@ -16,6 +16,7 @@ "in_list_view", "in_standard_filter", "in_global_search", + "in_preview", "bold", "allow_in_quick_entry", "translatable", @@ -93,6 +94,7 @@ }, { "default": "0", + "depends_on": "eval:!in_list([\"Section Break\", \"Column Break\", \"Button\", \"HTML\"], doc.fieldtype)", "fieldname": "reqd", "fieldtype": "Check", "label": "Mandatory", @@ -358,7 +360,7 @@ "default": "0", "fieldname": "allow_in_quick_entry", "fieldtype": "Check", - "label": " Allow in Quick Entry " + "label": "Allow in Quick Entry" }, { "fieldname": "property_depends_on_section", @@ -380,12 +382,18 @@ "fieldtype": "Code", "label": "Read Only Depends On", "options": "JS" + }, + { + "default": "0", + "fieldname": "in_preview", + "fieldtype": "Check", + "label": "In Preview" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-03-16 14:53:40.619043", + "modified": "2020-04-10 11:58:44.573537", "modified_by": "Administrator", "module": "Custom", "name": "Customize Form Field", diff --git a/frappe/database/mariadb/framework_mariadb.sql b/frappe/database/mariadb/framework_mariadb.sql index dbe53df4e4..46940cc846 100644 --- a/frappe/database/mariadb/framework_mariadb.sql +++ b/frappe/database/mariadb/framework_mariadb.sql @@ -217,6 +217,9 @@ CREATE TABLE `tabDocType` ( `allow_guest_to_view` int(1) NOT NULL DEFAULT 0, `route` varchar(255) DEFAULT NULL, `is_published_field` varchar(255) DEFAULT NULL, + `email_append_to` int(1) NOT NULL DEFAULT 0, + `subject_field` varchar(255) DEFAULT NULL, + `sender_field` varchar(255) DEFAULT NULL, PRIMARY KEY (`name`), KEY `parent` (`parent`) ) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/frappe/database/postgres/framework_postgres.sql b/frappe/database/postgres/framework_postgres.sql index 457f6c906a..26760dbcc9 100644 --- a/frappe/database/postgres/framework_postgres.sql +++ b/frappe/database/postgres/framework_postgres.sql @@ -222,6 +222,9 @@ CREATE TABLE "tabDocType" ( "allow_guest_to_view" smallint NOT NULL DEFAULT 0, "route" varchar(255) DEFAULT NULL, "is_published_field" varchar(255) DEFAULT NULL, + "email_append_to" smallint NOT NULL DEFAULT 0, + "subject_field" varchar(255) DEFAULT NULL, + "sender_field" varchar(255) DEFAULT NULL, PRIMARY KEY ("name") ) ; diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py index ef84114745..f4a15930c4 100644 --- a/frappe/desk/desktop.py +++ b/frappe/desk/desktop.py @@ -4,41 +4,62 @@ from __future__ import unicode_literals import frappe -import json -from frappe import _, DoesNotExistError +from json import loads, dumps +from frappe import _, DoesNotExistError, ValidationError, _dict from frappe.boot import get_allowed_pages, get_allowed_reports from six import string_types -from frappe.cache_manager import build_domain_restriced_doctype_cache, build_domain_restriced_page_cache, build_table_count_cache +from frappe.cache_manager import ( + build_domain_restriced_doctype_cache, + build_domain_restriced_page_cache, + build_table_count_cache +) class Workspace: def __init__(self, page_name): self.page_name = page_name - - def build_cache(self): - self.doc = frappe.get_doc("Desk Page", self.page_name) - self.get_pages_to_extend() + self.extended_cards = [] + self.extended_charts = [] + self.extended_shortcuts = [] user = frappe.get_user() user.build_permissions() - self.user = user + user_doc = frappe.get_doc('User', frappe.session.user) + self.blocked_modules = user_doc.get_blocked_modules() + self.doc = self.get_page_for_user() + + if self.doc.module in self.blocked_modules: + raise frappe.PermissionError + + self.user = user self.allowed_pages = get_allowed_pages() self.allowed_reports = get_allowed_reports() self.table_counts = get_table_with_counts() - self.restricted_doctypes = build_domain_restriced_doctype_cache() - self.restricted_pages = build_domain_restriced_page_cache() + self.restricted_doctypes = frappe.cache().get_value("domain_restricted_doctypes") or build_domain_restriced_doctype_cache() + self.restricted_pages = frappe.cache().get_value("domain_restricted_pages") or build_domain_restriced_page_cache() + + def get_page_for_user(self): + filters = { + 'extends': self.page_name, + 'for_user': frappe.session.user + } + pages = frappe.get_list("Desk Page", filters=filters) + if pages: + return frappe.get_doc("Desk Page", pages[0]) + + self.get_pages_to_extend() + return frappe.get_doc("Desk Page", self.page_name) def get_pages_to_extend(self): pages = frappe.get_all("Desk Page", filters={ "extends": self.page_name, - 'restrict_to_domain': ['in', frappe.get_active_domains()] + 'restrict_to_domain': ['in', frappe.get_active_domains()], + 'for_user': '', + 'module': ['not in', self.blocked_modules] }) pages = [frappe.get_doc("Desk Page", page['name']) for page in pages] - self.extended_cards = [] - self.extended_charts = [] - self.extended_shortcuts = [] for page in pages: self.extended_cards = self.extended_cards + page.cards @@ -61,17 +82,17 @@ class Workspace: def build_workspace(self): self.cards = { - 'label': self.doc.cards_label, + 'label': _(self.doc.cards_label), 'items': self.get_cards() } self.charts = { - 'label': self.doc.charts_label, + 'label': _(self.doc.charts_label), 'items': self.get_charts() } self.shortcuts = { - 'label': self.doc.shortcuts_label, + 'label': _(self.doc.shortcuts_label), 'items': self.get_shortcuts() } @@ -105,18 +126,21 @@ class Workspace: item["count"] = count + # Translate label + item["label"] = _(item.label) if item.label else _(item.name) + return item new_data = [] for section in cards: new_items = [] if isinstance(section.links, string_types): - links = json.loads(section.links) + links = loads(section.links) else: links = section.links for item in links: - item = frappe._dict(item) + item = _dict(item) # Condition: based on country if item.country and item.country != default_country: @@ -125,15 +149,15 @@ class Workspace: # Check if user is allowed to view if self.is_item_allowed(item.name, item.type): prepared_item = _prepare_item(item) - new_items.append(item) + new_items.append(prepared_item) if new_items: - if isinstance(section, frappe._dict): + if isinstance(section, _dict): new_section = section.copy() else: new_section = section.as_dict().copy() new_section["links"] = new_items - new_section["label"] = section.title + new_section["label"] = _(new_section["label"]) new_data.append(new_section) return new_data @@ -146,8 +170,10 @@ class Workspace: charts = charts + self.extended_charts for chart in charts: - chart.label = chart.label if chart.label else chart.chart_name - all_charts.append(chart) + if frappe.has_permission('Dashboard Chart', doc=chart.chart_name): + # Translate label + chart.label = _(chart.label) if chart.label else _(chart.chart_name) + all_charts.append(chart) return all_charts @@ -166,21 +192,23 @@ class Workspace: for item in shortcuts: new_item = item.as_dict().copy() - new_item['name'] = _(item.link_to) if self.is_item_allowed(item.link_to, item.type) and _in_active_domains(item): - if item.type == "Page": - page = self.allowed_pages[item.link_to] - new_item['label'] = _(page.get("title", frappe.unscrub(item.link_to))) if item.type == "Report": report = self.allowed_reports.get(item.link_to, {}) if report.get("report_type") in ["Query Report", "Script Report"]: new_item['is_query_report'] = 1 + else: + new_item['ref_doctype'] = report.get('ref_doctype') + + # Translate label + new_item["label"] = _(item.label) if item.label else _(item.link_to) items.append(new_item) return items @frappe.whitelist() +@frappe.read_only() def get_desktop_page(page): """Applies permissions, customizations and returns the configruration for a page on desk. @@ -191,9 +219,8 @@ def get_desktop_page(page): Returns: dict: dictionary of cards, charts and shortcuts to be displayed on website """ - wspace = Workspace(page) try: - wspace.build_cache() + wspace = Workspace(page) wspace.build_workspace() return { 'charts': wspace.charts, @@ -208,13 +235,18 @@ def get_desktop_page(page): return None @frappe.whitelist() -def get_desk_sidebar_items(): +def get_desk_sidebar_items(flatten=False): """Get list of sidebar items for desk """ # don't get domain restricted pages + blocked_modules = frappe.get_doc('User', frappe.session.user).get_blocked_modules() + filters = { 'restrict_to_domain': ['in', frappe.get_active_domains()], - 'extends_another_page': False + 'extends_another_page': 0, + 'is_standard': 1, + 'for_user': '', + 'module': ['not in', blocked_modules] } if not frappe.local.conf.developer_mode: @@ -223,12 +255,16 @@ def get_desk_sidebar_items(): # pages sorted based on pinned to top and then by name order_by = "pin_to_top desc, pin_to_bottom asc, name asc" pages = frappe.get_all("Desk Page", fields=["name", "category"], filters=filters, order_by=order_by, ignore_permissions=True) + if flatten: + return pages from collections import defaultdict sidebar_items = defaultdict(list) + # The order will be maintained while categorizing for page in pages: - # The order will be maintained while categorizing + # Translate label + page['label'] = _(page.get('name')) sidebar_items[page["category"]].append(page) return sidebar_items @@ -241,10 +277,14 @@ def get_table_with_counts(): def get_custom_reports_and_doctypes(module): return [ - frappe._dict({ - "title": "Custom", - "links": get_custom_doctype_list(module) + get_custom_report_list(module) - }) + _dict({ + "label": _("Custom Documents"), + "links": get_custom_doctype_list(module) + }), + _dict({ + "label": _("Custom Reports"), + "links": get_custom_report_list(module) + }), ] def get_custom_doctype_list(module): @@ -279,104 +319,116 @@ def get_custom_report_list(module): return out -def make_them_pages(): - """Helper function to make pages +def get_custom_workspace_for_user(page): + """Get custom page from desk_page if exists or create one + + Args: + page (stirng): Page name + + Returns: + Object: Document object """ - pages = [ - ('Desk', 'frappe', 'octicon octicon-calendar'), - ('Settings', 'frappe', 'octicon octicon-settings'), - ('Users and Permissions', 'frappe', 'octicon octicon-settings'), - ('Customization', 'frappe', 'octicon octicon-settings'), - ('Integrations', 'frappe', 'octicon octicon-globe'), - ('Core', 'frappe', 'octicon octicon-circuit-board'), - ('Website', 'frappe', 'octicon octicon-globe'), - ('Getting Started', 'erpnext', 'fa fa-check-square-o'), - ('Accounts', 'erpnext', 'octicon octicon-repo'), - ('Selling', 'erpnext', 'octicon octicon-tag'), - ('Buying', 'erpnext', 'octicon octicon-briefcase'), - ('Stock', 'erpnext', 'octicon octicon-package'), - ('Assets', 'erpnext', 'octicon octicon-database'), - ('Projects', 'erpnext', 'octicon octicon-rocket'), - ('CRM', 'erpnext', 'octicon octicon-broadcast'), - ('Support', 'erpnext', 'fa fa-check-square-o'), - ('HR', 'erpnext', 'octicon octicon-organization'), - ('Quality Management', 'erpnext', 'fa fa-check-square-o'), - ('Manufacturing', 'erpnext', 'octicon octicon-tools'), - ('Retail', 'erpnext', 'octicon octicon-credit-card'), - ('Education', 'erpnext', 'octicon octicon-mortar-board'), - ('Healthcare', 'erpnext', 'fa fa-heartbeat'), - ('Agriculture', 'erpnext', 'octicon octicon-globe'), - ('Non Profit', 'erpnext', 'octicon octicon-heart'), - ('Help', 'erpnext', 'octicon octicon-device-camera-video') - ] - - for page in pages: - print("Processing Page: {0}".format(page[0])) - make_them_cards(page[0], page[2]) + filters = { + 'extends': page, + 'for_user': frappe.session.user + } + pages = frappe.get_list("Desk Page", filters=filters) + if pages: + return frappe.get_doc("Desk Page", pages[0]) + doc = frappe.new_doc("Desk Page") + doc.extends = page + doc.for_user = frappe.session.user + return doc -def make_them_cards(page_name, from_module=None, to_module=None, icon=None): - from frappe.desk.moduleview import get +@frappe.whitelist() +def save_customization(page, config): + """Save customizations as a separate doctype in Desk page per user - if not from_module: - from_module = page_name + Args: + page (string): Name of the page to be edited + config (dict): Dictionary config of al widgets - if not to_module: - to_module = page_name + Returns: + Boolean: Customization saving status + """ + original_page = frappe.get_doc("Desk Page", page) + page_doc = get_custom_workspace_for_user(page) + + # Update field values + page_doc.update({ + "charts_label": original_page.charts_label, + "cards_label": original_page.cards_label, + "shortcuts_label": original_page.shortcuts_label, + "icon": original_page.icon, + "module": original_page.module, + "developer_mode_only": original_page.developer_mode_only, + "category": original_page.category + }) + + config = _dict(loads(config)) + if config.charts: + page_doc.charts = prepare_widget(config.charts, "Desk Chart", "charts") + if config.shortcuts: + page_doc.shortcuts = prepare_widget(config.shortcuts, "Desk Shortcut", "shortcuts") + if config.cards: + page_doc.cards = prepare_widget(config.cards, "Desk Card", "cards") + + # Set label + page_doc.label = page + '-' + frappe.session.user try: - modules = get(from_module)['data'] - except: - return + if page_doc.is_new(): + page_doc.insert(ignore_permissions=True) + else: + page_doc.save(ignore_permissions=True) + except (ValidationError, TypeError) as e: + # Create a json string to log + json_config = dumps(config, sort_keys=True, indent=4) - # Find or make page doc - if frappe.db.exists("Desk Page", page_name): - page = frappe.get_doc("Desk Page", page_name) - print("--- Got Page: {0}".format(page.name)) - else: - page = frappe.new_doc("Desk Page") - page.label = page_name - page.cards = [] - page.icon = icon - print("--- New Page: {0}".format(page.name)) + # Error log body + log = \ + """ + page: {0} + config: {1} + exception: {2} + """.format(page, json_config, e) + frappe.log_error(log, _("Could not save customization")) + return False - # Guess Which Module - if not to_module and frappe.db.exists("Module Def", page_name): - page.module = page_name + return True - if to_module: - page.module = to_module - elif frappe.db.exists("Module Def", page_name): - page.module = page_name - for data in modules: - # Create a New Card Child Doc - card = frappe.new_doc("Desk Card") +def prepare_widget(config, doctype, parentfield): + """Create widget child table entries with parent details - # Data clean up - for item in data['items']: - try: - del item['count'] - del item['incomplete_dependencies'] - except KeyError: - pass + Args: + config (dict): Dictionary containing widget config + doctype (string): Doctype name of the child table + parentfield (string): Parent field for the child table - # Set Child doc values - card.title = data['label'] - card.icon = data.get('icon') - # Pretty dump JSON - card.links = json.dumps(data['items'], indent=4, sort_keys=True) + Returns: + TYPE: List of Document objects + """ + if not config: + return [] + order = config.get('order') + widgets = config.get('widgets') + prepare_widget_list = [] + for idx, name in enumerate(order): + wid_config = widgets[name].copy() + # Some cleanup + wid_config.pop("name", None) - # Set Parent attributes - card.parent = page.name - card.parenttype = page.doctype - card.parentfield = "cards" + # New Doc + doc = frappe.new_doc(doctype) + doc.update(wid_config) - # Add cards to page doc - print("------- Adding Card: {0}".format(card.title)) - page.cards.append(card) + # Manually Set IDX + doc.idx = idx + 1 - # End it all - page.save() - frappe.db.commit() - return \ No newline at end of file + # Set Parent Field + doc.parentfield = parentfield + + prepare_widget_list.append(doc) + return prepare_widget_list diff --git a/frappe/desk/doctype/dashboard/dashboard.json b/frappe/desk/doctype/dashboard/dashboard.json index 239f35bea8..c177ee70ac 100644 --- a/frappe/desk/doctype/dashboard/dashboard.json +++ b/frappe/desk/doctype/dashboard/dashboard.json @@ -34,7 +34,7 @@ } ], "links": [], - "modified": "2020-01-26 20:00:10.069817", + "modified": "2020-03-25 21:09:37.080132", "modified_by": "Administrator", "module": "Desk", "name": "Dashboard", @@ -51,6 +51,27 @@ "role": "System Manager", "share": 1, "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Dashboard Manager", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1 } ], "quick_entry": 1, diff --git a/frappe/desk/doctype/dashboard/dashboard.py b/frappe/desk/doctype/dashboard/dashboard.py index c8f22d29b9..5c344956bf 100644 --- a/frappe/desk/doctype/dashboard/dashboard.py +++ b/frappe/desk/doctype/dashboard/dashboard.py @@ -12,3 +12,12 @@ class Dashboard(Document): # make all other dashboards non-default frappe.db.sql('''update tabDashboard set is_default = 0 where name != %s''', self.name) + +@frappe.whitelist() +def get_permitted_charts(dashboard_name): + permitted_charts = [] + dashboard = frappe.get_doc('Dashboard', dashboard_name) + for chart in dashboard.charts: + if frappe.has_permission('Dashboard Chart', doc=chart.chart): + permitted_charts.append(chart) + return permitted_charts diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json index 0a017a0de2..676cdbe24a 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json @@ -49,7 +49,8 @@ "fieldname": "chart_type", "fieldtype": "Select", "label": "Chart Type", - "options": "Count\nSum\nAverage\nGroup By\nCustom\nReport" + "options": "Count\nSum\nAverage\nGroup By\nCustom\nReport", + "set_only_once": 1 }, { "depends_on": "eval:doc.chart_type === 'Custom'", @@ -215,7 +216,7 @@ } ], "links": [], - "modified": "2020-03-13 19:19:37.162771", + "modified": "2020-04-08 18:54:36.739183", "modified_by": "Administrator", "module": "Desk", "name": "Dashboard Chart", @@ -232,6 +233,27 @@ "role": "System Manager", "share": 1, "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Dashboard Manager", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1 } ], "sort_field": "modified", diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index f01c976b9c..b2a6f0a0ff 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -10,8 +10,51 @@ import json from frappe.core.page.dashboard.dashboard import cache_source, get_from_date_from_timespan from frappe.utils import nowdate, add_to_date, getdate, get_last_day, formatdate, get_datetime from frappe.model.naming import append_number_if_name_exists +from frappe.boot import get_allowed_reports from frappe.model.document import Document + +def get_permission_query_conditions(user): + + if not user: + user = frappe.session.user + + if user == 'Administrator': + return + + roles = frappe.get_roles(user) + if "System Manager" in roles: + return None + + allowed_doctypes = tuple(frappe.permissions.get_doctypes_with_read()) + allowed_reports = tuple([key.encode('UTF8') for key in get_allowed_reports()]) + + return ''' + `tabDashboard Chart`.`document_type` in {allowed_doctypes} + or `tabDashboard Chart`.`report_name` in {allowed_reports} + '''.format( + allowed_doctypes=allowed_doctypes, + allowed_reports=allowed_reports + ) + + +def has_permission(doc, ptype, user): + roles = frappe.get_roles(user) + if "System Manager" in roles: + return True + + + if doc.chart_type == 'Report': + allowed_reports = tuple([key.encode('UTF8') for key in get_allowed_reports()]) + if doc.report_name in allowed_reports: + return True + else: + allowed_doctypes = tuple(frappe.permissions.get_doctypes_with_read()) + if doc.document_type in allowed_doctypes: + return True + + return False + @frappe.whitelist() @cache_source def get(chart_name = None, chart = None, no_cache = None, filters = None, from_date = None, diff --git a/frappe/desk/doctype/dashboard_settings/__init__.py b/frappe/desk/doctype/dashboard_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/desk/doctype/dashboard_settings/dashboard_settings.js b/frappe/desk/doctype/dashboard_settings/dashboard_settings.js new file mode 100644 index 0000000000..8e7966366d --- /dev/null +++ b/frappe/desk/doctype/dashboard_settings/dashboard_settings.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Dashboard Settings', { + // refresh: function(frm) { + + // } +}); diff --git a/frappe/desk/doctype/dashboard_settings/dashboard_settings.json b/frappe/desk/doctype/dashboard_settings/dashboard_settings.json new file mode 100644 index 0000000000..e1eb75db47 --- /dev/null +++ b/frappe/desk/doctype/dashboard_settings/dashboard_settings.json @@ -0,0 +1,51 @@ +{ + "actions": [], + "autoname": "Prompt", + "creation": "2020-03-31 19:41:45.785014", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "user", + "chart_config" + ], + "fields": [ + { + "fieldname": "user", + "fieldtype": "Link", + "label": "User", + "options": "User", + "read_only": 1 + }, + { + "fieldname": "chart_config", + "fieldtype": "Code", + "label": "Chart Configuration", + "options": "JSON", + "read_only": 1 + } + ], + "in_create": 1, + "links": [], + "modified": "2020-04-01 00:07:26.489561", + "modified_by": "Administrator", + "module": "Desk", + "name": "Dashboard Settings", + "owner": "Administrator", + "permissions": [ + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1, + "write": 1 + } + ], + "read_only": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/desk/doctype/dashboard_settings/dashboard_settings.py b/frappe/desk/doctype/dashboard_settings/dashboard_settings.py new file mode 100644 index 0000000000..4697d897fc --- /dev/null +++ b/frappe/desk/doctype/dashboard_settings/dashboard_settings.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document +import frappe +import json + +class DashboardSettings(Document): + pass + + +@frappe.whitelist() +def create_dashboard_settings(user): + if not frappe.db.exists("Dashboard Settings", user): + doc = frappe.new_doc('Dashboard Settings') + doc.name = user + doc.insert(ignore_permissions=True) + frappe.db.commit() + return doc + +def get_permission_query_conditions(user): + if not user: user = frappe.session.user + + return '''(`tabDashboard Settings`.name = '{user}')'''.format(user=user) + +@frappe.whitelist() +def save_chart_config(reset, config, chart_name): + reset = frappe.parse_json(reset) + doc = frappe.get_doc('Dashboard Settings', frappe.session.user) + chart_config = frappe.parse_json(doc.chart_config) or {} + + if reset: + chart_config[chart_name] = {} + else: + config = frappe.parse_json(config) + if not chart_name in chart_config: + chart_config[chart_name] = {} + chart_config[chart_name].update(config) + + frappe.db.set_value('Dashboard Settings', frappe.session.user, 'chart_config', json.dumps(chart_config)) \ No newline at end of file diff --git a/frappe/desk/doctype/desk_card/desk_card.json b/frappe/desk/doctype/desk_card/desk_card.json index 4ccffd4f58..dbcb4b0d5c 100644 --- a/frappe/desk/doctype/desk_card/desk_card.json +++ b/frappe/desk/doctype/desk_card/desk_card.json @@ -2,11 +2,12 @@ "actions": [], "creation": "2020-01-29 14:45:54.383089", "doctype": "DocType", + "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "title", + "label", "column_break_2", - "icon", + "hidden", "section_break_3", "links" ], @@ -18,13 +19,6 @@ "options": "JSON", "reqd": 1 }, - { - "fieldname": "title", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Title", - "reqd": 1 - }, { "fieldname": "section_break_3", "fieldtype": "Section Break" @@ -34,14 +28,23 @@ "fieldtype": "Column Break" }, { - "fieldname": "icon", + "default": "0", + "fieldname": "hidden", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Hidden" + }, + { + "fieldname": "label", "fieldtype": "Data", - "label": "Icon" + "in_list_view": 1, + "label": "Label", + "reqd": 1 } ], "istable": 1, "links": [], - "modified": "2020-02-03 12:40:42.595122", + "modified": "2020-03-31 14:38:06.303847", "modified_by": "Administrator", "module": "Desk", "name": "Desk Card", diff --git a/frappe/desk/doctype/desk_chart/desk_chart.json b/frappe/desk/doctype/desk_chart/desk_chart.json index c3c9231353..09deefd59d 100644 --- a/frappe/desk/doctype/desk_chart/desk_chart.json +++ b/frappe/desk/doctype/desk_chart/desk_chart.json @@ -26,7 +26,7 @@ ], "istable": 1, "links": [], - "modified": "2020-03-20 10:04:13.992228", + "modified": "2020-03-31 13:33:13.128804", "modified_by": "Administrator", "module": "Desk", "name": "Desk Chart", diff --git a/frappe/desk/doctype/desk_page/desk_page.js b/frappe/desk/doctype/desk_page/desk_page.js index 7af3e0e98c..3087a5f5b8 100644 --- a/frappe/desk/doctype/desk_page/desk_page.js +++ b/frappe/desk/doctype/desk_page/desk_page.js @@ -2,17 +2,21 @@ // For license information, please see license.txt frappe.ui.form.on('Desk Page', { - refresh: function(frm) { + setup: function(frm) { frm.get_field("is_standard").toggle(frappe.boot.developer_mode); frm.get_field("extends_another_page").toggle(frappe.boot.developer_mode); - if (!frappe.boot.developer_mode) { - frm.set_read_only(); - frm.fields - .filter(field => field.has_input) - .forEach(field => { - frm.set_df_property(field.df.fieldname, "read_only", "1"); - }); - frm.disable_save(); + if (!frappe.boot.developer_mode || frm.doc.for_user) { + frm.trigger('disable_form'); } + }, + + disable_form: function(frm) { + frm.set_read_only(); + frm.fields + .filter(field => field.has_input) + .forEach(field => { + frm.set_df_property(field.df.fieldname, "read_only", "1"); + }); + frm.disable_save(); } -}); +}); \ No newline at end of file diff --git a/frappe/desk/doctype/desk_page/desk_page.json b/frappe/desk/doctype/desk_page/desk_page.json index 6bc33d1326..7e6baf221b 100644 --- a/frappe/desk/doctype/desk_page/desk_page.json +++ b/frappe/desk/doctype/desk_page/desk_page.json @@ -9,6 +9,7 @@ "field_order": [ "label", "extends", + "for_user", "module", "category", "restrict_to_domain", @@ -36,7 +37,6 @@ "fieldname": "label", "fieldtype": "Data", "label": "Name", - "length": 22, "unique": 1 }, { @@ -52,6 +52,7 @@ "options": "Desk Chart" }, { + "depends_on": "eval:!doc.extends_another_page || !doc.is_standard", "fieldname": "shortcuts", "fieldtype": "Table", "label": "Shortcuts", @@ -136,16 +137,19 @@ "search_index": 1 }, { + "depends_on": "eval:!doc.extends_another_page || !doc.is_standard", "fieldname": "charts_label", "fieldtype": "Data", "label": "Label" }, { + "depends_on": "eval:!doc.extends_another_page || !doc.is_standard", "fieldname": "shortcuts_label", "fieldtype": "Data", "label": "Label" }, { + "depends_on": "eval:!doc.extends_another_page || !doc.is_standard", "fieldname": "cards_label", "fieldtype": "Data", "label": "Label" @@ -166,24 +170,36 @@ "default": "0", "fieldname": "is_standard", "fieldtype": "Check", - "label": "Is Standard" + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Is Standard", + "search_index": 1 }, { "default": "0", "fieldname": "extends_another_page", "fieldtype": "Check", - "label": "Extends Another Page" + "label": "Extends Another Page", + "search_index": 1 }, { "depends_on": "eval:doc.extends_another_page == 1", "fieldname": "extends", "fieldtype": "Link", + "in_standard_filter": 1, "label": "Extends", - "options": "Desk Page" + "options": "Desk Page", + "search_index": 1 + }, + { + "fieldname": "for_user", + "fieldtype": "Data", + "label": "For User", + "read_only": 1 } ], "links": [], - "modified": "2020-03-12 16:38:16.206732", + "modified": "2020-03-26 12:35:41.981432", "modified_by": "Administrator", "module": "Desk", "name": "Desk Page", diff --git a/frappe/desk/doctype/desk_shortcut/desk_shortcut.json b/frappe/desk/doctype/desk_shortcut/desk_shortcut.json index 6b57f250a9..9f8990732a 100644 --- a/frappe/desk/doctype/desk_shortcut/desk_shortcut.json +++ b/frappe/desk/doctype/desk_shortcut/desk_shortcut.json @@ -6,11 +6,11 @@ "engine": "InnoDB", "field_order": [ "type", - "icon", + "label", "column_break_4", "link_to", + "icon", "restrict_to_domain", - "is_query_report", "section_break_5", "stats_filter", "column_break_3", @@ -51,6 +51,7 @@ "label": "Format" }, { + "depends_on": "eval:doc.type == \"DocType\" && frappe.boot.developer_mode", "fieldname": "section_break_5", "fieldtype": "Section Break", "label": "Count Filter" @@ -61,13 +62,7 @@ "label": "Color" }, { - "default": "0", - "depends_on": "eval:doc.type === \"Report\"", - "fieldname": "is_query_report", - "fieldtype": "Check", - "label": "Is Query Report" - }, - { + "depends_on": "eval:frappe.boot.developer_mode", "fieldname": "icon", "fieldtype": "Data", "label": "Icon" @@ -77,15 +72,22 @@ "fieldtype": "Column Break" }, { + "depends_on": "eval:frappe.boot.developer_mode", "fieldname": "restrict_to_domain", "fieldtype": "Link", "label": "Restrict to Domain", "options": "Domain" + }, + { + "fieldname": "label", + "fieldtype": "Data", + "label": "Label", + "reqd": 1 } ], "istable": 1, "links": [], - "modified": "2020-03-11 13:09:00.180528", + "modified": "2020-04-07 19:04:23.645198", "modified_by": "Administrator", "module": "Desk", "name": "Desk Shortcut", diff --git a/frappe/desk/doctype/event/event.json b/frappe/desk/doctype/event/event.json index 032030ddef..5768f00f32 100644 --- a/frappe/desk/doctype/event/event.json +++ b/frappe/desk/doctype/event/event.json @@ -1,9 +1,11 @@ { + "actions": [], "allow_import": 1, "autoname": "EV.#####", "creation": "2013-06-10 13:17:47", "doctype": "DocType", "document_type": "Document", + "email_append_to": 1, "engine": "InnoDB", "field_order": [ "details", @@ -17,6 +19,7 @@ "starts_on", "ends_on", "status", + "sender", "all_day", "sync_with_google_calendar", "sb_00", @@ -262,11 +265,19 @@ "fieldtype": "Check", "label": "Pulled from Google Calendar", "read_only": 1 + }, + { + "fieldname": "sender", + "fieldtype": "Data", + "label": "Sender", + "options": "Email", + "read_only": 1 } ], "icon": "fa fa-calendar", "idx": 1, - "modified": "2019-08-08 16:01:19.489396", + "links": [], + "modified": "2020-01-14 21:47:15.825287", "modified_by": "Administrator", "module": "Desk", "name": "Event", @@ -297,8 +308,10 @@ } ], "read_only": 1, + "sender_field": "sender", "sort_field": "modified", "sort_order": "DESC", + "subject_field": "subject", "title_field": "subject", "track_changes": 1, "track_seen": 1, diff --git a/frappe/desk/doctype/notification_log/notification_log.py b/frappe/desk/doctype/notification_log/notification_log.py index 398a3de351..17eb6371b1 100644 --- a/frappe/desk/doctype/notification_log/notification_log.py +++ b/frappe/desk/doctype/notification_log/notification_log.py @@ -6,14 +6,13 @@ from __future__ import unicode_literals import frappe from frappe import _ from frappe.model.document import Document -from frappe.desk.doctype.notification_settings.notification_settings import (is_notifications_enabled, - is_email_notifications_enabled, is_email_notifications_enabled_for_type, set_seen_value) +from frappe.desk.doctype.notification_settings.notification_settings import (is_notifications_enabled, is_email_notifications_enabled_for_type, set_seen_value) class NotificationLog(Document): def after_insert(self): frappe.publish_realtime('notification', after_commit=True, user=self.for_user) set_notifications_as_unseen(self.for_user) - if is_email_notifications_enabled(self.for_user): + if is_email_notifications_enabled_for_type(self.for_user, self.type): send_notification_email(self) @@ -73,9 +72,6 @@ def make_notification_logs(doc, users): _doc.insert(ignore_permissions=True) def send_notification_email(doc): - is_type_enabled = is_email_notifications_enabled_for_type(doc.for_user, doc.type) - if not is_type_enabled: - return if doc.type == 'Energy Point' and doc.email_content is None: return diff --git a/frappe/desk/doctype/notification_settings/notification_settings.js b/frappe/desk/doctype/notification_settings/notification_settings.js index d4e3b08def..b8b7f37a4f 100644 --- a/frappe/desk/doctype/notification_settings/notification_settings.js +++ b/frappe/desk/doctype/notification_settings/notification_settings.js @@ -8,5 +8,14 @@ frappe.ui.form.on('Notification Settings', { route: '#modules/Settings', type: 'Custom' }); + }, + + refresh: (frm) => { + if (frappe.user.has_role('System Manager')) { + frm.add_custom_button('Go to Notification Settings List', () => { + frappe.set_route('List', 'Notification Settings'); + }); + } } + }); diff --git a/frappe/desk/doctype/notification_settings/notification_settings.py b/frappe/desk/doctype/notification_settings/notification_settings.py index 295b4c8afd..6b5a13ee27 100644 --- a/frappe/desk/doctype/notification_settings/notification_settings.py +++ b/frappe/desk/doctype/notification_settings/notification_settings.py @@ -25,6 +25,9 @@ def is_email_notifications_enabled(user): return enabled def is_email_notifications_enabled_for_type(user, notification_type): + if not is_email_notifications_enabled(user): + return False + fieldname = 'enable_email_' + frappe.scrub(notification_type) enabled = frappe.db.get_value('Notification Settings', user, fieldname) if enabled is None: @@ -59,7 +62,14 @@ def get_subscribed_documents(): def get_permission_query_conditions(user): if not user: user = frappe.session.user - return '''(`tabNotification Settings`.user = '{user}')'''.format(user=user) + if user == 'Administrator': + return + + roles = frappe.get_roles(user) + if "System Manager" in roles: + return '''(`tabNotification Settings`.name != 'Administrator')''' + + return '''(`tabNotification Settings`.name = '{user}')'''.format(user=user) @frappe.whitelist() def set_seen_value(value, user): diff --git a/frappe/desk/doctype/todo/todo.json b/frappe/desk/doctype/todo/todo.json index 508720a488..15e0e4abe1 100644 --- a/frappe/desk/doctype/todo/todo.json +++ b/frappe/desk/doctype/todo/todo.json @@ -1,8 +1,10 @@ { + "actions": [], "autoname": "hash", "creation": "2012-07-03 13:30:35", "doctype": "DocType", "document_type": "Setup", + "email_append_to": 1, "engine": "InnoDB", "field_order": [ "description_and_status", @@ -142,7 +144,8 @@ "fieldname": "sender", "fieldtype": "Data", "hidden": 1, - "label": "Sender" + "label": "Sender", + "options": "Email" }, { "fieldname": "assignment_rule", @@ -154,7 +157,8 @@ ], "icon": "fa fa-check", "idx": 2, - "modified": "2019-09-10 14:34:59.161750", + "links": [], + "modified": "2020-01-14 17:04:36.971002", "modified_by": "Administrator", "module": "Desk", "name": "ToDo", @@ -185,9 +189,11 @@ ], "quick_entry": 1, "search_fields": "description, reference_type, reference_name", + "sender_field": "sender", "sort_field": "modified", "sort_order": "DESC", + "subject_field": "description", "title_field": "description", "track_changes": 1, "track_seen": 1 -} +} \ No newline at end of file diff --git a/frappe/desk/doctype/todo/todo.py b/frappe/desk/doctype/todo/todo.py index 6cd7c68368..8e8102d093 100644 --- a/frappe/desk/doctype/todo/todo.py +++ b/frappe/desk/doctype/todo/todo.py @@ -8,8 +8,6 @@ import json from frappe.model.document import Document from frappe.utils import get_fullname -subject_field = "description" -sender_field = "sender" exclude_from_linked_with = True class ToDo(Document): diff --git a/frappe/desk/doctype/todo/todo_calendar.js b/frappe/desk/doctype/todo/todo_calendar.js new file mode 100644 index 0000000000..4545846cf9 --- /dev/null +++ b/frappe/desk/doctype/todo/todo_calendar.js @@ -0,0 +1,31 @@ +// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +// License: GNU General Public License v3. See license.txt + +frappe.views.calendar["ToDo"] = { + field_map: { + "start": "date", + "end": "date", + "id": "name", + "title": "description", + "allDay": "allDay", + "progress": "progress" + }, + gantt: true, + filters: [ + { + "fieldtype": "Link", + "fieldname": "reference_type", + "options": "Task", + "label": __("Task") + }, + { + "fieldtype": "Dynamic Link", + "fieldname": "reference_name", + "options": "reference_type", + "label": __("Task") + } + + ], + get_events_method: "frappe.desk.calendar.get_events" +}; + diff --git a/frappe/desk/form/meta.py b/frappe/desk/form/meta.py index 26fc6037fd..ba0e5c2216 100644 --- a/frappe/desk/form/meta.py +++ b/frappe/desk/form/meta.py @@ -196,8 +196,6 @@ class FormMeta(Meta): self.get("__messages").update(messages, as_value=True) def load_dashboard(self): - if self.custom: - return self.set('__dashboard', self.get_dashboard_data()) def load_kanban_meta(self): diff --git a/frappe/desk/form/utils.py b/frappe/desk/form/utils.py index 498ab50645..4c3bab2e23 100644 --- a/frappe/desk/form/utils.py +++ b/frappe/desk/form/utils.py @@ -56,18 +56,20 @@ def validate_link(): frappe.response['valid_value'] = valid_value frappe.response['message'] = 'Ok' + @frappe.whitelist() -def add_comment(reference_doctype, reference_name, content, comment_email): +def add_comment(reference_doctype, reference_name, content, comment_email, comment_by): """allow any logged user to post a comment""" doc = frappe.get_doc(dict( - doctype = 'Comment', - reference_doctype = reference_doctype, - reference_name = reference_name, - comment_email = comment_email, - comment_type = 'Comment' + doctype='Comment', + reference_doctype=reference_doctype, + reference_name=reference_name, + comment_email=comment_email, + comment_type='Comment', + comment_by=comment_by )) doc.content = extract_images_from_html(doc, content) - doc.insert(ignore_permissions = True) + doc.insert(ignore_permissions=True) follow_document(doc.reference_doctype, doc.reference_name, frappe.session.user) return doc.as_dict() diff --git a/frappe/desk/notifications.py b/frappe/desk/notifications.py index 3a8815ca71..109dd25f4f 100644 --- a/frappe/desk/notifications.py +++ b/frappe/desk/notifications.py @@ -268,8 +268,9 @@ def get_open_count(doctype, name, items=[]): "count": out, } - module = frappe.get_meta_module(doctype) - if hasattr(module, "get_timeline_data"): - out["timeline_data"] = module.get_timeline_data(doctype, name) + if not meta.custom: + module = frappe.get_meta_module(doctype) + if hasattr(module, "get_timeline_data"): + out["timeline_data"] = module.get_timeline_data(doctype, name) return out diff --git a/frappe/desk/page/setup_wizard/install_fixtures.py b/frappe/desk/page/setup_wizard/install_fixtures.py index e7e147fb7d..c857bd077f 100644 --- a/frappe/desk/page/setup_wizard/install_fixtures.py +++ b/frappe/desk/page/setup_wizard/install_fixtures.py @@ -8,16 +8,22 @@ from frappe import _ from frappe.desk.doctype.global_search_settings.global_search_settings import update_global_search_doctypes def install(): - update_genders_and_salutations() + update_genders() + update_salutations() update_global_search_doctypes() setup_email_linking() @frappe.whitelist() -def update_genders_and_salutations(): - default_genders = [_("Male"), _("Female"), _("Other")] - default_salutations = [_("Mr"), _("Ms"), _('Mx'), _("Dr"), _("Mrs"), _("Madam"), _("Miss"), _("Master"), _("Prof")] +def update_genders(): + default_genders = [_("Male"), _("Female"), _("Other"),_("Transgender"), _("Genderqueer"), _("Non-Conforming"),_("Prefer not to say")] records = [{'doctype': 'Gender', 'gender': d} for d in default_genders] - records += [{'doctype': 'Salutation', 'salutation': d} for d in default_salutations] + for record in records: + frappe.get_doc(record).insert(ignore_permissions=True, ignore_if_duplicate=True) + +@frappe.whitelist() +def update_salutations(): + default_salutations = [_("Mr"), _("Ms"), _('Mx'), _("Dr"), _("Mrs"), _("Madam"), _("Miss"), _("Master"), _("Prof")] + records = [{'doctype': 'Salutation', 'salutation': d} for d in default_salutations] for record in records: doc = frappe.new_doc(record.get("doctype")) doc.update(record) diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index d210af02fd..164f6389eb 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -8,7 +8,7 @@ import os, json from frappe import _ from frappe.modules import scrub, get_module_path -from frappe.utils import flt, cint, get_html_format, cstr, get_url_to_form +from frappe.utils import flt, cint, get_html_format, get_url_to_form from frappe.model.utils import render_include from frappe.translate import send_translations import frappe.desk.reportview @@ -16,6 +16,7 @@ from frappe.permissions import get_role_permissions from six import string_types, iteritems from datetime import timedelta from frappe.utils import gzip_decompress +from frappe.core.utils import ljust_list def get_report_doc(report_name): doc = frappe.get_doc("Report", report_name) @@ -42,44 +43,32 @@ def get_report_doc(report_name): return doc -def generate_report_result(report, filters=None, user=None): - status = None - if not user: - user = frappe.session.user - if not filters: - filters = [] +def generate_report_result(report, filters=None, user=None, custom_columns=None): + user = user or frappe.session.user + filters = filters or [] if filters and isinstance(filters, string_types): filters = json.loads(filters) - columns, result, message, chart, report_summary, skip_total_row = [], [], None, None, None, 0 + + res = [] + if report.report_type == "Query Report": - if not report.query: - status = "error" - frappe.msgprint(_("Must specify a Query to run"), raise_exception=True) - - if not report.query.lower().startswith("select"): - status = "error" - frappe.msgprint(_("Query must be a SELECT"), raise_exception=True) - - result = [list(t) for t in frappe.db.sql(report.query, filters)] - columns = [cstr(c[0]) for c in frappe.db.get_description()] + res = report.execute_query_report(filters) elif report.report_type == 'Script Report': res = report.execute_script_report(filters) - columns, result = res[0], res[1] - if len(res) > 2: - message = res[2] - if len(res) > 3: - chart = res[3] - if len(res) > 4: - report_summary = res[4] - if len(res) > 5: - skip_total_row = cint(res[5]) + columns, result, message, chart, report_summary, skip_total_row = \ + ljust_list(res, 6) if report.custom_columns: columns = json.loads(report.custom_columns) result = add_data_to_custom_columns(columns, result) + if custom_columns: + result = add_data_to_custom_columns(custom_columns, result) + + for custom_column in custom_columns: + columns.insert(custom_column['insert_after_index'] + 1, custom_column) if result: result = get_filtered_data(report.ref_doctype, columns, result, user) @@ -93,8 +82,8 @@ def generate_report_result(report, filters=None, user=None): "message": message, "chart": chart, "report_summary": report_summary, - "skip_total_row": skip_total_row, - "status": status, + "skip_total_row": skip_total_row or 0, + "status": None, "execution_time": frappe.cache().hget('report_execution_time', report.name) or 0 } @@ -161,7 +150,7 @@ def get_script(report_name): @frappe.whitelist() @frappe.read_only() -def run(report_name, filters=None, user=None, ignore_prepared_report=False): +def run(report_name, filters=None, user=None, ignore_prepared_report=False, custom_columns=None): report = get_report_doc(report_name) if not user: @@ -183,7 +172,7 @@ def run(report_name, filters=None, user=None, ignore_prepared_report=False): dn = "" result = get_prepared_report_result(report, filters, dn, user) else: - result = generate_report_result(report, filters, user) + result = generate_report_result(report, filters, user, custom_columns) result["add_total_row"] = report.add_total_row and not result.get('skip_total_row', False) @@ -253,7 +242,7 @@ def get_prepared_report_result(report, filters, dn="", user=None): columns = json.loads(doc.columns) if doc.columns else data[0] for column in columns: - if isinstance(column, dict): + if isinstance(column, dict) and column.get("label"): column["label"] = _(column["label"]) latest_report_data = { @@ -294,6 +283,8 @@ def export_query(): if isinstance(data.get("file_format_type"), string_types): file_format_type = data["file_format_type"] + custom_columns = frappe.parse_json(data["custom_columns"]) + include_indentation = data["include_indentation"] if isinstance(data.get("visible_idx"), string_types): visible_idx = json.loads(data.get("visible_idx")) @@ -301,13 +292,14 @@ def export_query(): visible_idx = None if file_format_type == "Excel": - data = run(report_name, filters) + data = run(report_name, filters, custom_columns=custom_columns) data = frappe._dict(data) if not data.columns: frappe.respond_as_web_page(_("No data to export"), _("You can try changing the filters of your report.")) return + data.columns = [col for col in data.columns if isinstance(col, dict) and not col.get('hidden')] columns = get_columns_dict(data.columns) from frappe.utils.xlsxutils import make_xlsx @@ -319,7 +311,7 @@ def export_query(): frappe.response['type'] = 'binary' -def build_xlsx_data(columns, data, visible_idx,include_indentation): +def build_xlsx_data(columns, data, visible_idx, include_indentation): result = [[]] # add column headings diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index 9bb12a4ec8..6102be61ce 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -367,8 +367,11 @@ def scrub_user_tags(tagcount): return rlist # used in building query in queries.py -def get_match_cond(doctype): - cond = DatabaseQuery(doctype).build_match_conditions() +def get_match_cond(doctype, as_condition=True): + cond = DatabaseQuery(doctype).build_match_conditions(as_condition=as_condition) + if not as_condition: + return cond + return ((' and ' + cond) if cond else "").replace("%", "%%") def build_match_conditions(doctype, user=None, as_condition=True): diff --git a/frappe/email/__init__.py b/frappe/email/__init__.py index f99536f9a8..d58b35040e 100644 --- a/frappe/email/__init__.py +++ b/frappe/email/__init__.py @@ -65,7 +65,7 @@ def get_communication_doctype(doctype, txt, searchfield, start, page_len, filter com_doctypes = [] if len(txt)<2: - for name in ["Customer", "Supplier"]: + for name in frappe.get_hooks("communication_doctypes"): try: module = load_doctype_module(name, suffix='_dashboard') if hasattr(module, 'get_data'): diff --git a/frappe/email/doctype/email_account/email_account.json b/frappe/email/doctype/email_account/email_account.json index e724102fdf..5c57a7f35d 100644 --- a/frappe/email/doctype/email_account/email_account.json +++ b/frappe/email/doctype/email_account/email_account.json @@ -66,6 +66,7 @@ "fieldtype": "Data", "in_global_search": 1, "label": "Email Address", + "options": "Email", "reqd": 1 }, { @@ -410,7 +411,7 @@ ], "icon": "fa fa-inbox", "links": [], - "modified": "2019-12-18 15:56:39.744520", + "modified": "2020-04-06 19:20:50.491146", "modified_by": "Administrator", "module": "Email", "name": "Email Account", diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index f3eb2188b7..c0a198f5e5 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -452,16 +452,15 @@ class EmailAccount(Document): def set_sender_field_and_subject_field(self): '''Identify the sender and subject fields from the `append_to` DocType''' # set subject_field and sender_field - meta_module = frappe.get_meta_module(self.append_to) meta = frappe.get_meta(self.append_to) + self.subject_field = None + self.sender_field = None - self.subject_field = getattr(meta_module, "subject_field", "subject") - if not meta.get_field(self.subject_field): - self.subject_field = None + if hasattr(meta, "subject_field"): + self.subject_field = meta.subject_field - self.sender_field = getattr(meta_module, "sender_field", "sender") - if not meta.get_field(self.sender_field): - self.sender_field = None + if hasattr(meta, "sender_field"): + self.sender_field = meta.sender_field def find_parent_based_on_subject_and_sender(self, communication, email): '''Find parent document based on subject and sender match''' @@ -675,8 +674,21 @@ class EmailAccount(Document): @frappe.whitelist() def get_append_to(doctype=None, txt=None, searchfield=None, start=None, page_len=None, filters=None): - if not txt: txt = "" - return [[d] for d in frappe.get_hooks("email_append_to") if txt in d] + txt = txt if txt else "" + email_append_to_list = [] + + # Set Email Append To DocTypes via DocType + filters = {"istable": 0, "issingle": 0, "email_append_to": 1} + for dt in frappe.get_all("DocType", filters=filters, fields=["name", "email_append_to"]): + email_append_to_list.append(dt.name) + + # Set Email Append To DocTypes set via Customize Form + for dt in frappe.get_list("Property Setter", filters={"property": "email_append_to", "value": 1}, fields=["doc_type"]): + email_append_to_list.append(dt.doc_type) + + email_append_to = [[d] for d in set(email_append_to_list) if txt in d] + + return email_append_to def test_internet(host="8.8.8.8", port=53, timeout=3): """Returns True if internet is connected diff --git a/frappe/email/doctype/email_domain/email_domain.py b/frappe/email/doctype/email_domain/email_domain.py index b6585d966b..08583dc228 100644 --- a/frappe/email/doctype/email_domain/email_domain.py +++ b/frappe/email/doctype/email_domain/email_domain.py @@ -39,7 +39,7 @@ class EmailDomain(Document): except Exception: frappe.throw(_("Incoming email account not correct")) - return None + finally: try: if self.use_imap: @@ -48,9 +48,10 @@ class EmailDomain(Document): test.quit() except Exception: pass + try: - if self.use_ssl_for_outgoing: - if not self.smtp_port: + if self.get('use_ssl_for_outgoing'): + if not self.get('smtp_port'): self.smtp_port = 465 sess = smtplib.SMTP_SSL((self.smtp_server or "").encode('utf-8'), @@ -62,28 +63,15 @@ class EmailDomain(Document): sess.quit() except Exception: frappe.throw(_("Outgoing email account not correct")) - return None - return def on_update(self): """update all email accounts using this domain""" - for email_account in frappe.get_all("Email Account", - filters={"domain": self.name}): - + for email_account in frappe.get_all("Email Account", filters={"domain": self.name}): try: - email_account = frappe.get_doc("Email Account", - email_account.name) - email_account.set("email_server",self.email_server) - email_account.set("use_imap",self.use_imap) - email_account.set("use_ssl",self.use_ssl) - email_account.set("use_tls",self.use_tls) - email_account.set("attachment_limit",self.attachment_limit) - email_account.set("smtp_server",self.smtp_server) - email_account.set("smtp_port",self.smtp_port) - email_account.set("use_ssl_for_outgoing", self.use_ssl_for_outgoing) - email_account.set("append_emails_to_sent_folder", self.append_emails_to_sent_folder) + email_account = frappe.get_doc("Email Account", email_account.name) + for attr in ["email_server", "use_imap", "use_ssl", "use_tls", "attachment_limit", "smtp_server", "smtp_port", "use_ssl_for_outgoing", "append_emails_to_sent_folder"]: + email_account.set(attr, self.get(attr, default=0)) email_account.save() + except Exception as e: - frappe.msgprint(email_account.name) - frappe.throw(e) - return None + frappe.msgprint(_("Error has occurred in {0}").format(email_account.name), raise_exception=e.__class__) diff --git a/frappe/email/doctype/email_group/email_group.json b/frappe/email/doctype/email_group/email_group.json index d02cead0b0..0d784d409a 100644 --- a/frappe/email/doctype/email_group/email_group.json +++ b/frappe/email/doctype/email_group/email_group.json @@ -1,120 +1,70 @@ { - "allow_copy": 0, - "allow_import": 1, - "allow_rename": 0, - "autoname": "field:title", - "beta": 0, - "creation": "2015-03-18 06:08:32.729800", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 0, + "actions": [], + "allow_import": 1, + "autoname": "field:title", + "creation": "2015-03-18 06:08:32.729800", + "doctype": "DocType", + "document_type": "Setup", + "field_order": [ + "title", + "total_subscribers", + "confirmation_email_template", + "welcome_email_template" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "title", - "fieldtype": "Data", - "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": "Title", - "length": 0, - "no_copy": 1, - "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 - }, + "fieldname": "title", + "fieldtype": "Data", + "label": "Title", + "no_copy": 1, + "reqd": 1, + "unique": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "fieldname": "total_subscribers", - "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Total Subscribers", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "default": "0", + "fieldname": "total_subscribers", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Total Subscribers", + "read_only": 1 + }, + { + "fieldname": "confirmation_email_template", + "fieldtype": "Link", + "label": "Confirmation Email Template", + "options": "Email Template" + }, + { + "fieldname": "welcome_email_template", + "fieldtype": "Link", + "label": "Welcome Email Template", + "options": "Email Template" } - ], - "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-02-27 19:01:17.203845", - "modified_by": "Administrator", - "module": "Email", - "name": "Email Group", - "name_case": "", - "owner": "Administrator", + ], + "links": [], + "modified": "2020-02-21 14:12:48.884738", + "modified_by": "Administrator", + "module": "Email", + "name": "Email Group", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 1, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Newsletter Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "import": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Newsletter Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 1, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 + ], + "quick_entry": 1, + "show_name_in_global_search": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/frappe/email/doctype/email_group/email_group.py b/frappe/email/doctype/email_group/email_group.py index 70210faf37..b19a134713 100755 --- a/frappe/email/doctype/email_group/email_group.py +++ b/frappe/email/doctype/email_group/email_group.py @@ -66,6 +66,10 @@ def import_from(name, doctype): def add_subscribers(name, email_list): if not isinstance(email_list, (list, tuple)): email_list = email_list.replace(",", "\n").split("\n") + + template = frappe.db.get_value('Email Group', name, 'welcome_email_template') + welcome_email = frappe.get_doc("Email Template", template) if template else None + count = 0 for email in email_list: email = email.strip() @@ -78,7 +82,9 @@ def add_subscribers(name, email_list): "doctype": "Email Group Member", "email_group": name, "email": parsed_email - }).insert(ignore_permissions = frappe.flags.ignore_permissions) + }).insert(ignore_permissions=frappe.flags.ignore_permissions) + + send_welcome_email(welcome_email, parsed_email, name) count += 1 else: @@ -90,3 +96,15 @@ def add_subscribers(name, email_list): return frappe.get_doc("Email Group", name).update_total_subscribers() +def send_welcome_email(welcome_email, email, email_group): + """Send welcome email for the subscribers of a given email group.""" + if not welcome_email: + return + + args = dict( + email=email, + email_group=email_group + ) + + message = frappe.render_template(welcome_email.response, args) + frappe.sendmail(email, subject=welcome_email.subject, message=message) diff --git a/frappe/email/doctype/newsletter/newsletter.js b/frappe/email/doctype/newsletter/newsletter.js index 7bf0ae6b9a..0f1e8dc57c 100644 --- a/frappe/email/doctype/newsletter/newsletter.js +++ b/frappe/email/doctype/newsletter/newsletter.js @@ -4,23 +4,65 @@ frappe.ui.form.on('Newsletter', { refresh(frm) { let doc = frm.doc; - if(!doc.__islocal && !cint(doc.email_sent) && !doc.__unsaved + if (!doc.__islocal && !cint(doc.email_sent) && !doc.__unsaved && in_list(frappe.boot.user.can_write, doc.doctype)) { - frm.add_custom_button(__('Send'), function() { - frm.call('send_emails').then(() => { - frm.refresh(); + frm.add_custom_button(__('Send Now'), function() { + frappe.confirm(__("Do you really want to send this email newsletter?"), function() { + frm.call('send_emails').then(() => { + frm.refresh(); + }); }); }, "fa fa-play", "btn-success"); } + if (!doc.__islocal && cint(doc.email_sent)) { + frm.set_df_property('schedule_send', "read_only", 1); + } frm.events.setup_dashboard(frm); - if(doc.__islocal && !doc.send_from) { + if (doc.__islocal && !doc.send_from) { let { fullname, email } = frappe.user_info(doc.owner); frm.set_value('send_from', `${fullname} <${email}>`); } }, + onload_post_render(frm) { + frm.trigger('setup_schedule_send'); + }, + + setup_schedule_send(frm) { + let today = new Date(); + + // setting datepicker options to set min date & min time + today.setHours(today.getHours() + 1 ); + frm.get_field('schedule_send').$input.datepicker({ + maxMinutes: 0, + minDate: today, + timeFormat: 'hh:00:00', + onSelect: function (fd, d, picker) { + if (!d) return; + var date = d.toDateString(); + if (date === today.toDateString()) { + picker.update({ + minHours: (today.getHours() + 1) + }); + } else { + picker.update({ + minHours: 0 + }); + } + frm.get_field('schedule_send').$input.trigger('change'); + } + }); + + + const $tp = frm.get_field('schedule_send').datepicker.timepicker; + $tp.$minutes.parent().css('display', 'none'); + $tp.$minutesText.css('display', 'none'); + $tp.$minutesText.prev().css('display', 'none'); + $tp.$seconds.parent().css('display', 'none'); + }, + setup_dashboard(frm) { if(!frm.doc.__islocal && cint(frm.doc.email_sent) && frm.doc.__onload && frm.doc.__onload.status_count) { diff --git a/frappe/email/doctype/newsletter/newsletter.json b/frappe/email/doctype/newsletter/newsletter.json index 1ef51027ed..719d51c176 100644 --- a/frappe/email/doctype/newsletter/newsletter.json +++ b/frappe/email/doctype/newsletter/newsletter.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_rename": 1, "creation": "2013-01-10 16:34:31", "description": "Create and Send Newsletters", @@ -6,9 +7,11 @@ "document_type": "Other", "engine": "InnoDB", "field_order": [ + "send_from", + "column_break_2", + "schedule_send", "recipients", "email_group", - "send_from", "email_sent", "newsletter_content", "subject", @@ -41,7 +44,7 @@ "default": "0", "fieldname": "email_sent", "fieldtype": "Check", - "label": "Email Sent?", + "label": "Email Sent", "no_copy": 1, "read_only": 1 }, @@ -115,14 +118,24 @@ "fieldname": "recipients", "fieldtype": "Section Break", "label": "Recipients" + }, + { + "fieldname": "schedule_send", + "fieldtype": "Datetime", + "label": "Schedule Send" + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" } ], "has_web_view": 1, "icon": "fa fa-envelope", "idx": 1, "is_published_field": "published", + "links": [], "max_attachments": 3, - "modified": "2019-09-06 22:15:55.471254", + "modified": "2020-03-02 06:26:51.622521", "modified_by": "Administrator", "module": "Email", "name": "Newsletter", diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py index b0d1756643..2469569892 100755 --- a/frappe/email/doctype/newsletter/newsletter.py +++ b/frappe/email/doctype/newsletter/newsletter.py @@ -11,7 +11,7 @@ from frappe.utils.verified_command import get_signed_params, verify_request from frappe.utils.background_jobs import enqueue from frappe.email.queue import send from frappe.email.doctype.email_group.email_group import add_subscribers -from frappe.utils import parse_addr +from frappe.utils import parse_addr, now_datetime from frappe.utils import validate_email_address @@ -42,7 +42,6 @@ class Newsletter(WebsiteGenerator): if self.recipients: if getattr(frappe.local, "is_ajax", False): self.validate_send() - # using default queue with a longer timeout as this isn't a scheduled task enqueue(send_newsletter, queue='default', timeout=6000, event='send_newsletter', newsletter=self.name) @@ -52,8 +51,6 @@ class Newsletter(WebsiteGenerator): frappe.msgprint(_("Scheduled to send to {0} recipients").format(len(self.recipients))) - frappe.db.set(self, "email_sent", 1) - frappe.db.set(self, 'scheduled_to_send', len(self.recipients)) else: frappe.msgprint(_("Newsletter should have atleast one recipient")) @@ -71,8 +68,8 @@ class Newsletter(WebsiteGenerator): attachments = [] if self.send_attachements: - files = frappe.get_all("File", fields = ["name"], filters = {"attached_to_doctype": "Newsletter", - "attached_to_name":self.name}, order_by="creation desc") + files = frappe.get_all("File", fields=["name"], filters={"attached_to_doctype": "Newsletter", + "attached_to_name": self.name}, order_by="creation desc") for file in files: try: @@ -82,17 +79,21 @@ class Newsletter(WebsiteGenerator): except IOError: frappe.throw(_("Unable to find attachment {0}").format(file.name)) - send(recipients = self.recipients, sender = sender, - subject = self.subject, message = self.message, - reference_doctype = self.doctype, reference_name = self.name, - add_unsubscribe_link = self.send_unsubscribe_link, attachments=attachments, - unsubscribe_method = "/unsubscribe", - unsubscribe_params = {"name": self.name}, - send_priority = 0, queue_separately=True) + send(recipients=self.recipients, sender=sender, + subject=self.subject, message=self.message, + reference_doctype=self.doctype, reference_name=self.name, + add_unsubscribe_link=self.send_unsubscribe_link, attachments=attachments, + unsubscribe_method="/unsubscribe", + unsubscribe_params={"name": self.name}, + send_priority=0, queue_separately=True) if not frappe.flags.in_test: frappe.db.auto_commit_on_many_writes = False + self.db_set("email_sent", 1) + self.db_set("schedule_send", now_datetime()) + self.db_set("scheduled_to_send", len(self.recipients)) + def get_recipients(self): """Get recipients from Email Group""" recipients_list = [] @@ -160,39 +161,52 @@ def create_lead(email_id): @frappe.whitelist(allow_guest=True) -def subscribe(email): +def subscribe(email, email_group=_('Website')): url = frappe.utils.get_url("/api/method/frappe.email.doctype.newsletter.newsletter.confirm_subscription") +\ - "?" + get_signed_params({"email": email}) + "?" + get_signed_params({"email": email, "email_group": email_group}) - messages = ( - _("Thank you for your interest in subscribing to our updates"), - _("Please verify your Email Address"), - url, - _("Click here to verify") - ) + email_template = frappe.db.get_value('Email Group', email_group, ['confirmation_email_template']) - content = """ -

{0}. {1}.

-

{3}

- """ + content='' + if email_template: + args = dict( + email=email, + confirmation_url=url, + email_group=email_group + ) - frappe.sendmail(email, subject=_("Confirm Your Email"), content=content.format(*messages)) + email_template = frappe.get_doc("Email Template", email_template) + content = frappe.render_template(email_template.response, args) + + if not content: + messages = ( + _("Thank you for your interest in subscribing to our updates"), + _("Please verify your Email Address"), + url, + _("Click here to verify") + ) + + content = """ +

{0}. {1}.

+

{3}

+ """.format(*messages) + + frappe.sendmail(email, subject=getattr('email_template', 'subject', '') or _("Confirm Your Email"), content=content) @frappe.whitelist(allow_guest=True) -def confirm_subscription(email): +def confirm_subscription(email, email_group=_('Website')): if not verify_request(): return - if not frappe.db.exists("Email Group", _("Website")): + if not frappe.db.exists("Email Group", email_group): frappe.get_doc({ "doctype": "Email Group", - "title": _("Website") + "title": email_group }).insert(ignore_permissions=True) - frappe.flags.ignore_permissions = True - add_subscribers(_("Website"), email) + add_subscribers(email_group, email) frappe.db.commit() frappe.respond_as_web_page(_("Confirmed"), @@ -212,7 +226,7 @@ def send_newsletter(newsletter): doc.db_set("email_sent", 0) frappe.db.commit() - frappe.log_error("send_newsletter") + frappe.log_error(title='Send Newsletter') raise @@ -250,3 +264,11 @@ def get_newsletter_list(doctype, txt, filters, limit_start, limit_page_length=20 '''.format(','.join(['%s'] * len(email_group_list)), limit_page_length, limit_start), email_group_list, as_dict=1) +def send_scheduled_email(): + """Send scheduled newsletter to the recipients.""" + scheduled_newsletter = frappe.get_all('Newsletter', filters = { + 'schedule_send': ('<=', now_datetime()), + 'email_sent': 0 + }, fields = ['name'], ignore_ifnull=True) + for newsletter in scheduled_newsletter: + send_newsletter(newsletter.name) diff --git a/frappe/email/doctype/newsletter/test_newsletter.py b/frappe/email/doctype/newsletter/test_newsletter.py index 04af44f78a..5a13f99e56 100644 --- a/frappe/email/doctype/newsletter/test_newsletter.py +++ b/frappe/email/doctype/newsletter/test_newsletter.py @@ -3,8 +3,9 @@ from __future__ import unicode_literals import frappe, unittest +from frappe.utils import getdate, add_days -from frappe.email.doctype.newsletter.newsletter import confirmed_unsubscribe +from frappe.email.doctype.newsletter.newsletter import confirmed_unsubscribe, send_scheduled_email from six.moves.urllib.parse import unquote test_dependencies = ["Email Group"] @@ -58,7 +59,7 @@ class TestNewsletter(unittest.TestCase): self.assertTrue(email in recipients) @staticmethod - def send_newsletter(published=0): + def send_newsletter(published=0, schedule_send=None): frappe.db.sql("delete from `tabEmail Queue`") frappe.db.sql("delete from `tabEmail Queue Recipient`") frappe.db.sql("delete from `tabNewsletter`") @@ -67,11 +68,16 @@ class TestNewsletter(unittest.TestCase): "subject": "_Test Newsletter", "send_from": "Test Sender ", "message": "Testing my news.", - "published": published + "published": published, + "schedule_send": schedule_send }).insert(ignore_permissions=True) newsletter.append("email_group", {"email_group": "_Test Email Group"}) newsletter.save() + if schedule_send: + send_scheduled_email() + return + newsletter.send_emails() return newsletter.name @@ -89,4 +95,13 @@ class TestNewsletter(unittest.TestCase): doc = frappe.get_doc("Newsletter", newsletter_name) doc.get_context(context) self.assertEqual(context.no_cache, 1) - self.assertTrue("attachments" not in list(context)) \ No newline at end of file + self.assertTrue("attachments" not in list(context)) + + def test_schedule_send(self): + self.send_newsletter(schedule_send=add_days(getdate(), -1)) + + email_queue_list = [frappe.get_doc('Email Queue', e.name) for e in frappe.get_all("Email Queue")] + self.assertEqual(len(email_queue_list), 4) + recipients = [e.recipients[0].recipient for e in email_queue_list] + for email in emails: + self.assertTrue(email in recipients) \ No newline at end of file diff --git a/frappe/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py index 6a3dd89873..8c011ade65 100644 --- a/frappe/email/doctype/notification/notification.py +++ b/frappe/email/doctype/notification/notification.py @@ -335,4 +335,4 @@ def evaluate_alert(doc, alert, event): frappe.utils.get_link_to_form('Error Log', error_log.name))) def get_context(doc): - return {"doc": doc, "nowdate": nowdate, "frappe.utils": frappe.utils} + return {"doc": doc, "nowdate": nowdate, "frappe": frappe._dict(utils=frappe.utils)} diff --git a/frappe/exceptions.py b/frappe/exceptions.py index 3d63f4b2b4..9a1c1fb0b0 100644 --- a/frappe/exceptions.py +++ b/frappe/exceptions.py @@ -78,6 +78,8 @@ class TimestampMismatchError(ValidationError): pass class EmptyTableError(ValidationError): pass class LinkExistsError(ValidationError): pass class InvalidEmailAddressError(ValidationError): pass +class InvalidNameError(ValidationError): pass +class InvalidPhoneNumberError(ValidationError): pass class TemplateNotFoundError(ValidationError): pass class UniqueValidationError(ValidationError): pass class AppNotInstalledError(ValidationError): pass @@ -90,4 +92,8 @@ class SecurityException(Exception): pass class InvalidColumnName(ValidationError): pass class IncompatibleApp(ValidationError): pass class InvalidDates(ValidationError): pass -class DataTooLongException(ValidationError): pass \ No newline at end of file +class DataTooLongException(ValidationError): pass +# OAuth exceptions +class InvalidAuthorizationHeader(CSRFTokenError): pass +class InvalidAuthorizationPrefix(CSRFTokenError): pass +class InvalidAuthorizationToken(CSRFTokenError): pass diff --git a/frappe/handler.py b/frappe/handler.py index f4fbf56792..6e0bf7a6be 100755 --- a/frappe/handler.py +++ b/frappe/handler.py @@ -8,7 +8,7 @@ import frappe.utils import frappe.sessions import frappe.desk.form.run_method from frappe.utils.response import build_response -from frappe.api import validate_auth_via_api_keys +from frappe.api import validate_auth from frappe.utils import cint from frappe.core.doctype.server_script.server_script_utils import run_server_script_api from werkzeug.wrappers import Response @@ -16,7 +16,7 @@ from six import string_types def handle(): """handle request""" - validate_auth_via_api_keys() + validate_auth() cmd = frappe.local.form_dict.cmd data = None diff --git a/frappe/hooks.py b/frappe/hooks.py index c44c05fdf4..2561399a78 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -86,14 +86,17 @@ permission_query_conditions = { "Event": "frappe.desk.doctype.event.event.get_permission_query_conditions", "ToDo": "frappe.desk.doctype.todo.todo.get_permission_query_conditions", "User": "frappe.core.doctype.user.user.get_permission_query_conditions", + "Dashboard Settings": "frappe.desk.doctype.dashboard_settings.dashboard_settings.get_permission_query_conditions", "Notification Log": "frappe.desk.doctype.notification_log.notification_log.get_permission_query_conditions", + "Dashboard Chart": "frappe.desk.doctype.dashboard_chart.dashboard_chart.get_permission_query_conditions", "Notification Settings": "frappe.desk.doctype.notification_settings.notification_settings.get_permission_query_conditions", "Note": "frappe.desk.doctype.note.note.get_permission_query_conditions", "Kanban Board": "frappe.desk.doctype.kanban_board.kanban_board.get_permission_query_conditions", "Contact": "frappe.contacts.address_and_contact.get_permission_query_conditions_for_contact", "Address": "frappe.contacts.address_and_contact.get_permission_query_conditions_for_address", "Communication": "frappe.core.doctype.communication.communication.get_permission_query_conditions_for_communication", - "Workflow Action": "frappe.workflow.doctype.workflow_action.workflow_action.get_permission_query_conditions" + "Workflow Action": "frappe.workflow.doctype.workflow_action.workflow_action.get_permission_query_conditions", + "Prepared Report": "frappe.core.doctype.prepared_report.prepared_report.get_permission_query_condition" } has_permission = { @@ -101,12 +104,14 @@ has_permission = { "ToDo": "frappe.desk.doctype.todo.todo.has_permission", "User": "frappe.core.doctype.user.user.has_permission", "Note": "frappe.desk.doctype.note.note.has_permission", + "Dashboard Chart": "frappe.desk.doctype.dashboard_chart.dashboard_chart.has_permission", "Kanban Board": "frappe.desk.doctype.kanban_board.kanban_board.has_permission", "Contact": "frappe.contacts.address_and_contact.has_permission", "Address": "frappe.contacts.address_and_contact.has_permission", "Communication": "frappe.core.doctype.communication.communication.has_permission", "Workflow Action": "frappe.workflow.doctype.workflow_action.workflow_action.has_permission", - "File": "frappe.core.doctype.file.file.has_permission" + "File": "frappe.core.doctype.file.file.has_permission", + "Prepared Report": "frappe.core.doctype.prepared_report.prepared_report.has_permission" } has_website_permission = { @@ -133,13 +138,11 @@ doc_events = { ], "on_trash": [ "frappe.desk.notifications.clear_doctype_notifications", - "frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions", - "frappe.cache_manager.build_table_count_cache" + "frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions" ], "on_change": [ "frappe.social.doctype.energy_point_rule.energy_point_rule.process_energy_points" - ], - "after_insert": "frappe.cache_manager.build_table_count_cache" + ] }, "Event": { "after_insert": "frappe.integrations.doctype.google_calendar.google_calendar.insert_event_in_google_calendar", @@ -151,9 +154,11 @@ doc_events = { "on_update": "frappe.integrations.doctype.google_contacts.google_contacts.update_contacts_to_google_contacts", }, "DocType": { + "after_insert": "frappe.cache_manager.build_domain_restriced_doctype_cache", "after_save": "frappe.cache_manager.build_domain_restriced_doctype_cache", }, "Page": { + "after_insert": "frappe.cache_manager.build_domain_restriced_page_cache", "after_save": "frappe.cache_manager.build_domain_restriced_page_cache", }, "Event Update Log": { @@ -184,7 +189,8 @@ scheduler_events = { "frappe.desk.page.backups.backups.delete_downloadable_backups", "frappe.deferred_insert.save_to_db", "frappe.desk.form.document_follow.send_hourly_updates", - "frappe.integrations.doctype.google_calendar.google_calendar.sync" + "frappe.integrations.doctype.google_calendar.google_calendar.sync", + "frappe.email.doctype.newsletter.newsletter.send_scheduled_email" ], "daily": [ "frappe.email.queue.clear_outbox", diff --git a/frappe/integrations/desk_page/integrations/integrations.json b/frappe/integrations/desk_page/integrations/integrations.json index 6ea871cd90..9201e223f8 100644 --- a/frappe/integrations/desk_page/integrations/integrations.json +++ b/frappe/integrations/desk_page/integrations/integrations.json @@ -1,25 +1,29 @@ { "cards": [ { - "links": "[\n {\n \"description\": \"Dropbox backup settings\",\n \"label\": \"Dropbox Settings\",\n \"name\": \"Dropbox Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"S3 Backup Settings\",\n \"label\": \"S3 Backup Settings\",\n \"name\": \"S3 Backup Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Google Drive Backup.\",\n \"label\": \"Google Drive\",\n \"name\": \"Google Drive\",\n \"type\": \"doctype\"\n }\n]", - "title": "Backup" + "hidden": 0, + "label": "Backup", + "links": "[\n {\n \"description\": \"Dropbox backup settings\",\n \"label\": \"Dropbox Settings\",\n \"name\": \"Dropbox Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"S3 Backup Settings\",\n \"label\": \"S3 Backup Settings\",\n \"name\": \"S3 Backup Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Google Drive Backup.\",\n \"label\": \"Google Drive\",\n \"name\": \"Google Drive\",\n \"type\": \"doctype\"\n }\n]" }, { - "links": "[\n {\n \"description\": \"Google API Settings.\",\n \"label\": \"Google Settings\",\n \"name\": \"Google Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Google Contacts Integration.\",\n \"label\": \"Google Contacts\",\n \"name\": \"Google Contacts\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Google Calendar Integration.\",\n \"label\": \"Google Calendar\",\n \"name\": \"Google Calendar\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Google Drive Integration.\",\n \"label\": \"Google Drive\",\n \"name\": \"Google Drive\",\n \"type\": \"doctype\"\n }\n]", - "title": "Google Services" + "hidden": 0, + "label": "Google Services", + "links": "[\n {\n \"description\": \"Google API Settings.\",\n \"label\": \"Google Settings\",\n \"name\": \"Google Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Google Contacts Integration.\",\n \"label\": \"Google Contacts\",\n \"name\": \"Google Contacts\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Google Calendar Integration.\",\n \"label\": \"Google Calendar\",\n \"name\": \"Google Calendar\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Google Drive Integration.\",\n \"label\": \"Google Drive\",\n \"name\": \"Google Drive\",\n \"type\": \"doctype\"\n }\n]" }, { - "links": "[\n {\n \"description\": \"Webhooks calling API requests into web apps\",\n \"label\": \"Webhook\",\n \"name\": \"Webhook\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Slack Webhooks for internal integration\",\n \"label\": \"Slack Webhook URL\",\n \"name\": \"Slack Webhook URL\",\n \"type\": \"doctype\"\n }\n]", - "title": "Webhook" + "hidden": 0, + "label": "Webhook", + "links": "[\n {\n \"description\": \"Webhooks calling API requests into web apps\",\n \"label\": \"Webhook\",\n \"name\": \"Webhook\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Slack Webhooks for internal integration\",\n \"label\": \"Slack Webhook URL\",\n \"name\": \"Slack Webhook URL\",\n \"type\": \"doctype\"\n }\n]" }, { - "links": "[\n {\n \"description\": \"Enter keys to enable login via Facebook, Google, GitHub.\",\n \"label\": \"Social Login Key\",\n \"name\": \"Social Login Key\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Ldap settings\",\n \"label\": \"LDAP Settings\",\n \"name\": \"LDAP Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Register OAuth Client App\",\n \"label\": \"OAuth Client\",\n \"name\": \"OAuth Client\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Settings for OAuth Provider\",\n \"label\": \"OAuth Provider Settings\",\n \"name\": \"OAuth Provider Settings\",\n \"type\": \"doctype\"\n }\n]", - "title": "Authentication" + "hidden": 0, + "label": "Authentication", + "links": "[\n {\n \"description\": \"Enter keys to enable login via Facebook, Google, GitHub.\",\n \"label\": \"Social Login Key\",\n \"name\": \"Social Login Key\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Ldap settings\",\n \"label\": \"LDAP Settings\",\n \"name\": \"LDAP Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Register OAuth Client App\",\n \"label\": \"OAuth Client\",\n \"name\": \"OAuth Client\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Settings for OAuth Provider\",\n \"label\": \"OAuth Provider Settings\",\n \"name\": \"OAuth Provider Settings\",\n \"type\": \"doctype\"\n }\n]" }, { - "icon": "fa fa-star", - "links": "[\n {\n \"description\": \"Braintree payment gateway settings\",\n \"label\": \"Braintree Settings\",\n \"name\": \"Braintree Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"PayPal payment gateway settings\",\n \"label\": \"PayPal Settings\",\n \"name\": \"PayPal Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Razorpay Payment gateway settings\",\n \"label\": \"Razorpay Settings\",\n \"name\": \"Razorpay Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Stripe payment gateway settings\",\n \"label\": \"Stripe Settings\",\n \"name\": \"Stripe Settings\",\n \"type\": \"doctype\"\n }\n]", - "title": "Payments" + "hidden": 0, + "label": "Payments", + "links": "[\n {\n \"description\": \"Braintree payment gateway settings\",\n \"label\": \"Braintree Settings\",\n \"name\": \"Braintree Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"PayPal payment gateway settings\",\n \"label\": \"PayPal Settings\",\n \"name\": \"PayPal Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Razorpay Payment gateway settings\",\n \"label\": \"Razorpay Settings\",\n \"name\": \"Razorpay Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Stripe payment gateway settings\",\n \"label\": \"Stripe Settings\",\n \"name\": \"Stripe Settings\",\n \"type\": \"doctype\"\n }\n]" } ], "category": "Administration", @@ -34,7 +38,7 @@ "idx": 0, "is_standard": 1, "label": "Integrations", - "modified": "2020-03-12 16:30:42.823316", + "modified": "2020-04-01 11:24:40.751651", "modified_by": "Administrator", "module": "Integrations", "name": "Integrations", diff --git a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.json b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.json index 33d34e0210..858469647a 100644 --- a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.json +++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.json @@ -1,487 +1,129 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2016-09-21 10:12:57.399174", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "System", - "editable_grid": 1, + "creation": "2016-09-21 10:12:57.399174", + "doctype": "DocType", + "document_type": "System", + "editable_grid": 1, + "field_order": [ + "enabled", + "send_notifications_to", + "send_email_for_successful_backup", + "backup_frequency", + "limit_no_of_backups", + "no_of_backups", + "file_backup", + "app_access_key", + "app_secret_key", + "allow_dropbox_access", + "dropbox_access_key", + "dropbox_access_secret", + "dropbox_access_token" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "enabled", - "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": "Enabled", - "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, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "enabled", + "fieldtype": "Check", + "label": "Enabled" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "send_notifications_to", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Send Notifications To", - "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": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "send_notifications_to", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Send Notifications To", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "1", - "description": "Note: By default emails for failed backups are sent.", - "fieldname": "send_email_for_successful_backup", - "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": "Send Email for Successful Backup", - "length": 0, - "no_copy": 0, - "options": "", - "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, - "translatable": 0, - "unique": 0 - }, + "default": "1", + "description": "Note: By default emails for failed backups are sent.", + "fieldname": "send_email_for_successful_backup", + "fieldtype": "Check", + "label": "Send Email for Successful Backup" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "backup_frequency", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Backup Frequency", - "length": 0, - "no_copy": 0, - "options": "\nDaily\nWeekly", - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "backup_frequency", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Backup Frequency", + "options": "\nDaily\nWeekly", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "limit_no_of_backups", - "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": "Limit Number of DB Backups", - "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, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "limit_no_of_backups", + "fieldtype": "Check", + "label": "Limit Number of DB Backups" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "5", - "depends_on": "eval:doc.limit_no_of_backups", - "fieldname": "no_of_backups", - "fieldtype": "Int", - "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": "Number of DB Backups", - "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, - "translatable": 0, - "unique": 0 - }, + "default": "5", + "depends_on": "eval:doc.limit_no_of_backups", + "fieldname": "no_of_backups", + "fieldtype": "Int", + "label": "Number of DB Backups" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "1", - "fieldname": "file_backup", - "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": "File Backup", - "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, - "translatable": 0, - "unique": 0 - }, + "default": "1", + "fieldname": "file_backup", + "fieldtype": "Check", + "label": "File Backup" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fieldname": "app_access_key", - "fieldtype": "Data", - "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": "App Access Key", - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "app_access_key", + "fieldtype": "Data", + "label": "App Access Key" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fieldname": "app_secret_key", - "fieldtype": "Password", - "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": "App Secret Key", - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "app_secret_key", + "fieldtype": "Password", + "label": "App Secret Key" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "allow_dropbox_access", - "fieldtype": "Button", - "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": "Allow Dropbox Access", - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "allow_dropbox_access", + "fieldtype": "Button", + "label": "Allow Dropbox Access" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "dropbox_access_key", - "fieldtype": "Password", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Dropbox Access Key", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "dropbox_access_key", + "fieldtype": "Password", + "hidden": 1, + "label": "Dropbox Access Key", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "dropbox_access_secret", - "fieldtype": "Password", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Dropbox Access Secret", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "dropbox_access_secret", + "fieldtype": "Password", + "hidden": 1, + "label": "Dropbox Access Secret", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "dropbox_access_token", - "fieldtype": "Password", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Dropbox Access Token", - "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, - "translatable": 0, - "unique": 0 + "fieldname": "dropbox_access_token", + "fieldtype": "Password", + "hidden": 1, + "label": "Dropbox Access Token" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 1, - "is_submittable": 0, - "issingle": 1, - "istable": 0, - "max_attachments": 0, - "modified": "2019-01-03 05:44:40.520943", - "modified_by": "Administrator", - "module": "Integrations", - "name": "Dropbox Settings", - "name_case": "", - "owner": "Administrator", + ], + "in_create": 1, + "issingle": 1, + "modified": "2019-08-22 16:26:44.468391", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Dropbox Settings", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 0, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 1, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 -} + ], + "read_only": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py index dd4768f8b3..2a036f4838 100644 --- a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py +++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py @@ -3,22 +3,25 @@ # For license information, please see license.txt from __future__ import unicode_literals +import dropbox +import json import frappe import os from frappe import _ from frappe.model.document import Document -import dropbox, json +from frappe.integrations.offsite_backup_utils import get_latest_backup_file, send_email, validate_file_size +from frappe.integrations.utils import make_post_request +from frappe.utils import (cint, get_request_site_address, + get_files_path, get_backups_path, get_url, encode) from frappe.utils.backups import new_backup from frappe.utils.background_jobs import enqueue from six.moves.urllib.parse import urlparse, parse_qs -from frappe.integrations.utils import make_post_request from rq.timeouts import JobTimeoutException -from frappe.utils import (cint, split_emails, get_request_site_address, - get_files_path, get_backups_path, get_url, encode) from six import text_type ignore_list = [".DS_Store"] + class DropboxSettings(Document): def onload(self): if not self.app_access_key and frappe.conf.dropbox_access_key: @@ -48,10 +51,12 @@ def take_backup_to_dropbox(retry_count=0, upload_db_backup=True): did_not_upload, error_log = [], [] try: if cint(frappe.db.get_value("Dropbox Settings", None, "enabled")): + validate_file_size() + did_not_upload, error_log = backup_to_dropbox(upload_db_backup) if did_not_upload: raise Exception - send_email(True, "Dropbox") + send_email(True, "Dropbox", "Dropbox Settings", "send_notifications_to") except JobTimeoutException: if retry_count < 2: args = { @@ -66,34 +71,8 @@ def take_backup_to_dropbox(retry_count=0, upload_db_backup=True): else: file_and_error = [" - ".join(f) for f in zip(did_not_upload, error_log)] error_message = ("\n".join(file_and_error) + "\n" + frappe.get_traceback()) - frappe.errprint(error_message) - send_email(False, "Dropbox", error_message) -def send_email(success, service_name, error_status=None): - if success: - if frappe.db.get_value("Dropbox Settings", None, "send_email_for_successful_backup") == '0': - return - - subject = "Backup Upload Successful" - message ="""

Backup Uploaded Successfully

Hi there, this is just to inform you - that your backup was successfully uploaded to your %s account. So relax!

- """ % service_name - - else: - subject = "[Warning] Backup Upload Failed" - message ="""

Backup Upload Failed

Oops, your automated backup to %s - failed.

-

Error message:
-

%s
-

-

Please contact your system manager for more information.

- """ % (service_name, error_status) - - if not frappe.db: - frappe.connect() - - recipients = split_emails(frappe.db.get_value("Dropbox Settings", None, "send_notifications_to")) - frappe.sendmail(recipients=recipients, subject=subject, message=message) + send_email(False, "Dropbox", "Dropbox Settings", "send_notifications_to", error_message) def backup_to_dropbox(upload_db_backup=True): if not frappe.db: @@ -114,8 +93,12 @@ def backup_to_dropbox(upload_db_backup=True): dropbox_client = dropbox.Dropbox(dropbox_settings['access_token']) if upload_db_backup: - backup = new_backup(ignore_files=True) - filename = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_db)) + if frappe.flags.create_new_backup: + backup = new_backup(ignore_files=True) + filename = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_db)) + else: + filename = get_latest_backup_file() + upload_file_to_dropbox(filename, "/database", dropbox_client) # delete older databases diff --git a/frappe/integrations/doctype/dropbox_settings/test_dropbox_settings.py b/frappe/integrations/doctype/dropbox_settings/test_dropbox_settings.py new file mode 100644 index 0000000000..539fc417f2 --- /dev/null +++ b/frappe/integrations/doctype/dropbox_settings/test_dropbox_settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestDropboxSettings(unittest.TestCase): + pass diff --git a/frappe/integrations/doctype/google_drive/google_drive.py b/frappe/integrations/doctype/google_drive/google_drive.py index 8078c702c0..60ee173bbf 100644 --- a/frappe/integrations/doctype/google_drive/google_drive.py +++ b/frappe/integrations/doctype/google_drive/google_drive.py @@ -19,6 +19,7 @@ from apiclient.http import MediaFileUpload from frappe.utils import get_backups_path, get_bench_path from frappe.utils.backups import new_backup from frappe.integrations.doctype.google_settings.google_settings import get_auth_url +from frappe.integrations.offsite_backup_utils import get_latest_backup_file, send_email, validate_file_size SCOPES = "https://www.googleapis.com/auth/drive" @@ -183,13 +184,16 @@ def upload_system_backup_to_google_drive(): check_for_folder_in_google_drive() account.load_from_db() - progress(1, "Backing up Data.") - backup = new_backup() - - fileurl_backup = os.path.basename(backup.backup_path_db) - fileurl_public_files = os.path.basename(backup.backup_path_files) - fileurl_private_files = os.path.basename(backup.backup_path_private_files) + validate_file_size() + if frappe.flags.create_new_backup: + set_progress(1, "Backing up Data.") + backup = new_backup() + fileurl_backup = os.path.basename(backup.backup_path_db) + fileurl_public_files = os.path.basename(backup.backup_path_files) + fileurl_private_files = os.path.basename(backup.backup_path_private_files) + else: + fileurl_backup, fileurl_public_files, fileurl_private_files = get_latest_backup_file(with_files=True) for fileurl in [fileurl_backup, fileurl_public_files, fileurl_private_files]: file_metadata = { @@ -203,15 +207,14 @@ def upload_system_backup_to_google_drive(): frappe.throw(_("Google Drive - Could not locate locate - {0}").format(e)) try: - progress(2, "Uploading backup to Google Drive.") + set_progress(2, "Uploading backup to Google Drive.") google_drive.files().create(body=file_metadata, media_body=media, fields="id").execute() except HttpError as e: - send_email(success=False, error=e) - frappe.msgprint(_("Google Drive - Could not upload backup - Error {0}").format(e)) + send_email(False, "Google Drive", "Google Drive", "email", error_status=e) - progress(3, "Uploading successful.") + set_progress(3, "Uploading successful.") frappe.db.set_value("Google Drive", None, "last_backup_on", frappe.utils.now_datetime()) - send_email(success=True) + send_email(True, "Google Drive", "Google Drive", "email") return _("Google Drive Backup Successful.") def daily_backup(): @@ -226,30 +229,5 @@ def get_absolute_path(filename): file_path = os.path.join(get_backups_path()[2:], filename) return "{0}/sites/{1}".format(get_bench_path(), file_path) -def progress(progress, message): +def set_progress(progress, message): frappe.publish_realtime("upload_to_google_drive", dict(progress=progress, total=3, message=message), user=frappe.session.user) - -def send_email(success, error=None): - if success: - if not frappe.db.get_single_value("Google Drive", "send_email_for_successful_backup"): - return - - subject = "Backup Upload Successful" - message = """

Backup Uploaded Successfully

Hi there, this is just to inform you - that your backup was successfully uploaded to Google Drive.

- """ - else: - subject = "[Warning] Backup Upload Failed" - message = """

Backup Upload Failed

Oops, your automated backup to Google Drive - failed.

-

Error message:
-

{0}
-

-

Please contact your system manager for more information.

- """.format(error) - - frappe.sendmail( - recipients=frappe.db.get_single_value("Google Drive", "email"), - subject=subject, - message=message - ) \ No newline at end of file diff --git a/frappe/integrations/doctype/ldap_settings/ldap_settings.py b/frappe/integrations/doctype/ldap_settings/ldap_settings.py index c0f12df04a..558f7117c0 100644 --- a/frappe/integrations/doctype/ldap_settings/ldap_settings.py +++ b/frappe/integrations/doctype/ldap_settings/ldap_settings.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe -from frappe import _ +from frappe import _, safe_encode from frappe.model.document import Document @@ -19,7 +19,7 @@ class LDAPSettings(Document): else: frappe.throw(_("LDAP Search String needs to end with a placeholder, eg sAMAccountName={0}")) - def connect_to_ldap(self, base_dn, password): + def connect_to_ldap(self, base_dn, password, read_only=True): try: import ldap3 import ssl @@ -44,7 +44,7 @@ class LDAPSettings(Document): user=base_dn, password=password, auto_bind=bind_type, - read_only=True, + read_only=read_only, raise_exceptions=True) return conn @@ -170,6 +170,36 @@ class LDAPSettings(Document): else: frappe.throw(_("Invalid username or password")) + def reset_password(self, user, password, logout_sessions=False): + from ldap3 import HASHED_SALTED_SHA, MODIFY_REPLACE + from ldap3.utils.hashed import hashed + + search_filter = "({0}={1})".format(self.ldap_email_field, user) + + conn = self.connect_to_ldap(self.base_dn, self.get_password(raise_exception=False), + read_only=False) + + if conn.search( + search_base=self.organizational_unit, + search_filter=search_filter, + attributes=self.get_ldap_attributes() + ): + if conn.entries and conn.entries[0]: + entry_dn = conn.entries[0].entry_dn + hashed_password = hashed(HASHED_SALTED_SHA, safe_encode(password)) + changes = {'userPassword': [(MODIFY_REPLACE, [hashed_password])]} + if conn.modify(entry_dn, changes=changes): + if logout_sessions: + from frappe.sessions import clear_sessions + clear_sessions(user=user, force=True) + frappe.msgprint(_("Password changed successfully.")) + else: + frappe.throw(_("Failed to change password.")) + else: + frappe.throw(_("No Entry for the User {0} found within LDAP!").format(user)) + else: + frappe.throw(_("No LDAP User found for email: {0}").format(user)) + def convert_ldap_entry_to_dict(self, user_entry): # support multiple email values @@ -211,3 +241,11 @@ def login(): # because of a GET request! frappe.db.commit() + + +@frappe.whitelist() +def reset_password(user, password, logout): + ldap = frappe.get_doc("LDAP Settings") + if not ldap.enabled: + frappe.throw(_("LDAP is not enabled.")) + ldap.reset_password(user, password, logout_sessions=int(logout)) diff --git a/frappe/integrations/doctype/oauth_client/oauth_client.json b/frappe/integrations/doctype/oauth_client/oauth_client.json index 47ede6e280..d0d45c36ab 100644 --- a/frappe/integrations/doctype/oauth_client/oauth_client.json +++ b/frappe/integrations/doctype/oauth_client/oauth_client.json @@ -1,517 +1,517 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "", - "beta": 0, - "creation": "2016-08-24 14:07:21.955052", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 1, - "engine": "InnoDB", + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "autoname": "", + "beta": 0, + "creation": "2016-08-24 14:07:21.955052", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "fieldname": "client_id", - "fieldtype": "Data", - "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": "App Client ID", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "", + "fieldname": "client_id", + "fieldtype": "Data", + "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": "App Client ID", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "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, - "fieldname": "app_name", - "fieldtype": "Data", - "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": "App Name", - "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": 1, - "search_index": 0, - "set_only_once": 0, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "app_name", + "fieldtype": "Data", + "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": "App Name", + "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": 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": "user", - "fieldtype": "Link", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "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": 0, - "search_index": 0, - "set_only_once": 0, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "user", + "fieldtype": "Link", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "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": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "cb_1", - "fieldtype": "Column 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, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "cb_1", + "fieldtype": "Column 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, + "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 - }, + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "client_secret", - "fieldtype": "Data", - "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": "App Client Secret", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "client_secret", + "fieldtype": "Data", + "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": "App Client Secret", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "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, - "description": "If checked, users will not see the Confirm Access dialog.", - "fieldname": "skip_authorization", - "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": "Skip Authorization", - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "If checked, users will not see the Confirm Access dialog.", + "fieldname": "skip_authorization", + "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": "Skip Authorization", + "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 - }, + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", - "fieldname": "sb_1", - "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": "", - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "", + "fieldname": "sb_1", + "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": "", + "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 - }, + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "all openid", - "description": "A list of resources which the Client App will have access to after the user allows it.
e.g. project", - "fieldname": "scopes", - "fieldtype": "Text", - "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": "Scopes", - "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": 1, - "search_index": 0, - "set_only_once": 0, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "all openid", + "description": "A list of resources which the Client App will have access to after the user allows it.
e.g. project", + "fieldname": "scopes", + "fieldtype": "Text", + "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": "Scopes", + "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": 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": "cb_3", - "fieldtype": "Column 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, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "cb_3", + "fieldtype": "Column 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, + "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 - }, + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "URIs for receiving authorization code once the user allows access, as well as failure responses. Typically a REST endpoint exposed by the Client App.\n
e.g. http://hostname//api/method/frappe.www.login.login_via_facebook", - "fieldname": "redirect_uris", - "fieldtype": "Text", - "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": "Redirect URIs", - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "URIs for receiving authorization code once the user allows access, as well as failure responses. Typically a REST endpoint exposed by the Client App.\n
e.g. http://hostname//api/method/frappe.www.login.login_via_facebook", + "fieldname": "redirect_uris", + "fieldtype": "Text", + "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": "Redirect URIs", + "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, - "fieldname": "default_redirect_uri", - "fieldtype": "Data", - "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": "Default Redirect URI", - "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": 1, - "search_index": 0, - "set_only_once": 0, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "default_redirect_uri", + "fieldtype": "Data", + "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": "Default Redirect URI", + "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": 1, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "collapsible_depends_on": "1", - "columns": 0, - "fieldname": "sb_advanced", - "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": " Advanced Settings", - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "collapsible_depends_on": "1", + "columns": 0, + "fieldname": "sb_advanced", + "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": "Advanced Settings", + "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 - }, + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "grant_type", - "fieldtype": "Select", - "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": "Grant Type", - "length": 0, - "no_copy": 0, - "options": "Authorization Code\nImplicit", - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "grant_type", + "fieldtype": "Select", + "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": "Grant Type", + "length": 0, + "no_copy": 0, + "options": "Authorization Code\nImplicit", + "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, - "fieldname": "cb_2", - "fieldtype": "Column 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, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "cb_2", + "fieldtype": "Column 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, + "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 - }, + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Code", - "fieldname": "response_type", - "fieldtype": "Select", - "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": "Response Type", - "length": 0, - "no_copy": 0, - "options": "Code\nToken", - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "Code", + "fieldname": "response_type", + "fieldtype": "Select", + "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": "Response Type", + "length": 0, + "no_copy": 0, + "options": "Code\nToken", + "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 } - ], - "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-10-05 21:07:39.476360", - "modified_by": "Administrator", - "module": "Integrations", - "name": "OAuth Client", - "name_case": "", - "owner": "Administrator", + ], + "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": "2020-04-07 21:07:39.476360", + "modified_by": "Administrator", + "module": "Integrations", + "name": "OAuth Client", + "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, + "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": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "app_name", - "track_changes": 1, + ], + "quick_entry": 0, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "app_name", + "track_changes": 1, "track_seen": 0 } \ No newline at end of file diff --git a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json index 93fff995f0..bbdbf74a67 100755 --- a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json +++ b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json @@ -1,397 +1,110 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2017-09-04 20:57:20.129205", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "creation": "2017-09-04 20:57:20.129205", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "enabled", + "notify_email", + "send_email_for_successful_backup", + "frequency", + "access_key_id", + "secret_access_key", + "region", + "endpoint_url", + "bucket", + "backup_limit" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "enabled", - "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": "Enable Automatic Backup", - "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, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "enabled", + "fieldtype": "Check", + "label": "Enable Automatic Backup" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "notify_email", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Send Notifications To", - "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": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "notify_email", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Send Notifications To", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "1", - "description": "Note: By default emails for failed backups are sent.", - "fetch_if_empty": 0, - "fieldname": "send_email_for_successful_backup", - "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": "Send Email for Successful Backup", - "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, - "translatable": 0, - "unique": 0 - }, + "default": "1", + "description": "Note: By default emails for failed backups are sent.", + "fieldname": "send_email_for_successful_backup", + "fieldtype": "Check", + "label": "Send Email for Successful Backup" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "frequency", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Backup Frequency", - "length": 0, - "no_copy": 0, - "options": "Daily\nWeekly\nMonthly\nNone", - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "frequency", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Backup Frequency", + "options": "Daily\nWeekly\nMonthly\nNone", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "access_key_id", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Access Key ID", - "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": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "access_key_id", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Access Key ID", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "secret_access_key", - "fieldtype": "Password", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Secret Access Key", - "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": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "secret_access_key", + "fieldtype": "Password", + "in_list_view": 1, + "label": "Secret Access Key", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "us-east-1", - "description": "See https://docs.aws.amazon.com/de_de/general/latest/gr/rande.html#s3_region for details.", - "fetch_if_empty": 0, - "fieldname": "region", - "fieldtype": "Select", - "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": "Region", - "length": 0, - "no_copy": 0, - "options": "us-east-1\nus-east-2\nus-west-1\nus-west-2\nap-south-1\nap-southeast-1\nap-southeast-2\nap-northeast-1\nap-northeast-2\nap-northeast-3\nca-central-1\ncn-north-1\ncn-northwest-1\neu-central-1\neu-west-1\neu-west-2\neu-west-3\neu-north-1\nsa-east-1", - "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, - "translatable": 0, - "unique": 0 - }, + "default": "us-east-1", + "description": "See https://docs.aws.amazon.com/de_de/general/latest/gr/rande.html#s3_region for details.", + "fieldname": "region", + "fieldtype": "Select", + "label": "Region", + "options": "us-east-1\nus-east-2\nus-west-1\nus-west-2\nap-south-1\nap-southeast-1\nap-southeast-2\nap-northeast-1\nap-northeast-2\nap-northeast-3\nca-central-1\ncn-north-1\ncn-northwest-1\neu-central-1\neu-west-1\neu-west-2\neu-west-3\neu-north-1\nsa-east-1" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "endpoint_url", - "fieldtype": "Data", - "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": "Endpoint URL", - "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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "endpoint_url", + "fieldtype": "Data", + "label": "Endpoint URL" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "bucket", - "fieldtype": "Data", - "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": "Bucket", - "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": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "bucket", + "fieldtype": "Data", + "label": "Bucket", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "backup_limit", - "fieldtype": "Int", - "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": "Backup Limit", - "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": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "backup_limit", + "fieldtype": "Int", + "label": "Backup Limit", + "reqd": 1 } - ], - "has_web_view": 0, - "hide_toolbar": 1, - "idx": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 1, - "istable": 0, - "max_attachments": 0, - "menu_index": 0, - "modified": "2019-04-10 03:56:55.632017", - "modified_by": "Administrator", - "module": "Integrations", - "name": "S3 Backup Settings", - "name_case": "", - "owner": "Administrator", + ], + "hide_toolbar": 1, + "issingle": 1, + "modified": "2019-08-22 16:26:04.774571", + "modified_by": "Administrator", + "module": "Integrations", + "name": "S3 Backup Settings", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 0, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py index 55b9e63a4d..7e69da922c 100755 --- a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py +++ b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py @@ -8,12 +8,14 @@ import os.path import frappe import boto3 from frappe import _ +from frappe.integrations.offsite_backup_utils import get_latest_backup_file, send_email, validate_file_size from frappe.model.document import Document -from frappe.utils import cint, split_emails +from frappe.utils import cint from frappe.utils.background_jobs import enqueue from rq.timeouts import JobTimeoutException from botocore.exceptions import ClientError + class S3BackupSettings(Document): def validate(self): @@ -49,7 +51,7 @@ class S3BackupSettings(Document): @frappe.whitelist() def take_backup(): - "Enqueue longjob for taking backup to s3" + """Enqueue longjob for taking backup to s3""" enqueue("frappe.integrations.doctype.s3_backup_settings.s3_backup_settings.take_backups_s3", queue='long', timeout=1500) frappe.msgprint(_("Queued for backup. It may take a few minutes to an hour.")) @@ -65,22 +67,21 @@ def take_backups_weekly(): def take_backups_monthly(): take_backups_if("Monthly") - def take_backups_if(freq): if cint(frappe.db.get_value("S3 Backup Settings", None, "enabled")): if frappe.db.get_value("S3 Backup Settings", None, "frequency") == freq: take_backups_s3() - @frappe.whitelist() def take_backups_s3(retry_count=0): try: + validate_file_size() backup_to_s3() - send_email(True, "S3 Backup Settings") + send_email(True, "Amazon S3", "S3 Backup Settings", "notify_email") except JobTimeoutException: if retry_count < 2: args = { - "retry_count" :retry_count + 1 + "retry_count": retry_count + 1 } enqueue("frappe.integrations.doctype.s3_backup_settings.s3_backup_settings.take_backups_s3", queue='long', timeout=1500, **args) @@ -89,31 +90,10 @@ def take_backups_s3(retry_count=0): except Exception: notify() + def notify(): error_message = frappe.get_traceback() - frappe.errprint(error_message) - send_email(False, "S3 Backup Settings", error_message) - -def send_email(success, service_name, error_status=None): - if success: - if frappe.db.get_value("S3 Backup Settings", None, "send_email_for_successful_backup") == '0': - return - - subject = "Backup Upload Successful" - message = """

Backup Uploaded Successfully!

Hi there, this is just to inform you - that your backup was successfully uploaded to your Amazon S3 bucket. So relax!

""" - - else: - subject = "[Warning] Backup Upload Failed" - message = """

Backup Upload Failed!

Oops, your automated backup to Amazon S3 failed. -

Error message: %s

Please contact your system manager - for more information.

""" % error_status - - if not frappe.db: - frappe.connect() - - recipients = split_emails(frappe.db.get_value("S3 Backup Settings", None, "notify_email")) - frappe.sendmail(recipients=recipients, subject=subject, message=message) + send_email(False, 'Amazon S3', "S3 Backup Settings", "notify_email", error_message) def backup_to_s3(): @@ -130,11 +110,15 @@ def backup_to_s3(): endpoint_url=doc.endpoint_url or 'https://s3.amazonaws.com' ) - backup = new_backup(ignore_files=False, backup_path_db=None, + if frappe.flags.create_new_backup: + backup = new_backup(ignore_files=False, backup_path_db=None, backup_path_files=None, backup_path_private_files=None, force=True) - db_filename = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_db)) - files_filename = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_files)) - private_files = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_private_files)) + db_filename = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_db)) + files_filename = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_files)) + private_files = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_private_files)) + else: + db_filename, files_filename, private_files = get_latest_backup_file(with_files=True) + folder = os.path.basename(db_filename)[:15] + '/' # for adding datetime to folder name @@ -143,8 +127,8 @@ def backup_to_s3(): upload_file_to_s3(files_filename, folder, conn, bucket) delete_old_backups(doc.backup_limit, bucket) -def upload_file_to_s3(filename, folder, conn, bucket): +def upload_file_to_s3(filename, folder, conn, bucket): destpath = os.path.join(folder, os.path.basename(filename)) try: print("Uploading file:", filename) @@ -156,7 +140,7 @@ def upload_file_to_s3(filename, folder, conn, bucket): def delete_old_backups(limit, bucket): - all_backups = list() + all_backups = [] doc = frappe.get_single("S3 Backup Settings") backup_limit = int(limit) diff --git a/frappe/integrations/offsite_backup_utils.py b/frappe/integrations/offsite_backup_utils.py new file mode 100644 index 0000000000..c280a1d9dd --- /dev/null +++ b/frappe/integrations/offsite_backup_utils.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +import glob +import os +from frappe.utils import split_emails, get_backups_path + + +def send_email(success, service_name, doctype, email_field, error_status=None): + recipients = get_recipients(service_name, email_field) + if not recipients: + frappe.log_error("No Email Recipient found for {0}".format(service_name), + "{0}: Failed to send backup status email".format(service_name)) + return + + if success: + if not frappe.db.get_value(doctype, None, "send_email_for_successful_backup"): + return + + subject = "Backup Upload Successful" + message = """ +

Backup Uploaded Successfully!

+

Hi there, this is just to inform you that your backup was successfully uploaded to your {0} bucket. So relax!

""".format(service_name) + + else: + subject = "[Warning] Backup Upload Failed" + message = """ +

Backup Upload Failed!

+

Oops, your automated backup to {0} failed.

+

Error message: {1}

+

Please contact your system manager for more information.

""".format(service_name, error_status) + + frappe.sendmail(recipients=recipients, subject=subject, message=message) + + +def get_recipients(service_name, email_field): + if not frappe.db: + frappe.connect() + + return split_emails(frappe.db.get_value(service_name, None, email_field)) + + +def get_latest_backup_file(with_files=False): + + def get_latest(file_ext): + file_list = glob.glob(os.path.join(get_backups_path(), file_ext)) + return max(file_list, key=os.path.getctime) + + latest_file = get_latest('*.sql.gz') + + if with_files: + latest_public_file_bak = get_latest('*-files.tar') + latest_private_file_bak = get_latest('*-private-files.tar') + return latest_file, latest_public_file_bak, latest_private_file_bak + + return latest_file + + +def get_file_size(file_path, unit): + if not unit: + unit = 'MB' + + file_size = os.path.getsize(file_path) + + memory_size_unit_mapper = {'KB': 1, 'MB': 2, 'GB': 3, 'TB': 4} + i = 0 + while i < memory_size_unit_mapper[unit]: + file_size = file_size / 1000.0 + i += 1 + + return file_size + + +def validate_file_size(): + frappe.flags.create_new_backup = True + latest_file = get_latest_backup_file() + file_size = get_file_size(latest_file, unit='GB') + + if file_size > 1: + frappe.flags.create_new_backup = False diff --git a/frappe/integrations/utils.py b/frappe/integrations/utils.py index 811b007131..808affe47a 100644 --- a/frappe/integrations/utils.py +++ b/frappe/integrations/utils.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2015, Frappe Technologies and contributors +# Copyright (c) 2019, Frappe Technologies and contributors # For license information, please see license.txt from __future__ import unicode_literals diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py index 1fe92d7a67..93ef78df7b 100644 --- a/frappe/model/__init__.py +++ b/frappe/model/__init__.py @@ -48,6 +48,7 @@ table_fields = ('Table', 'Table MultiSelect') core_doctypes_list = ('DocType', 'DocField', 'DocPerm', 'DocType Action', 'DocType Link', 'User', 'Role', 'Has Role', 'Page', 'Module Def', 'Print Format', 'Report', 'Customize Form', 'Customize Form Field', 'Property Setter', 'Custom Field', 'Custom Script') +data_field_options = ('Email', 'Name', 'Phone') def copytables(srctype, src, srcfield, tartype, tar, tarfield, srcfields, tarfields=[]): if not tarfields: diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 569cea9d5f..feeb96898a 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -11,10 +11,12 @@ from frappe.model import default_fields, table_fields from frappe.model.naming import set_new_name from frappe.model.utils.link_count import notify_link_count from frappe.modules import load_doctype_module -from frappe.model import display_fieldtypes, data_fieldtypes +from frappe.model import display_fieldtypes from frappe.utils.password import get_decrypted_password, set_encrypted_password -from frappe.utils import (cint, flt, now, cstr, strip_html, getdate, get_datetime, to_timedelta, +from frappe.utils import (cint, flt, now, cstr, strip_html, sanitize_html, sanitize_email, cast_fieldtype) +from frappe.utils.html_utils import unescape_html +from bs4 import BeautifulSoup max_positive_value = { 'smallint': 2 ** 15, @@ -287,7 +289,7 @@ class BaseDocument(object): if k in default_fields: del doc[k] - for key in ("_user_tags", "__islocal", "__onload", "_liked_by", "__run_link_triggers"): + for key in ("_user_tags", "__islocal", "__onload", "_liked_by", "__run_link_triggers", "__unsaved"): if self.get(key): doc[key] = self.get(key) @@ -502,7 +504,19 @@ class BaseDocument(object): for _df in fields_to_fetch: if self.is_new() or self.docstatus != 1 or _df.allow_on_submit: - setattr(self, _df.fieldname, values[_df.fetch_from.split('.')[-1]]) + fetch_from_fieldname = _df.fetch_from.split('.')[-1] + value = values[fetch_from_fieldname] + if _df.fieldtype == 'Small Text' or _df.fieldtype == 'Text' or _df.fieldtype == 'Data': + if fetch_from_fieldname in default_fields: + from frappe.model.meta import get_default_df + fetch_from_df = get_default_df(fetch_from_fieldname) + else: + fetch_from_df = frappe.get_meta(doctype).get_field(fetch_from_fieldname) + + fetch_from_ft = fetch_from_df.get('fieldtype') + if fetch_from_ft == 'Text Editor' and value: + value = unescape_html(strip_html(value)) + setattr(self, _df.fieldname, value) notify_link_count(doctype, docname) @@ -544,6 +558,30 @@ class BaseDocument(object): frappe.throw(_('{0} {1} cannot be "{2}". It should be one of "{3}"').format(prefix, label, value, comma_options)) + def _validate_data_fields(self): + from frappe.core.doctype.user.user import STANDARD_USERS + + # data_field options defined in frappe.model.data_field_options + for data_field in self.meta.get_data_fields(): + data = self.get(data_field.fieldname) + data_field_options = data_field.get("options") + old_fieldtype = data_field.get("oldfieldtype") + + if old_fieldtype and old_fieldtype != "Data": + continue + + if data_field_options == "Email": + if (self.owner in STANDARD_USERS) and (data in STANDARD_USERS): + continue + for email_address in frappe.utils.split_emails(data): + frappe.utils.validate_email_address(email_address, throw=True) + + if data_field_options == "Name": + frappe.utils.validate_name(data, throw=True) + + if data_field_options == "Phone": + frappe.utils.validate_phone_number(data, throw=True) + def _validate_constants(self): if frappe.flags.in_import or self.is_new() or self.flags.ignore_validate_constants: return @@ -648,7 +686,7 @@ class BaseDocument(object): # doesn't look like html so no need continue - elif "" in value and not ("" in value and not bool(BeautifulSoup(value, "html.parser").find()): # should be handled separately via the markdown converter function continue diff --git a/frappe/model/create_new.py b/frappe/model/create_new.py index f697d8051a..91fb079fca 100644 --- a/frappe/model/create_new.py +++ b/frappe/model/create_new.py @@ -74,11 +74,9 @@ def set_user_and_static_default_values(doc): def get_user_default_value(df, defaults, doctype_user_permissions, allowed_records, default_doc): # don't set defaults for "User" link field using User Permissions! if df.fieldtype == "Link" and df.options != "User": - # 1 - look in user permissions only for document_type==Setup - # We don't want to include permissions of transactions to be used for defaults. - if (frappe.get_meta(df.options).document_type=="Setup" - and not df.ignore_user_permissions and default_doc): - return default_doc + # If user permission has Is Default enabled or single-user permission has found against respective doctype. + if (not df.ignore_user_permissions and default_doc): + return default_doc # 2 - Look in user defaults user_default = defaults.get(df.fieldname) diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index d77898020d..c0d2c4eef9 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -210,7 +210,7 @@ def check_permission_and_not_submitted(doc): # check if submitted if doc.docstatus == 1: - frappe.msgprint(_("{0} {1}: Submitted Record cannot be deleted.").format(_(doc.doctype), doc.name), + frappe.msgprint(_("{0} {1}: Submitted Record cannot be deleted. You must {2} Cancel {3} it first.").format(_(doc.doctype), doc.name, "", ""), raise_exception=True) def check_if_doc_is_linked(doc, method="Delete"): diff --git a/frappe/model/document.py b/frappe/model/document.py index 66dd7e3c58..5e01f5e65f 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -329,6 +329,10 @@ class Document(BaseDocument): self.update_children() self.run_post_save_methods() + # clear unsaved flag + if hasattr(self, "__unsaved"): + delattr(self, "__unsaved") + return self def copy_attachments_from_amended_from(self): @@ -468,6 +472,7 @@ class Document(BaseDocument): def _validate(self): self._validate_mandatory() + self._validate_data_fields() self._validate_selects() self._validate_length() self._extract_images_from_text_editor() @@ -477,6 +482,7 @@ class Document(BaseDocument): children = self.get_all_children() for d in children: + d._validate_data_fields() d._validate_selects() d._validate_length() d._extract_images_from_text_editor() @@ -1316,6 +1322,9 @@ def make_event_update_log(doc, update_type): def check_doctype_has_consumers(doctype): """Check if doctype has event consumers for event streaming""" + if not frappe.db.exists("DocType", "Event Consumer"): + return False + event_consumers = frappe.get_all('Event Consumer') for event_consumer in event_consumers: consumer = frappe.get_doc('Event Consumer', event_consumer.name) diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 5065684311..2321e0c22a 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -68,7 +68,7 @@ def load_doctype_from_file(doctype): class Meta(Document): _metaclass = True default_fields = list(default_fields)[1:] - special_doctypes = ("DocField", "DocPerm", "Role", "DocType", "Module Def") + special_doctypes = ("DocField", "DocPerm", "Role", "DocType", "Module Def", 'DocType Action', 'DocType Link') def __init__(self, doctype): self._fields = {} @@ -128,6 +128,9 @@ class Meta(Document): def get_link_fields(self): return self.get("fields", {"fieldtype": "Link", "options":["!=", "[Select]"]}) + def get_data_fields(self): + return self.get("fields", {"fieldtype": "Data"}) + def get_dynamic_link_fields(self): if not hasattr(self, '_dynamic_link_fields'): self._dynamic_link_fields = self.get("fields", {"fieldtype": "Dynamic Link"}) @@ -165,7 +168,8 @@ class Meta(Document): def get_valid_columns(self): if not hasattr(self, "_valid_columns"): - if self.name in ("DocType", "DocField", "DocPerm", 'DocType Action', 'DocType Link'): + table_exists = frappe.db.table_exists(self.name) + if self.name in self.special_doctypes and table_exists: self._valid_columns = get_table_columns(self.name) else: self._valid_columns = self.default_fields + \ @@ -421,17 +425,19 @@ class Meta(Document): implemented in other Frappe applications via hooks. ''' data = frappe._dict() - try: - module = load_doctype_module(self.name, suffix='_dashboard') - if hasattr(module, 'get_data'): - data = frappe._dict(module.get_data()) - except ImportError: - pass + if not self.custom: + try: + module = load_doctype_module(self.name, suffix='_dashboard') + if hasattr(module, 'get_data'): + data = frappe._dict(module.get_data()) + except ImportError: + pass self.add_doctype_links(data) - for hook in frappe.get_hooks("override_doctype_dashboards", {}).get(self.name, []): - data = frappe.get_attr(hook)(data=data) + if not self.custom: + for hook in frappe.get_hooks("override_doctype_dashboards", {}).get(self.name, []): + data = frappe.get_attr(hook)(data=data) return data diff --git a/frappe/model/naming.py b/frappe/model/naming.py index 78d2c462e1..ffaf84e2b3 100644 --- a/frappe/model/naming.py +++ b/frappe/model/naming.py @@ -110,7 +110,11 @@ def make_autoname(key="", doctype="", doc=""): if "#" not in key: key = key + ".#####" elif "." not in key: - frappe.throw(_("Invalid naming series (. missing)") + (_(" for {0}").format(doctype) if doctype else "")) + error_message = _("Invalid naming series (. missing)") + if doctype: + error_message = _("Invalid naming series (. missing) for {0}").format(doctype) + + frappe.throw(error_message) parts = key.split('.') n = parse_naming_series(parts, doctype, doc) diff --git a/frappe/model/workflow.py b/frappe/model/workflow.py index b134f2f8dc..4384e7c8f5 100644 --- a/frappe/model/workflow.py +++ b/frappe/model/workflow.py @@ -45,20 +45,29 @@ def get_transitions(doc, workflow = None, raise_exception=False): transitions = [] for transition in workflow.transitions: if transition.state == current_state and transition.allowed in roles: - if transition.condition: - # if condition, evaluate - # access to frappe.db.get_value and frappe.db.get_list - success = frappe.safe_eval(transition.condition, - dict(frappe = frappe._dict( - db = frappe._dict(get_value = frappe.db.get_value, get_list=frappe.db.get_list), - session = frappe.session - )), - dict(doc = doc)) - if not success: - continue + if not is_transition_condition_satisfied(transition, doc): + continue transitions.append(transition.as_dict()) return transitions +def get_workflow_safe_globals(): + # access to frappe.db.get_value and frappe.db.get_list + return dict( + frappe=frappe._dict( + db=frappe._dict( + get_value=frappe.db.get_value, + get_list=frappe.db.get_list + ), + session=frappe.session + ) + ) + +def is_transition_condition_satisfied(transition, doc): + if not transition.condition: + return True + else: + return frappe.safe_eval(transition.condition, get_workflow_safe_globals(), dict(doc=doc.as_dict())) + @frappe.whitelist() def apply_workflow(doc, action): '''Allow workflow action on the current doc''' @@ -185,7 +194,7 @@ def bulk_workflow_approval(docnames, doctype, action): from collections import defaultdict # dictionaries for logging - errored_transactions = defaultdict(list) + failed_transactions = defaultdict(list) successful_transactions = defaultdict(list) # WARN: message log is cleared @@ -206,7 +215,7 @@ def bulk_workflow_approval(docnames, doctype, action): if e.args: message += " : {0}".format(e.args[0]) message_dict = {"docname": docname, "message": message} - errored_transactions[docname].append(message_dict) + failed_transactions[docname].append(message_dict) frappe.db.rollback() frappe.log_error(frappe.get_traceback(), "Workflow {0} threw an error for {1} {2}".format(action, doctype, docname)) @@ -219,20 +228,20 @@ def bulk_workflow_approval(docnames, doctype, action): message_dict = {"docname": docname, "message": message.get("message")} if message.get("raise_exception", False): - errored_transactions[docname].append(message_dict) + failed_transactions[docname].append(message_dict) else: successful_transactions[docname].append(message_dict) else: successful_transactions[docname].append({"docname": docname, "message": None}) - if errored_transactions and successful_transactions: + if failed_transactions and successful_transactions: indicator = "orange" - elif errored_transactions: + elif failed_transactions: indicator = "red" else: indicator = "green" - print_workflow_log(errored_transactions, _("Errored Transactions"), doctype, indicator) + print_workflow_log(failed_transactions, _("Failed Transactions"), doctype, indicator) print_workflow_log(successful_transactions, _("Successful Transactions"), doctype, indicator) def print_workflow_log(messages, title, doctype, indicator): diff --git a/frappe/patches.txt b/frappe/patches.txt index a33b4d68b0..cbda8cf677 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -270,3 +270,6 @@ execute:frappe.delete_doc_if_exists('DocType', 'GSuite Settings') execute:frappe.delete_doc_if_exists('DocType', 'GSuite Templates') execute:frappe.delete_doc_if_exists('DocType', 'GCalendar Account') execute:frappe.delete_doc_if_exists('DocType', 'GCalendar Settings') +frappe.patches.v12_0.remove_parent_and_parenttype_from_print_formats +execute:from frappe.desk.page.setup_wizard.install_fixtures import update_genders;update_genders() +frappe.patches.v13_0.website_theme_custom_scss diff --git a/frappe/patches/v12_0/move_email_and_phone_to_child_table.py b/frappe/patches/v12_0/move_email_and_phone_to_child_table.py index b18a7487f3..12680609d5 100644 --- a/frappe/patches/v12_0/move_email_and_phone_to_child_table.py +++ b/frappe/patches/v12_0/move_email_and_phone_to_child_table.py @@ -1,21 +1,24 @@ import frappe def execute(): + frappe.reload_doc("contacts", "doctype", "contact_email") + frappe.reload_doc("contacts", "doctype", "contact_phone") + frappe.reload_doc("contacts", "doctype", "contact") + contact_details = frappe.db.sql(""" SELECT `name`, `email_id`, `phone`, `mobile_no`, `modified_by`, `creation`, `modified` FROM `tabContact` + where not exists (select * from `tabContact Email` + where `tabContact Email`.parent=`tabContact`.name + and `tabContact Email`.email_id=`tabContact`.email_id) """, as_dict=True) - frappe.reload_doc("contacts", "doctype", "contact_email") - frappe.reload_doc("contacts", "doctype", "contact_phone") - frappe.reload_doc("contacts", "doctype", "contact") email_values = [] phone_values = [] for count, contact_detail in enumerate(contact_details): phone_counter = 1 is_primary = 1 - if contact_detail.email_id: email_values.append(( 1, diff --git a/frappe/patches/v12_0/remove_parent_and_parenttype_from_print_formats.py b/frappe/patches/v12_0/remove_parent_and_parenttype_from_print_formats.py new file mode 100644 index 0000000000..1a3c56da59 --- /dev/null +++ b/frappe/patches/v12_0/remove_parent_and_parenttype_from_print_formats.py @@ -0,0 +1,14 @@ +import frappe + +def execute(): + frappe.db.sql(""" + UPDATE + `tabPrint Format` + SET + `tabPrint Format`.`parent`='', + `tabPrint Format`.`parenttype`='', + `tabPrint Format`.parentfield='' + WHERE + `tabPrint Format`.parent != '' + OR `tabPrint Format`.parenttype != '' + """) \ No newline at end of file diff --git a/frappe/patches/v13_0/__init__.py b/frappe/patches/v13_0/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/patches/v13_0/website_theme_custom_scss.py b/frappe/patches/v13_0/website_theme_custom_scss.py new file mode 100644 index 0000000000..9d5a78643e --- /dev/null +++ b/frappe/patches/v13_0/website_theme_custom_scss.py @@ -0,0 +1,10 @@ +import frappe + +def execute(): + frappe.reload_doctype('Website Theme') + for theme in frappe.get_all('Website Theme'): + doc = frappe.get_doc('Website Theme', theme.name) + if not doc.custom_scss and doc.theme_scss: + # move old theme to new theme + doc.custom_scss = doc.theme_scss + doc.save() \ No newline at end of file diff --git a/frappe/patches/v8_0/update_gender_and_salutation.py b/frappe/patches/v8_0/update_gender_and_salutation.py index c990e9c4aa..bcd9d4cbd7 100644 --- a/frappe/patches/v8_0/update_gender_and_salutation.py +++ b/frappe/patches/v8_0/update_gender_and_salutation.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals import frappe -from frappe.desk.page.setup_wizard.install_fixtures import update_genders_and_salutations +from frappe.desk.page.setup_wizard.install_fixtures import update_genders, update_salutations def execute(): frappe.db.set_value("DocType", "Contact", "module", "Contacts") @@ -11,4 +11,5 @@ def execute(): frappe.reload_doc('contacts', 'doctype', 'gender') frappe.reload_doc('contacts', 'doctype', 'salutation') - update_genders_and_salutations() \ No newline at end of file + update_genders() + update_salutations() \ No newline at end of file diff --git a/frappe/permissions.py b/frappe/permissions.py index a0d1677fac..0d766aec8d 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -307,7 +307,7 @@ def has_controller_permissions(doc, ptype, user=None): return None def get_doctypes_with_read(): - return list(set([p.parent for p in get_valid_perms()])) + return list(set([p.parent if type(p.parent) == str else p.parent.encode('UTF8') for p in get_valid_perms()])) def get_valid_perms(doctype=None, user=None): '''Get valid permissions for the current user from DocPerm and Custom DocPerm''' diff --git a/frappe/printing/page/print_format_builder/print_format_builder.js b/frappe/printing/page/print_format_builder/print_format_builder.js index d0a3379609..4e049d120a 100644 --- a/frappe/printing/page/print_format_builder/print_format_builder.js +++ b/frappe/printing/page/print_format_builder/print_format_builder.js @@ -441,18 +441,16 @@ frappe.PrintFormatBuilder = Class.extend({ }); }, setup_field_settings: function() { - - this.page.main.find(".field-settings").on("click", () => { - var field = $(this).parent(); - + this.page.main.find(".field-settings").on("click", e => { + const field = $(e.currentTarget).parent(); // new dialog var d = new frappe.ui.Dialog({ title: "Set Properties", fields: [ { - label:__("Label"), - fieldname:"label", - fieldtype:"Data" + label: __("Label"), + fieldname: "label", + fieldtype: "Data" }, { label: __("Align Value"), @@ -485,7 +483,7 @@ frappe.PrintFormatBuilder = Class.extend({ }); // set current value - if(field.attr('data-align')) { + if (field.attr('data-align')) { d.set_value('align', field.attr('data-align')); } else { d.set_value('align', 'left'); diff --git a/frappe/public/build.json b/frappe/public/build.json index 55f637e79f..afea6bf2af 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -93,6 +93,7 @@ "public/css/font-awesome.css", "public/css/octicons/octicons.css", "public/less/desk.less", + "public/less/module.less", "public/less/flex.less", "public/less/indicator.less", "public/less/avatar.less", diff --git a/frappe/public/js/frappe/chat.js b/frappe/public/js/frappe/chat.js index 810de89874..8b1c09ac93 100644 --- a/frappe/public/js/frappe/chat.js +++ b/frappe/public/js/frappe/chat.js @@ -1566,7 +1566,11 @@ class extends Component { const alert = // TODO: ellipses content ` - ${frappe.user.first_name(r.user)}: ${r.content} + + + + + ${frappe.user.first_name(r.user)}: ${r.content} ` frappe.show_alert(alert, 15, { @@ -1575,6 +1579,11 @@ class extends Component { this.base.firstChild._component.toggle() }.bind(this, r) }) + frappe.notify(`${frappe.user.first_name(r.user)}`, { + body: r.content, + icon: frappe.user.image(r.user), + tag: r.user + }) } if ( r.room === state.room.name ) { diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index a1418f9149..b5046d4b12 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -86,6 +86,14 @@ frappe.Application = Class.extend({ this.show_update_available(); } + if (!frappe.boot.developer_mode) { + let console_security_message = __("Using this console may allow attackers to impersonate you and steal your information. Do not enter or paste code that you do not understand."); + console.log( + `%c${console_security_message}`, + "font-size: large" + ); + } + this.show_notes(); if (frappe.boot.is_first_startup) { diff --git a/frappe/public/js/frappe/form/controls/base_control.js b/frappe/public/js/frappe/form/controls/base_control.js index 2adb5435e3..c1ba41ab16 100644 --- a/frappe/public/js/frappe/form/controls/base_control.js +++ b/frappe/public/js/frappe/form/controls/base_control.js @@ -152,6 +152,7 @@ frappe.ui.form.Control = Class.extend({ () => me.set_model_value(value), () => { me.set_mandatory && me.set_mandatory(value); + me.set_invalid && me.set_invalid(); if(me.df.change || me.df.onchange) { // onchange event specified in df diff --git a/frappe/public/js/frappe/form/controls/base_input.js b/frappe/public/js/frappe/form/controls/base_input.js index 8a8ac271c7..0dbaaeb63c 100644 --- a/frappe/public/js/frappe/form/controls/base_input.js +++ b/frappe/public/js/frappe/form/controls/base_input.js @@ -179,6 +179,9 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({ set_mandatory: function(value) { this.$wrapper.toggleClass("has-error", (this.df.reqd && is_null(value)) ? true : false); }, + set_invalid: function () { + this.$wrapper.toggleClass("has-error", (this.df.invalid ? true : false)); + }, set_bold: function() { if(this.$input) { this.$input.toggleClass("bold", !!(this.df.bold || this.df.reqd)); diff --git a/frappe/public/js/frappe/form/controls/code.js b/frappe/public/js/frappe/form/controls/code.js index 0648ad6e22..6b40201001 100644 --- a/frappe/public/js/frappe/form/controls/code.js +++ b/frappe/public/js/frappe/form/controls/code.js @@ -9,6 +9,12 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({ this.ace_editor_target = $('
') .appendTo(this.input_area); + this.expanded = false; + this.$expand_button = $(``).click(() => { + this.expanded = !this.expanded; + this.refresh_height(); + this.toggle_label(); + }).appendTo(this.$input_wrapper); // styling this.ace_editor_target.addClass('border rounded'); this.ace_editor_target.css('height', 300); @@ -26,6 +32,16 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({ }, 300)); }, + refresh_height() { + this.ace_editor_target.css('height', this.expanded ? 600 : 300); + this.editor.resize(); + }, + + toggle_label() { + const button_label = this.expanded ? __('Collapse') : __('Expand'); + this.$expand_button.text(button_label); + }, + set_language() { const language_map = { 'Javascript': 'ace/mode/javascript', diff --git a/frappe/public/js/frappe/form/controls/data.js b/frappe/public/js/frappe/form/controls/data.js index 6dc8c3d387..c943ec89bb 100644 --- a/frappe/public/js/frappe/form/controls/data.js +++ b/frappe/public/js/frappe/form/controls/data.js @@ -87,56 +87,32 @@ frappe.ui.form.ControlData = frappe.ui.form.ControlInput.extend({ return val==null ? "" : val; }, validate: function(v) { + if (!v) { + return ''; + } if(this.df.is_filter) { return v; } if(this.df.options == 'Phone') { - if(v+''=='') { - return ''; - } - var v1 = ''; - // phone may start with + and must only have numbers later, '-' and ' ' are stripped - v = v.replace(/ /g, '').replace(/-/g, '').replace(/\(/g, '').replace(/\)/g, ''); - - // allow initial +,0,00 - if(v && v.substr(0,1)=='+') { - v1 = '+'; v = v.substr(1); - } - if(v && v.substr(0,2)=='00') { - v1 += '00'; v = v.substr(2); - } - if(v && v.substr(0,1)=='0') { - v1 += '0'; v = v.substr(1); - } - v1 += cint(v) + ''; - return v1; + this.df.invalid = !validate_phone(v); + return v; + } else if (this.df.options == 'Name') { + this.df.invalid = !validate_name(v); + return v; } else if(this.df.options == 'Email') { - if(v+''=='') { - return ''; - } - var email_list = frappe.utils.split_emails(v); if (!email_list) { - // invalid email return ''; } else { - var invalid_email = false; + let email_invalid = false; email_list.forEach(function(email) { if (!validate_email(email)) { - frappe.msgprint(__("Invalid Email: {0}", [email])); - invalid_email = true; + email_invalid = true; } }); - - if (invalid_email) { - // at least 1 invalid email - return ''; - } else { - // all good - return v; - } + this.df.invalid = email_invalid; + return v; } - } else { return v; } diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js index 0615cea314..9d8241f5a7 100644 --- a/frappe/public/js/frappe/form/controls/link.js +++ b/frappe/public/js/frappe/form/controls/link.js @@ -461,6 +461,7 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ }); } }, + set_fetch_values: function(df, docname, fetch_values) { var fl = this.frm.fetch_dict[df.fieldname].fields; for(var i=0; i < fl.length; i++) { diff --git a/frappe/public/js/frappe/form/controls/multiselect_list.js b/frappe/public/js/frappe/form/controls/multiselect_list.js index 3e8dc21dca..cd86bdd767 100644 --- a/frappe/public/js/frappe/form/controls/multiselect_list.js +++ b/frappe/public/js/frappe/form/controls/multiselect_list.js @@ -18,6 +18,7 @@ frappe.ui.form.ControlMultiSelectList = frappe.ui.form.ControlData.extend({ this.$list_wrapper = $(template); this.$input = $(''); this.input = this.$input.get(0); + this.has_input = true; this.$list_wrapper.prependTo(this.input_area); this.$filter_input = this.$list_wrapper.find('input'); this.$list_wrapper.on('click', '.dropdown-menu', e => { diff --git a/frappe/public/js/frappe/form/controls/text_editor.js b/frappe/public/js/frappe/form/controls/text_editor.js index b35c92c1ae..4e18b081cc 100644 --- a/frappe/public/js/frappe/form/controls/text_editor.js +++ b/frappe/public/js/frappe/form/controls/text_editor.js @@ -147,7 +147,7 @@ frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({ [{ 'color': [] }, { 'background': [] }], ['blockquote', 'code-block'], ['link', 'image'], - [{ 'list': 'ordered' }, { 'list': 'bullet' }], + [{ 'list': 'ordered' }, { 'list': 'bullet' }, { 'list': 'check' }], [{ 'align': [] }], [{ 'indent': '-1'}, { 'indent': '+1' }], [{'table': [ diff --git a/frappe/public/js/frappe/form/dashboard.js b/frappe/public/js/frappe/form/dashboard.js index c4c739dbcc..3b6ccd9a5c 100644 --- a/frappe/public/js/frappe/form/dashboard.js +++ b/frappe/public/js/frappe/form/dashboard.js @@ -125,8 +125,9 @@ frappe.ui.form.Dashboard = Class.extend({ }, format_percent: function(title, percent) { - var width = cint(percent) < 1 ? 1 : cint(percent); - var progress_class = "progress-bar-success"; + const percentage = cint(percent); + const width = percentage < 0 ? 100 : percentage; + const progress_class = percentage < 0 ? "progress-bar-danger" : "progress-bar-success"; return [{ title: title, diff --git a/frappe/public/js/frappe/form/footer/timeline.js b/frappe/public/js/frappe/form/footer/timeline.js index 593f987a9a..beec168dfd 100644 --- a/frappe/public/js/frappe/form/footer/timeline.js +++ b/frappe/public/js/frappe/form/footer/timeline.js @@ -561,10 +561,9 @@ frappe.ui.form.Timeline = class Timeline { } let updater_reference_link = null; - - if (!$.isEmptyObject(data.updater_reference)) { + let updater_reference = data.updater_reference; + if (!$.isEmptyObject(updater_reference)) { let label = updater_reference.label || __('via {0}', [updater_reference.doctype]); - let updater_reference = data.updater_reference; updater_reference_link = frappe.utils.get_form_link( updater_reference.doctype, updater_reference.docname, @@ -703,7 +702,8 @@ frappe.ui.form.Timeline = class Timeline { reference_doctype: this.frm.doctype, reference_name: this.frm.docname, content: comment, - comment_email: frappe.session.user + comment_email: frappe.session.user, + comment_by: frappe.session.user_fullname }, btn: btn, callback: function(r) { diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index b55c822ba6..82478db707 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -184,13 +184,7 @@ frappe.ui.form.Form = class FrappeForm { frappe.model.on(me.doctype, "*", function(fieldname, value, doc) { // set input if(doc.name===me.docname) { - if ((value==='' || value===null) && !doc[fieldname]) { - // both the incoming and outgoing values are falsy - // the texteditor, summernote, changes nulls to empty strings on render, - // so ignore those changes - } else { - me.dirty(); - } + me.dirty(); let field = me.fields_dict[fieldname]; field && field.refresh(fieldname); @@ -1052,7 +1046,7 @@ frappe.ui.form.Form = class FrappeForm { } is_dirty() { - return this.doc.__unsaved; + return !!this.doc.__unsaved; } is_new() { diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index 0e36e671cc..31d62dc445 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -550,6 +550,7 @@ export default class GridRow { hide_form() { frappe.dom.unfreeze(); this.row.toggle(true); + frappe.utils.scroll_to(this.row, true, 15); this.refresh(); if(cur_frm) cur_frm.cur_grid = null; this.wrapper.removeClass("grid-row-open"); diff --git a/frappe/public/js/frappe/form/grid_row_form.js b/frappe/public/js/frappe/form/grid_row_form.js index 73f0856c08..f93640936f 100644 --- a/frappe/public/js/frappe/form/grid_row_form.js +++ b/frappe/public/js/frappe/form/grid_row_form.js @@ -9,6 +9,7 @@ export default class GridRowForm { var me = this; this.make_form(); this.form_area.empty(); + frappe.utils.scroll_to(0, false, 0, this.wrapper.find('.grid-form-body')); this.layout = new frappe.ui.form.Layout({ fields: this.row.docfields, diff --git a/frappe/public/js/frappe/form/save.js b/frappe/public/js/frappe/form/save.js index fbc35634f4..7efbbe2d3d 100644 --- a/frappe/public/js/frappe/form/save.js +++ b/frappe/public/js/frappe/form/save.js @@ -21,7 +21,7 @@ frappe.ui.form.save = function (frm, action, callback, btn) { remove_empty_rows(); $(frm.wrapper).addClass('validated-form'); - if (check_mandatory()) { + if ((action !== 'Save' || frm.is_dirty()) && check_mandatory()) { _call({ method: "frappe.desk.form.save.savedocs", args: { doc: frm.doc, action: action }, @@ -36,6 +36,7 @@ frappe.ui.form.save = function (frm, action, callback, btn) { freeze_message: freeze_message }); } else { + frappe.show_alert({message: __("Document not updated"), indicator: "yellow"}); $(btn).prop("disabled", false); } }; diff --git a/frappe/public/js/frappe/form/sidebar/form_sidebar.js b/frappe/public/js/frappe/form/sidebar/form_sidebar.js index 02caf25557..a145e47149 100644 --- a/frappe/public/js/frappe/form/sidebar/form_sidebar.js +++ b/frappe/public/js/frappe/form/sidebar/form_sidebar.js @@ -69,7 +69,7 @@ frappe.ui.form.Sidebar = Class.extend({ }, refresh: function() { - if(this.frm.doc.__islocal) { + if (this.frm.doc.__islocal) { this.sidebar.toggle(false); } else { this.sidebar.toggle(true); @@ -81,12 +81,34 @@ frappe.ui.form.Sidebar = Class.extend({ } this.frm.viewers.refresh(); this.frm.tags && this.frm.tags.refresh(this.frm.get_docinfo().tags); - this.sidebar.find(".modified-by").html(__("{0} edited this {1}", - ["" + frappe.user.full_name(this.frm.doc.modified_by) + "", - "
" + comment_when(this.frm.doc.modified)])); - this.sidebar.find(".created-by").html(__("{0} created this {1}", - ["" + frappe.user.full_name(this.frm.doc.owner) + "", - "
" + comment_when(this.frm.doc.creation)])); + + if (this.frm.doc.route && cint(frappe.boot.website_tracking_enabled)) { + let route = this.frm.doc.route; + frappe.utils.get_page_view_count(route).then((res) => { + this.sidebar + .find(".pageview-count") + .html( + __("{0} Page Views", [String(res.message).bold()]) + ); + }); + } + + this.sidebar + .find(".modified-by") + .html( + __("{0} edited this {1}", [ + frappe.user.full_name(this.frm.doc.modified_by).bold(), + "
" + comment_when(this.frm.doc.modified), + ]) + ); + this.sidebar + .find(".created-by") + .html( + __("{0} created this {1}", [ + frappe.user.full_name(this.frm.doc.owner).bold(), + "
" + comment_when(this.frm.doc.creation), + ]) + ); this.refresh_like(); frappe.ui.form.set_user_image(this.frm); diff --git a/frappe/public/js/frappe/form/templates/form_sidebar.html b/frappe/public/js/frappe/form/templates/form_sidebar.html index b611557c43..30b2205bae 100644 --- a/frappe/public/js/frappe/form/templates/form_sidebar.html +++ b/frappe/public/js/frappe/form/templates/form_sidebar.html @@ -105,6 +105,7 @@ diff --git a/frappe/public/js/frappe/form/workflow.js b/frappe/public/js/frappe/form/workflow.js index 4eb33a5f28..4c59e8219b 100644 --- a/frappe/public/js/frappe/form/workflow.js +++ b/frappe/public/js/frappe/form/workflow.js @@ -29,20 +29,18 @@ frappe.ui.form.States = Class.extend({ }); frappe.workflow.get_transitions(me.frm.doc).then((transitions) => { - var next_html = $.map(transitions, - function(d) { - return d.action.bold() + __(" by Role ") + d.allowed; - }).join(", ") || __("None: End of Workflow").bold(); + const next_actions = $.map(transitions, d => `${d.action.bold()} ${__("by Role")} ${d.allowed}`) + .join(", ") || __("None: End of Workflow").bold(); + + const document_editable_by = frappe.workflow.get_document_state(me.frm.doctype, state).allow_edit.bold(); + + $(d.body).html(` +

${__("Current status")}: ${state.bold()}

+

${__("Document is only editable by users with role")}: ${document_editable_by}

+

${__("Next actions")}: ${next_actions}

+

${__("{0}: Other permission rules may also apply", [__('Note').bold()])}

+ `).css({padding: '15px'}); - $(d.body).html("

"+__("Current status")+": " + state.bold() + "

" - + "

"+__("Document is only editable by users of role")+": " - + frappe.workflow.get_document_state(me.frm.doctype, - state).allow_edit.bold() + "

" - + "

"+__("Next actions")+": "+ next_html +"

" - + (me.frm.doc.__islocal ? ("
" - +__("Workflow will start after saving.")+"
") : "") - + "

"+__("Note: Other permission rules may also apply")+"

" - ).css({padding: '15px'}); d.show(); }); }, true); @@ -115,7 +113,7 @@ frappe.ui.form.States = Class.extend({ } else { this.setup_btn(added); } - + }); }, diff --git a/frappe/public/js/frappe/list/list_sidebar_group_by.js b/frappe/public/js/frappe/list/list_sidebar_group_by.js index bd37b71ae4..7aa62dcb5f 100644 --- a/frappe/public/js/frappe/list/list_sidebar_group_by.js +++ b/frappe/public/js/frappe/list/list_sidebar_group_by.js @@ -53,15 +53,18 @@ frappe.views.ListGroupBy = class ListGroupBy { render_group_by_items() { let get_item_html = (fieldname) => { - let label; - let fieldtype; + let label, fieldtype; if (fieldname === 'assigned_to') { label = __('Assigned To'); } else if (fieldname === 'owner') { label = __('Created By'); } else { label = frappe.meta.get_label(this.doctype, fieldname); - fieldtype = frappe.meta.get_docfield(this.doctype, fieldname).fieldtype; + let docfield = frappe.meta.get_docfield(this.doctype, fieldname); + if (!docfield) { + return; + } + fieldtype = docfield.fieldtype; } return `
`); }; @@ -79,8 +78,10 @@ export default class Desktop { }; const make_category_title = name => { + // DO NOT REMOVE: Comment to load translation + // __("Modules") __("Domains") __("Places") __("Administration") let $title = $( - `` + `` ); $title.appendTo(this.sidebar); }; @@ -106,8 +107,6 @@ export default class Desktop { } this.current_page = page; localStorage.current_desk_page = page; - frappe.set_route("workspace", page); - this.pages[page] ? this.pages[page].show() : this.make_page(page); } @@ -131,20 +130,20 @@ export default class Desktop { this.pages[page] = $page; return $page; } - - setup_events() {} } class DesktopPage { constructor({ container, page_name }) { + frappe.desk_page = this; this.container = container; this.page_name = page_name; this.sections = {}; this.allow_customization = false; - this.make(); + this.reload(); } show() { + frappe.desk_page = this; this.page.show(); } @@ -152,8 +151,34 @@ class DesktopPage { this.page.hide(); } + reload() { + this.in_customize_mode = false; + this.page && this.page.remove(); + this.make(); + this.setup_events(); + } + + make_customization_link() { + this.customize_link = $(`
${__('Customize Workspace')}
`); + this.customize_link.appendTo(this.page); + this.customize_link.on('click', () => { + this.customize(); + }); + + this.save_or_discard_link = $(`
+ ${__('Save')} / ${__('Discard')} +
`).hide(); + + this.save_or_discard_link.appendTo(this.page); + this.save_or_discard_link.find(".save-customization").on("click", () => this.save_customization()); + this.save_or_discard_link.find(".discard-customization").on("click", () => this.reload()); + this.page.addClass('allow-customization'); + } + make() { - this.make_page(); + this.page = $(`
`); + this.page.appendTo(this.container); + this.get_data().then(res => { this.data = res.message; // this.make_onboarding(); @@ -163,21 +188,37 @@ class DesktopPage { return; } - this.allow_customization = this.data.allow_customization || false; - - !this.sections["onboarding"] && - this.data.charts.items.length && - this.make_charts(); - this.data.shortcuts.items.length && this.make_shortcuts(); - this.data.cards.items.length && this.make_cards(); + this.refresh(); }); } - make_page() { - this.page = $( - `
` - ); - this.page.appendTo(this.container); + refresh() { + this.page.empty(); + this.allow_customization = this.data.allow_customization || false; + + if (frappe.is_mobile()) { + this.allow_customization = false; + } + + this.allow_customization && this.make_customization_link(); + + let create_shortcuts_and_cards = () => { + this.make_shortcuts(); + this.make_cards(); + + if (this.allow_customization) { + // Move the widget group up to align with labels if customization is allowed + $('.desk-page .widget-group:visible:first').css('margin-top', '-25px'); + } + }; + + if (!this.sections["onboarding"]) { + this.make_charts().then(() => { + create_shortcuts_and_cards(); + }); + } else { + create_shortcuts_and_cards(); + } } get_data() { @@ -186,72 +227,110 @@ class DesktopPage { }); } - make_onboarding() { - this.sections["onboarding"] = new frappe.widget.WidgetGroup({ - title: `Getting Started`, - container: this.page, - type: "onboarding", - columns: 1, - widgets: [ - { - label: "Unlock Great Customer Experience", - subtitle: "Just a few steps, and you’re good to go.", - steps: [ - { - label: "Configure Lead Sources", - completed: true - }, - { - label: "Add Your Leads", - completed: false - }, - { - label: "Create Your First Opportunity", - completed: false - }, - { - label: "Onboard your Sales Team", - completed: false - }, - { - label: "Assign Territories", - completed: false - } - ] - } - ] + setup_events() { + $(document.body).on('toggleFullWidth', () => this.refresh()); + } + + customize() { + if (this.in_customize_mode) { + return; + } + + // It may be possible the chart area is hidden since it has no widgets + // So the margin-top: -25px would be applied to the shortcut group + // We need to remove this as the chart group will be visible during customization + $('.desk-page .widget-group:visible:first').css('margin-top', '0px'); + + this.customize_link.hide(); + this.save_or_discard_link.show(); + + Object.keys(this.sections).forEach(section => { + this.sections[section].customize(); + }); + this.in_customize_mode = true; + + // Move the widget group up to align with labels if customization is allowed + $('.desk-page .widget-group:visible:first').css('margin-top', '-25px'); + } + + save_customization() { + const config = {}; + + if (this.sections.charts) config.charts = this.sections.charts.get_widget_config(); + if (this.sections.shortcuts) config.shortcuts = this.sections.shortcuts.get_widget_config(); + if (this.sections.cards) config.cards = this.sections.cards.get_widget_config(); + + frappe.call('frappe.desk.desktop.save_customization', { + page: this.page_name, + config: config + }).then(res => { + if (res.message) { + frappe.msgprint({ message: __("Customizations Saved Successfully"), title: __("Success")}); + this.reload(); + } else { + frappe.throw({message: __("Something went wrong while saving customizations"), title: __("Failed")}); + this.reload(); + } }); } make_charts() { - this.sections["charts"] = new frappe.widget.WidgetGroup({ - title: this.data.charts.label || `${this.page_name} Dashboard`, - container: this.page, - type: "chart", - columns: 1, - allow_sorting: false, - widgets: this.data.charts.items + return frappe.dashboard_utils.get_dashboard_settings().then(settings => { + let chart_config = settings.chart_config? JSON.parse(settings.chart_config): {}; + if (this.data.charts.items) { + this.data.charts.items.map(chart => { + chart.chart_settings = chart_config[chart.chart_name] || {}; + }); + } + + this.sections["charts"] = new frappe.widget.WidgetGroup({ + title: this.data.charts.label || __('{} Dashboard', [__(this.page_name)]), + container: this.page, + type: "chart", + columns: 1, + options: { + allow_sorting: this.allow_customization, + allow_create: this.allow_customization, + allow_delete: this.allow_customization, + allow_hiding: false, + allow_edit: true, + max_widget_count: 2, + }, + widgets: this.data.charts.items + }); }); } make_shortcuts() { this.sections["shortcuts"] = new frappe.widget.WidgetGroup({ - title: this.data.shortcuts.label || `Your Shortcuts`, + title: this.data.shortcuts.label || __(`Your Shortcuts`), container: this.page, - type: "bookmark", + type: "shortcut", columns: 3, - allow_sorting: this.allow_customization && frappe.is_mobile(), + options: { + allow_sorting: this.allow_customization, + allow_create: this.allow_customization, + allow_delete: this.allow_customization, + allow_hiding: false, + allow_edit: true, + }, widgets: this.data.shortcuts.items }); } make_cards() { let cards = new frappe.widget.WidgetGroup({ - title: this.data.cards.label || `Reports & Masters`, + title: this.data.cards.label || __(`Reports & Masters`), container: this.page, type: "links", columns: 3, - allow_sorting: this.allow_customization && frappe.is_mobile(), + options: { + allow_sorting: this.allow_customization, + allow_create: false, + allow_delete: false, + allow_hiding: this.allow_customization, + allow_edit: false, + }, widgets: this.data.cards.items }); diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 1d7065e70d..be24273cb5 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -242,7 +242,6 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { () => this.setup_filters(), () => this.set_route_filters(), () => this.report_settings.onload && this.report_settings.onload(this), - () => this.get_user_settings(), () => this.refresh() ]); } @@ -616,6 +615,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { prepare_report_data(data) { this.raw_data = data; this.columns = this.prepare_columns(data.columns); + this.custom_columns = []; this.data = this.prepare_data(data.result); this.linked_doctypes = this.get_linked_doctypes(); this.tree_report = this.data.some(d => 'indent' in d); @@ -849,13 +849,6 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { setTimeout(preview_chart, 500); } - get_user_settings() { - return frappe.model.user_settings.get(this.report_name) - .then(user_settings => { - this.user_settings = user_settings; - }); - } - prepare_columns(columns) { return columns.map(column => { column = frappe.report_utils.prepare_field_from_column(column); @@ -1091,7 +1084,12 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { ], ({ file_format, include_indentation }) => { this.make_access_log('Export', file_format); if (file_format === 'CSV') { - const column_row = this.columns.map(col => col.label); + const column_row = this.columns.reduce((acc, col) => { + if (!col.hidden) { + acc.push(col.label); + } + return acc; + }, []); const data = this.get_data_for_csv(include_indentation); const out = [column_row].concat(data); @@ -1110,6 +1108,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { const args = { cmd: 'frappe.desk.query_report.export_query', report_name: this.report_name, + custom_columns: this.custom_columns.length? this.custom_columns: [], file_format_type: file_format, filters: filters, visible_idx, @@ -1275,16 +1274,20 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { primary_action: (values) => { const custom_columns = []; let df = frappe.meta.get_docfield(values.doctype, values.field); + const insert_after_index = this.columns + .findIndex(column => column.label === values.insert_after); custom_columns.push({ fieldname: df.fieldname, fieldtype: df.fieldtype, label: df.label, + insert_after_index: insert_after_index, link_field: this.doctype_field_map[values.doctype], doctype: values.doctype, options: df.fieldtype === "Link" ? df.options : undefined, width: 100 }); + this.custom_columns = this.custom_columns.concat(custom_columns); frappe.call({ method: 'frappe.desk.query_report.get_data_for_custom_field', args: { @@ -1294,7 +1297,8 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { callback: (r) => { const custom_data = r.message; const link_field = this.doctype_field_map[values.doctype]; - this.add_custom_column(custom_columns, custom_data, link_field, values.field, values.insert_after); + + this.add_custom_column(custom_columns, custom_data, link_field, values.field, insert_after_index); d.hide(); } }); @@ -1369,11 +1373,9 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { } } - add_custom_column(custom_column, custom_data, link_field, column_field, insert_after) { + add_custom_column(custom_column, custom_data, link_field, column_field, insert_after_index) { const column = this.prepare_columns(custom_column); - const insert_after_index = this.columns - .findIndex(column => column.label === insert_after); this.columns.splice(insert_after_index + 1, 0, column[0]); this.data.forEach(row => { diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js index b479c4c101..43540f449d 100644 --- a/frappe/public/js/frappe/views/reports/report_view.js +++ b/frappe/public/js/frappe/views/reports/report_view.js @@ -837,6 +837,9 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { fieldtype: 'MultiCheck', columns: 2, options: columns[this.doctype] + .filter(df => { + return !df.hidden; + }) .map(df => ({ label: __(df.label), value: df.fieldname, @@ -858,6 +861,9 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { fieldtype: 'MultiCheck', columns: 2, options: columns[cdt] + .filter(df => { + return !df.hidden; + }) .map(df => ({ label: __(df.label), value: df.fieldname, diff --git a/frappe/public/js/frappe/web_form/web_form.js b/frappe/public/js/frappe/web_form/web_form.js index 5f4b012c4e..62a339f650 100644 --- a/frappe/public/js/frappe/web_form/web_form.js +++ b/frappe/public/js/frappe/web_form/web_form.js @@ -82,7 +82,7 @@ export default class WebForm extends frappe.ui.FieldGroup { } setup_cancel_button() { - this.add_button_to_header("Cancel", "light", () => this.cancel()); + this.add_button_to_header(__("Cancel"), "light", () => this.cancel()); } setup_delete_button() { @@ -105,13 +105,14 @@ export default class WebForm extends frappe.ui.FieldGroup { this.validate && this.validate(); // validation hack: get_values will check for missing data - let isvalid = super.get_values(this.allow_incomplete); + let doc_values = super.get_values(this.allow_incomplete); - if (!isvalid) return; + if (!doc_values) return; if (window.saving) return; let for_payment = Boolean(this.accept_payment && !this.doc.paid); + Object.assign(this.doc, doc_values); this.doc.doctype = this.doc_type; this.doc.web_form_name = this.name; @@ -174,7 +175,7 @@ export default class WebForm extends frappe.ui.FieldGroup { title: __("Saved Successfully"), secondary_action: () => { if (this.success_url) { - window.location.pathname = this.success_url; + window.location.href = this.success_url; } else if(this.login_required) { window.location.href = window.location.pathname + "?name=" + data.name; @@ -187,4 +188,4 @@ export default class WebForm extends frappe.ui.FieldGroup { this.success_message || __("Your information has been submitted"); success_dialog.set_message(success_message); } -} \ No newline at end of file +} diff --git a/frappe/public/js/frappe/web_form/webform_script.js b/frappe/public/js/frappe/web_form/webform_script.js index 7bf7162101..53d9701774 100644 --- a/frappe/public/js/frappe/web_form/webform_script.js +++ b/frappe/public/js/frappe/web_form/webform_script.js @@ -2,13 +2,13 @@ import WebFormList from './web_form_list' import WebForm from './web_form' frappe.ready(function() { + let query_params = frappe.utils.get_query_params(); let wrapper = $(".web-form-wrapper"); - let is_list = parseInt(wrapper.data('is-list')); + let is_list = parseInt(wrapper.data('is-list')) || query_params.is_list; let webform_doctype = wrapper.data('web-form-doctype'); let webform_name = wrapper.data('web-form'); let login_required = parseInt(wrapper.data('login-required')); let allow_delete = parseInt(wrapper.data('allow-delete')); - let query_params = frappe.utils.get_query_params(); let doc_name = query_params.name || ''; let is_new = query_params.new; @@ -38,7 +38,7 @@ frappe.ready(function() { settings: { allow_delete } - }) + }); } function show_form() { @@ -113,8 +113,9 @@ frappe.ready(function() { df.only_select = true; } if (["Attach", "Attach Image"].includes(df.fieldtype)) { - if (!df.options) + if (typeof df.options !== "object") { df.options = {}; + } df.options.disable_file_browser = true; } }); diff --git a/frappe/public/js/frappe/widgets/base_widget.js b/frappe/public/js/frappe/widgets/base_widget.js index 7461f2ddc0..730157ed4c 100644 --- a/frappe/public/js/frappe/widgets/base_widget.js +++ b/frappe/public/js/frappe/widgets/base_widget.js @@ -1,3 +1,5 @@ +import get_dialog_constructor from './widget_dialog.js'; + export default class Widget { constructor(opts) { Object.assign(this, opts); @@ -8,22 +10,71 @@ export default class Widget { this.set_title(); this.set_actions(); this.set_body(); + this.setup_events(); } - customize() { + get_config() { + return { + name: this.name, + label: this.label + }; + } + + customize(options) { + this.in_customize_mode = true; + this.action_area.empty(); + + options.allow_delete && + this.add_custom_button( + '', + () => this.delete() + ); + + options.allow_sorting && + this.add_custom_button( + '', + null, + "drag-handle" + ); + + if (options.allow_hiding) { + if (this.hidden) { + this.widget.removeClass("hidden"); + this.body.css("opacity", 0.5); + this.title_field.css("opacity", 0.5); + this.footer.css("opacity", 0.5); + } + const classname = this.hidden ? 'fa fa-eye' : 'fa fa-eye-slash'; + this.add_custom_button( + ``, + () => this.hide_or_show(), + "show-or-hide-button" + ); + + this.show_or_hide_button = this.action_area.find( + ".show-or-hide-button" + ); + } + + options.allow_edit && + this.add_custom_button( + '', + () => this.edit() + ); } make() { this.make_widget(); this.widget.appendTo(this.container); - this.setup_events(); } make_widget() { - this.widget = $(`
+ this.widget = $(`
-
+
@@ -37,13 +88,74 @@ export default class Widget { this.action_area = this.widget.find(".widget-control"); this.head = this.widget.find(".widget-head"); this.footer = this.widget.find(".widget-footer"); - this.set_title(); - this.set_actions(); - this.set_body(); + this.refresh(); } set_title() { - this.title_field[0].innerHTML = this.label || this.name; + this.title_field[0].innerHTML = this.label; + } + + add_custom_button(html, action, class_name = "") { + let button = $( + `` + ); + button.click(event => { + event.stopPropagation(); + action && action(); + }); + button.appendTo(this.action_area); + } + + delete() { + this.widget.addClass("zoomOutDelete"); + // wait for animation + setTimeout(() => { + this.widget.remove(); + this.options.on_delete && this.options.on_delete(this.name); + }, 300); + } + + edit() { + const dialog_class = get_dialog_constructor(this.widget_type); + + this.edit_dialog = new dialog_class({ + label: this.label, + type: this.widget_type, + values: this.get_config(), + primary_action: (data) => { + Object.assign(this, data); + data.name = this.name; + + this.refresh(); + }, + primary_action_label: __("Save") + }); + + this.edit_dialog.make(); + } + + hide_or_show() { + if (!this.hidden) { + this.body.css("opacity", 0.5); + this.title_field.css("opacity", 0.5); + this.footer.css("opacity", 0.5); + this.hidden = true; + } else { + this.body.css("opacity", 1); + this.title_field.css("opacity", 1); + this.footer.css("opacity", 1); + this.hidden = false; + } + this.show_or_hide_button.empty(); + + const classname = this.hidden ? 'fa fa-eye' : 'fa fa-eye-slash'; + $(``).appendTo( + this.show_or_hide_button + ); + } + + setup_events() { + // } set_actions() { @@ -53,8 +165,4 @@ export default class Widget { set_body() { // } - - setup_events() { - // - } -} \ No newline at end of file +} diff --git a/frappe/public/js/frappe/widgets/chart_widget.js b/frappe/public/js/frappe/widgets/chart_widget.js index 3388890776..9c03f08523 100644 --- a/frappe/public/js/frappe/widgets/chart_widget.js +++ b/frappe/public/js/frappe/widgets/chart_widget.js @@ -9,12 +9,19 @@ export default class ChartWidget extends Widget { this.height = 240; } - refresh() { - this.make_chart(); + get_config() { + return { + name: this.name, + chart_name: this.chart_name, + label: this.label, + }; } - customize() { - this.setup_customize_actions(); + refresh() { + delete this.dashboard_chart; + this.set_title(); + this.set_body(); + this.make_chart(); } set_body() { @@ -62,34 +69,31 @@ export default class ChartWidget extends Widget { make_chart() { this.get_settings().then(() => { + if (!this.chart_settings) { + this.chart_settings = {}; + } this.setup_container(); this.prepare_chart_object(); - this.action_area.empty(); - this.prepare_chart_actions(); - this.setup_filter_button(); + if (!this.in_customize_mode) { + this.action_area.empty(); + this.prepare_chart_actions(); + this.setup_filter_button(); - if ( - this.chart_doc.timeseries && - this.chart_doc.chart_type !== "Custom" - ) { - this.render_time_series_filters(); + if ( + this.chart_doc.timeseries && + this.chart_doc.chart_type !== "Custom" + ) { + this.render_time_series_filters(); + } } - this.fetch_and_update_chart(); }); } - setup_customize_actions() { - this.action_area.empty(); - const buttons = $(` - `); - buttons.appendTo(this.action_area); - } - render_time_series_filters() { let filters = [ { - label: this.chart_doc.timespan, + label: this.chart_settings.timespan || this.chart_doc.timespan, options: [ "Select Date Range", "Last Year", @@ -114,15 +118,22 @@ export default class ChartWidget extends Widget { this.head.css('flex-direction', "row"); } + this.save_chart_config_for_user({ + 'timespan': this.selected_timespan, + 'from_date': null, + 'to_date': null + + }); this.fetch_and_update_chart(); } } }, { - label: this.chart_doc.time_interval, + label: this.chart_settings.time_interval || this.chart_doc.time_interval, options: ["Yearly", "Quarterly", "Monthly", "Weekly", "Daily"], action: selected_item => { this.selected_time_interval = selected_item; + this.save_chart_config_for_user({'time_interval': this.selected_time_interval}); this.fetch_and_update_chart(); } } @@ -138,10 +149,10 @@ export default class ChartWidget extends Widget { fetch_and_update_chart() { this.args = { - timespan: this.selected_timespan, - time_interval: this.selected_time_interval, - from_date: this.selected_from_date, - to_date: this.selected_to_date + timespan: this.selected_timespan || this.chart_settings.timespan, + time_interval: this.selected_time_interval || this.chart_settings.time_interval, + from_date: this.selected_from_date || this.chart_settings.from_date, + to_date: this.selected_to_date || this.chart_settings.to_date }; this.fetch(this.filters, true, this.args).then(data => { @@ -176,16 +187,19 @@ export default class ChartWidget extends Widget { fieldname: "from_date", placeholder: "Date Range", input_class: "input-xs", + default: [this.chart_settings.from_date, this.chart_settings.to_date], reqd: 1, change: () => { let selected_date_range = this.date_range_field.get_value(); this.selected_from_date = selected_date_range[0]; this.selected_to_date = selected_date_range[1]; - if ( - selected_date_range && - selected_date_range.length == 2 - ) { + if (selected_date_range && selected_date_range.length == 2) { + this.save_chart_config_for_user({ + 'timespan': this.selected_timespan, + 'from_date': this.selected_from_date, + 'to_date': this.selected_to_date, + }); this.fetch_and_update_chart(); } } @@ -235,7 +249,7 @@ export default class ChartWidget extends Widget { } }, { - label: __("Edit..."), + label: __("Edit"), action: "action-edit", handler: () => { frappe.set_route( @@ -244,6 +258,15 @@ export default class ChartWidget extends Widget { this.chart_doc.name ); } + }, + { + label: __("Reset Chart"), + action: "action-list", + handler: () => { + this.reset_chart(); + delete this.dashboard_chart; + this.make_chart(); + } } ]; @@ -334,6 +357,7 @@ export default class ChartWidget extends Widget { } else { me.filters = values; } + me.save_chart_config_for_user({'filters': me.filters}); me.fetch_and_update_chart(); } }, @@ -350,6 +374,21 @@ export default class ChartWidget extends Widget { dialog.set_values(this.filters); } + reset_chart() { + this.save_chart_config_for_user(null, 1); + this.chart_settings = {}; + this.filters = null; + } + + save_chart_config_for_user(config, reset=0) { + Object.assign(this.chart_settings, config); + frappe.xcall('frappe.desk.doctype.dashboard_settings.dashboard_settings.save_chart_config', { + 'reset': reset, + 'config': this.chart_settings, + 'chart_name': this.chart_doc.chart_name + }); + } + create_filter_group_and_add_filters(parent) { this.filter_group = new frappe.ui.FilterGroup({ parent: parent, @@ -390,9 +429,7 @@ export default class ChartWidget extends Widget { } fetch(filters, refresh = false, args) { - let method = this.settings - ? this.settings.method - : "frappe.desk.doctype.dashboard_chart.dashboard_chart.get"; + let method = this.settings.method; if (this.chart_doc.chart_type == "Report") { args = { @@ -406,10 +443,10 @@ export default class ChartWidget extends Widget { filters: filters, refresh: refresh ? 1 : 0, time_interval: - args && args.time_interval ? args.time_interval : null, - timespan: args && args.timespan ? args.timespan : null, - from_date: args && args.from_date ? args.from_date : null, - to_date: args && args.to_date ? args.to_date : null + args && args.time_interval? args.time_interval: null, + timespan: args && args.timespan? args.timespan: null, + from_date: args && args.from_date? args.from_date: null, + to_date: args && args.to_date? args.to_date: null }; } return frappe.xcall(method, args); @@ -481,8 +518,9 @@ export default class ChartWidget extends Widget { } prepare_chart_object() { + let saved_filters = this.chart_settings.filters || null; this.filters = - this.filters || JSON.parse(this.chart_doc.filters_json || "[]"); + saved_filters || this.filters || JSON.parse(this.chart_doc.filters_json || "[]"); } get_settings() { @@ -519,6 +557,9 @@ export default class ChartWidget extends Widget { }; return Promise.resolve(); } else { + this.settings = { + method: "frappe.desk.doctype.dashboard_chart.dashboard_chart.get" + }; return Promise.resolve(); } }); diff --git a/frappe/public/js/frappe/widgets/links_widget.js b/frappe/public/js/frappe/widgets/links_widget.js index e8012b03d8..f7bca23c47 100644 --- a/frappe/public/js/frappe/widgets/links_widget.js +++ b/frappe/public/js/frappe/widgets/links_widget.js @@ -6,8 +6,13 @@ export default class LinksWidget extends Widget { super(opts); } - refresh() { - // + get_config() { + return { + name: this.name, + links: JSON.stringify(this.links), + label: this.label, + hidden: this.hidden, + }; } set_body() { @@ -75,21 +80,22 @@ export default class LinksWidget extends Widget { const popover = link.find(".module-link-popover"); link_label.mouseover(() => { + if (this.in_customize_mode) return; popover.show(); }); link_label.mouseout(() => popover.hide()); } else { - if (link_label.hasClass("help-video-link")) { - link_label.click(event => { + link_label.click(event => { + if (this.in_customize_mode) return; + + if (link_label.hasClass("help-video-link")) { let yt_id = event.target.dataset.youtubeid; frappe.help.show_video(yt_id); - }); - } else { - link_label.click(event => { + } else { let route = event.target.dataset.route; frappe.set_route(route); - }); - } + } + }); } }); } diff --git a/frappe/public/js/frappe/widgets/new_widget.js b/frappe/public/js/frappe/widgets/new_widget.js new file mode 100644 index 0000000000..787cb3a79c --- /dev/null +++ b/frappe/public/js/frappe/widgets/new_widget.js @@ -0,0 +1,52 @@ +import get_dialog_constructor from "./widget_dialog.js"; + +export default class NewWidget { + constructor(opts) { + Object.assign(this, opts); + this.make(); + } + + customize() { + return; + } + + make() { + this.make_widget(); + this.widget.appendTo(this.container); + this.setup_events(); + } + + get_title() { + // DO NOT REMOVE: Comment to load translation + // __("New Chart") __("New Shortcut") + return __(`New ${frappe.utils.to_title_case(this.type)}`); + } + + make_widget() { + this.widget = $(`
+ + ${this.get_title()} +
`); + this.body = this.widget; + } + + setup_events() { + this.widget.on("click", () => this.open_dialog()); + } + + open_dialog() { + const dialog_class = get_dialog_constructor(this.type); + + this.dialog = new dialog_class({ + label: this.label, + type: this.type, + values: false, + primary_action: this.on_create, + }); + + this.dialog.make(); + } + + delete() { + this.widget.remove(); + } +} diff --git a/frappe/public/js/frappe/widgets/onboarding_widget.js b/frappe/public/js/frappe/widgets/onboarding_widget.js index ea9428972e..736af01cc5 100644 --- a/frappe/public/js/frappe/widgets/onboarding_widget.js +++ b/frappe/public/js/frappe/widgets/onboarding_widget.js @@ -3,7 +3,6 @@ import Widget from "./base_widget.js"; export default class OnboardingWidget extends Widget { constructor(opts) { super(opts); - window.onb = this; } refresh() { } diff --git a/frappe/public/js/frappe/widgets/shortcut_widget.js b/frappe/public/js/frappe/widgets/shortcut_widget.js index 81ab4f05e4..572569839c 100644 --- a/frappe/public/js/frappe/widgets/shortcut_widget.js +++ b/frappe/public/js/frappe/widgets/shortcut_widget.js @@ -1,48 +1,62 @@ import Widget from "./base_widget.js"; import { generate_route } from "./utils"; -// import { get_luminosity, shadeColor } from "./utils"; - -String.prototype.format = function () { - var i = 0, args = arguments; - return this.replace(/{}/g, function () { - return typeof args[i] != 'undefined' ? args[i++] : ''; - }); -}; export default class ShortcutWidget extends Widget { constructor(opts) { super(opts); } - refresh() { - // + get_config() { + return { + name: this.name, + icon: this.icon, + label: this.label, + format: this.format, + link_to: this.link_to, + color: this.color, + restrict_to_domain: this.restrict_to_domain, + stats_filter: this.stats_filter, + type: this.type, + }; } setup_events() { this.widget.click(() => { - let route = generate_route(this) - frappe.set_route(route) - }) + if (this.in_customize_mode) return; + + let route = generate_route({ + route: this.route, + name: this.link_to, + type: this.type, + is_query_report: this.is_query_report, + doctype: this.ref_doctype + }); + + frappe.set_route(route); + }); } set_actions() { - this.widget.addClass('shortcut-widget-box'); - const get_filter = new Function(`return ${this.stats_filter}`) + if (this.in_customize_mode) return; + + this.widget.addClass("shortcut-widget-box"); + const get_filter = new Function(`return ${this.stats_filter}`); if (this.type == "DocType" && this.stats_filter) { - frappe.db.count(this.link_to, { - filters: get_filter() - }).then(count => this.set_count(count)) + frappe.db + .count(this.link_to, { + filters: get_filter(), + }) + .then((count) => this.set_count(count)); } } set_title() { if (this.icon) { this.title_field[0].innerHTML = `
- + ${this.label || this.name} -
` - } - else { +
`; + } else { super.set_title(); } } @@ -50,19 +64,22 @@ export default class ShortcutWidget extends Widget { set_count(count) { const get_label = () => { if (this.format) { - return this.format.format(count); + return this.format.replace(/{}/g, count); } - return count - } + return count; + }; this.action_area.empty(); const label = get_label(); const buttons = $(`
${label}
`); - if(this.color) { - buttons.css('background-color', this.color); - buttons.css('color', frappe.ui.color.get_contrast_color(this.color)) + if (this.color) { + buttons.css("background-color", this.color); + buttons.css( + "color", + frappe.ui.color.get_contrast_color(this.color) + ); } buttons.appendTo(this.action_area); } -} \ No newline at end of file +} diff --git a/frappe/public/js/frappe/widgets/widget_dialog.js b/frappe/public/js/frappe/widgets/widget_dialog.js new file mode 100644 index 0000000000..77e2d65ea3 --- /dev/null +++ b/frappe/public/js/frappe/widgets/widget_dialog.js @@ -0,0 +1,266 @@ +class WidgetDialog { + constructor(opts) { + Object.assign(this, opts); + this.editing = Boolean(this.values && Object.keys(this.values).length); + } + + make() { + this.make_dialog(); + this.setup_dialog_events(); + this.dialog.show(); + + this.editing && this.set_default_values(); + } + + make_dialog() { + this.dialog = new frappe.ui.Dialog({ + title: this.get_title(), + fields: this.get_fields(), + primary_action: (data) => { + data = this.process_data(data); + + if (!this.editing) { + data.name = `${this.type}-${this.label}-${frappe.utils.get_random(20)}`; + } + + this.dialog.hide(); + this.primary_action(data); + }, + primary_action_label: this.primary_action_label || __("Add"), + }); + } + + get_title() { + // DO NOT REMOVE: Comment to load translation + // __("New Chart") __("New Shortcut") __("Edit Chart") __("Edit Shortcut") + + let action = this.editing ? "Edit" : "Add"; + return __(`${action} ${frappe.utils.to_title_case(this.type)}`); + } + + get_fields() { + // + } + + set_default_values() { + return this.dialog.set_values(this.values); + } + + process_data(data) { + return data; + } + + setup_dialog_events() { + // + } + + hide_field(fieldname) { + this.dialog.set_df_property(fieldname, "hidden", true); + } + + show_field(fieldname) { + this.dialog.set_df_property(fieldname, "hidden", false); + } +} + +class ChartDialog extends WidgetDialog { + constructor(opts) { + super(opts); + } + + get_fields() { + return [ + { + fieldtype: "Link", + fieldname: "chart_name", + label: "Chart Name", + options: "Dashboard Chart", + reqd: 1, + }, + { + fieldtype: "Data", + fieldname: "label", + label: "Label", + }, + ]; + } + + process_data(data) { + data.label = data.label ? data.label : data.chart_name; + return data; + } +} + +class ShortcutDialog extends WidgetDialog { + constructor(opts) { + super(opts); + } + + hide_filters() { + this.hide_field("count_section_break"); + this.hide_field("filters_section_break"); + } + + show_filters() { + this.show_field("count_section_break"); + this.show_field("filters_section_break"); + } + + get_fields() { + return [ + { + fieldtype: "Select", + fieldname: "type", + label: "Type", + reqd: 1, + options: "DocType\nReport\nPage", + onchange: () => { + if (this.dialog.get_value("type") == "DocType") { + this.dialog.fields_dict.link_to.get_query = () => { + return { filters: { istable: false } }; + }; + } else { + this.dialog.fields_dict.link_to.get_query = null; + } + }, + }, + { + fieldtype: "Data", + fieldname: "label", + label: "Label", + }, + { + fieldtype: "Column Break", + fieldname: "column_break_4", + }, + { + fieldtype: "Dynamic Link", + fieldname: "link_to", + label: "Link To", + reqd: 1, + options: "type", + onchange: () => { + if (this.dialog.get_value("type") == "DocType") { + let doctype = this.dialog.get_value("link_to"); + + doctype && + frappe.db + .get_value("DocType", doctype, "issingle") + .then((res) => { + if (res.message && res.message.issingle) { + this.hide_filters(); + } else { + this.setup_filter(doctype); + this.show_filters(); + } + }); + } else { + this.hide_filters(); + } + }, + }, + { + fieldtype: "Section Break", + fieldname: "filters_section_break", + label: "Count Filter", + hidden: 1, + }, + { + fieldtype: "HTML", + fieldname: "filter_area_loading", + }, + { + fieldtype: "HTML", + fieldname: "filter_area", + hidden: 1, + }, + { + fieldtype: "Section Break", + fieldname: "count_section_break", + label: "Count Customizations", + hidden: 1, + }, + { + fieldtype: "Color", + fieldname: "color", + label: "Color", + }, + { + fieldtype: "Column Break", + fieldname: "column_break_3", + }, + { + fieldtype: "Data", + fieldname: "format", + label: "Format", + description: "For Example: {} Open", + }, + ]; + } + + set_default_values() { + super.set_default_values().then(() => { + this.dialog.fields_dict.link_to.df.onchange(); + }); + } + + process_data(data) { + let stats_filter = {}; + + if (this.dialog.get_value("type") == "DocType" && this.filter_group) { + let filters = this.filter_group.get_filters(); + filters.forEach((arr) => { + stats_filter[arr[1]] = [arr[2], arr[3]]; + }); + + data.stats_filter = JSON.stringify(stats_filter); + } + + data.label = data.label + ? data.label + : frappe.model.unscrub(data.link_to); + + return data; + } + + setup_filter(doctype) { + if (this.filter_group) { + this.filter_group.wrapper.empty(); + delete this.filter_group; + } + + let $loading = this.dialog.get_field("filter_area_loading").$wrapper; + $(`Loading Filters...`).appendTo($loading); + + this.filters = []; + + if (this.values && this.values.stats_filter) { + const filters_json = JSON.parse(this.values.stats_filter); + this.filters = Object.keys(filters_json).map((filter) => { + let val = filters_json[filter]; + return [this.values.link_to, filter, val[0], val[1], false]; + }); + } + + this.filter_group = new frappe.ui.FilterGroup({ + parent: this.dialog.get_field("filter_area").$wrapper, + doctype: doctype, + on_change: () => {}, + }); + + frappe.model.with_doctype(doctype, () => { + this.filter_group.add_filters_to_filter_group(this.filters); + this.hide_field("filter_area_loading"); + this.show_field("filter_area"); + }); + } +} + +export default function get_dialog_constructor(type) { + const widget_map = { + chart: ChartDialog, + shortcut: ShortcutDialog, + }; + + return widget_map[type] || WidgetDialog; +} diff --git a/frappe/public/js/frappe/widgets/widget_group.js b/frappe/public/js/frappe/widgets/widget_group.js index 36fb2c9efc..4124e4b76e 100644 --- a/frappe/public/js/frappe/widgets/widget_group.js +++ b/frappe/public/js/frappe/widgets/widget_group.js @@ -3,46 +3,31 @@ import BaseWidget from "../widgets/base_widget"; import ShortcutWidget from "../widgets/shortcut_widget"; import LinksWidget from "../widgets/links_widget"; import OnboardingWidget from "../widgets/onboarding_widget"; +import NewWidget from "../widgets/new_widget"; -frappe.provide('frappe.widget') +frappe.provide("frappe.widget"); const widget_factory = { chart: ChartWidget, base: BaseWidget, - bookmark: ShortcutWidget, + shortcut: ShortcutWidget, links: LinksWidget, - onboarding: OnboardingWidget + onboarding: OnboardingWidget, }; export default class WidgetGroup { constructor(opts) { Object.assign(this, opts); - // opts = { - // title: "CRM Dashboard", - // container: $(''), - // widgets: [ - // {type: "dashboard", width: "Full", options: {}}. - // {type: "dashboard", width: "Full", options: {}} - // ], - // allow_delete: true, - // allow_create: true, - // allow_rearrange: true, - // hide_edit_option: false, - // collapsible: false - // } - window.wid_area = this; + this.widgets_list = []; + this.widgets_dict = {}; + this.widget_order = []; this.make(); } make() { this.make_container(); - this.refresh(); - } - - refresh() { - this.title && this.set_title(this.title); + this.title && this.set_title(); this.widgets && this.make_widgets(); - this.allow_sorting && this.setup_sortable(); } make_container() { @@ -58,22 +43,94 @@ export default class WidgetGroup { this.title_area = widget_area.find(".widget-group-title"); this.control_area = widget_area.find(".widget-group-control"); this.body = widget_area.find(".widget-group-body"); + !this.widgets.length && this.widget_area.hide(); widget_area.appendTo(this.container); } - set_title(title) { + set_title() { this.title_area[0].innerText = this.title; } make_widgets() { - this.body.empty() + this.body.empty(); + this.widgets.forEach((widget) => { + this.add_widget(widget); + }); + } + + add_widget(widget) { const widget_class = widget_factory[this.type]; - this.widgets.forEach(widget => { - new widget_class({ - ...widget, - container: this.body - }) + let widget_object = new widget_class({ + ...widget, + widget_type: this.type, + container: this.body, + options: { + ...this.options, + on_delete: (name) => this.on_delete(name), + }, + }); + + this.widgets_list.push(widget_object); + this.widgets_dict[widget.name] = widget_object; + + return widget_object; + } + + customize() { + this.widget_area.show(); + this.widgets_list.forEach((wid) => { + wid.customize(this.options); + }); + + this.options.allow_create && this.setup_new_widget(); + this.options.allow_sorting && this.setup_sortable(); + } + + setup_new_widget() { + const max = this.options + ? this.options.max_widget_count || Number.POSITIVE_INFINITY + : Number.POSITIVE_INFINITY; + + if (this.widgets_list.length < max) { + this.new_widget = new NewWidget({ + container: this.body, + type: this.type, + on_create: (config) => { + // Remove new widget + this.new_widget.delete(); + delete this.new_widget; + + config.in_customize_mode = 1; + + // Add new widget and customize it + let wid = this.add_widget(config); + wid.customize(this.options); + + // Put back the new widget if required + if (this.widgets_list.length < max) { + this.setup_new_widget(); + } + }, + }); + } + } + + on_delete(name) { + this.widgets_list = this.widgets_list.filter((wid) => name != wid.name); + delete this.widgets_dict[name]; + this.update_widget_order(); + + if (!this.new_widget) this.setup_new_widget(); + } + + update_widget_order() { + this.widget_order = []; + this.body.children().each((index, element) => { + let name = element.dataset.widgetName; + if (name) { + this.widget_order.push(name); + } }); } @@ -81,13 +138,26 @@ export default class WidgetGroup { const container = this.body[0]; this.sortable = new Sortable(container, { animation: 150, - onEnd: () => { - console.log("Sorting") - }, - // onChoose: (evt) => this.sortable_config.on_choose(evt, container), - // onStart: (evt) => this.sortable_config.on_start(evt, container) + handle: ".drag-handle", + onEnd: () => this.update_widget_order(), }); } + + get_widget_config() { + this.update_widget_order(); + let prepared_dict = {}; + + this.widgets_list.forEach((wid) => { + let config = wid.get_config(); + let name = config.docname ? config.docname : config.name; + prepared_dict[name] = config; + }); + + return { + order: this.widget_order, + widgets: prepared_dict, + }; + } } -frappe.widget.WidgetGroup = WidgetGroup; \ No newline at end of file +frappe.widget.WidgetGroup = WidgetGroup; diff --git a/frappe/public/less/common.less b/frappe/public/less/common.less index b0aca5cb47..c2de48d553 100644 --- a/frappe/public/less/common.less +++ b/frappe/public/less/common.less @@ -32,6 +32,10 @@ details > summary { cursor: pointer; } +details > summary:focus { + outline: none; +} + .text-color { color: @text-color !important; } diff --git a/frappe/public/less/desktop.less b/frappe/public/less/desktop.less index 9df4addf61..2230c59ac9 100644 --- a/frappe/public/less/desktop.less +++ b/frappe/public/less/desktop.less @@ -76,6 +76,24 @@ position: relative; min-height: 1px; padding-right: 15px; + + .desk-page.allow-customization { + .customize-options { + text-align: right; + margin-top: 7px; + color: @text-muted; + z-index: 99; + + .save-customization { + cursor: pointer; + color: @text-color; + } + + .discard-customization { + cursor: pointer; + } + } + } } @media (max-width: 768px) { @@ -90,6 +108,9 @@ .widget-group { margin-bottom: 25px; + // -webkit-animation-name: slideInUp; + // animation-name: slideInUp; + // animation-duration: 0.4s; .widget-group-head { display: flex; @@ -201,6 +222,17 @@ margin-left: 5px; } + .drag-handle { + cursor: all-scroll; + cursor: -webkit-grabbing; + + &:active { + cursor: grabbing; + cursor: -moz-grabbing; + cursor: -webkit-grabbing; + } + } + .dashboard-date-field { .clearfix, .help-box { @@ -220,6 +252,16 @@ border-color: @disabled-background } + &.new-widget { + min-height: 65px; + background-color: @disabled-background; + color: @text-muted; + display: flex; + align-content: center; + justify-content: center; + cursor: pointer; + } + // Overrides for each widgets &.dashboard-widget-box { padding: 10px 15px !important; @@ -300,6 +342,14 @@ .widget-head { margin-top: 5px; margin-bottom: 5px; + + .widget-title { + i { + color: @text-muted; + font-size: 18px; + margin-right: 6px; + } + } } } @@ -357,23 +407,153 @@ border-radius: 10px; } -.pill-green { - background: #71b92c; - // color: #000; +@-webkit-keyframes smallBounce { + from, + 20%, + 53%, + 80%, + to { + -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + + 40%, + 43% { + -webkit-animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + -webkit-transform: translate3d(0, -12px, 0); + transform: translate3d(0, -12px, 0); + } + + 70% { + -webkit-animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + -webkit-transform: translate3d(0, -6px, 0); + transform: translate3d(0, -6px, 0); + } + + 90% { + -webkit-transform: translate3d(0, -4px, 0); + transform: translate3d(0, -4px, 0); + } } -.pill-red { - background: @red; +@keyframes smallBounce { + from, + 20%, + 53%, + 80%, + to { + -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + + 40%, + 43% { + -webkit-animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + -webkit-transform: translate3d(0, -12px, 0); + transform: translate3d(0, -12px, 0); + } + + 70% { + -webkit-animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + -webkit-transform: translate3d(0, -6px, 0); + transform: translate3d(0, -6px, 0); + } + + 90% { + -webkit-transform: translate3d(0, -4px, 0); + transform: translate3d(0, -4px, 0); + } } -.pill-blue { - background: @blue; +.small-bounce { + -webkit-animation-name: smallBounce; + animation-name: smallBounce; + -webkit-transform-origin: center bottom; + transform-origin: center bottom; + animation-duration: 1s; } -.pill-yellow { - background: @yellow; +@-webkit-keyframes slideInUp { + from { + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + visibility: visible; + } + + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } } -.pill-orange { - background: @orange; +@keyframes slideInUp { + from { + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + visibility: visible; + } + + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +.slide-in-up { + -webkit-animation-name: slideInUp; + animation-name: slideInUp; + animation-duration: 1s; +} + + +@-webkit-keyframes pulse { + from { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + + 50% { + -webkit-transform: scale3d(1.05, 1.05, 1.05); + transform: scale3d(1.05, 1.05, 1.05); + } + + to { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} + +@keyframes pulse { + from { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + + 50% { + -webkit-transform: scale3d(1.05, 1.05, 1.05); + transform: scale3d(1.05, 1.05, 1.05); + } + + to { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} + +.zoomOutDelete { + // -webkit-animation-name: zoomOut; + // animation-name: zoomOut; + // animation-duration: 1s; + transition: opacity 0.2s, visibility 0.2s, transform 0.2s; + transform: scale3d(0.5, 0.5, 0.5); + opacity: 0; + visibility: hidden; } \ No newline at end of file diff --git a/frappe/public/less/form.less b/frappe/public/less/form.less index b72d249aab..8e43b05122 100644 --- a/frappe/public/less/form.less +++ b/frappe/public/less/form.less @@ -770,6 +770,7 @@ h6.uppercase, .h6.uppercase { .help-box { margin-top: 3px; + margin-bottom: 6px; } pre { diff --git a/frappe/public/less/form_grid.less b/frappe/public/less/form_grid.less index 28f08635ba..ed457a9ce8 100644 --- a/frappe/public/less/form_grid.less +++ b/frappe/public/less/form_grid.less @@ -262,6 +262,11 @@ border-bottom: 1px solid @border-color; } +.grid-form-body { + max-height: 75vh; + overflow-y: auto; +} + .grid-header-toolbar { display: flow-root; } diff --git a/frappe/public/less/link_preview.less b/frappe/public/less/link_preview.less index 5bc9767815..22a61a79e7 100644 --- a/frappe/public/less/link_preview.less +++ b/frappe/public/less/link_preview.less @@ -37,6 +37,7 @@ padding-bottom: 5px; max-width: 330px; min-width: 200px; + overflow-wrap: break-word; .preview-field { padding-bottom: 10px; diff --git a/frappe/public/less/module.less b/frappe/public/less/module.less new file mode 100644 index 0000000000..f924778864 --- /dev/null +++ b/frappe/public/less/module.less @@ -0,0 +1,147 @@ +@import "variables.less"; + +.module-head { + padding: 15px 30px; + border-bottom: 1px solid @light-border-color; +} + +.module-head h1 { + padding: 0px; + margin: 0px; +} + +.module-body { + padding: 0px 15px; + + .section-head { + margin-bottom: 15px; + margin-top: 0px; + } +} + +.module-section { + border-bottom: 1px solid @light-border-color; + + .module-section-link { + line-height: 1.5em; + // font-size: 14px; + } +} + +.module-section-column { + padding: 30px; +} + +@media(min-width: @screen-xs) { + .module-section:nth-child(even) { + background-color: @light-bg; + } + + .module-section:last-child { + border-bottom: none; + } +} + +@media(max-width: @screen-sm) { + .module-body { + margin-top: 15px; + border-top: 1px solid @border-color; + } +} + +@media(max-width: @screen-xs) { + .module-body { + margin-top: 0; + border-top: 1px solid transparent; + } +} + +@media(max-width: @screen-xs) { + .module-section { + border: none; + } + + .module-section-column { + border-bottom: 1px solid @light-border-color; + } + + .module-section-column:nth-child(even) { + background-color: @light-bg; + } + + .module-section:last-child .module-section-column:last-child { + border-bottom: none; + } +} + + +.module-item { + margin: 0px; + padding: 7px; + font-weight: 400; + border-bottom: 1px solid @border-color; + cursor: pointer; + transition: 0.2s; + -webkit-transition: 0.2s; +} + +.module-item h4 { + display: inline-block; +} + +.module-item .module-item-description { + margin-top: -5px; +} + +.module-item .badge { + margin-top: -2px; + margin-left: 3px; +} + +.module-item:hover, .module-item:focus { + background-color: @panel-bg; +} + +.module-item:last-child { + border: none; +} + +.module-link.active .icon-chevron-right { + margin-top: 4px; + display: block !important; +} + +.module-item-progress { + margin-bottom: 10px; + height: 17px; +} + +.module-item-progress-total { + height: 7px; + background-color: #999999; + width: 0px; +} + +.module-item-progress-open { + height: 7px; + background-color: red; + width: 0px; +} + +@media(max-width: @screen-xs) { + + body[data-route^="Module"] { + .page-title { + width: 100%; + } + + + .page-actions { + display: none !important; + } + + .layout-main-section { + border-bottom: 0px; + } + } +} diff --git a/frappe/public/less/sidebar.less b/frappe/public/less/sidebar.less index ac5a5c33d5..28dae1a948 100644 --- a/frappe/public/less/sidebar.less +++ b/frappe/public/less/sidebar.less @@ -273,7 +273,8 @@ body[data-route^="Module"] .main-menu { } .layout-side-section .form-sidebar { - .modified-by { + .modified-by, + .pageview-count { margin-bottom: 15px; } } diff --git a/frappe/public/scss/website.scss b/frappe/public/scss/website.scss index a297054ee2..546110bd5c 100644 --- a/frappe/public/scss/website.scss +++ b/frappe/public/scss/website.scss @@ -4,8 +4,34 @@ @import "multilevel-dropdown"; @import "website-image"; +html { + height: 100%; +} + body { + min-height: 100%; + display: flex; + flex-direction: column; font-size: 16px; + + > div { + flex: 1 0 auto; + } +} + +footer { + flex-shrink: 0; +} + +// make navbar padding consistent with the page +.navbar { + padding-left: 0; + padding-right: 0; + + .container { + padding-left: 15px; + padding-right: 15px; + } } .navbar.bg-dark { diff --git a/frappe/social/doctype/energy_point_log/energy_point_log.py b/frappe/social/doctype/energy_point_log/energy_point_log.py index 8353c3aa5f..31de1b8a60 100644 --- a/frappe/social/doctype/energy_point_log/energy_point_log.py +++ b/frappe/social/doctype/energy_point_log/energy_point_log.py @@ -9,6 +9,8 @@ import json from frappe.model.document import Document from frappe.desk.doctype.notification_log.notification_log import enqueue_create_notification,\ get_title, get_title_html +from frappe.desk.doctype.notification_settings.notification_settings\ + import is_email_notifications_enabled_for_type, is_email_notifications_enabled from frappe.utils import cint, get_fullname, getdate, get_link_to_form class EnergyPointLog(Document): @@ -300,6 +302,10 @@ def send_summary(timespan): if not is_energy_point_enabled(): return + + if not is_email_notifications_enabled_for_type(frappe.session.user, 'Energy Point'): + return + from_date = frappe.utils.add_to_date(None, weeks=-1) if timespan == 'Monthly': from_date = frappe.utils.add_to_date(None, months=-1) diff --git a/frappe/templates/base.html b/frappe/templates/base.html index 0fc51033f1..907a7b6113 100644 --- a/frappe/templates/base.html +++ b/frappe/templates/base.html @@ -78,7 +78,11 @@ {%- endblock -%} {%- block navbar -%} - {% include "templates/includes/navbar/navbar.html" %} + {%- if navbar_content -%} + {{ navbar_content }} + {%- else -%} + {% include "templates/includes/navbar/navbar.html" %} + {%- endif -%} {%- endblock -%} {% block content %} @@ -86,7 +90,11 @@ {% endblock %} {%- block footer -%} - {% include "templates/includes/footer/footer.html" %} + {%- if footer_content -%} + {{ footer_content }} + {%- else -%} + {% include "templates/includes/footer/footer.html" %} + {%- endif -%} {%- endblock -%} {% block base_scripts %} diff --git a/frappe/templates/emails/delete_data_confirmation.html b/frappe/templates/emails/delete_data_confirmation.html index e483bd59ed..126d8bcb4b 100644 --- a/frappe/templates/emails/delete_data_confirmation.html +++ b/frappe/templates/emails/delete_data_confirmation.html @@ -7,5 +7,6 @@ {{ _("Confirm Request") }}

- {{_("You can also copy-paste this ")}} {{_("Verification Link")}}{{_(" to your browser")}} + {% set verification_link = '{{ _("Verification Link") }}' %} + {{_("You can also copy-paste this {0} to your browser").format(verification_link) }}

\ No newline at end of file diff --git a/frappe/templates/includes/breadcrumbs.html b/frappe/templates/includes/breadcrumbs.html index c287fa8223..3fda731372 100644 --- a/frappe/templates/includes/breadcrumbs.html +++ b/frappe/templates/includes/breadcrumbs.html @@ -4,9 +4,10 @@
{% block page_container %} -
+
{% endmacro %} -{% macro container_attributes() %} -id="page-{{ name or route | e }}" data-path="{{ pathname | e }}" {%- if page_or_generator=="Generator" %}source-type="Generator" data-doctype="{{ doctype }}"{% endif %} -{% endmacro %} +{% macro container_attributes() -%} +id="page-{{ name or route | e }}" data-path="{{ pathname | e }}" + {%- if page_or_generator=="Generator" %}source-type="Generator" data-doctype="{{ doctype }}"{%- endif %} + {%- if source_content_type %}source-content-type="{{ source_content_type }}"{%- endif %} +{%- endmacro %} {% if show_sidebar %}
diff --git a/frappe/tests/test_db_update.py b/frappe/tests/test_db_update.py index 34378de3af..f243aa268f 100644 --- a/frappe/tests/test_db_update.py +++ b/frappe/tests/test_db_update.py @@ -11,6 +11,7 @@ class TestDBUpdate(unittest.TestCase): frappe.reload_doctype('User', force=True) frappe.model.meta.trim_tables('User') make_property_setter(doctype, 'bio', 'fieldtype', 'Text', 'Data') + make_property_setter(doctype, 'middle_name', 'fieldtype', 'Data', 'Text') make_property_setter(doctype, 'enabled', 'default', '1', 'Int') frappe.db.updatedb(doctype) diff --git a/frappe/tests/test_document.py b/frappe/tests/test_document.py index 1e92015602..470ab35fb6 100644 --- a/frappe/tests/test_document.py +++ b/frappe/tests/test_document.py @@ -181,7 +181,7 @@ class TestDocument(unittest.TestCase): # css attributes xss = '
Test
' - escaped_xss = '
Test
' + escaped_xss = '
Test
' d.subject += xss d.save() d.reload() diff --git a/frappe/tests/test_global_search.py b/frappe/tests/test_global_search.py index 01067c85dd..5cffbaaf64 100644 --- a/frappe/tests/test_global_search.py +++ b/frappe/tests/test_global_search.py @@ -191,3 +191,6 @@ class TestGlobalSearch(unittest.TestCase): frappe.db.commit() results = global_search.web_search('unsubscribe') self.assertTrue('Unsubscribe' in results[0].content) + results = global_search.web_search(text='unsubscribe', + scope="manufacturing\" UNION ALL SELECT 1,2,3,4,doctype from __global_search") + self.assertTrue(results == []) diff --git a/frappe/tests/test_sitemap.py b/frappe/tests/test_sitemap.py index 455a80eb3e..22669000c1 100644 --- a/frappe/tests/test_sitemap.py +++ b/frappe/tests/test_sitemap.py @@ -5,6 +5,8 @@ from frappe.utils import get_html_for_route class TestSitemap(unittest.TestCase): def test_sitemap(self): + from frappe.test_runner import make_test_records + make_test_records('Blog Post') blogs = frappe.db.get_all('Blog Post', {'published': 1}, ['route'], limit=1) xml = get_html_for_route('sitemap.xml') self.assertTrue('/about' in xml) diff --git a/frappe/translations/de.csv b/frappe/translations/de.csv index efeb68df62..18d66591b3 100644 --- a/frappe/translations/de.csv +++ b/frappe/translations/de.csv @@ -63,7 +63,6 @@ apps/frappe/frappe/templates/emails/download_data.html,We have received a reques DocType: System Settings,"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.","Falls diese Option aktiviert ist, wird die Passwortstärke auf der Grundlage des Minimum Password Score Wertes erzwungen. Ein Wert von 2 ist mittelstark und 4 sehr stark." DocType: About Us Settings,"""Team Members"" or ""Management""",„Teammitglieder“ oder „Management“ apps/frappe/frappe/core/doctype/doctype/doctype.py,Default for 'Check' type of field must be either '0' or '1',Standard für 'Prüfen'-Feldtyp muss entweder '0' oder '1' sein -apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,Yesterday,Gestern DocType: Contact,Designation,Bezeichnung apps/frappe/frappe/email/doctype/email_account/email_account.py,Automatic Linking can be activated only for one Email Account.,Die automatische Verknüpfung kann nur für ein E-Mail-Konto aktiviert werden. DocType: Test Runner,Test Runner,Tester @@ -120,7 +119,6 @@ DocType: Dashboard Chart,Timespan,Zeitspanne apps/frappe/frappe/public/js/frappe/file_uploader/FileUploader.vue,Web Link,Weblink DocType: Deleted Document,Restored,Restauriert apps/frappe/frappe/public/js/frappe/form/print.js,"Error connecting to QZ Tray Application...

You need to have QZ Tray application installed and running, to use the Raw Print feature.

Click here to Download and install QZ Tray.
Click here to learn more about Raw Printing.","Fehler beim Verbinden mit der QZ-Tray-Anwendung ...

Sie müssen die QZ Tray-Anwendung installiert haben und ausführen, um die Raw Print-Funktion verwenden zu können.

Klicken Sie hier, um QZ Tray herunterzuladen und zu installieren .
Klicken Sie hier, um mehr über den Rohdruck zu erfahren ." -apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,1 minute ago,Vor 1 Minute apps/frappe/frappe/core/page/permission_manager/permission_manager_help.html,"Apart from System Manager, roles with Set User Permissions right can set permissions for other users for that Document Type.",Zusätzlich zum System-Manager können Rollen mit der Erlaubnis Benutzer anzulegen Berechtigungen für andere Nutzer für diesen Dokumententyp setzen. apps/frappe/frappe/website/doctype/website_theme/website_theme.js,Configure Theme,Thema konfigurieren DocType: Company History,Company History,Unternehmensgeschichte @@ -971,7 +969,6 @@ apps/frappe/frappe/public/js/frappe/list/list_view.js,Share URL,URL teilen DocType: System Settings,Allow Consecutive Login Attempts ,Erlaube aufeinanderfolgende Login-Versuche apps/frappe/frappe/templates/pages/integrations/stripe_checkout.html,An error occured during the payment process. Please contact us.,Während des Bezahlvorgangs ist ein Fehler aufgetreten. Bitte kontaktieren Sie uns. DocType: Onboarding Slide,If Slide Type is Create or Settings there should be a 'create_onboarding_docs' method in the {ref_doctype}.py file bound to be executed after the slide is completed.,"Wenn der Folientyp "Erstellen" oder "Einstellungen" ist, sollte die Methode "create_onboarding_docs" in der Datei "{ref_doctype} .py" enthalten sein, die nach Abschluss der Folie ausgeführt werden soll." -apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} days ago,vor {0} Tag(en) DocType: Email Account,Awaiting Password,Warte auf Passwort DocType: Address,Address Line 1,Adresse Zeile 1 apps/frappe/frappe/public/js/frappe/ui/filters/filter.js,Not Descendants Of,Nicht Nachkommen von @@ -1360,7 +1357,6 @@ apps/frappe/frappe/social/doctype/energy_point_rule/energy_point_rule.py,Referen DocType: PayPal Settings,PayPal Settings,PayPal-Einstellungen apps/frappe/frappe/core/page/permission_manager/permission_manager.js,Select Document Type,Dokumenttyp auswählen apps/frappe/frappe/utils/nestedset.py,Cannot delete {0} as it has child nodes,"{0} kann nicht gelöscht werden, da es Unterknoten gibt" -apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} minutes ago,vor {0} Minute(n) apps/frappe/frappe/automation/doctype/assignment_rule/assignment_rule.py,Assignment Day {0} has been repeated.,Der Zuordnungstag {0} wurde wiederholt. DocType: Kanban Board Column,lightblue,hellblau apps/frappe/frappe/integrations/doctype/webhook/webhook.py,Same Field is entered more than once,Gleiches Feld wird mehrmals eingegeben @@ -1787,7 +1783,6 @@ DocType: Notification Log,Assignment,Zuordnung DocType: Notification,Slack Channel,Slack-Kanal DocType: About Us Team Member,Image Link,Bildverknüpfung DocType: Auto Email Report,Report Filters,Berichtsfilter -apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,now,jetzt DocType: Workflow State,step-backward,Schritt zurück apps/frappe/frappe/utils/boilerplate.py,{app_title},{app_title} apps/frappe/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py,Please set Dropbox access keys in your site config,Bitte Dropbox-Zugriffsdaten in den Einstellungen der Seite setzen @@ -2520,7 +2515,6 @@ apps/frappe/frappe/public/js/frappe/form/print.js,QZ Tray Connection Active!,QZ- DocType: Contact Us Settings,Settings for Contact Us Page,Einstellungen Kontakt DocType: Server Script,Script Type,Skripttyp DocType: Print Settings,Enable Print Server,Aktivieren Sie den Druckserver -apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} weeks ago,vor {0} Woche(n) DocType: Email Account,Footer,Fußzeile apps/frappe/frappe/config/integrations.py,Authentication,Authentifizierung apps/frappe/frappe/utils/verified_command.py,Invalid Link,Ungültige Verknüpfung @@ -3444,7 +3438,6 @@ apps/frappe/frappe/integrations/doctype/ldap_settings/ldap_settings.py,Please In apps/frappe/frappe/core/doctype/data_import/log_details.html,Row Status,Zeilenstatus DocType: S3 Backup Settings,sa-east-1,sa-east-1 DocType: Workflow Transition,Workflow Transition,Workflow-Übergang -apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} months ago,vor {0} Monate(n) apps/frappe/frappe/custom/doctype/custom_field/custom_field.py,Custom Fields can only be added to a standard DocType.,Benutzerdefinierte Felder können nur zu einem Standard-DocType hinzugefügt werden. apps/frappe/frappe/public/js/frappe/list/list_sidebar_group_by.js,Created By,Erstellt von apps/frappe/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py,Dropbox Setup,Dropbox-Setup @@ -3500,7 +3493,6 @@ apps/frappe/frappe/website/doctype/website_theme/website_theme.js,Theme Colors,T apps/frappe/frappe/printing/page/print_format_builder/print_format_builder.js,Select a DocType to make a new format,"DocType auswählen, um ein neues Format zu erstellen" apps/frappe/frappe/desk/page/user_profile/user_profile.js,User does not exist,Benutzer existiert nicht apps/frappe/frappe/automation/doctype/auto_repeat/auto_repeat.py,'Recipients' not specified,"Keine ""Empfänger"" angegeben" -apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,just now,gerade eben apps/frappe/frappe/public/js/frappe/ui/filters/edit_filter.html,Apply,Anwenden DocType: Footer Item,Policy,Politik apps/frappe/frappe/integrations/utils.py,{0} Settings not found,{0} Einstellungen nicht gefunden @@ -3592,7 +3584,6 @@ DocType: Auto Email Report,Period,Periode apps/frappe/frappe/core/doctype/data_import_beta/data_import_beta.js,About {0} minute remaining,Noch ungefähr {0} Minuten apps/frappe/frappe/www/login.py,Invalid Login Token,Invalid Login Token apps/frappe/frappe/public/js/frappe/chat.js,Discard,Verwerfen -apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,1 hour ago,vor 1 Stunde DocType: Website Settings,Home Page,Startseite DocType: Error Snapshot,Parent Error Snapshot,Momentaufnahme des übergeordneten Fehlers apps/frappe/frappe/public/js/frappe/data_import/import_preview.js,Map columns from {0} to fields in {1},Ordnen Sie Spalten von {0} Feldern in {1} zu. @@ -3804,7 +3795,6 @@ DocType: Webhook,on_update_after_submit,on_update_after_submit DocType: System Settings,Allow Login using User Name,Login mit Benutzernamen zulassen apps/frappe/frappe/core/doctype/report/report.js,Enable Report,Bericht aktivieren DocType: DocField,Display Depends On,Anzeige ist abhängig von -apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,> {0} year(s) ago,> {0} Jahr (e) her DocType: Social Login Key,API Endpoint,API-Endpunkt DocType: Web Page,Insert Code,Code einfügen DocType: Data Migration Run,Current Mapping Type,Aktueller Kartentyp @@ -4188,3 +4178,23 @@ DocType: DocField,Ignore User Permissions,Ignorieren von Benutzerberechtigungen apps/frappe/frappe/public/js/frappe/web_form/web_form.js,Saved Successfully,Erfolgreich gespeichert apps/frappe/frappe/core/doctype/user/user.py,Please ask your administrator to verify your sign-up,Bitte fragen Sie Ihren Administrator Ihre Anmeldung bis zum überprüfen DocType: Domain Settings,Active Domains,Aktive Domains +apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,Now,Jetzt +apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} m,{0} m +apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} h,{0} h +apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} D,{0} T +apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} W,{0} W +apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} M,{0} M +apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} Y,{0} J +apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,Just now,Gerade eben +apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,1 minute ago,Vor einer Minute +apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} minutes ago,Vor {0} Minuten +apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,1 hour ago,Vor einer Stunde +apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} hours ago,Vor {0} Stunden +apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,Yesterday,Gestern +apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} days ago,Vor {0} Tagen +apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,1 week ago,Vor einer Woche +apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} weeks ago,Vor {0} Wochen +apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,1 month ago,Vor einem Monat +apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} months ago,Vor {0} Monaten +apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,1 year ago,Vor einem Jahr +apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} years ago,Vor {0} Jahren \ No newline at end of file diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index 82e6ea1b45..34432839bb 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -75,11 +75,34 @@ def extract_email_id(email): email_id = email_id.decode("utf-8", "ignore") return email_id -def validate_email_add(email_str, throw=False): +def validate_phone_number(phone_number, throw=False): + """Returns True if valid phone number""" + if not phone_number: + return False + + phone_number = phone_number.strip() + match = re.match(r"([0-9\ \+\_\-\,\.\*\#\(\)]){1,20}$", phone_number) + + if not match and throw: + frappe.throw(frappe._("{0} is not a valid Phone Number").format(phone_number), frappe.InvalidPhoneNumberError) + + return bool(match) + +def validate_name(name, throw=False): + """Returns True if the name is valid + valid names may have unicode and ascii characters, dash, quotes, numbers + anything else is considered invalid """ - validate_email_add will be renamed to the validate_email_address in v12 - """ - return validate_email_address(email_str, throw=False) + if not name: + return False + + name = name.strip() + match = re.match(r"^[\w][\w\'\-]*([ \w][\w\'\-]+)*$", name) + + if not match and throw: + frappe.throw(frappe._("{0} is not a valid Name").format(name), frappe.InvalidNameError) + + return bool(match) def validate_email_address(email_str, throw=False): """Validates the email string""" @@ -98,15 +121,15 @@ def validate_email_address(email_str, throw=False): _valid = False else: - e = extract_email_id(e) - match = re.match("[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?", e.lower()) if e else None + email_id = extract_email_id(e) + match = re.match("[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?", email_id.lower()) if email_id else None if not match: _valid = False else: matched = match.group(0) if match: - match = matched==e.lower() + match = matched==email_id.lower() if not _valid: if throw: @@ -691,4 +714,4 @@ def get_html_for_route(route): set_request(method='GET', path=route) response = render.render() html = frappe.safe_decode(response.get_data()) - return html \ No newline at end of file + return html diff --git a/frappe/utils/background_jobs.py b/frappe/utils/background_jobs.py index 03f063e058..4b37e850f0 100755 --- a/frappe/utils/background_jobs.py +++ b/frappe/utils/background_jobs.py @@ -73,7 +73,7 @@ def enqueue(method, queue='default', timeout=None, event=None, def enqueue_doc(doctype, name=None, method=None, queue='default', timeout=300, now=False, **kwargs): '''Enqueue a method to be run on a document''' - enqueue('frappe.utils.background_jobs.run_doc_method', doctype=doctype, name=name, + return enqueue('frappe.utils.background_jobs.run_doc_method', doctype=doctype, name=name, doc_method=method, queue=queue, timeout=timeout, now=now, **kwargs) def run_doc_method(doctype, name, doc_method, **kwargs): diff --git a/frappe/utils/boilerplate.py b/frappe/utils/boilerplate.py index b81d802a07..96ed3303ed 100755 --- a/frappe/utils/boilerplate.py +++ b/frappe/utils/boilerplate.py @@ -151,6 +151,10 @@ app_license = "{app_license}" # web_include_css = "/assets/{app_name}/css/{app_name}.css" # web_include_js = "/assets/{app_name}/js/{app_name}.js" +# include js, css files in header of web form +# webform_include_js = {{"doctype": "public/js/doctype.js"}} +# webform_include_css = {{"doctype": "public/css/doctype.css"}} + # include js in page # page_js = {{"page" : "public/js/file.js"}} diff --git a/frappe/utils/change_log.py b/frappe/utils/change_log.py index c94a247796..776fb825c2 100644 --- a/frappe/utils/change_log.py +++ b/frappe/utils/change_log.py @@ -174,9 +174,12 @@ def parse_latest_non_beta_release(response): Returns json : json object pertaining to the latest non-beta release """ - for release in response: - if release['prerelease'] == True: continue - return release + version_list = [release.get('tag_name').strip('v') for release in response if not release.get('prerelease')] + + if version_list: + return sorted(version_list, key=Version, reverse=True)[0] + + return None def check_release_on_github(app): # Check if repo remote is on github @@ -199,12 +202,11 @@ def check_release_on_github(app): org_name = remote_url.split('/')[3] r = requests.get('https://api.github.com/repos/{}/{}/releases'.format(org_name, app)) - if r.status_code == 200 and r.json(): + if r.ok: lastest_non_beta_release = parse_latest_non_beta_release(r.json()) - return Version(lastest_non_beta_release['tag_name'].strip('v')), org_name - else: - # In case of an improper response or if there are no releases - return None + return Version(lastest_non_beta_release), org_name + # In case of an improper response or if there are no releases + return None def add_message_to_redis(update_json): # "update-message" will store the update message string diff --git a/frappe/utils/data.py b/frappe/utils/data.py index 7d58594657..d6a3a6bce6 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -54,8 +54,8 @@ def get_datetime(datetime_str=None): elif isinstance(datetime_str, datetime.date): return datetime.datetime.combine(datetime_str, datetime.time()) - # dateutil parser does not agree with dates like 0001-01-01 - if not datetime_str or (datetime_str or "").startswith("0001-01-01"): + # dateutil parser does not agree with dates like "0001-01-01" or "0000-00-00" + if not datetime_str or (datetime_str or "").startswith(("0001-01-01", "0000-00-00")): return None try: diff --git a/frappe/utils/global_search.py b/frappe/utils/global_search.py index 4b50745a74..3c4b9583f8 100644 --- a/frappe/utils/global_search.py +++ b/frappe/utils/global_search.py @@ -499,22 +499,29 @@ def web_search(text, scope=None, start=0, limit=20): common_query = ''' SELECT `doctype`, `name`, `content`, `title`, `route` FROM `__global_search` WHERE {conditions} - LIMIT {limit} OFFSET {start}''' + LIMIT %(limit)s OFFSET %(start)s''' - scope_condition = '`route` like "{}%" AND '.format(scope) if scope else '' + scope_condition = '`route` like %(scope)s AND ' if scope else '' published_condition = '`published` = 1 AND ' mariadb_conditions = postgres_conditions = ' '.join([published_condition, scope_condition]) # https://mariadb.com/kb/en/library/full-text-index-overview/#in-boolean-mode text = '"{}"'.format(text) - mariadb_conditions += 'MATCH(`content`) AGAINST ({} IN BOOLEAN MODE)'.format(frappe.db.escape(text)) - postgres_conditions += 'TO_TSVECTOR("content") @@ PLAINTO_TSQUERY({})'.format(frappe.db.escape(text)) + mariadb_conditions += 'MATCH(`content`) AGAINST (%(text)s IN BOOLEAN MODE)' + postgres_conditions += 'TO_TSVECTOR("content") @@ PLAINTO_TSQUERY(%(text)s)' + + values = { + "scope": "".join([scope, "%"]) if scope else '', + "limit": limit, + "start": start, + "text": text + } result = frappe.db.multisql({ - 'mariadb': common_query.format(conditions=mariadb_conditions, limit=limit, start=start), - 'postgres': common_query.format(conditions=postgres_conditions, limit=limit, start=start) - }, as_dict=True) - tmp_result=[] + 'mariadb': common_query.format(conditions=mariadb_conditions), + 'postgres': common_query.format(conditions=postgres_conditions) + }, values=values, as_dict=True) + tmp_result = [] for i in result: if i in results or not results: tmp_result.append(i) diff --git a/frappe/utils/html_utils.py b/frappe/utils/html_utils.py index 3de647894b..62161408eb 100644 --- a/frappe/utils/html_utils.py +++ b/frappe/utils/html_utils.py @@ -105,6 +105,11 @@ def get_icon_html(icon, small=False): else: return "".format(icon=icon) +def unescape_html(value): + from six.moves.html_parser import HTMLParser + h = HTMLParser() + return h.unescape(value) + # adapted from https://raw.githubusercontent.com/html5lib/html5lib-python/4aa79f113e7486c7ec5d15a6e1777bfe546d3259/html5lib/sanitizer.py acceptable_elements = [ 'a', 'abbr', 'acronym', 'address', 'area', diff --git a/frappe/utils/jinja.py b/frappe/utils/jinja.py index c73858f1e2..6c18e73c7b 100644 --- a/frappe/utils/jinja.py +++ b/frappe/utils/jinja.py @@ -76,10 +76,7 @@ def render_template(template, context, is_path=None, safe_render=True): if not template: return "" - # if it ends with .html then its a freaking path, not html - if (is_path - or template.startswith("templates/") - or (template.endswith('.html') and '\n' not in template)): + if (is_path or guess_is_path(template)): return get_jenv().get_template(template).render(context) else: if safe_render and ".__" in template: @@ -89,6 +86,16 @@ def render_template(template, context, is_path=None, safe_render=True): except TemplateError: throw(title="Jinja Template Error", msg="
{template}
{tb}
".format(template=template, tb=get_traceback())) +def guess_is_path(template): + # template can be passed as a path or content + # if its single line and ends with a html, then its probably a path + if not '\n' in template and '.' in template: + extn = template.rsplit('.')[-1] + if extn in ('html', 'css', 'scss', 'py'): + return True + + return False + def get_jloader(): import frappe diff --git a/frappe/utils/logger.py b/frappe/utils/logger.py index d40e2565cb..5a77434cde 100755 --- a/frappe/utils/logger.py +++ b/frappe/utils/logger.py @@ -5,7 +5,7 @@ from logging.handlers import RotatingFileHandler from six import text_type default_log_level = logging.DEBUG -LOG_FILENAME = '../logs/frappe.log' +LOG_FILENAME = '../logs/{}-frappe.log'.format(frappe.local.site) def get_logger(module, with_more_info=True): if module in frappe.loggers: @@ -57,4 +57,3 @@ def set_log_level(level): '''Use this method to set log level to something other than the default DEBUG''' frappe.log_level = getattr(logging, (level or '').upper(), None) or default_log_level frappe.loggers = {} - diff --git a/frappe/utils/password.py b/frappe/utils/password.py index da5cdecc55..b939607b19 100644 --- a/frappe/utils/password.py +++ b/frappe/utils/password.py @@ -131,9 +131,9 @@ def create_auth_table(): frappe.db.create_auth_table() def encrypt(pwd): - if len(pwd) > 100: - # encrypting > 100 chars will lead to truncation - frappe.throw(_('Password cannot be more than 100 characters long')) + if len(pwd) > 127: + # encrypting > 127 chars will lead to truncation + frappe.throw(_('Password cannot be more than 127 characters long')) cipher_suite = Fernet(encode(get_encryption_key())) cipher_text = cstr(cipher_suite.encrypt(encode(pwd))) diff --git a/frappe/utils/redis_wrapper.py b/frappe/utils/redis_wrapper.py index b0c0990e85..385bac8e4a 100644 --- a/frappe/utils/redis_wrapper.py +++ b/frappe/utils/redis_wrapper.py @@ -43,7 +43,7 @@ class RedisWrapper(redis.Redis): try: if expires_in_sec: - self.setex(key, expires_in_sec, pickle.dumps(val)) + self.setex(name=key, time=expires_in_sec, value=pickle.dumps(val)) else: self.set(key, pickle.dumps(val)) diff --git a/frappe/utils/response.py b/frappe/utils/response.py index 1dfbbe5516..fe7af072cf 100644 --- a/frappe/utils/response.py +++ b/frappe/utils/response.py @@ -218,6 +218,6 @@ def send_private_file(path): def handle_session_stopped(): frappe.respond_as_web_page(_("Updating"), - _("Your system is being updated. Please refresh again after a few moments"), + _("Your system is being updated. Please refresh again after a few moments."), http_status_code=503, indicator_color='orange', fullpage = True, primary_action=None) return frappe.website.render.render("message", http_status_code=503) diff --git a/frappe/utils/safe_exec.py b/frappe/utils/safe_exec.py index 76bc18cabc..f80d819907 100644 --- a/frappe/utils/safe_exec.py +++ b/frappe/utils/safe_exec.py @@ -11,6 +11,7 @@ from frappe.website.utils import (get_shade, get_toc, get_next_link) from frappe.modules import scrub from frappe.www.printview import get_visible_columns import frappe.exceptions +import frappe.integrations.utils class ServerScriptNotEnabled(frappe.PermissionError): pass @@ -78,6 +79,8 @@ def get_safe_globals(): user=user, csrf_token=frappe.local.session.data.csrf_token if getattr(frappe.local, "session", None) else '' ), + make_get_request = frappe.integrations.utils.make_get_request, + make_post_request = frappe.integrations.utils.make_post_request, socketio_port=frappe.conf.socketio_port, get_hooks=frappe.get_hooks, ), diff --git a/frappe/utils/scheduler.py b/frappe/utils/scheduler.py index 0a1eb555ad..596595a160 100755 --- a/frappe/utils/scheduler.py +++ b/frappe/utils/scheduler.py @@ -43,9 +43,8 @@ def enqueue_events_for_all_sites(): for site in sites: try: enqueue_events_for_site(site=site) - except: - # it should try to enqueue other sites - print(frappe.get_traceback()) + except Exception as e: + print(e.__class__, 'Failed to enqueue events for site: {}'.format(site)) def enqueue_events_for_site(site): def log_and_raise(): diff --git a/frappe/website/context.py b/frappe/website/context.py index 5ea48b61f1..9ed68926c7 100644 --- a/frappe/website/context.py +++ b/frappe/website/context.py @@ -223,24 +223,24 @@ def add_metatags(context): tags = frappe._dict(context.get("metatags") or {}) if tags: - if not "twitter:card" in tags: - tags["twitter:card"] = "summary_large_image" - if not "og:type" in tags: tags["og:type"] = "article" - if tags.get("name"): - tags["og:title"] = tags["twitter:title"] = tags["name"] + name = tags.get('name') or tags.get('title') + if name: + tags["og:title"] = tags["twitter:title"] = name - if tags.get("title"): - tags["og:title"] = tags["twitter:title"] = tags["title"] - - if tags.get("description"): - tags["og:description"] = tags["twitter:description"] = tags["description"] + description = tags.get("description") or context.description + if description: + tags['description'] = tags["og:description"] = tags["twitter:description"] = description image = tags.get('image', context.image or None) if image: tags["og:image"] = tags["twitter:image:src"] = tags["image"] = frappe.utils.get_url(image) + tags['twitter:card'] = "summary_large_image" + + if context.author or tags.get('author'): + tags['author'] = context.author or tags.get('author') if context.path: tags['og:url'] = tags['url'] = frappe.utils.get_url(context.path) @@ -248,11 +248,6 @@ def add_metatags(context): if context.published_on: tags['datePublished'] = context.published_on - if context.author: - tags['author'] = context.author - - if context.description: - tags['description'] = context.description tags['language'] = frappe.local.lang or 'en' diff --git a/frappe/website/desk_page/website/website.json b/frappe/website/desk_page/website/website.json index 915a9d2554..9171018523 100644 --- a/frappe/website/desk_page/website/website.json +++ b/frappe/website/desk_page/website/website.json @@ -1,26 +1,29 @@ { "cards": [ { - "icon": "fa fa-cog", - "links": "[\n {\n \"description\": \"Setup of top navigation bar, footer and logo.\",\n \"label\": \"Website Settings\",\n \"name\": \"Website Settings\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"List of themes for Website.\",\n \"label\": \"Website Theme\",\n \"name\": \"Website Theme\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Javascript to append to the head section of the page.\",\n \"label\": \"Website Script\",\n \"name\": \"Website Script\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Settings for About Us Page.\",\n \"label\": \"About Us Settings\",\n \"name\": \"About Us Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Settings for Contact Us Page.\",\n \"label\": \"Contact Us Settings\",\n \"name\": \"Contact Us Settings\",\n \"type\": \"doctype\"\n }\n]", - "title": "Setup" + "hidden": 0, + "label": "Setup", + "links": "[\n {\n \"description\": \"Setup of top navigation bar, footer and logo.\",\n \"label\": \"Website Settings\",\n \"name\": \"Website Settings\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"List of themes for Website.\",\n \"label\": \"Website Theme\",\n \"name\": \"Website Theme\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Javascript to append to the head section of the page.\",\n \"label\": \"Website Script\",\n \"name\": \"Website Script\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Settings for About Us Page.\",\n \"label\": \"About Us Settings\",\n \"name\": \"About Us Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Settings for Contact Us Page.\",\n \"label\": \"Contact Us Settings\",\n \"name\": \"Contact Us Settings\",\n \"type\": \"doctype\"\n }\n]" }, { - "links": "[\n {\n \"description\": \"Single Post (article).\",\n \"label\": \"Blog Post\",\n \"name\": \"Blog Post\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"A user who posts blogs.\",\n \"label\": \"Blogger\",\n \"name\": \"Blogger\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Categorize blog posts.\",\n \"label\": \"Blog Category\",\n \"name\": \"Blog Category\",\n \"type\": \"doctype\"\n }\n]", - "title": "Blog" + "hidden": 0, + "label": "Blog", + "links": "[\n {\n \"description\": \"Single Post (article).\",\n \"label\": \"Blog Post\",\n \"name\": \"Blog Post\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"A user who posts blogs.\",\n \"label\": \"Blogger\",\n \"name\": \"Blogger\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Categorize blog posts.\",\n \"label\": \"Blog Category\",\n \"name\": \"Blog Category\",\n \"type\": \"doctype\"\n }\n]" }, { - "icon": "fa fa-star", - "links": "[\n {\n \"description\": \"Content web page.\",\n \"label\": \"Web Page\",\n \"name\": \"Web Page\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"User editable form on Website.\",\n \"label\": \"Web Form\",\n \"name\": \"Web Form\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Website Sidebar\",\n \"name\": \"Website Sidebar\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Embed image slideshows in website pages.\",\n \"label\": \"Website Slideshow\",\n \"name\": \"Website Slideshow\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Add meta tags to your web pages\",\n \"label\": \"Website Route Meta\",\n \"name\": \"Website Route Meta\",\n \"type\": \"doctype\"\n }\n]", - "title": "Web Site" + "hidden": 0, + "label": "Web Site", + "links": "[\n {\n \"description\": \"Content web page.\",\n \"label\": \"Web Page\",\n \"name\": \"Web Page\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"User editable form on Website.\",\n \"label\": \"Web Form\",\n \"name\": \"Web Form\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Website Sidebar\",\n \"name\": \"Website Sidebar\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Embed image slideshows in website pages.\",\n \"label\": \"Website Slideshow\",\n \"name\": \"Website Slideshow\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Add meta tags to your web pages\",\n \"label\": \"Website Route Meta\",\n \"name\": \"Website Route Meta\",\n \"type\": \"doctype\"\n }\n]" }, { - "links": "[\n {\n \"label\": \"Portal Settings\",\n \"name\": \"Portal Settings\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n }\n]", - "title": "Portal" + "hidden": 0, + "label": "Portal", + "links": "[\n {\n \"label\": \"Portal Settings\",\n \"name\": \"Portal Settings\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n }\n]" }, { - "links": "[\n {\n \"label\": \"Help Category\",\n \"name\": \"Help Category\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Help Article\",\n \"name\": \"Help Article\",\n \"type\": \"doctype\"\n }\n]", - "title": "Knowledge Base" + "hidden": 0, + "label": "Knowledge Base", + "links": "[\n {\n \"label\": \"Help Category\",\n \"name\": \"Help Category\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Help Article\",\n \"name\": \"Help Article\",\n \"type\": \"doctype\"\n }\n]" } ], "category": "Modules", @@ -34,7 +37,7 @@ "idx": 0, "is_standard": 1, "label": "Website", - "modified": "2020-03-12 16:30:43.092622", + "modified": "2020-04-01 11:24:40.726934", "modified_by": "Administrator", "module": "Website", "name": "Website", @@ -45,30 +48,30 @@ { "color": "", "format": "{} Published", - "is_query_report": 0, + "label": "Blog Post", "link_to": "Blog Post", "stats_filter": "{\"published\":\"1\"}", "type": "DocType" }, { "format": "{} Active", - "is_query_report": 0, + "label": "Blogger", "link_to": "Blogger", "stats_filter": "{\"disabled\": 0}", "type": "DocType" }, { - "is_query_report": 0, + "label": "Web Page", "link_to": "Web Page", "type": "DocType" }, { - "is_query_report": 0, + "label": "Web Form", "link_to": "Web Form", "type": "DocType" }, { - "is_query_report": 0, + "label": "Website Settings", "link_to": "Website Settings", "type": "DocType" } diff --git a/frappe/website/doctype/blog_post/blog_post.json b/frappe/website/doctype/blog_post/blog_post.json index b1ee667540..9944cbf4b2 100644 --- a/frappe/website/doctype/blog_post/blog_post.json +++ b/frappe/website/doctype/blog_post/blog_post.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_guest_to_view": 1, "allow_import": 1, "creation": "2013-03-28 10:35:30", @@ -9,6 +10,7 @@ "title", "published_on", "published", + "disable_comments", "column_break_3", "blog_category", "blogger", @@ -114,14 +116,22 @@ "fieldtype": "Check", "hidden": 1, "label": "Email Sent" + }, + { + "default": "0", + "description": "Comments on this blog post will be disabled if checked.", + "fieldname": "disable_comments", + "fieldtype": "Check", + "label": "Disable Comments" } ], "has_web_view": 1, "icon": "fa fa-quote-left", "idx": 1, "is_published_field": "published", + "links": [], "max_attachments": 5, - "modified": "2019-11-18 11:14:56.402471", + "modified": "2020-04-08 19:58:13.672332", "modified_by": "Administrator", "module": "Website", "name": "Blog Post", diff --git a/frappe/website/doctype/blog_post/blog_post.py b/frappe/website/doctype/blog_post/blog_post.py index 8dbc176f89..148ba15be7 100644 --- a/frappe/website/doctype/blog_post/blog_post.py +++ b/frappe/website/doctype/blog_post/blog_post.py @@ -44,8 +44,12 @@ class BlogPost(WebsiteGenerator): WHERE `name`=%s""", (self.blogger,)) def on_update(self): + super(BlogPost, self).on_update() clear_cache("writers") + def on_trash(self): + super(BlogPost, self).on_trash() + def get_context(self, context): # this is for double precaution. usually it wont reach this code if not published if not cint(self.published): diff --git a/frappe/website/doctype/blog_post/templates/blog_post.html b/frappe/website/doctype/blog_post/templates/blog_post.html index da9c3ab83b..285223a2af 100644 --- a/frappe/website/doctype/blog_post/templates/blog_post.html +++ b/frappe/website/doctype/blog_post/templates/blog_post.html @@ -28,9 +28,12 @@ {% include "templates/includes/blog/blogger.html" %} {% endif %} -
- {% include 'templates/includes/comments/comments.html' %} -
+ {% if not disable_comments %} +
+ {% include 'templates/includes/comments/comments.html' %} +
+ {% endif %} +