diff --git a/.eslintrc b/.eslintrc index f94193e00e..4ea7f0edff 100644 --- a/.eslintrc +++ b/.eslintrc @@ -54,6 +54,7 @@ "Taggle": true, "Gantt": true, "Slick": true, + "Webcam": true, "PhotoSwipe": true, "PhotoSwipeUI_Default": true, "fluxify": true, diff --git a/.github/logo.png b/.github/logo.png new file mode 100644 index 0000000000..258408edfa Binary files /dev/null and b/.github/logo.png differ diff --git a/.gitignore b/.gitignore index 75bee9a091..436a08414e 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ locale dist/ build/ frappe/docs/current +.vscode +node_modules \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..fe7159848b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.linting.pylintEnabled": false +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..956e51befb --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2016-2017 Frappé Technologies Pvt. Ltd. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000..f30645229e --- /dev/null +++ b/Makefile @@ -0,0 +1,2 @@ +clean: + python setup.py clean \ No newline at end of file diff --git a/README.md b/README.md index 9621b58b46..b8427fc055 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,33 @@ -## Frappé Framework +
+ +

+ + frappé + +

+

+ a web framework with "batteries included" +

+
+ it's pronounced - fra-pay +
+
-[![Build Status](https://travis-ci.org/frappe/frappe.png)](https://travis-ci.org/frappe/frappe) +
+ + + + + + +
Full-stack web application framework that uses Python and MariaDB on the server side and a tightly integrated client side library. Built for [ERPNext](https://erpnext.com) +### Table of Contents +* [Installation](#installation) +* [License](#license) + ### Installation [Install via Frappé Bench](https://github.com/frappe/bench) @@ -20,5 +44,4 @@ For details and documentation, see the website [https://frappe.io](https://frappe.io) ### License - -MIT License +This repository has been released under the [MIT License](LICENSE). diff --git a/frappe/__init__.py b/frappe/__init__.py index c1f1a4567d..9e7039acd4 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -14,7 +14,7 @@ import os, sys, importlib, inspect, json from .exceptions import * from .utils.jinja import get_jenv, get_template, render_template, get_email_from_template -__version__ = '9.0.10' +__version__ = '9.1.0' __title__ = "Frappe Framework" local = Local() @@ -602,7 +602,7 @@ def set_value(doctype, docname, fieldname, value=None): import frappe.client return frappe.client.set_value(doctype, docname, fieldname, value) -def get_doc(arg1, arg2=None): +def get_doc(*args, **kwargs): """Return a `frappe.model.document.Document` object of the given type and name. :param arg1: DocType name as string **or** document JSON. @@ -619,7 +619,7 @@ def get_doc(arg1, arg2=None): """ import frappe.model.document - return frappe.model.document.get_doc(arg1, arg2) + return frappe.model.document.get_doc(*args, **kwargs) def get_last_doc(doctype): """Get last created document of this type.""" diff --git a/frappe/build.js b/frappe/build.js index 45ab9bc9cf..9a4f0c2fc9 100644 --- a/frappe/build.js +++ b/frappe/build.js @@ -123,10 +123,9 @@ function pack(output_path, inputs, minify) { } function babelify(content, path, minify) { - let presets = ['es2015', 'es2016']; - // if(minify) { - // presets.push('babili'); // new babel minifier - // } + let presets = ['env']; + // Minification doesn't work when loading Frappe Desk + // Avoid for now, trace the error and come back. try { return babel.transform(content, { presets: presets, diff --git a/frappe/build.py b/frappe/build.py index 2a201fee6f..e0ce46f923 100644 --- a/frappe/build.py +++ b/frappe/build.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals, print_function from frappe.utils.minify import JavascriptMinify import subprocess +import warnings from six import iteritems, text_type @@ -25,12 +26,12 @@ def setup(): except ImportError: pass app_paths = [os.path.dirname(pymodule.__file__) for pymodule in pymodules] -def bundle(no_compress, make_copy=False, verbose=False): +def bundle(no_compress, make_copy=False, restore=False, verbose=False): """concat / minify js files""" # build js files setup() - make_asset_dirs(make_copy=make_copy) + make_asset_dirs(make_copy=make_copy, restore=restore) # new nodejs build system command = 'node --use_strict ../apps/frappe/frappe/build.js --build' @@ -60,7 +61,8 @@ def watch(no_compress): # time.sleep(3) -def make_asset_dirs(make_copy=False): +def make_asset_dirs(make_copy=False, restore=False): + # don't even think of making assets_path absolute - rm -rf ahead. assets_path = os.path.join(frappe.local.sites_path, "assets") for dir_path in [ os.path.join(assets_path, 'js'), @@ -80,11 +82,28 @@ def make_asset_dirs(make_copy=False): for source, target in symlinks: source = os.path.abspath(source) - if not os.path.exists(target) and os.path.exists(source): - if make_copy: - shutil.copytree(source, target) + if os.path.exists(source): + if restore: + if os.path.exists(target): + if os.path.islink(target): + os.unlink(target) + else: + shutil.rmtree(target) + shutil.copytree(source, target) + elif make_copy: + if os.path.exists(target): + warnings.warn('Target {target} already exists.'.format(target = target)) + else: + shutil.copytree(source, target) else: + if os.path.exists(target): + if os.path.islink(target): + os.unlink(target) + else: + shutil.rmtree(target) os.symlink(source, target) + else: + warnings.warn('Source {source} does not exists.'.format(source = source)) def build(no_compress=False, verbose=False): assets_path = os.path.join(frappe.local.sites_path, "assets") diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index 710327b5fe..9b4d40dcd5 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -8,13 +8,14 @@ from frappe.utils import update_progress_bar @click.command('build') @click.option('--make-copy', is_flag=True, default=False, help='Copy the files instead of symlinking') +@click.option('--restore', is_flag=True, default=False, help='Copy the files instead of symlinking with force') @click.option('--verbose', is_flag=True, default=False, help='Verbose') -def build(make_copy=False, verbose=False): +def build(make_copy=False, restore = False, verbose=False): "Minify + concatenate JS and CSS files, build translations" import frappe.build import frappe frappe.init('') - frappe.build.bundle(False, make_copy=make_copy, verbose=verbose) + frappe.build.bundle(False, make_copy=make_copy, restore = restore, verbose=verbose) @click.command('watch') def watch(): diff --git a/frappe/config/integrations.py b/frappe/config/integrations.py index 92604b716b..cb69cb2a6d 100644 --- a/frappe/config/integrations.py +++ b/frappe/config/integrations.py @@ -72,6 +72,12 @@ def get_data(): "name": "GSuite Templates", "description": _("Google GSuite Templates to integration with DocTypes"), }, + { + "type": "doctype", + "name": "Webhook", + "description": _("Webhooks calling API requests into web apps"), + } + ] } ] diff --git a/frappe/contacts/doctype/address/address.json b/frappe/contacts/doctype/address/address.json index c3b655cec5..d663957580 100644 --- a/frappe/contacts/doctype/address/address.json +++ b/frappe/contacts/doctype/address/address.json @@ -353,7 +353,8 @@ "label": "Email Address", "length": 0, "no_copy": 0, - "permlevel": 0, + "options": "Email", + "permlevel": 0, "print_hide": 0, "print_hide_if_no_value": 0, "read_only": 0, diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py index ac25b97c73..d259b60cbd 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -25,7 +25,7 @@ class Communication(Document): """create email flag queue""" if self.communication_type == "Communication" and self.communication_medium == "Email" \ and self.sent_or_received == "Received" and self.uid and self.uid != -1: - + email_flag_queue = frappe.db.get_value("Email Flag Queue", { "communication": self.name, "is_completed": 0}) @@ -69,7 +69,7 @@ class Communication(Document): def after_insert(self): if not (self.reference_doctype and self.reference_name): return - + if self.reference_doctype == "Communication" and self.sent_or_received == "Sent": frappe.db.set_value("Communication", self.reference_name, "status", "Replied") @@ -94,9 +94,10 @@ class Communication(Document): def on_update(self): """Update parent status as `Open` or `Replied`.""" - update_parent_status(self) - update_comment_in_doc(self) - self.bot_reply() + if self.comment_type != 'Updated': + update_parent_status(self) + update_comment_in_doc(self) + self.bot_reply() def on_trash(self): if (not self.flags.ignore_permissions @@ -264,7 +265,7 @@ def has_permission(doc, ptype, user): if (doc.reference_doctype == "Communication" and doc.reference_name == doc.name) \ or (doc.timeline_doctype == "Communication" and doc.timeline_name == doc.name): return - + if doc.reference_doctype and doc.reference_name: if frappe.has_permission(doc.reference_doctype, ptype="read", doc=doc.reference_name): return True @@ -277,7 +278,9 @@ def get_permission_query_conditions_for_communication(user): if not user: user = frappe.session.user - if "Super Email User" in frappe.get_roles(user): + roles = frappe.get_roles(user) + + if "Super Email User" in roles or "System Manager" in roles: return None else: accounts = frappe.get_all("User Email", filters={ "parent": user }, diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py index 166aa4d84d..c17ac1f5c8 100755 --- a/frappe/core/doctype/communication/email.py +++ b/frappe/core/doctype/communication/email.py @@ -166,9 +166,6 @@ def _notify(doc, print_html=None, print_format=None, attachments=None, def update_parent_status(doc): """Update status of parent document based on who is replying.""" - if doc.communication_type != "Communication": - return - parent = doc.get_parent_doc() if not parent: return diff --git a/frappe/core/doctype/docshare/docshare.json b/frappe/core/doctype/docshare/docshare.json index 83d837b7f8..345d31f0be 100644 --- a/frappe/core/doctype/docshare/docshare.json +++ b/frappe/core/doctype/docshare/docshare.json @@ -1,264 +1,324 @@ { - "allow_copy": 0, - "allow_import": 1, - "allow_rename": 0, - "autoname": "hash", - "beta": 0, - "creation": "2015-02-04 04:33:36.330477", - "custom": 0, - "description": "Internal record of document shares", - "docstatus": 0, - "doctype": "DocType", - "document_type": "System", - "editable_grid": 0, + "allow_copy": 0, + "allow_import": 1, + "allow_rename": 0, + "autoname": "hash", + "beta": 0, + "creation": "2015-02-04 04:33:36.330477", + "custom": 0, + "description": "Internal record of document shares", + "docstatus": 0, + "doctype": "DocType", + "document_type": "System", + "editable_grid": 0, "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "user", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "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": 1, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "user", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 1, + "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": 1, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "share_doctype", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Document Type", - "length": 0, - "no_copy": 0, - "options": "DocType", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 1, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "share_doctype", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Document Type", + "length": 0, + "no_copy": 0, + "options": "DocType", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 1, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "share_name", - "fieldtype": "Dynamic Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Document Name", - "length": 0, - "no_copy": 0, - "options": "share_doctype", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 1, - "set_only_once": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "share_name", + "fieldtype": "Dynamic Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Document Name", + "length": 0, + "no_copy": 0, + "options": "share_doctype", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 1, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "fieldname": "read", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Read", - "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_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "0", + "fieldname": "read", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Read", + "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_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "fieldname": "write", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Write", - "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_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "0", + "fieldname": "write", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Write", + "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_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "fieldname": "share", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Share", - "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_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "0", + "fieldname": "share", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Share", + "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_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "everyone", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Everyone", - "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_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "everyone", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Everyone", + "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_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "1", + "fieldname": "notify_by_email", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Notify by email", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 1, + "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": "1", + "fieldname": "notify_by_email", + "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": "Notify by email", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 1, + "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 } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 1, - "in_dialog": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2016-12-29 14:40:40.284335", - "modified_by": "Administrator", - "module": "Core", - "name": "DocShare", - "name_case": "", - "owner": "Administrator", + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 1, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2017-09-15 15:58:34.126438", + "modified_by": "Administrator", + "module": "Core", + "name": "DocShare", + "name_case": "", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 0, - "export": 1, - "if_owner": 0, - "import": 1, - "is_custom": 0, - "permlevel": 0, - "print": 0, - "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": 0, + "export": 1, + "if_owner": 0, + "import": 1, + "permlevel": 0, + "print": 0, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, "write": 1 } - ], - "quick_entry": 0, - "read_only": 1, - "read_only_onload": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 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 -} \ No newline at end of file +} diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index b0fff5b941..fda42d7557 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -595,6 +595,15 @@ 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"] + for field in depends_on_fields: + depends_on = docfield.get(field, None) + if depends_on and ("=" in depends_on) and \ + re.match("""[\w\.:_]+\s*={1}\s*[\w\.@'"]+""", depends_on): + frappe.throw(_("Invalid {0} condition").format(frappe.unscrub(field)), frappe.ValidationError) + fields = meta.get("fields") fieldname_list = [d.fieldname for d in fields] @@ -620,6 +629,7 @@ def validate_fields(meta): check_in_global_search(d) check_illegal_default(d) check_unique_and_text(d) + check_illegal_depends_on_conditions(d) check_fold(fields) check_search_fields(meta, fields) @@ -753,6 +763,9 @@ def validate_permissions(doctype, for_remove=False): def make_module_and_roles(doc, perm_fieldname="permissions"): """Make `Module Def` and `Role` records if already not made. Called while installing.""" try: + if doc.restrict_to_domain and not frappe.db.exists('Domain', doc.restrict_to_domain): + frappe.get_doc(dict(doctype='Domain', domain=doc.restrict_to_domain)).insert() + if not frappe.db.exists("Module Def", doc.module): m = frappe.get_doc({"doctype": "Module Def", "module_name": doc.module}) m.app_name = frappe.local.module_app[frappe.scrub(doc.module)] diff --git a/frappe/core/doctype/doctype/test_doctype.py b/frappe/core/doctype/doctype/test_doctype.py index 81320c2f38..41a4267c97 100644 --- a/frappe/core/doctype/doctype/test_doctype.py +++ b/frappe/core/doctype/doctype/test_doctype.py @@ -10,13 +10,22 @@ import unittest class TestDocType(unittest.TestCase): - def new_doctype(self, name, unique=0): + def new_doctype(self, name, unique=0, depends_on=''): return frappe.get_doc({ "doctype": "DocType", "module": "Core", "custom": 1, - "fields": [{"label": "Some Field", "fieldname": "some_fieldname", "fieldtype": "Data", "unique": unique}], - "permissions": [{"role": "System Manager", "read": 1}], + "fields": [{ + "label": "Some Field", + "fieldname": "some_fieldname", + "fieldtype": "Data", + "unique": unique, + "depends_on": depends_on, + }], + "permissions": [{ + "role": "System Manager", + "read": 1 + }], "name": name }) @@ -71,4 +80,28 @@ class TestDocType(unittest.TestCase): field.fieldtype = "HTML" field.label = "Some HTML Field" doc.search_fields = "some_fieldname,some_html_field" - self.assertRaises(frappe.ValidationError, doc.save) \ No newline at end of file + self.assertRaises(frappe.ValidationError, doc.save) + + def test_depends_on_fields(self): + doc = self.new_doctype("Test Depends On", depends_on="eval:doc.__islocal == 0") + doc.insert() + + # check if the assignment operation is allowed in depends_on + field = doc.fields[0] + field.depends_on = "eval:doc.__islocal = 0" + self.assertRaises(frappe.ValidationError, doc.save) + + def test_all_depends_on_fields_conditions(self): + import re + + docfields = frappe.get_all("DocField", or_filters={ + "ifnull(depends_on, '')": ("!=", ''), + "ifnull(collapsible_depends_on, '')": ("!=", '') + }, fields=["parent", "depends_on", "collapsible_depends_on", "fieldname", "fieldtype"]) + + pattern = """[\w\.:_]+\s*={1}\s*[\w\.@'"]+""" + for field in docfields: + for depends_on in ["depends_on", "collapsible_depends_on"]: + condition = field.get(depends_on) + if condition: + self.assertFalse(re.match(pattern, condition)) \ No newline at end of file diff --git a/frappe/core/doctype/domain_settings/domain_settings.js b/frappe/core/doctype/domain_settings/domain_settings.js index 39cd8d5dd9..1750573111 100644 --- a/frappe/core/doctype/domain_settings/domain_settings.js +++ b/frappe/core/doctype/domain_settings/domain_settings.js @@ -42,9 +42,11 @@ frappe.DomainsEditor = frappe.CheckboxEditor.extend({ get_template: function() { return ` -
- - {{__(item)}} +
+
`; }, diff --git a/frappe/core/doctype/language/language.py b/frappe/core/doctype/language/language.py index 83d68acb36..8c7e01cb62 100644 --- a/frappe/core/doctype/language/language.py +++ b/frappe/core/doctype/language/language.py @@ -14,7 +14,7 @@ def export_languages_json(): languages = frappe.db.get_all('Language', fields=['name', 'language_name']) languages = [{'name': d.language_name, 'code': d.name} for d in languages] - languages.sort(lambda a,b: 1 if a['code'] > b['code'] else -1) + languages.sort(key = lambda a: a['code']) with open(frappe.get_app_path('frappe', 'geo', 'languages.json'), 'w') as f: f.write(frappe.as_json(languages)) diff --git a/frappe/core/doctype/user/test_user.py b/frappe/core/doctype/user/test_user.py index c9ed243841..7d25254e4b 100644 --- a/frappe/core/doctype/user/test_user.py +++ b/frappe/core/doctype/user/test_user.py @@ -162,18 +162,13 @@ class TestUser(unittest.TestCase): # from frappe.frappeclient import FrappeClient # update_site_config('deny_multiple_sessions', 0) # - # print 'conn1' # conn1 = FrappeClient(get_url(), "test@example.com", "Eastern_43A1W", verify=False) # test_request(conn1) # - # print 'conn2' # conn2 = FrappeClient(get_url(), "test@example.com", "Eastern_43A1W", verify=False) # test_request(conn2) # # update_site_config('deny_multiple_sessions', 1) - # - # print 'conn3' - # # conn3 = FrappeClient(get_url(), "test@example.com", "Eastern_43A1W", verify=False) # test_request(conn3) # diff --git a/frappe/core/page/data_import_tool/data_import_tool.py b/frappe/core/page/data_import_tool/data_import_tool.py index 0bf1e97bb2..8338848561 100644 --- a/frappe/core/page/data_import_tool/data_import_tool.py +++ b/frappe/core/page/data_import_tool/data_import_tool.py @@ -36,7 +36,7 @@ def import_file_by_path(path, ignore_links=False, overwrite=False, submit=False, def export_csv(doctype, path): from frappe.core.page.data_import_tool.exporter import get_template - with open(path, "w") as csvfile: + with open(path, "wb") as csvfile: get_template(doctype=doctype, all_doctypes="Yes", with_data="Yes") csvfile.write(frappe.response.result.encode("utf-8")) diff --git a/frappe/core/page/data_import_tool/exporter.py b/frappe/core/page/data_import_tool/exporter.py index 4f7bf8a067..7920059957 100644 --- a/frappe/core/page/data_import_tool/exporter.py +++ b/frappe/core/page/data_import_tool/exporter.py @@ -81,7 +81,7 @@ def get_template(doctype=None, parent_doctype=None, all_doctypes="No", with_data if field and ((select_columns and f[0] in select_columns[dt]) or not select_columns): tablecolumns.append(field) - tablecolumns.sort(lambda a, b: int(a.idx - b.idx)) + tablecolumns.sort(key = lambda a: int(a.idx)) _column_start_end = frappe._dict(start=0) diff --git a/frappe/core/page/data_import_tool/test_exporter_fixtures.py b/frappe/core/page/data_import_tool/test_exporter_fixtures.py index f99664fb8f..bfc9c80ae1 100644 --- a/frappe/core/page/data_import_tool/test_exporter_fixtures.py +++ b/frappe/core/page/data_import_tool/test_exporter_fixtures.py @@ -16,7 +16,7 @@ class TestDataImportFixtures(unittest.TestCase): def test_Custom_Script_fixture_simple(self): fixture = "Custom Script" path = frappe.scrub(fixture) + "_original_style.csv" - # print "teste done {}".format(path) + export_csv(fixture, path) self.assertTrue(True) os.remove(path) @@ -24,7 +24,7 @@ class TestDataImportFixtures(unittest.TestCase): def test_Custom_Script_fixture_simple_name_equal_default(self): fixture = ["Custom Script", {"name":["Item-Client"]}] path = frappe.scrub(fixture[0]) + "_simple_name_equal_default.csv" - # print "teste done {}".format(path) + export_csv(fixture, path) self.assertTrue(True) os.remove(path) @@ -32,7 +32,7 @@ class TestDataImportFixtures(unittest.TestCase): def test_Custom_Script_fixture_simple_name_equal(self): fixture = ["Custom Script", {"name":["Item-Client"],"op":"="}] path = frappe.scrub(fixture[0]) + "_simple_name_equal.csv" - # print "teste done {}".format(path) + export_csv(fixture, path) self.assertTrue(True) os.remove(path) @@ -40,7 +40,7 @@ class TestDataImportFixtures(unittest.TestCase): def test_Custom_Script_fixture_simple_name_not_equal(self): fixture = ["Custom Script", {"name":["Item-Client"],"op":"!="}] path = frappe.scrub(fixture[0]) + "_simple_name_not_equal.csv" - # print "teste done {}".format(path) + export_csv(fixture, path) self.assertTrue(True) os.remove(path) @@ -49,7 +49,7 @@ class TestDataImportFixtures(unittest.TestCase): def test_Custom_Script_fixture_simple_name_at_least_equal(self): fixture = ["Custom Script", {"name":"Item-Cli"}] path = frappe.scrub(fixture[0]) + "_simple_name_at_least_equal.csv" - # print "teste done {}".format(path) + export_csv(fixture, path) self.assertTrue(True) os.remove(path) @@ -57,7 +57,7 @@ class TestDataImportFixtures(unittest.TestCase): def test_Custom_Script_fixture_multi_name_equal(self): fixture = ["Custom Script", {"name":["Item-Client", "Customer-Client"],"op":"="}] path = frappe.scrub(fixture[0]) + "_multi_name_equal.csv" - # print "teste done {}".format(path) + export_csv(fixture, path) self.assertTrue(True) os.remove(path) @@ -65,7 +65,7 @@ class TestDataImportFixtures(unittest.TestCase): def test_Custom_Script_fixture_multi_name_not_equal(self): fixture = ["Custom Script", {"name":["Item-Client", "Customer-Client"],"op":"!="}] path = frappe.scrub(fixture[0]) + "_multi_name_not_equal.csv" - # print "teste done {}".format(path) + export_csv(fixture, path) self.assertTrue(True) os.remove(path) @@ -73,7 +73,7 @@ class TestDataImportFixtures(unittest.TestCase): def test_Custom_Script_fixture_empty_object(self): fixture = ["Custom Script", {}] path = frappe.scrub(fixture[0]) + "_empty_object_should_be_all.csv" - # print "teste done {}".format(path) + export_csv(fixture, path) self.assertTrue(True) os.remove(path) @@ -81,7 +81,7 @@ class TestDataImportFixtures(unittest.TestCase): def test_Custom_Script_fixture_just_list(self): fixture = ["Custom Script"] path = frappe.scrub(fixture[0]) + "_just_list_should_be_all.csv" - # print "teste done {}".format(path) + export_csv(fixture, path) self.assertTrue(True) os.remove(path) @@ -90,7 +90,7 @@ class TestDataImportFixtures(unittest.TestCase): def test_Custom_Script_fixture_rex_no_flags(self): fixture = ["Custom Script", {"name":r"^[i|A]"}] path = frappe.scrub(fixture[0]) + "_rex_no_flags.csv" - # print "teste done {}".format(path) + export_csv(fixture, path) self.assertTrue(True) os.remove(path) @@ -98,7 +98,7 @@ class TestDataImportFixtures(unittest.TestCase): def test_Custom_Script_fixture_rex_with_flags(self): fixture = ["Custom Script", {"name":r"^[i|A]", "flags":"L,M"}] path = frappe.scrub(fixture[0]) + "_rex_with_flags.csv" - # print "teste done {}".format(path) + export_csv(fixture, path) self.assertTrue(True) os.remove(path) @@ -107,7 +107,7 @@ class TestDataImportFixtures(unittest.TestCase): def test_Custom_Field_fixture_simple(self): fixture = "Custom Field" path = frappe.scrub(fixture) + "_original_style.csv" - # print "teste done {}".format(path) + export_csv(fixture, path) self.assertTrue(True) os.remove(path) @@ -115,7 +115,7 @@ class TestDataImportFixtures(unittest.TestCase): def test_Custom_Field_fixture_simple_name_equal_default(self): fixture = ["Custom Field", {"name":["Item-vat"]}] path = frappe.scrub(fixture[0]) + "_simple_name_equal_default.csv" - # print "teste done {}".format(path) + export_csv(fixture, path) self.assertTrue(True) os.remove(path) @@ -123,7 +123,7 @@ class TestDataImportFixtures(unittest.TestCase): def test_Custom_Field_fixture_simple_name_equal(self): fixture = ["Custom Field", {"name":["Item-vat"],"op":"="}] path = frappe.scrub(fixture[0]) + "_simple_name_equal.csv" - # print "teste done {}".format(path) + export_csv(fixture, path) self.assertTrue(True) os.remove(path) @@ -131,7 +131,7 @@ class TestDataImportFixtures(unittest.TestCase): def test_Custom_Field_fixture_simple_name_not_equal(self): fixture = ["Custom Field", {"name":["Item-vat"],"op":"!="}] path = frappe.scrub(fixture[0]) + "_simple_name_not_equal.csv" - # print "teste done {}".format(path) + export_csv(fixture, path) self.assertTrue(True) os.remove(path) @@ -140,7 +140,7 @@ class TestDataImportFixtures(unittest.TestCase): def test_Custom_Field_fixture_simple_name_at_least_equal(self): fixture = ["Custom Field", {"name":"Item-va"}] path = frappe.scrub(fixture[0]) + "_simple_name_at_least_equal.csv" - # print "teste done {}".format(path) + export_csv(fixture, path) self.assertTrue(True) os.remove(path) @@ -148,7 +148,7 @@ class TestDataImportFixtures(unittest.TestCase): def test_Custom_Field_fixture_multi_name_equal(self): fixture = ["Custom Field", {"name":["Item-vat", "Bin-vat"],"op":"="}] path = frappe.scrub(fixture[0]) + "_multi_name_equal.csv" - # print "teste done {}".format(path) + export_csv(fixture, path) self.assertTrue(True) os.remove(path) @@ -156,7 +156,7 @@ class TestDataImportFixtures(unittest.TestCase): def test_Custom_Field_fixture_multi_name_not_equal(self): fixture = ["Custom Field", {"name":["Item-vat", "Bin-vat"],"op":"!="}] path = frappe.scrub(fixture[0]) + "_multi_name_not_equal.csv" - # print "teste done {}".format(path) + export_csv(fixture, path) self.assertTrue(True) os.remove(path) @@ -164,7 +164,7 @@ class TestDataImportFixtures(unittest.TestCase): def test_Custom_Field_fixture_empty_object(self): fixture = ["Custom Field", {}] path = frappe.scrub(fixture[0]) + "_empty_object_should_be_all.csv" - # print "teste done {}".format(path) + export_csv(fixture, path) self.assertTrue(True) os.remove(path) @@ -172,7 +172,7 @@ class TestDataImportFixtures(unittest.TestCase): def test_Custom_Field_fixture_just_list(self): fixture = ["Custom Field"] path = frappe.scrub(fixture[0]) + "_just_list_should_be_all.csv" - # print "teste done {}".format(path) + export_csv(fixture, path) self.assertTrue(True) os.remove(path) @@ -181,7 +181,7 @@ class TestDataImportFixtures(unittest.TestCase): def test_Custom_Field_fixture_rex_no_flags(self): fixture = ["Custom Field", {"name":r"^[r|L]"}] path = frappe.scrub(fixture[0]) + "_rex_no_flags.csv" - # print "teste done {}".format(path) + export_csv(fixture, path) self.assertTrue(True) os.remove(path) @@ -189,7 +189,7 @@ class TestDataImportFixtures(unittest.TestCase): def test_Custom_Field_fixture_rex_with_flags(self): fixture = ["Custom Field", {"name":r"^[i|A]", "flags":"L,M"}] path = frappe.scrub(fixture[0]) + "_rex_with_flags.csv" - # print "teste done {}".format(path) + export_csv(fixture, path) self.assertTrue(True) os.remove(path) @@ -199,7 +199,7 @@ class TestDataImportFixtures(unittest.TestCase): def test_Doctype_fixture_simple(self): fixture = "ToDo" path = "Doctype_" + frappe.scrub(fixture) + "_original_style_should_be_all.csv" - # print "teste done {}".format(path) + export_csv(fixture, path) self.assertTrue(True) os.remove(path) @@ -207,7 +207,7 @@ class TestDataImportFixtures(unittest.TestCase): def test_Doctype_fixture_simple_name_equal_default(self): fixture = ["ToDo", {"name":["TDI00000008"]}] path = "Doctype_" + frappe.scrub(fixture[0]) + "_simple_name_equal_default.csv" - # print "teste done {}".format(path) + export_csv(fixture, path) self.assertTrue(True) os.remove(path) @@ -215,7 +215,7 @@ class TestDataImportFixtures(unittest.TestCase): def test_Doctype_fixture_simple_name_equal(self): fixture = ["ToDo", {"name":["TDI00000002"],"op":"="}] path = "Doctype_" + frappe.scrub(fixture[0]) + "_simple_name_equal.csv" - # print "teste done {}".format(path) + export_csv(fixture, path) self.assertTrue(True) os.remove(path) @@ -223,7 +223,7 @@ class TestDataImportFixtures(unittest.TestCase): def test_Doctype_simple_name_not_equal(self): fixture = ["ToDo", {"name":["TDI00000002"],"op":"!="}] path = "Doctype_" + frappe.scrub(fixture[0]) + "_simple_name_not_equal.csv" - # print "teste done {}".format(path) + export_csv(fixture, path) self.assertTrue(True) os.remove(path) @@ -232,7 +232,7 @@ class TestDataImportFixtures(unittest.TestCase): def test_Doctype_fixture_simple_name_at_least_equal(self): fixture = ["ToDo", {"name":"TDI"}] path = "Doctype_" + frappe.scrub(fixture[0]) + "_simple_name_at_least_equal.csv" - # print "teste done {}".format(path) + export_csv(fixture, path) self.assertTrue(True) os.remove(path) @@ -240,7 +240,7 @@ class TestDataImportFixtures(unittest.TestCase): def test_Doctype_multi_name_equal(self): fixture = ["ToDo", {"name":["TDI00000002", "TDI00000008"],"op":"="}] path = "Doctype_" + frappe.scrub(fixture[0]) + "_multi_name_equal.csv" - # print "teste done {}".format(path) + export_csv(fixture, path) self.assertTrue(True) os.remove(path) @@ -248,7 +248,7 @@ class TestDataImportFixtures(unittest.TestCase): def test_Doctype_multi_name_not_equal(self): fixture = ["ToDo", {"name":["TDI00000002", "TDI00000008"],"op":"!="}] path = "Doctype_" + frappe.scrub(fixture[0]) + "_multi_name_not_equal.csv" - # print "teste done {}".format(path) + export_csv(fixture, path) self.assertTrue(True) os.remove(path) @@ -256,7 +256,7 @@ class TestDataImportFixtures(unittest.TestCase): def test_Doctype_fixture_empty_object(self): fixture = ["ToDo", {}] path = "Doctype_" + frappe.scrub(fixture[0]) + "_empty_object_should_be_all.csv" - # print "teste done {}".format(path) + export_csv(fixture, path) self.assertTrue(True) os.remove(path) @@ -264,7 +264,7 @@ class TestDataImportFixtures(unittest.TestCase): def test_Doctype_fixture_just_list(self): fixture = ["ToDo"] path = "Doctype_" + frappe.scrub(fixture[0]) + "_just_list_should_be_all.csv" - # print "teste done {}".format(path) + export_csv(fixture, path) self.assertTrue(True) os.remove(path) @@ -273,7 +273,7 @@ class TestDataImportFixtures(unittest.TestCase): def test_Doctype_fixture_rex_no_flags(self): fixture = ["ToDo", {"name":r"^TDi"}] path = "Doctype_" + frappe.scrub(fixture[0]) + "_rex_no_flags_should_be_all.csv" - # print "teste done {}".format(path) + export_csv(fixture, path) self.assertTrue(True) os.remove(path) @@ -281,7 +281,7 @@ class TestDataImportFixtures(unittest.TestCase): def test_Doctype_fixture_rex_with_flags(self): fixture = ["ToDo", {"name":r"^TDi", "flags":"L,M"}] path = "Doctype_" + frappe.scrub(fixture[0]) + "_rex_with_flags_should_be_none.csv" - # print "teste done {}".format(path) + export_csv(fixture, path) self.assertTrue(True) os.remove(path) diff --git a/frappe/core/page/desktop/desktop.js b/frappe/core/page/desktop/desktop.js index eae5b7a35d..d18f7dd55c 100644 --- a/frappe/core/page/desktop/desktop.js +++ b/frappe/core/page/desktop/desktop.js @@ -112,13 +112,17 @@ $.extend(frappe.desktop, { }, setup_module_click: function() { + frappe.desktop.wiggling = false; + if(frappe.list_desktop) { frappe.desktop.wrapper.on("click", ".desktop-list-item", function() { frappe.desktop.open_module($(this)); }); } else { frappe.desktop.wrapper.on("click", ".app-icon", function() { - frappe.desktop.open_module($(this).parent()); + if ( !frappe.desktop.wiggling ) { + frappe.desktop.open_module($(this).parent()); + } }); } frappe.desktop.wrapper.on("click", ".circle", function() { @@ -127,6 +131,116 @@ $.extend(frappe.desktop, { frappe.ui.notifications.show_open_count_list(doctype); } }); + + frappe.desktop.setup_wiggle(); + }, + + setup_wiggle: () => { + // Wiggle, Wiggle, Wiggle. + const DURATION_LONG_PRESS = 1000; + // lesser the antidode, more the wiggle (like your drunk uncle) + // 75 seems good to replicate the iOS feels. + const WIGGLE_ANTIDODE = 75; + + var timer_id = 0; + const $cases = frappe.desktop.wrapper.find('.case-wrapper'); + const $icons = frappe.desktop.wrapper.find('.app-icon'); + const $notis = $(frappe.desktop.wrapper.find('.circle').toArray().filter((object) => { + // This hack is so bad, I should punch myself. + // Seriously, punch yourself. + const text = $(object).find('.circle-text').html(); + + return text; + })); + + const clearWiggle = () => { + const $closes = $cases.find('.module-remove'); + $closes.hide(); + $notis.show(); + + $icons.trigger('stopRumble'); + + frappe.desktop.wiggling = false; + }; + + // initiate wiggling. + $icons.jrumble({ + speed: WIGGLE_ANTIDODE // seems neat enough to match the iOS way + }); + + frappe.desktop.wrapper.on('mousedown', '.app-icon', () => { + timer_id = setTimeout(() => { + frappe.desktop.wiggling = true; + // hide all notifications. + $notis.hide(); + + $cases.each((i) => { + const $case = $($cases[i]); + const template = + ` +
+
+ + × + +
+
+ `; + + $case.append(template); + const $close = $case.find('.module-remove'); + const name = $case.attr('title'); + $close.click(() => { + // good enough to create dynamic dialogs? + const dialog = new frappe.ui.Dialog({ + title: __(`Hide ${name}?`) + }); + dialog.set_primary_action(__('Hide'), () => { + frappe.call({ + method: 'frappe.desk.doctype.desktop_icon.desktop_icon.hide', + args: { name: name }, + freeze: true, + callback: (response) => + { + if ( response.message ) { + location.reload(); + } + } + }) + + dialog.hide(); + + clearWiggle(); + }); + // Hacks, Hacks and Hacks. + var $cancel = dialog.get_close_btn(); + $cancel.click(() => { + clearWiggle(); + }); + $cancel.html(__(`Cancel`)); + + dialog.show(); + }); + }); + + $icons.trigger('startRumble'); + }, DURATION_LONG_PRESS); + }); + frappe.desktop.wrapper.on('mouseup mouseleave', '.app-icon', () => { + clearTimeout(timer_id); + }); + + // also stop wiggling if clicked elsewhere. + $('body').click((event) => { + if ( frappe.desktop.wiggling ) { + const $target = $(event.target); + // our target shouldn't be .app-icons or .close + const $parent = $target.parents('.case-wrapper'); + if ( $parent.length == 0 ) + clearWiggle(); + } + }); + // end wiggle }, open_module: function(parent) { @@ -212,8 +326,8 @@ $.extend(frappe.desktop, { notifier.toggle(sum ? true : false); var circle = notifier.find(".circle-text"); var text = sum || ''; - if(text > 20) { - text = '20+'; + if(text > 99) { + text = '99+'; } if(circle.length) { diff --git a/frappe/custom/doctype/custom_field/custom_field.py b/frappe/custom/doctype/custom_field/custom_field.py index c81ad6cdf4..6eb3eef544 100644 --- a/frappe/custom/doctype/custom_field/custom_field.py +++ b/frappe/custom/doctype/custom_field/custom_field.py @@ -18,8 +18,8 @@ class CustomField(Document): if not self.label: frappe.throw(_("Label is mandatory")) # remove special characters from fieldname - self.fieldname = filter(lambda x: x.isdigit() or x.isalpha() or '_', - cstr(self.label).lower().replace(' ','_')) + self.fieldname = "".join(filter(lambda x: x.isdigit() or x.isalpha() or '_', + cstr(self.label).lower().replace(' ','_'))) # fieldnames should be lowercase self.fieldname = self.fieldname.lower() diff --git a/frappe/data/Framework.sql b/frappe/data/Framework.sql index 6aed34520c..4d4fd0f2c0 100644 --- a/frappe/data/Framework.sql +++ b/frappe/data/Framework.sql @@ -117,6 +117,7 @@ CREATE TABLE `tabDocType` ( `editable_grid` int(1) NOT NULL DEFAULT 1, `track_changes` int(1) NOT NULL DEFAULT 0, `module` varchar(255) DEFAULT NULL, + `restrict_to_domain` varchar(255) DEFAULT NULL, `app` varchar(255) DEFAULT NULL, `autoname` varchar(255) DEFAULT NULL, `name_case` varchar(255) DEFAULT NULL, diff --git a/frappe/defaults.py b/frappe/defaults.py index b4a9d5cfab..9fbc55a0d0 100644 --- a/frappe/defaults.py +++ b/frappe/defaults.py @@ -92,7 +92,19 @@ def set_default(key, value, parent, parenttype="__default"): :param value: Default value. :param parent: Usually, **User** to whom the default belongs. :param parenttype: [optional] default is `__default`.""" - frappe.db.sql("""delete from `tabDefaultValue` where defkey=%s and parent=%s""", (key, parent)) + if frappe.db.sql(''' + select + defkey + from + tabDefaultValue + where + defkey=%s and parent=%s + for update''', (key, parent)): + frappe.db.sql(""" + delete from + `tabDefaultValue` + where + defkey=%s and parent=%s""", (key, parent)) if value != None: add_default(key, value, parent) diff --git a/frappe/desk/doctype/desktop_icon/desktop_icon.py b/frappe/desk/doctype/desktop_icon/desktop_icon.py index 1319ffba49..91de421e8f 100644 --- a/frappe/desk/doctype/desktop_icon/desktop_icon.py +++ b/frappe/desk/doctype/desktop_icon/desktop_icon.py @@ -95,7 +95,7 @@ def get_desktop_icons(user=None): icon.hidden = 1 # sort by idx - user_icons.sort(lambda a, b: 1 if a.idx > b.idx else -1) + user_icons.sort(key = lambda a: a.idx) # translate for d in user_icons: @@ -404,3 +404,16 @@ palette = ( ('#4F8EA8', 1), ('#428B46', 1) ) + +@frappe.whitelist() +def hide(name, user = None): + if not user: + user = frappe.session.user + + try: + set_hidden(name, user, hidden = 1) + clear_desktop_icons_cache() + except Exception: + return False + + return True \ No newline at end of file diff --git a/frappe/desk/doctype/todo/test_todo.js b/frappe/desk/doctype/todo/test_todo.js new file mode 100644 index 0000000000..de508991cf --- /dev/null +++ b/frappe/desk/doctype/todo/test_todo.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: ToDo", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially([ + // insert a new ToDo + () => frappe.tests.make('ToDo', [ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/frappe/desk/doctype/todo/todo.json b/frappe/desk/doctype/todo/todo.json index 9f1b4ae452..ca70fd1006 100644 --- a/frappe/desk/doctype/todo/todo.json +++ b/frappe/desk/doctype/todo/todo.json @@ -112,20 +112,18 @@ "bold": 0, "collapsible": 0, "columns": 0, - "fieldname": "color", - "fieldtype": "Color", + "fieldname": "column_break_2", + "fieldtype": "Column Break", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, "in_global_search": 0, - "in_list_view": 1, + "in_list_view": 0, "in_standard_filter": 0, - "label": "Color", "length": 0, "no_copy": 0, "permlevel": 0, - "precision": "", "print_hide": 0, "print_hide_if_no_value": 0, "read_only": 0, @@ -142,18 +140,20 @@ "bold": 0, "collapsible": 0, "columns": 0, - "fieldname": "column_break_2", - "fieldtype": "Column Break", + "fieldname": "color", + "fieldtype": "Color", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, "in_global_search": 0, - "in_list_view": 0, + "in_list_view": 1, "in_standard_filter": 0, + "label": "Color", "length": 0, "no_copy": 0, "permlevel": 0, + "precision": "", "print_hide": 0, "print_hide_if_no_value": 0, "read_only": 0, @@ -544,7 +544,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-09-05 12:54:58.044162", + "modified": "2017-09-30 13:57:29.398598", "modified_by": "Administrator", "module": "Desk", "name": "ToDo", diff --git a/frappe/desk/form/linked_with.py b/frappe/desk/form/linked_with.py index e5192b1edb..39850c4a16 100644 --- a/frappe/desk/form/linked_with.py +++ b/frappe/desk/form/linked_with.py @@ -119,7 +119,7 @@ def _get_linked_doctypes(doctype): if not dt in ret: ret[dt] = {"get_parent": True} - for dt in ret.keys(): + for dt in list(ret.keys()): try: doctype_module = load_doctype_module(dt) except ImportError: diff --git a/frappe/desk/page/setup_wizard/setup_wizard.js b/frappe/desk/page/setup_wizard/setup_wizard.js index 02264e065f..54be2ca009 100644 --- a/frappe/desk/page/setup_wizard/setup_wizard.js +++ b/frappe/desk/page/setup_wizard/setup_wizard.js @@ -26,11 +26,7 @@ frappe.setup = { } frappe.pages['setup-wizard'].on_page_load = function(wrapper) { - // setup page ui - $(".navbar:first").toggle(false); - - var requires = ["/assets/frappe/css/animate.min.css"].concat( - frappe.boot.setup_wizard_requires || []); + var requires = (frappe.boot.setup_wizard_requires || []); frappe.require(requires, function() { frappe.call({ @@ -96,17 +92,23 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides { } setup_keyboard_nav() { - this.container.on('keydown', (e) => { - if(e.which === 13) { - var $target = $(e.target); - if($target.hasClass('prev-btn')) { - $target.trigger('click'); - } else { - this.container.find('.next-btn').trigger('click'); - e.preventDefault(); - } + $('body').on('keydown', this.handle_enter_press.bind(this)); + } + + disable_keyboard_nav() { + $('body').off('keydown', this.handle_enter_press.bind(this)); + } + + handle_enter_press(e) { + if (e.which === frappe.ui.keyCode.ENTER) { + var $target = $(e.target); + if($target.hasClass('prev-btn')) { + $target.trigger('click'); + } else { + this.container.find('.next-btn').trigger('click'); + e.preventDefault(); } - }); + } } before_show_slide() { @@ -118,6 +120,11 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides { } show_slide(id) { + if (id === this.slides.length) { + // show_slide called on last slide + this.action_on_complete(); + return; + } super.show_slide(id); frappe.set_route(this.page_name, id + ""); } @@ -172,6 +179,7 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides { if (!this.current_slide.set_values()) return; this.update_values(); this.show_working_state(); + this.disable_keyboard_nav(); return frappe.call({ method: "frappe.desk.page.setup_wizard.setup_wizard.setup_complete", args: {args: this.values}, @@ -181,8 +189,8 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides { localStorage.setItem("session_last_route", frappe.setup.welcome_page); } setTimeout(function() { - // frappe.ui.toolbar.clear_cache(); - window.location = "/desk"; + // Reload + window.location.href = ''; }, 2000); setTimeout(()=> { $('body').removeClass('setup-state'); @@ -241,6 +249,13 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides { } get_message(title, message="", loading=false) { + const loading_html = loading + ? '
' + : `
+ +
`; + return $(`
@@ -251,12 +266,7 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides {

${message}

- ${loading - ? '
' - : `
-
` - } + ${loading_html}
`); diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index cacfa25e9a..09afea8b3d 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -259,7 +259,7 @@ def get_stats(stats, doctype, filters=[]): stats[tag] = scrub_user_tags(tagcount) stats[tag].append([_("No Tags"), frappe.get_list(doctype, fields=[tag, "count(*)"], - filters=filters +["({0} = ',' or {0} is null)".format(tag)], as_list=True)[0][1]]) + filters=filters +["({0} = ',' or {0} = '' or {0} is null)".format(tag)], as_list=True)[0][1]]) else: stats[tag] = tagcount @@ -269,7 +269,6 @@ def get_stats(stats, doctype, filters=[]): except MySQLdb.OperationalError: # raised when _user_tags column is added on the fly pass - return stats @frappe.whitelist() diff --git a/frappe/docs/assets/img/webhook.png b/frappe/docs/assets/img/webhook.png new file mode 100644 index 0000000000..859b5f57bd Binary files /dev/null and b/frappe/docs/assets/img/webhook.png differ diff --git a/frappe/docs/user/en/guides/integration/webhooks.md b/frappe/docs/user/en/guides/integration/webhooks.md new file mode 100644 index 0000000000..ff7df4015f --- /dev/null +++ b/frappe/docs/user/en/guides/integration/webhooks.md @@ -0,0 +1,103 @@ +# Webhooks + +Webhooks are "user-defined HTTP callbacks". You can create webhook which triggers on Doc Event of the selected DocType. When the `doc_events` occurs, the source site makes an HTTP request to the URI configured for the webhook. Users can configure them to cause events on one site to invoke behaviour on another. + +#### Configure Webhook + +To add Webhook go to + +> Integrations > External Documents > Webhook + +Webhook + + + +1. Select the DocType for which hook needs to be triggered e.g. Note +2. Select the DocEvent for which hook needs to be triggered e.g. on_trash +3. Enter a valid request URL. On occurence of DocEvent, POST request with doc's json as data is made to the URL. +4. Optionally you can add headers to the request to be made. Useful for sending api key if required. +5. Optionally you can select fields and set its `key` to be sent as data json + +e.g. Webhook + +- **DocType** : `Quotation` +- **Doc Event** : `on_update` +- **Request URL** : `https://httpbin.org/post` +- **Webhook Data** : + 1. **Fieldname** : `name` and **Key** : `id` + 2. **Fieldname** : `items` and **Key** : `lineItems` + +Note: if no headers or data is present, request will be made without any header or body + +Example response of request sent by frappe server on `Quotation` - `on_update` to https://httpbin.org/post: + +``` +{ + "args": {}, + "data": "{\"lineItems\": [{\"stock_qty\": 1.0, \"base_price_list_rate\": 1.0, \"image\": \"\", \"creation\": \"2017-09-14 13:41:58.373023\", \"base_amount\": 1.0, \"qty\": 1.0, \"margin_rate_or_amount\": 0.0, \"rate\": 1.0, \"owner\": \"Administrator\", \"stock_uom\": \"Unit\", \"base_net_amount\": 1.0, \"page_break\": 0, \"modified_by\": \"Administrator\", \"base_net_rate\": 1.0, \"discount_percentage\": 0.0, \"item_name\": \"I1\", \"amount\": 1.0, \"actual_qty\": 0.0, \"net_rate\": 1.0, \"conversion_factor\": 1.0, \"warehouse\": \"Finished Goods - R\", \"docstatus\": 0, \"prevdoc_docname\": null, \"uom\": \"Unit\", \"description\": \"I1\", \"parent\": \"QTN-00001\", \"brand\": null, \"gst_hsn_code\": null, \"base_rate\": 1.0, \"item_code\": \"I1\", \"projected_qty\": 0.0, \"margin_type\": \"\", \"doctype\": \"Quotation Item\", \"rate_with_margin\": 0.0, \"pricing_rule\": null, \"price_list_rate\": 1.0, \"name\": \"QUOD/00001\", \"idx\": 1, \"item_tax_rate\": \"{}\", \"item_group\": \"Products\", \"modified\": \"2017-09-14 17:09:51.239271\", \"parenttype\": \"Quotation\", \"customer_item_code\": null, \"net_amount\": 1.0, \"prevdoc_doctype\": null, \"parentfield\": \"items\"}], \"id\": \"QTN-00001\"}", + "files": {}, + "form": {}, + "headers": { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate", + "Connection": "close", + "Content-Length": "1075", + "Host": "httpbin.org", + "User-Agent": "python-requests/2.18.1" + }, + "json": { + "id": "QTN-00001", + "lineItems": [ + { + "actual_qty": 0.0, + "amount": 1.0, + "base_amount": 1.0, + "base_net_amount": 1.0, + "base_net_rate": 1.0, + "base_price_list_rate": 1.0, + "base_rate": 1.0, + "brand": null, + "conversion_factor": 1.0, + "creation": "2017-09-14 13:41:58.373023", + "customer_item_code": null, + "description": "I1", + "discount_percentage": 0.0, + "docstatus": 0, + "doctype": "Quotation Item", + "gst_hsn_code": null, + "idx": 1, + "image": "", + "item_code": "I1", + "item_group": "Products", + "item_name": "I1", + "item_tax_rate": "{}", + "margin_rate_or_amount": 0.0, + "margin_type": "", + "modified": "2017-09-14 17:09:51.239271", + "modified_by": "Administrator", + "name": "QUOD/00001", + "net_amount": 1.0, + "net_rate": 1.0, + "owner": "Administrator", + "page_break": 0, + "parent": "QTN-00001", + "parentfield": "items", + "parenttype": "Quotation", + "prevdoc_docname": null, + "prevdoc_doctype": null, + "price_list_rate": 1.0, + "pricing_rule": null, + "projected_qty": 0.0, + "qty": 1.0, + "rate": 1.0, + "rate_with_margin": 0.0, + "stock_qty": 1.0, + "stock_uom": "Unit", + "uom": "Unit", + "warehouse": "Finished Goods - R" + } + ] + }, + "url": "https://httpbin.org/post" +} +``` \ No newline at end of file diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index 307e2e46ff..53cb6b8dd7 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -273,7 +273,6 @@ class EmailAccount(Document): "uid_reindexed": uid_reindexed } communication = self.insert_communication(msg, args=args) - #self.notify_update() except SentEmailInInbox: frappe.db.rollback() diff --git a/frappe/email/doctype/email_alert/email_alert.js b/frappe/email/doctype/email_alert/email_alert.js index 40bd5b41cd..3f7423bc1b 100755 --- a/frappe/email/doctype/email_alert/email_alert.js +++ b/frappe/email/doctype/email_alert/email_alert.js @@ -6,13 +6,24 @@ frappe.email_alert = { } frappe.model.with_doctype(frm.doc.document_type, function() { - var get_select_options = function(df) { + let get_select_options = function(df) { return {value: df.fieldname, label: df.fieldname + " (" + __(df.label) + ")"}; } - var fields = frappe.get_doc("DocType", frm.doc.document_type).fields; + let get_date_change_options = function() { + let date_options = $.map(fields, function(d) { + return (d.fieldtype=="Date" || d.fieldtype=="Datetime")? + get_select_options(d) : null; + }); + // append creation and modified date to Date Change field + return date_options.concat([ + { value: "creation", label: `creation (${__('Created On')})` }, + { value: "modified", label: `modified (${__('Last Modified Date')})` } + ]); + } - var options = $.map(fields, + let fields = frappe.get_doc("DocType", frm.doc.document_type).fields; + let options = $.map(fields, function(d) { return in_list(frappe.model.no_value_type, d.fieldtype) ? null : get_select_options(d); }); @@ -21,11 +32,9 @@ frappe.email_alert = { frm.set_df_property("set_property_after_alert", "options", [""].concat(options)); // set date changed options - frm.set_df_property("date_changed", "options", $.map(fields, - function(d) { return (d.fieldtype=="Date" || d.fieldtype=="Datetime") ? - get_select_options(d) : null; })); + frm.set_df_property("date_changed", "options", get_date_change_options()); - var email_fields = $.map(fields, + let email_fields = $.map(fields, function(d) { return (d.options == "Email" || (d.options=='User' && d.fieldtype=='Link')) ? get_select_options(d) : null; }); @@ -48,7 +57,14 @@ frappe.ui.form.on("Email Alert", { "istable": 0 } } - }) + }); + frm.set_query("print_format", function() { + return { + "filters": { + "doc_type": frm.doc.document_type + } + } + }); }, refresh: function(frm) { frappe.email_alert.setup_fieldname_select(frm); diff --git a/frappe/email/doctype/email_alert/email_alert.json b/frappe/email/doctype/email_alert/email_alert.json index 197705b89e..fd3763cd14 100755 --- a/frappe/email/doctype/email_alert/email_alert.json +++ b/frappe/email/doctype/email_alert/email_alert.json @@ -705,36 +705,6 @@ "set_only_once": 0, "unique": 0 }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "attach_print", - "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": "Attach Print", - "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, @@ -794,6 +764,99 @@ "search_index": 0, "set_only_once": 0, "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "collapsible_depends_on": "attach_print", + "columns": 0, + "fieldname": "column_break_25", + "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": "Print 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": "attach_print", + "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": "Attach Print", + "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, + "depends_on": "attach_print", + "fieldname": "print_format", + "fieldtype": "Link", + "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": "Print Format", + "length": 0, + "no_copy": 0, + "options": "Print Format", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 } ], "has_web_view": 0, @@ -808,7 +871,7 @@ "istable": 0, "max_attachments": 0, "menu_index": 0, - "modified": "2017-08-13 22:43:49.079330", + "modified": "2017-09-26 20:10:00.061780", "modified_by": "Administrator", "module": "Email", "name": "Email Alert", diff --git a/frappe/email/doctype/email_alert/email_alert.py b/frappe/email/doctype/email_alert/email_alert.py index 4dfa877856..10a964d88f 100755 --- a/frappe/email/doctype/email_alert/email_alert.py +++ b/frappe/email/doctype/email_alert/email_alert.py @@ -117,7 +117,7 @@ def get_context(context): please enable Allow Print For {0} in Print Settings""".format(status)), title=_("Error in Email Alert")) else: - return [frappe.attach_print(doc.doctype, doc.name)] + return [frappe.attach_print(doc.doctype, doc.name, None, self.print_format)] context = get_context(doc) recipients = [] diff --git a/frappe/email/doctype/email_queue/email_queue.json b/frappe/email/doctype/email_queue/email_queue.json index 4445f60a02..e4ebc153b5 100644 --- a/frappe/email/doctype/email_queue/email_queue.json +++ b/frappe/email/doctype/email_queue/email_queue.json @@ -503,7 +503,7 @@ "columns": 0, "fieldname": "attachments", "fieldtype": "Code", - "hidden": 1, + "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, @@ -517,7 +517,7 @@ "precision": "", "print_hide": 0, "print_hide_if_no_value": 0, - "read_only": 0, + "read_only": 1, "remember_last_selected_value": 0, "report_hide": 0, "reqd": 0, @@ -537,7 +537,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-07-07 16:29:15.780393", + "modified": "2017-09-25 15:39:21.781324", "modified_by": "Administrator", "module": "Email", "name": "Email Queue", diff --git a/frappe/email/queue.py b/frappe/email/queue.py index 55713f074f..d34592f055 100755 --- a/frappe/email/queue.py +++ b/frappe/email/queue.py @@ -478,7 +478,7 @@ def prepare_message(email, recipient, recipients_list): if email.add_unsubscribe_link and email.reference_doctype: # is missing the check for unsubscribe message but will not add as there will be no unsubscribe url unsubscribe_url = get_unsubcribed_url(email.reference_doctype, email.reference_name, recipient, email.unsubscribe_method, email.unsubscribe_params) - message = message.replace("", quopri.encodestring(unsubscribe_url)) + message = message.replace("", quopri.encodestring(unsubscribe_url.encode()).decode()) if email.expose_recipients == "header": pass @@ -494,7 +494,7 @@ def prepare_message(email, recipient, recipients_list): email_sent_message = _("This email was sent to {0} and copied to {1}").format(email_sent_to,email_sent_cc) else: email_sent_message = _("This email was sent to {0}").format(email_sent_to) - message = message.replace("", quopri.encodestring(email_sent_message)) + message = message.replace("", quopri.encodestring(email_sent_message.encode()).decode()) message = message.replace("", recipient) diff --git a/frappe/hooks.py b/frappe/hooks.py index 9bdd8352a6..707b2fac37 100755 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -11,7 +11,7 @@ app_color = "orange" source_link = "https://github.com/frappe/frappe" app_license = "MIT" -develop_version = '8.x.x-beta' +develop_version = '9.x.x-develop' app_email = "info@frappe.io" diff --git a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py index 3ceae47a0e..a839049d7b 100644 --- a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py +++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py @@ -146,7 +146,7 @@ def upload_from_folder(path, dropbox_folder, dropbox_client, did_not_upload, err def upload_file_to_dropbox(filename, folder, dropbox_client): create_folder_if_not_exists(folder, dropbox_client) chunk_size = 4 * 1024 * 1024 - file_size = os.path.getsize(filename) + file_size = os.path.getsize(encode(filename)) mode = (dropbox.files.WriteMode.overwrite) f = open(encode(filename), 'rb') diff --git a/frappe/integrations/doctype/stripe_settings/stripe_settings.py b/frappe/integrations/doctype/stripe_settings/stripe_settings.py index d72b435667..c96b508743 100644 --- a/frappe/integrations/doctype/stripe_settings/stripe_settings.py +++ b/frappe/integrations/doctype/stripe_settings/stripe_settings.py @@ -7,7 +7,7 @@ import frappe from frappe.model.document import Document from frappe import _ from six.moves.urllib.parse import urlencode -from frappe.utils import get_url, call_hook_method, cint +from frappe.utils import get_url, call_hook_method, cint, flt from frappe.integrations.utils import make_get_request, make_post_request, create_request_log, create_payment_gateway class StripeSettings(Document): @@ -62,7 +62,7 @@ class StripeSettings(Document): "Bearer {0}".format(self.get_password(fieldname="secret_key", raise_exception=False))} data = { - "amount": cint(self.data.amount)*100, + "amount": cint(flt(self.data.amount)*100), "currency": self.data.currency, "source": self.data.stripe_token_id, "description": self.data.description diff --git a/frappe/integrations/doctype/webhook/__init__.py b/frappe/integrations/doctype/webhook/__init__.py new file mode 100644 index 0000000000..3c8bf1e2af --- /dev/null +++ b/frappe/integrations/doctype/webhook/__init__.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies and contributors +# For license information, please see license.txt + +import frappe + +def run_webhooks(doc, method): + '''Run webhooks for this method''' + if frappe.flags.in_import or frappe.flags.in_patch or frappe.flags.in_install: + return + + if frappe.flags.webhooks_executed is None: + frappe.flags.webhooks_executed = {} + + if frappe.flags.webhooks == None: + # load webhooks from cache + webhooks = frappe.cache().get_value('webhooks') + if webhooks==None: + # query webhooks + webhooks_list = frappe.get_all('Webhook', + fields=["name", "webhook_docevent", "webhook_doctype"]) + + # make webhooks map for cache + webhooks = {} + for w in webhooks_list: + webhooks.setdefault(w.webhook_doctype, []).append(w) + frappe.cache().set_value('webhooks', webhooks) + + frappe.flags.webhooks = webhooks + + # get webhooks for this doctype + webhooks_for_doc = frappe.flags.webhooks.get(doc.doctype, None) + + if not webhooks_for_doc: + # no webhooks, quit + return + + def _webhook_request(webhook): + if not webhook.name in frappe.flags.webhooks_executed.get(doc.name, []): + frappe.enqueue("frappe.integrations.doctype.webhook.webhook.enqueue_webhook", doc=doc, webhook=webhook) + + # keep list of webhooks executed for this doc in this request + # so that we don't run the same webhook for the same document multiple times + # in one request + frappe.flags.webhooks_executed.setdefault(doc.name, []).append(webhook.name) + + event_list = ["on_update", "after_insert", "on_submit", "on_cancel", "on_trash"] + + if not doc.flags.in_insert: + # value change is not applicable in insert + event_list.append('on_change') + event_list.append('before_update_after_submit') + + for webhook in webhooks_for_doc: + event = method if method in event_list else None + if event and webhook.webhook_docevent == event: + _webhook_request(webhook) diff --git a/frappe/integrations/doctype/webhook/test_webhook.js b/frappe/integrations/doctype/webhook/test_webhook.js new file mode 100644 index 0000000000..799b952bed --- /dev/null +++ b/frappe/integrations/doctype/webhook/test_webhook.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: Webhook", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially([ + // insert a new Webhook + () => frappe.tests.make('Webhook', [ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/frappe/integrations/doctype/webhook/test_webhook.py b/frappe/integrations/doctype/webhook/test_webhook.py new file mode 100644 index 0000000000..c43d431670 --- /dev/null +++ b/frappe/integrations/doctype/webhook/test_webhook.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest + +class TestWebhook(unittest.TestCase): + def test_validate_docevents(self): + doc = frappe.new_doc("Webhook") + doc.webhook_doctype = "User" + doc.webhook_docevent = "on_submit" + doc.request_url = "https://httpbin.org/post" + self.assertRaises(frappe.ValidationError, doc.save) + def test_validate_request_url(self): + doc = frappe.new_doc("Webhook") + doc.webhook_doctype = "User" + doc.webhook_docevent = "after_insert" + doc.request_url = "httpbin.org?post" + self.assertRaises(frappe.ValidationError, doc.save) diff --git a/frappe/integrations/doctype/webhook/webhook.js b/frappe/integrations/doctype/webhook/webhook.js new file mode 100644 index 0000000000..3fc78e0d2c --- /dev/null +++ b/frappe/integrations/doctype/webhook/webhook.js @@ -0,0 +1,46 @@ +// Copyright (c) 2017, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.webhook = { + set_fieldname_select: function(frm) { + var doc = frm.doc; + if (doc.webhook_doctype) { + frappe.model.with_doctype(doc.webhook_doctype, function() { + var fields = $.map(frappe.get_doc("DocType", frm.doc.webhook_doctype).fields, function(d) { + if (frappe.model.no_value_type.indexOf(d.fieldtype) === -1 || + d.fieldtype === 'Table') { + return { label: d.label + ' (' + d.fieldtype + ')', value: d.fieldname }; + } + else if (d.fieldtype === 'Currency' || d.fieldtype === 'Float') { + return { label: d.label, value: d.fieldname }; + } + else { + return null; + } + }); + fields.unshift({"label":"Name (Doc Name)","value":"name"}); + frappe.meta.get_docfield("Webhook Data", "fieldname", frm.doc.name).options = [""].concat(fields); + }); + } + } +}; + +frappe.ui.form.on('Webhook', { + refresh: function(frm) { + frappe.webhook.set_fieldname_select(frm); + }, + webhook_doctype: function(frm) { + frappe.webhook.set_fieldname_select(frm); + } +}); + +frappe.ui.form.on("Webhook Data", { + fieldname: function(frm, doctype, name) { + var doc = frappe.get_doc(doctype, name); + var df = $.map(frappe.get_doc("DocType", frm.doc.webhook_doctype).fields, function(d) { + return doc.fieldname == d.fieldname ? d : null; + })[0]; + doc.key = df != undefined ? df.fieldname : "name"; + frm.refresh_field("webhook_data"); + } +}); diff --git a/frappe/integrations/doctype/webhook/webhook.json b/frappe/integrations/doctype/webhook/webhook.json new file mode 100644 index 0000000000..1a3866085a --- /dev/null +++ b/frappe/integrations/doctype/webhook/webhook.json @@ -0,0 +1,367 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2017-09-08 16:16:13.060641", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "", + "fieldname": "sb_doc_events", + "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": "Doc Events", + "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": "webhook_doctype", + "fieldtype": "Link", + "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": "DocType", + "length": 0, + "no_copy": 0, + "options": "DocType", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 1, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "cb_doc_events", + "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": "webhook_docevent", + "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": "Doc Event", + "length": 0, + "no_copy": 0, + "options": "after_insert\non_update\non_submit\non_cancel\non_trash\non_update_after_submit\non_change", + "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": 1, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "sb_webhook", + "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": "Webhook Request", + "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": "request_url", + "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": "Request 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": 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": "sb_webhook_headers", + "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": "Webhook Headers", + "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": "webhook_headers", + "fieldtype": "Table", + "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": "Headers", + "length": 0, + "no_copy": 0, + "options": "Webhook Header", + "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": "sb_webhook_data", + "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": "Webhook Data", + "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": "webhook_data", + "fieldtype": "Table", + "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": "Data", + "length": 0, + "no_copy": 0, + "options": "Webhook Data", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2017-09-14 13:16:53.974340", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Webhook", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 0, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1, + "track_seen": 0 +} \ No newline at end of file diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py new file mode 100644 index 0000000000..6ded0d0ac8 --- /dev/null +++ b/frappe/integrations/doctype/webhook/webhook.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +import json, requests +from frappe import _ +from frappe.model.document import Document +from six.moves.urllib.parse import urlparse +from time import sleep + +class Webhook(Document): + def autoname(self): + self.name = self.webhook_doctype + "-" + self.webhook_docevent + + def validate(self): + self.validate_docevent() + self.validate_request_url() + self.validate_repeating_fields() + + def on_update(self): + frappe.cache().delete_value('webhooks') + + def validate_docevent(self): + if self.webhook_doctype: + is_submittable = frappe.get_value("DocType", self.webhook_doctype, "is_submittable") + if not is_submittable and self.webhook_docevent in ["on_submit", "on_cancel", "on_update_after_submit"]: + frappe.throw(_("DocType must be Submittable for the selected Doc Event")) + + def validate_request_url(self): + try: + request_url = urlparse(self.request_url).netloc + if not request_url: + raise frappe.ValidationError + except Exception as e: + frappe.throw(_("Check Request URL"), exc=e) + + def validate_repeating_fields(self): + """Error when Same Field is entered multiple times in webhook_data""" + webhook_data = [] + for entry in self.webhook_data: + webhook_data.append(entry.fieldname) + + if len(webhook_data)!= len(set(webhook_data)): + frappe.throw(_("Same Field is entered more than once")) + +def enqueue_webhook(doc, webhook): + webhook = frappe.get_doc("Webhook", webhook.get("name")) + headers = {} + data = {} + if webhook.webhook_headers: + for h in webhook.webhook_headers: + if h.get("key") and h.get("value"): + headers[h.get("key")] = h.get("value") + if webhook.webhook_data: + for w in webhook.webhook_data: + for k, v in doc.as_dict().items(): + if k == w.fieldname: + data[w.key] = v + for i in range(3): + try: + r = requests.post(webhook.request_url, data=json.dumps(data), headers=headers, timeout=5) + r.raise_for_status() + frappe.logger().debug({"webhook_success":r.text}) + break + except Exception as e: + frappe.logger().debug({"webhook_error":e, "try": i+1}) + sleep(3*i + 1) + if i !=2: + continue + else: + raise e diff --git a/frappe/integrations/doctype/webhook_data/__init__.py b/frappe/integrations/doctype/webhook_data/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/integrations/doctype/webhook_data/webhook_data.json b/frappe/integrations/doctype/webhook_data/webhook_data.json new file mode 100644 index 0000000000..96ae7f786a --- /dev/null +++ b/frappe/integrations/doctype/webhook_data/webhook_data.json @@ -0,0 +1,130 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2017-09-14 12:08:50.302810", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "fieldname", + "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": "Fieldname", + "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": "cb_doc_data", + "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": "key", + "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": "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, + "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": 1, + "max_attachments": 0, + "modified": "2017-09-14 13:16:58.252176", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Webhook Data", + "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 +} \ No newline at end of file diff --git a/frappe/integrations/doctype/webhook_data/webhook_data.py b/frappe/integrations/doctype/webhook_data/webhook_data.py new file mode 100644 index 0000000000..b7d989410f --- /dev/null +++ b/frappe/integrations/doctype/webhook_data/webhook_data.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, 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 WebhookData(Document): + pass diff --git a/frappe/integrations/doctype/webhook_header/__init__.py b/frappe/integrations/doctype/webhook_header/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/integrations/doctype/webhook_header/webhook_header.json b/frappe/integrations/doctype/webhook_header/webhook_header.json new file mode 100644 index 0000000000..315d28335f --- /dev/null +++ b/frappe/integrations/doctype/webhook_header/webhook_header.json @@ -0,0 +1,101 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2017-09-08 16:27:39.195379", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "key", + "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": "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, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "value", + "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": "Value", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 1, + "max_attachments": 0, + "modified": "2017-09-08 16:28:20.025612", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Webhook Header", + "name_case": "", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1, + "track_seen": 0 +} \ No newline at end of file diff --git a/frappe/integrations/doctype/webhook_header/webhook_header.py b/frappe/integrations/doctype/webhook_header/webhook_header.py new file mode 100644 index 0000000000..11d3ee4085 --- /dev/null +++ b/frappe/integrations/doctype/webhook_header/webhook_header.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, 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 WebhookHeader(Document): + pass diff --git a/frappe/model/document.py b/frappe/model/document.py index 3d588b4689..56a14cff8c 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -15,17 +15,18 @@ import hashlib, json from frappe.model import optional_fields from frappe.utils.file_manager import save_url from frappe.utils.global_search import update_global_search +from frappe.integrations.doctype.webhook import run_webhooks # once_only validation # methods -def get_doc(arg1, arg2=None): +def get_doc(*args, **kwargs): """returns a frappe.model.Document object. :param arg1: Document dict or DocType name. :param arg2: [optional] document name. - There are two ways to call `get_doc` + There are multiple ways to call `get_doc` # will fetch the latest user object (with child table) from the database user = get_doc("User", "test@example.com") @@ -38,23 +39,39 @@ def get_doc(arg1, arg2=None): {"role": "System Manager"} ] }) + + # create new object with keyword arguments + user = get_doc(doctype='User', email_id='test@example.com') """ - if isinstance(arg1, BaseDocument): - return arg1 - elif isinstance(arg1, string_types): - doctype = arg1 - else: - doctype = arg1.get("doctype") + if args: + if isinstance(args[0], BaseDocument): + # already a document + return args[0] + elif isinstance(args[0], string_types): + doctype = args[0] + + elif isinstance(args[0], dict): + # passed a dict + kwargs = args[0] + + else: + raise ValueError('First non keyword argument must be a string or dict') + + if kwargs: + if 'doctype' in kwargs: + doctype = kwargs['doctype'] + else: + raise ValueError('"doctype" is a required key') controller = get_controller(doctype) if controller: - return controller(arg1, arg2) + return controller(*args, **kwargs) - raise ImportError(arg1) + raise ImportError(doctype) class Document(BaseDocument): """All controllers inherit from `Document`.""" - def __init__(self, arg1, arg2=None): + def __init__(self, *args, **kwargs): """Constructor. :param arg1: DocType name as string or document **dict** @@ -67,29 +84,37 @@ class Document(BaseDocument): self._default_new_docs = {} self.flags = frappe._dict() - if arg1 and isinstance(arg1, string_types): - if not arg2: + if args and args[0] and isinstance(args[0], string_types): + # first arugment is doctype + if len(args)==1: # single - self.doctype = self.name = arg1 + self.doctype = self.name = args[0] else: - self.doctype = arg1 - if isinstance(arg2, dict): + self.doctype = args[0] + if isinstance(args[1], dict): # filter - self.name = frappe.db.get_value(arg1, arg2, "name") + self.name = frappe.db.get_value(args[0], args[1], "name") if self.name is None: - frappe.throw(_("{0} {1} not found").format(_(arg1), arg2), frappe.DoesNotExistError) + frappe.throw(_("{0} {1} not found").format(_(args[0]), args[1]), + frappe.DoesNotExistError) else: - self.name = arg2 + self.name = args[1] self.load_from_db() + return - elif isinstance(arg1, dict): - super(Document, self).__init__(arg1) + if args and args[0] and isinstance(args[0], dict): + # first argument is a dict + kwargs = args[0] + + if kwargs: + # init base document + super(Document, self).__init__(kwargs) self.init_valid_columns() else: # incorrect arguments. let's not proceed. - raise frappe.DataError("Document({0}, {1})".format(arg1, arg2)) + raise ValueError('Illegal arguments') def reload(self): """Reload document from database""" @@ -335,13 +360,18 @@ class Document(BaseDocument): self._doc_before_save = frappe.get_doc(self.doctype, self.name) return self._doc_before_save - def set_new_name(self): + def set_new_name(self, force=False): """Calls `frappe.naming.se_new_name` for parent and child docs.""" + if self.flags.name_set and not force: + return + set_new_name(self) # set name for children for d in self.get_all_children(): set_new_name(d) + self.flags.name_set = True + def get_title(self): '''Get the document title based on title_field or `title` or `name`''' return self.get(self.meta.get_title_field()) @@ -625,7 +655,7 @@ class Document(BaseDocument): name=self.name)) def _validate_links(self): - if self.flags.ignore_links: + if self.flags.ignore_links or self._action == "cancel": return invalid_links, cancelled_links = self.get_invalid_links() @@ -672,6 +702,7 @@ class Document(BaseDocument): out = Document.hook(fn)(self, *args, **kwargs) self.run_email_alerts(method) + run_webhooks(self, method) return out @@ -998,7 +1029,7 @@ class Document(BaseDocument): def get_signature(self): """Returns signature (hash) for private URL.""" - return hashlib.sha224(get_datetime_str(self.creation)).hexdigest() + return hashlib.sha224(get_datetime_str(self.creation).encode()).hexdigest() def get_liked_by(self): liked_by = getattr(self, "_liked_by", None) diff --git a/frappe/patches/v6_20x/remove_roles_from_website_user.py b/frappe/patches/v6_20x/remove_roles_from_website_user.py index 347491898f..499ad5ddf4 100644 --- a/frappe/patches/v6_20x/remove_roles_from_website_user.py +++ b/frappe/patches/v6_20x/remove_roles_from_website_user.py @@ -1,6 +1,8 @@ import frappe def execute(): + frappe.reload_doc("core", "doctype", "user_email") + frappe.reload_doc("core", "doctype", "user") for user_name in frappe.get_all('User', filters={'user_type': 'Website User'}): user = frappe.get_doc('User', user_name) if user.roles: diff --git a/frappe/public/build.json b/frappe/public/build.json index 913adfe735..8d10760cf1 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -23,6 +23,7 @@ "public/js/frappe/misc/rating_icons.html" ], "js/control.min.js": [ + "public/js/frappe/ui/capture.js", "public/js/frappe/form/controls/base_control.js", "public/js/frappe/form/controls/base_input.js", "public/js/frappe/form/controls/data.js", @@ -55,12 +56,15 @@ "js/dialog.min.js": [ "public/js/frappe/dom.js", "public/js/frappe/ui/modal.html", + "public/js/frappe/form/formatters.js", "public/js/frappe/form/layout.js", "public/js/frappe/ui/field_group.js", "public/js/frappe/form/link_selector.js", "public/js/frappe/form/multi_select_dialog.js", "public/js/frappe/ui/dialog.js", + "public/js/frappe/ui/capture.js", + "public/js/frappe/form/controls/base_control.js", "public/js/frappe/form/controls/base_input.js", "public/js/frappe/form/controls/data.js", @@ -130,7 +134,9 @@ "public/js/lib/jSignature.min.js", "public/js/frappe/translate.js", "public/js/lib/datepicker/datepicker.min.js", - "public/js/lib/datepicker/locale-all.js" + "public/js/lib/datepicker/locale-all.js", + "public/js/lib/jquery.jrumble.min.js", + "public/js/lib/webcam.min.js" ], "js/desk.min.js": [ "public/js/frappe/class.js", @@ -168,6 +174,7 @@ "public/js/frappe/form/link_selector.js", "public/js/frappe/form/multi_select_dialog.js", "public/js/frappe/ui/dialog.js", + "public/js/frappe/ui/capture.js", "public/js/frappe/ui/app_icon.js", "public/js/frappe/model/model.js", diff --git a/frappe/public/css/desk.css b/frappe/public/css/desk.css index 6f61d46f08..4f9342c1f1 100644 --- a/frappe/public/css/desk.css +++ b/frappe/public/css/desk.css @@ -398,9 +398,9 @@ fieldset[disabled] .form-control { width: 100%; } .awesomplete > ul { - z-index: 1041; + z-index: 1041 !important; transition: none; - background: #fff; + background-color: #fff; max-height: 200px; overflow-y: auto; overflow-x: hidden; @@ -624,6 +624,9 @@ li.user-progress .progress-bar { .frappe-rtl textarea { direction: rtl; } +.frappe-rtl .checkbox .disp-area { + margin-right: -20px; +} .text-editor { height: 400px; background-color: white; diff --git a/frappe/public/css/list.css b/frappe/public/css/list.css index 49ecce16de..abed4bc105 100644 --- a/frappe/public/css/list.css +++ b/frappe/public/css/list.css @@ -288,6 +288,10 @@ display: flex; flex-wrap: wrap; } +.image-view-container .image-view-row { + display: flex; + border-bottom: 1px solid #ebeff2; +} .image-view-container .image-view-item { flex: 0 0 25%; padding: 15px; @@ -360,6 +364,18 @@ border-bottom: 1px solid #EBEFF2; } } +.item-selector { + border: 1px solid #d1d8dd; +} +.item-selector .image-view-row { + width: 100%; +} +.item-selector .image-field { + height: 120px; +} +.item-selector .placeholder-text { + font-size: 48px; +} .image-view-container.three-column .image-view-item { flex: 0 0 33.33333333%; } diff --git a/frappe/public/css/report-rtl.css b/frappe/public/css/report-rtl.css index cc87b52bbf..26709a55e3 100644 --- a/frappe/public/css/report-rtl.css +++ b/frappe/public/css/report-rtl.css @@ -1,3 +1,11 @@ .grid-report { direction: ltr; } + +.chart_area{ + direction: ltr; +} + +.grid-report .show-zero{ + direction: rtl ; +} diff --git a/frappe/public/css/website.css b/frappe/public/css/website.css index 6e33918c6c..5e1d9b2bc8 100644 --- a/frappe/public/css/website.css +++ b/frappe/public/css/website.css @@ -981,3 +981,7 @@ li.footer-child-item { margin: 15px 0px; max-width: 100%; } +.blog-list-item { + padding-top: 30px; + padding-bottom: 30px; +} diff --git a/frappe/public/images/default-avatar.png b/frappe/public/images/default-avatar.png new file mode 100644 index 0000000000..b6413c4912 Binary files /dev/null and b/frappe/public/images/default-avatar.png differ diff --git a/frappe/public/js/frappe/assets.js b/frappe/public/js/frappe/assets.js index 8cdbfeafe0..71c576e291 100644 --- a/frappe/public/js/frappe/assets.js +++ b/frappe/public/js/frappe/assets.js @@ -85,7 +85,7 @@ frappe.assets = { frappe.assets.executed_.push(path) } } - callback(); + callback && callback(); }, // check if the asset exists in diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 0610e6b551..343c39d5f4 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -260,7 +260,7 @@ frappe.Application = Class.extend({ $.each(frappe.boot.notification_info.open_count_doctype, function(doctype, count) { if(count) { $('.open-notification.global[data-doctype="'+ doctype +'"]') - .removeClass("hide").html(count > 20 ? "20+" : count); + .removeClass("hide").html(count > 99 ? "99+" : count); } else { $('.open-notification.global[data-doctype="'+ doctype +'"]') .addClass("hide"); @@ -355,7 +355,7 @@ frappe.Application = Class.extend({ }, make_nav_bar: function() { // toolbar - if(frappe.boot) { + if(frappe.boot && !frappe.boot.in_setup_wizard) { frappe.frappe_toolbar = new frappe.ui.toolbar.Toolbar(); } diff --git a/frappe/public/js/frappe/form/controls/check.js b/frappe/public/js/frappe/form/controls/check.js index f7672658ea..b54571b166 100644 --- a/frappe/public/js/frappe/form/controls/check.js +++ b/frappe/public/js/frappe/form/controls/check.js @@ -5,7 +5,7 @@ frappe.ui.form.ControlCheck = frappe.ui.form.ControlData.extend({
\ \

\ diff --git a/frappe/public/js/frappe/form/controls/color.js b/frappe/public/js/frappe/form/controls/color.js index d13c22d19d..6e8880a72a 100644 --- a/frappe/public/js/frappe/form/controls/color.js +++ b/frappe/public/js/frappe/form/controls/color.js @@ -36,9 +36,11 @@ frappe.ui.form.ControlColor = frappe.ui.form.ControlData.extend({ set_formatted_input: function(value) { this._super(value); - if(!value) value = '#ffffff'; + if (!value) value = '#FFFFFF'; + const contrast = frappe.ui.color.get_contrast_color(value); + this.$input.css({ - "background-color": value + "background-color": value, "color": contrast }); }, bind_events: function () { diff --git a/frappe/public/js/frappe/form/controls/text_editor.js b/frappe/public/js/frappe/form/controls/text_editor.js index 00af468754..cd478f9f48 100644 --- a/frappe/public/js/frappe/form/controls/text_editor.js +++ b/frappe/public/js/frappe/form/controls/text_editor.js @@ -6,6 +6,28 @@ frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({ this.setup_drag_drop(); this.setup_image_dialog(); this.setting_count = 0; + + $(document).on('form-refresh', () => { + // reset last keystroke when a new form is loaded + this.last_keystroke_on = null; + }) + }, + render_camera_button: (context) => { + var ui = $.summernote.ui; + var button = ui.button({ + contents: '', + tooltip: 'Camera', + click: () => { + const capture = new frappe.ui.Capture(); + capture.open(); + + capture.click((data) => { + context.invoke('editor.insertImage', data); + }); + } + }); + + return button.render(); }, make_editor: function() { var me = this; @@ -25,9 +47,12 @@ frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({ ['color', ['color']], ['para', ['ul', 'ol', 'paragraph', 'hr']], //['height', ['height']], - ['media', ['link', 'picture', 'video', 'table']], + ['media', ['link', 'picture', 'camera', 'video', 'table']], ['misc', ['fullscreen', 'codeview']] ], + buttons: { + camera: this.render_camera_button, + }, keyMap: { pc: { 'CTRL+ENTER': '' @@ -54,7 +79,7 @@ frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({ me.parse_validate_and_set_in_model(value); }, onKeydown: function(e) { - me._last_change_on = new Date(); + me.last_keystroke_on = new Date(); var key = frappe.ui.keys.get_key(e); // prevent 'New DocType (Ctrl + B)' shortcut in editor if(['ctrl+b', 'meta+b'].indexOf(key) !== -1) { @@ -80,6 +105,7 @@ frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({ 'outdent': 'fa fa-outdent', 'arrowsAlt': 'fa fa-arrows-alt', 'bold': 'fa fa-bold', + 'camera': 'fa fa-camera', 'caret': 'caret', 'circle': 'fa fa-circle', 'close': 'fa fa-close', @@ -184,20 +210,30 @@ frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({ if(this.setting_count > 2) { // we don't understand how the internal triggers work, - // so if someone is setting the value third time, then quit + // so if someone is setting the value third time in 500ms, + // then quit return; } this.setting_count += 1; - let time_since_last_keystroke = moment() - moment(this._last_change_on); + let time_since_last_keystroke = moment() - moment(this.last_keystroke_on); - if(!this._last_change_on || (time_since_last_keystroke > 3000)) { + if(!this.last_keystroke_on || (time_since_last_keystroke > 3000)) { + // if 3 seconds have passed since the last keystroke and + // we have not set any value in the last 1 second, do this setTimeout(() => this.setting_count = 0, 500); this.editor.summernote('code', value || ''); + this.last_keystroke_on = null; } else { + // user is probably still in the middle of typing + // so lets not mess up the html by re-updating it + // keep checking every second if our 3 second barrier + // has been completed, so that we can refresh the html this._setting_value = setInterval(() => { if(time_since_last_keystroke > 3000) { + // 3 seconds done! lets refresh + // safe to update if(this.last_value !== this.get_input_value()) { // if not already in sync, reset this.editor.summernote('code', this.last_value || ''); @@ -205,6 +241,9 @@ frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({ clearInterval(this._setting_value); this._setting_value = null; this.setting_count = 0; + + // clear timestamp of last keystroke + this.last_keystroke_on = null; } }, 1000); } diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index f1a5ab10fe..93fdabdb27 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -170,8 +170,9 @@ frappe.ui.form.Grid = Class.extend({ } else { // redraw var _scroll_y = $(document).scrollTop(); - this.make_head(); + // to hide checkbox if grid is not editable + this.header_row && this.header_row.toggle_check(); if(!this.grid_rows) { this.grid_rows = []; @@ -652,7 +653,7 @@ frappe.ui.form.Grid = Class.extend({ var btn = this.custom_buttons[label]; if(!btn) { btn = $('') - .css('margin-right', '10px') + .css('margin-right', '4px') .prependTo(this.grid_buttons) .on('click', click); this.custom_buttons[label] = btn; diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index aee170b35c..cea9c04551 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -2,10 +2,10 @@ frappe.ui.form.GridRow = Class.extend({ init: function(opts) { this.on_grid_fields_dict = {}; this.on_grid_fields = []; - this.row_check_html = ''; this.columns = {}; this.columns_list = []; $.extend(this, opts); + this.row_check_html = ''; this.make(); }, make: function() { @@ -121,6 +121,8 @@ frappe.ui.form.GridRow = Class.extend({ if(this.grid_form) { this.grid_form.layout && this.grid_form.layout.refresh(this.doc); } + + this.toggle_check(); }, render_template: function() { this.set_row_index(); @@ -592,4 +594,10 @@ frappe.ui.form.GridRow = Class.extend({ toggle_editable: function(fieldname, editable) { this.set_field_property(fieldname, 'read_only', editable ? 0 : 1); }, + toggle_check: function() { + // to hide checkbox if grid is not editable + this.wrapper + .find('.grid-row-check') + .css("display", this.grid.is_editable()? 'block':'none'); + } }); \ No newline at end of file diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js index 9ec3ee38a3..2334ab9876 100644 --- a/frappe/public/js/frappe/form/layout.js +++ b/frappe/public/js/frappe/form/layout.js @@ -441,7 +441,12 @@ frappe.ui.form.Layout = Class.extend({ var parent = this.frm ? this.frm.doc : null; if(expression.substr(0,5)=='eval:') { - out = eval(expression.substr(5)); + try { + out = eval(expression.substr(5)); + } catch(e) { + frappe.throw(__('Invalid "depends_on" expression')); + } + } else if(expression.substr(0,3)=='fn:' && this.frm) { out = this.frm.script_manager.trigger(expression.substr(3), this.doctype, this.docname); } else { diff --git a/frappe/public/js/frappe/form/share.js b/frappe/public/js/frappe/form/share.js index 828ea69ede..a44a61eeb4 100644 --- a/frappe/public/js/frappe/form/share.js +++ b/frappe/public/js/frappe/form/share.js @@ -140,7 +140,8 @@ frappe.ui.form.Share = Class.extend({ user: user, read: $(d.body).find(".add-share-read").prop("checked") ? 1 : 0, write: $(d.body).find(".add-share-write").prop("checked") ? 1 : 0, - share: $(d.body).find(".add-share-share").prop("checked") ? 1 : 0 + share: $(d.body).find(".add-share-share").prop("checked") ? 1 : 0, + notify: $(d.body).find(".add-share-notify").prop("checked") ? 1 : 0 }, btn: this, callback: function(r) { diff --git a/frappe/public/js/frappe/form/templates/grid_body.html b/frappe/public/js/frappe/form/templates/grid_body.html index 8b3a1b6082..5fef1379b9 100644 --- a/frappe/public/js/frappe/form/templates/grid_body.html +++ b/frappe/public/js/frappe/form/templates/grid_body.html @@ -7,7 +7,9 @@ +
+
+
+
+ +
+
+
+ {% endif %} +
\ No newline at end of file diff --git a/frappe/public/js/frappe/socketio_client.js b/frappe/public/js/frappe/socketio_client.js index 1512f487e6..c7dd77594f 100644 --- a/frappe/public/js/frappe/socketio_client.js +++ b/frappe/public/js/frappe/socketio_client.js @@ -73,7 +73,7 @@ frappe.socketio = { frappe.socketio.doc_subscribe(frm.doctype, frm.docname); }); - $(document).on("form_refresh", function(e, frm) { + $(document).on("form-refresh", function(e, frm) { if (frm.is_new()) { return; } diff --git a/frappe/public/js/frappe/ui/capture.js b/frappe/public/js/frappe/ui/capture.js new file mode 100644 index 0000000000..f555243975 --- /dev/null +++ b/frappe/public/js/frappe/ui/capture.js @@ -0,0 +1,94 @@ +frappe.ui.Capture = class +{ + constructor (options = { }) + { + this.options = Object.assign({}, frappe.ui.Capture.DEFAULT_OPTIONS, options); + this.dialog = new frappe.ui.Dialog(); + this.template = + ` +
+
+
+
+
+ +
+
+
+ + + +
+
+ + +
+
+
+ `; + $(this.dialog.body).append(this.template); + + this.$btnBarSnap = $(this.dialog.body).find('#frappe-capture-btn-toolbar-snap'); + this.$btnBarKnap = $(this.dialog.body).find('#frappe-capture-btn-toolbar-knap'); + this.$btnBarKnap.hide(); + + Webcam.set(this.options); + } + + open ( ) + { + this.dialog.show(); + + Webcam.attach('#frappe-capture'); + } + + freeze ( ) + { + this.$btnBarSnap.hide(); + this.$btnBarKnap.show(); + + Webcam.freeze(); + } + + unfreeze ( ) + { + this.$btnBarSnap.show(); + this.$btnBarKnap.hide(); + + Webcam.unfreeze(); + } + + click (callback) + { + $(this.dialog.body).find('#frappe-capture-btn-snap').click(() => { + this.freeze(); + + $(this.dialog.body).find('#frappe-capture-btn-discard').click(() => { + this.unfreeze(); + }); + + $(this.dialog.body).find('#frappe-capture-btn-accept').click(() => { + Webcam.snap((data) => { + callback(data); + }); + + this.hide(); + }); + }); + } + + hide ( ) + { + Webcam.reset(); + + $(this.dialog.$wrapper).remove(); + } +}; +frappe.ui.Capture.DEFAULT_OPTIONS = +{ + width: 480, height: 320, flip_horiz: true +}; \ No newline at end of file diff --git a/frappe/public/js/frappe/ui/toolbar/notifications.js b/frappe/public/js/frappe/ui/toolbar/notifications.js index 48095c1b8b..dbab78b986 100644 --- a/frappe/public/js/frappe/ui/toolbar/notifications.js +++ b/frappe/public/js/frappe/ui/toolbar/notifications.js @@ -43,7 +43,7 @@ frappe.ui.notifications = { // switch colour on the navbar and disable if no notifications $(".navbar-new-comments") - .html(this.total > 20 ? '20+' : this.total) + .html(this.total > 99 ? '99+' : this.total) .toggleClass("navbar-new-comments-true", this.total ? true : false) .parent().toggleClass("disabled", this.total ? false : true); }, diff --git a/frappe/public/js/frappe/ui/toolbar/search_utils.js b/frappe/public/js/frappe/ui/toolbar/search_utils.js index e8d65f0f19..f355e8547b 100644 --- a/frappe/public/js/frappe/ui/toolbar/search_utils.js +++ b/frappe/public/js/frappe/ui/toolbar/search_utils.js @@ -51,7 +51,7 @@ frappe.search.utils = { var out = { route: match[1] } - if(match[1][0]==='Form') { + if(match[1][0]==='Form' && match[1][2]) { if(match[1][1] !== match[1][2]) { out.label = __(match[1][1]) + " " + match[1][2].bold(); out.value = __(match[1][1]) + " " + match[1][2]; diff --git a/frappe/public/js/frappe/upload.js b/frappe/public/js/frappe/upload.js index 0a1225d2a5..bb727aa415 100644 --- a/frappe/public/js/frappe/upload.js +++ b/frappe/public/js/frappe/upload.js @@ -12,7 +12,13 @@ frappe.upload = { // whether to show public/private checkbox or not opts.show_private = !("is_private" in opts); - + + // make private by default + if (!("options" in opts) || ("options" in opts && + (!opts.options.toLowerCase()=="public" && !opts.options.toLowerCase()=="image"))) { + opts.is_private = 1; + } + var d = null; // create new dialog if no parent given if(!opts.parent) { @@ -237,7 +243,6 @@ frappe.upload = { if (args.file_size) { frappe.upload.validate_max_file_size(args.file_size); } - if(opts.on_attach) { opts.on_attach(args) } else { @@ -252,7 +257,7 @@ frappe.upload = { frappe.upload.upload_to_server(fileobj, args, opts); }, __("Private or Public?")); } else { - if ("is_private" in opts) { + if (!("is_private" in args) && "is_private" in opts) { args["is_private"] = opts.is_private; } diff --git a/frappe/public/js/frappe/views/image/image_view.js b/frappe/public/js/frappe/views/image/image_view.js index 5656c3e78e..6e094909f3 100644 --- a/frappe/public/js/frappe/views/image/image_view.js +++ b/frappe/public/js/frappe/views/image/image_view.js @@ -14,6 +14,12 @@ frappe.views.ImageView = frappe.views.ListRenderer.extend({ this._super(); this.page_title = this.page_title + ' ' + __('Images'); }, + prepare_data: function(data) { + data = this._super(data); + // absolute url if cordova, else relative + data._image_url = this.get_image_url(data); + return data; + }, render_image_view: function () { var html = this.items.map(this.render_item.bind(this)).join(""); this.container = $('
') @@ -22,14 +28,12 @@ frappe.views.ImageView = frappe.views.ListRenderer.extend({ this.container.append(html); }, render_item: function (item) { - var image_url = this.get_image_url(item); var indicator = this.get_indicator_html(item); return frappe.render_template("image_view_item_row", { data: item, indicator: indicator, subject: this.get_subject_html(item, true), additional_columns: this.additional_columns, - item_image: image_url, color: frappe.get_palette(item.item_name) }); }, @@ -112,8 +116,8 @@ frappe.views.GalleryView = Class.extend({ } return { - src: i.image, - msrc: i.image, + src: i._image_url, + msrc: i._image_url, name: i.name, w: width, h: height, diff --git a/frappe/public/js/frappe/views/image/image_view_item_row.html b/frappe/public/js/frappe/views/image/image_view_item_row.html index 80692b8bb8..7eca23dddb 100644 --- a/frappe/public/js/frappe/views/image/image_view_item_row.html +++ b/frappe/public/js/frappe/views/image/image_view_item_row.html @@ -13,18 +13,18 @@
- {% if (!item_image) { %} + {% if (!data._image_url) { %} {%= frappe.get_abbr(data._title) %} {% } %} - {% if (item_image) { %} - {{data.title}} + {% if (data._image_url) { %} + {{data.title}} {% } %}