diff --git a/.eslintrc b/.eslintrc index 4ea7f0edff..84cdc6bb85 100644 --- a/.eslintrc +++ b/.eslintrc @@ -119,7 +119,6 @@ "getCookies": true, "get_url_arg": true, "QUnit": true, - "Snap": true, - "mina": true + "JsBarcode": true } } diff --git a/.travis.yml b/.travis.yml index ef03adb693..38f61ba37b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,6 +21,7 @@ install: - sudo apt-get purge -y mysql-common mysql-server mysql-client - nvm install v7.10.0 - wget https://raw.githubusercontent.com/frappe/bench/master/playbooks/install.py + - sudo python install.py --develop --user travis --without-bench-setup - sudo pip install -e ~/bench @@ -42,6 +43,7 @@ before_script: - echo "USE mysql;\nCREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe';\nFLUSH PRIVILEGES;\n" | mysql -u root -ptravis - echo "USE mysql;\nGRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost';\n" | mysql -u root -ptravis + - cd ~/frappe-bench - bench use test_site - bench reinstall --yes diff --git a/frappe/__init__.py b/frappe/__init__.py index 1ad39e0b32..0d77a099df 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -6,7 +6,7 @@ globals attached to frappe module """ from __future__ import unicode_literals, print_function -from six import iteritems, text_type, string_types +from six import iteritems, binary_type, text_type, string_types from werkzeug.local import Local, release_local import os, sys, importlib, inspect, json @@ -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.1.11' +__version__ = '9.2.0' __title__ = "Frappe Framework" local = Local() @@ -61,7 +61,7 @@ def as_unicode(text, encoding='utf-8'): return text elif text==None: return '' - elif isinstance(text, string_types): + elif isinstance(text, binary_type): return text_type(text, encoding) else: return text_type(text) @@ -475,13 +475,26 @@ def only_for(roles): if not roles.intersection(myroles): raise PermissionError +def get_domain_data(module): + try: + domain_data = get_hooks('domains') + if module in domain_data: + return _dict(get_attr(get_hooks('domains')[module][0] + '.data')) + else: + return _dict() + except ImportError: + if local.flags.in_test: + return _dict() + else: + raise + + def clear_cache(user=None, doctype=None): """Clear **User**, **DocType** or global cache. :param user: If user is given, only user cache is cleared. :param doctype: If doctype is given, only DocType cache is cleared.""" import frappe.sessions - from frappe.core.doctype.domain_settings.domain_settings import clear_domain_cache if doctype: import frappe.model.meta frappe.model.meta.clear_cache(doctype) @@ -493,7 +506,6 @@ def clear_cache(user=None, doctype=None): frappe.sessions.clear_cache() translate.clear_cache() reset_metadata_version() - clear_domain_cache() local.cache = {} local.new_doc_templates = {} @@ -1319,6 +1331,20 @@ def enqueue(*args, **kwargs): import frappe.utils.background_jobs return frappe.utils.background_jobs.enqueue(*args, **kwargs) +def enqueue_doc(*args, **kwargs): + ''' + Enqueue method to be executed using a background worker + + :param doctype: DocType of the document on which you want to run the event + :param name: Name of the document on which you want to run the event + :param method: method string or method object + :param queue: (optional) should be either long, default or short + :param timeout: (optional) should be set according to the functions + :param kwargs: keyword arguments to be passed to the method + ''' + import frappe.utils.background_jobs + return frappe.utils.background_jobs.enqueue_doc(*args, **kwargs) + def get_doctype_app(doctype): def _get_doctype_app(): doctype_module = local.db.get_value("DocType", doctype, "module") @@ -1371,4 +1397,4 @@ def get_system_settings(key): def get_active_domains(): from frappe.core.doctype.domain_settings.domain_settings import get_active_domains - return get_active_domains() \ No newline at end of file + return get_active_domains() diff --git a/frappe/app.py b/frappe/app.py index 79cfdfe442..b2e19beff0 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -128,7 +128,7 @@ def handle_exception(e): http_status_code = getattr(e, "http_status_code", 500) return_as_message = False - if frappe.local.is_ajax or 'application/json' in frappe.local.request.headers.get('Accept', ''): + if frappe.local.is_ajax or 'application/json' in frappe.get_request_header('Accept'): # handle ajax responses first # if the request is ajax, send back the trace or error message response = frappe.utils.response.report_error(http_status_code) diff --git a/frappe/build.js b/frappe/build.js index 9a4f0c2fc9..de12e31ca0 100644 --- a/frappe/build.js +++ b/frappe/build.js @@ -60,11 +60,11 @@ function watch() { io.emit('reload_css', filename); } }); - watch_js(function (filename) { - if(socket_connection) { - io.emit('reload_js', filename); - } - }); + // watch_js(function (filename) { + // if(socket_connection) { + // io.emit('reload_js', filename); + // } + // }); watch_build_json(); }); diff --git a/frappe/config/integrations.py b/frappe/config/integrations.py index cb69cb2a6d..7c28372382 100644 --- a/frappe/config/integrations.py +++ b/frappe/config/integrations.py @@ -32,6 +32,11 @@ def get_data(): "name": "Dropbox Settings", "description": _("Dropbox backup settings"), }, + { + "type": "doctype", + "name": "S3 Backup Settings", + "description": _("S3 Backup Settings"), + }, ] }, { diff --git a/frappe/core/doctype/communication/comment.py b/frappe/core/doctype/communication/comment.py index b5f0f141bc..ca98383f94 100644 --- a/frappe/core/doctype/communication/comment.py +++ b/frappe/core/doctype/communication/comment.py @@ -81,7 +81,17 @@ def notify_mentions(doc): return sender_fullname = get_fullname(frappe.session.user) - parent_doc_label = "{0} {1}".format(_(doc.reference_doctype), doc.reference_name) + title_field = frappe.get_meta(doc.reference_doctype).get_title_field() + title = doc.reference_name if title_field == "name" else \ + frappe.db.get_value(doc.reference_doctype, doc.reference_name, title_field) + + if title != doc.reference_name: + parent_doc_label = "{0}: {1} (#{2})".format(_(doc.reference_doctype), + title, doc.reference_name) + else: + parent_doc_label = "{0}: {1}".format(_(doc.reference_doctype), + doc.reference_name) + subject = _("{0} mentioned you in a comment").format(sender_fullname) recipients = [frappe.db.get_value("User", {"enabled": 1, "username": username, "user_type": "System User"}) diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py index d259b60cbd..5a63381d91 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -9,7 +9,7 @@ from frappe.utils import validate_email_add, get_fullname, strip_html, cstr from frappe.core.doctype.communication.comment import (notify_mentions, update_comment_in_doc, on_trash) from frappe.core.doctype.communication.email import (validate_email, - notify, _notify, update_parent_status) + notify, _notify, update_parent_mins_to_first_response) from frappe.utils.bot import BotReply from frappe.utils import parse_addr @@ -95,7 +95,7 @@ class Communication(Document): def on_update(self): """Update parent status as `Open` or `Replied`.""" if self.comment_type != 'Updated': - update_parent_status(self) + update_parent_mins_to_first_response(self) update_comment_in_doc(self) self.bot_reply() diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py index c17ac1f5c8..28c2e5ff0d 100755 --- a/frappe/core/doctype/communication/email.py +++ b/frappe/core/doctype/communication/email.py @@ -164,32 +164,30 @@ def _notify(doc, print_html=None, print_format=None, attachments=None, is_notification=True if doc.sent_or_received =="Received" else False ) -def update_parent_status(doc): - """Update status of parent document based on who is replying.""" +def update_parent_mins_to_first_response(doc): + """Update mins_to_first_communication of parent document based on who is replying.""" parent = doc.get_parent_doc() if not parent: return - # update parent status only if we create the Email communication + # update parent mins_to_first_communication only if we create the Email communication # ignore in case of only Comment is added if doc.communication_type == "Comment": return status_field = parent.meta.get_field("status") - if status_field: options = (status_field.options or '').splitlines() - # if status has a "Replied" option, then update the status - if 'Replied' in options: - to_status = "Open" if doc.sent_or_received=="Received" else "Replied" - - if to_status in options: - parent.db_set("status", to_status) + # if status has a "Replied" option, then update the status for received communication + if ('Replied' in options) and doc.sent_or_received=="Received": + parent.db_set("status", "Open") + else: + # update the modified date for document + parent.update_modified() update_mins_to_first_communication(parent, doc) parent.run_method('notify_communication', doc) - parent.notify_update() def get_recipients_and_cc(doc, recipients, cc, fetched_from_email_account=False): diff --git a/frappe/core/doctype/docfield/docfield.json b/frappe/core/doctype/docfield/docfield.json index dac2ff6423..b00b0c7b07 100644 --- a/frappe/core/doctype/docfield/docfield.json +++ b/frappe/core/doctype/docfield/docfield.json @@ -96,7 +96,7 @@ "no_copy": 0, "oldfieldname": "fieldtype", "oldfieldtype": "Select", - "options": "Attach\nAttach Image\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nHeading\nHTML\nImage\nInt\nLink\nLong Text\nPassword\nPercent\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nText\nText Editor\nTime\nSignature", + "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nHeading\nHTML\nImage\nInt\nLink\nLong Text\nPassword\nPercent\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nText\nText Editor\nTime\nSignature", "permlevel": 0, "print_hide": 0, "print_hide_if_no_value": 0, @@ -1364,7 +1364,7 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2017-08-29 15:30:55.489568", + "modified": "2017-10-07 19:20:15.888708", "modified_by": "Administrator", "module": "Core", "name": "DocField", diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index c5b9dc436a..be045fbb02 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -321,7 +321,7 @@ class DocType(Document): def export_doc(self): """Export to standard folder `[module]/doctype/[name]/[name].json`.""" from frappe.modules.export_file import export_to_files - export_to_files(record_list=[['DocType', self.name]]) + export_to_files(record_list=[['DocType', self.name]], create_init=True) def import_doc(self): """Import from standard folder `[module]/doctype/[name]/[name].json`.""" diff --git a/frappe/core/doctype/domain/domain.py b/frappe/core/doctype/domain/domain.py index d8a571537c..c6b1766235 100644 --- a/frappe/core/doctype/domain/domain.py +++ b/frappe/core/doctype/domain/domain.py @@ -4,7 +4,89 @@ from __future__ import unicode_literals import frappe + from frappe.model.document import Document +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields class Domain(Document): - pass + '''Domain documents are created automatically when DocTypes + with "Restricted" domains are imported during + installation or migration''' + def setup_domain(self): + '''Setup domain icons, permissions, custom fields etc.''' + self.setup_data() + self.setup_roles() + self.setup_properties() + self.set_values() + if not int(frappe.db.get_single_value('System Settings', 'setup_complete') or 0): + # if setup not complete, setup desktop etc. + self.setup_sidebar_items() + self.setup_desktop_icons() + self.set_default_portal_role() + + if self.data.custom_fields: + create_custom_fields(self.data.custom_fields) + + if self.data.on_setup: + # custom on_setup method + frappe.get_attr(self.data.on_setup)() + + + def setup_roles(self): + '''Enable roles that are restricted to this domain''' + if self.data.restricted_roles: + for role_name in self.data.restricted_roles: + role = frappe.get_doc('Role', role_name) + role.disabled = 0 + role.save() + + def setup_data(self, domain=None): + '''Load domain info via hooks''' + self.data = frappe.get_domain_data(self.name) + + def get_domain_data(self, module): + return frappe.get_attr(frappe.get_hooks('domains')[self.name] + '.data') + + def set_default_portal_role(self): + '''Set default portal role based on domain''' + if self.data.get('default_portal_role'): + frappe.db.set_value('Portal Settings', None, 'default_role', + self.data.get('default_portal_role')) + + def setup_desktop_icons(self): + '''set desktop icons form `data.desktop_icons`''' + from frappe.desk.doctype.desktop_icon.desktop_icon import set_desktop_icons + if self.data.desktop_icons: + set_desktop_icons(self.data.desktop_icons) + + def setup_properties(self): + if self.data.properties: + for args in self.data.properties: + frappe.make_property_setter(args) + + + def set_values(self): + '''set values based on `data.set_value`''' + if self.data.set_value: + for args in self.data.set_value: + doc = frappe.get_doc(args[0], args[1] or args[0]) + doc.set(args[2], args[3]) + doc.save() + + def setup_sidebar_items(self): + '''Enable / disable sidebar items''' + if self.data.allow_sidebar_items: + # disable all + frappe.db.sql('update `tabPortal Menu Item` set enabled=0') + + # enable + frappe.db.sql('''update `tabPortal Menu Item` set enabled=1 + where route in ({0})'''.format(', '.join(['"{0}"'.format(d) for d in self.data.allow_sidebar_items]))) + + if self.data.remove_sidebar_items: + # disable all + frappe.db.sql('update `tabPortal Menu Item` set enabled=1') + + # enable + frappe.db.sql('''update `tabPortal Menu Item` set enabled=0 + where route in ({0})'''.format(', '.join(['"{0}"'.format(d) for d in self.data.remove_sidebar_items]))) diff --git a/frappe/core/doctype/domain_settings/domain_settings.py b/frappe/core/doctype/domain_settings/domain_settings.py index cfe5010835..b3e1b133ae 100644 --- a/frappe/core/doctype/domain_settings/domain_settings.py +++ b/frappe/core/doctype/domain_settings/domain_settings.py @@ -7,8 +7,46 @@ import frappe from frappe.model.document import Document class DomainSettings(Document): + def set_active_domains(self, domains): + self.active_domains = [] + for d in domains: + self.append('active_domains', dict(domain=d)) + self.save() + def on_update(self): - clear_domain_cache() + for d in self.active_domains: + domain = frappe.get_doc('Domain', d.domain) + domain.setup_domain() + + self.restrict_roles_and_modules() + frappe.clear_cache() + + def restrict_roles_and_modules(self): + '''Disable all restricted roles and set `restrict_to_domain` property in Module Def''' + active_domains = frappe.get_active_domains() + all_domains = (frappe.get_hooks('domains') or {}).keys() + + def remove_role(role): + frappe.db.sql('delete from `tabHas Role` where role=%s', role) + frappe.set_value('Role', role, 'disabled', 1) + + for domain in all_domains: + data = frappe.get_domain_data(domain) + if not frappe.db.get_value('Domain', domain): + frappe.get_doc(dict(doctype='Domain', domain=domain)).insert() + if 'modules' in data: + for module in data.get('modules'): + frappe.db.set_value('Module Def', module, 'restrict_to_domain', domain) + + if 'restricted_roles' in data: + for role in data['restricted_roles']: + if not frappe.db.get_value('Role', role): + frappe.get_doc(dict(doctype='Role', role_name=role)).insert() + frappe.db.set_value('Role', role, 'restrict_to_domain', domain) + + if domain not in active_domains: + remove_role(role) + def get_active_domains(): """ get the domains set in the Domain Settings as active domain """ @@ -33,6 +71,3 @@ def get_active_modules(): return active_modules return frappe.cache().get_value('active_modules', _get_active_modules) - -def clear_domain_cache(): - frappe.cache().delete_key(['active_domains', 'active_modules']) diff --git a/frappe/core/doctype/feedback_trigger/test_feedback_trigger.py b/frappe/core/doctype/feedback_trigger/test_feedback_trigger.py index 1b07c432d5..c9c4fca527 100644 --- a/frappe/core/doctype/feedback_trigger/test_feedback_trigger.py +++ b/frappe/core/doctype/feedback_trigger/test_feedback_trigger.py @@ -72,6 +72,7 @@ class TestFeedbackTrigger(unittest.TestCase): }).insert(ignore_permissions=True) # check if feedback mail alert is triggered + todo.reload() todo.status = "Closed" todo.save(ignore_permissions=True) @@ -112,6 +113,7 @@ class TestFeedbackTrigger(unittest.TestCase): reference_doctype="ToDo", reference_name=todo.name, feedback="Thank You !!", rating=4, fullname="Test User") # auto feedback request should trigger only once + todo.reload() todo.save(ignore_permissions=True) email_queue = frappe.db.sql("""select name from `tabEmail Queue` where reference_doctype='ToDo' and reference_name='{0}'""".format(todo.name)) @@ -125,11 +127,10 @@ class TestFeedbackTrigger(unittest.TestCase): "communication_type": "Feedback" }) self.assertFalse(communications) - + feedback_requests = frappe.get_all("Feedback Request", { "reference_doctype": "ToDo", "reference_name": todo.name, "is_feedback_submitted": 0 }) self.assertFalse(feedback_requests) - \ No newline at end of file diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py index fd53fad44b..25cd8e5e67 100644 --- a/frappe/core/doctype/report/report.py +++ b/frappe/core/doctype/report/report.py @@ -84,7 +84,7 @@ class Report(Document): if self.is_standard == 'Yes' and (frappe.local.conf.get('developer_mode') or 0) == 1: export_to_files(record_list=[['Report', self.name]], - record_module=self.module) + record_module=self.module, create_init=True) self.create_report_py() diff --git a/frappe/core/doctype/sms_parameter/sms_parameter.json b/frappe/core/doctype/sms_parameter/sms_parameter.json index b5648ade80..43b93ed182 100755 --- a/frappe/core/doctype/sms_parameter/sms_parameter.json +++ b/frappe/core/doctype/sms_parameter/sms_parameter.json @@ -71,6 +71,36 @@ "set_only_once": 0, "unique": 0, "width": "150px" + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "header", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Header", + "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, @@ -83,8 +113,8 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2017-07-22 22:52:53.309396", - "modified_by": "chude.osiegbu@manqala.com", + "modified": "2017-10-13 16:48:00.518463", + "modified_by": "Administrator", "module": "Core", "name": "SMS Parameter", "owner": "Administrator", diff --git a/frappe/core/doctype/sms_settings/sms_settings.json b/frappe/core/doctype/sms_settings/sms_settings.json index ac911f2ecb..948329d081 100755 --- a/frappe/core/doctype/sms_settings/sms_settings.json +++ b/frappe/core/doctype/sms_settings/sms_settings.json @@ -159,6 +159,36 @@ "search_index": 0, "set_only_once": 0, "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "use_post", + "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": "Use POST", + "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, diff --git a/frappe/core/doctype/sms_settings/sms_settings.py b/frappe/core/doctype/sms_settings/sms_settings.py index 9e25869241..588b08a229 100644 --- a/frappe/core/doctype/sms_settings/sms_settings.py +++ b/frappe/core/doctype/sms_settings/sms_settings.py @@ -65,13 +65,17 @@ def send_sms(receiver_list, msg, sender_name = '', success_msg = True): def send_via_gateway(arg): ss = frappe.get_doc('SMS Settings', 'SMS Settings') args = {ss.message_parameter: arg.get('message')} + headers={'Accept': "text/plain, text/html, */*"} for d in ss.get("parameters"): + if d.header == 1: + headers.update({d.parameter: d.value}) + continue args[d.parameter] = d.value success_list = [] for d in arg.get('receiver_list'): args[ss.receiver_parameter] = d - status = send_request(ss.sms_gateway_url, args) + status = send_request(ss.sms_gateway_url, headers, args, ss.use_post) if 200 <= status < 300: success_list.append(d) @@ -83,9 +87,12 @@ def send_via_gateway(arg): frappe.msgprint(_("SMS sent to following numbers: {0}").format("\n" + "\n".join(success_list))) -def send_request(gateway_url, params): +def send_request(gateway_url, headers, params, use_post=False): import requests - response = requests.get(gateway_url, params = params, headers={'Accept': "text/plain, text/html, */*"}) + if use_post: + response = requests.post(gateway_url, headers=headers, data=params) + else: + response = requests.get(gateway_url, headers=headers, params=params) response.raise_for_status() return response.status_code diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 448b491c69..092e9440b1 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -160,37 +160,37 @@ "unique": 0 }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "is_first_startup", - "fieldtype": "Check", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Is First Startup", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "is_first_startup", + "fieldtype": "Check", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Is First Startup", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -1019,40 +1019,40 @@ "unique": 0 }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "depends_on": "enable_two_factor_auth", - "fieldname": "bypass_2fa_for_retricted_ip_users", - "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": "Bypass Two Factor Auth for users who login from restricted IP Address", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "0", + "depends_on": "enable_two_factor_auth", + "fieldname": "bypass_2fa_for_retricted_ip_users", + "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": "Bypass Two Factor Auth for users who login from restricted IP Address", + "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, @@ -1268,6 +1268,36 @@ "search_index": 0, "set_only_once": 0, "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "hide_footer_in_auto_email_reports", + "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": "Hide footer in auto email reports", + "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, @@ -1281,8 +1311,8 @@ "issingle": 1, "istable": 0, "max_attachments": 0, - "modified": "2017-09-13 13:26:11.045262", - "modified_by": "shri@zerodha.com", + "modified": "2017-10-15 20:29:46.700707", + "modified_by": "Administrator", "module": "Core", "name": "System Settings", "name_case": "", diff --git a/frappe/core/doctype/system_settings/system_settings.py b/frappe/core/doctype/system_settings/system_settings.py index ef2863fd46..bd35edb880 100644 --- a/frappe/core/doctype/system_settings/system_settings.py +++ b/frappe/core/doctype/system_settings/system_settings.py @@ -14,7 +14,7 @@ from frappe.twofactor import toggle_two_factor_auth class SystemSettings(Document): def validate(self): enable_password_policy = cint(self.enable_password_policy) and True or False - minimum_password_score = cint(self.minimum_password_score) or 0 + minimum_password_score = cint(getattr(self, 'minimum_password_score', 0)) or 0 if enable_password_policy and minimum_password_score <= 0: frappe.throw(_("Please select Minimum Password Score")) elif not enable_password_policy: diff --git a/frappe/core/doctype/system_settings/test_system_settings.py b/frappe/core/doctype/system_settings/test_system_settings.py new file mode 100644 index 0000000000..82d0ddbd7c --- /dev/null +++ b/frappe/core/doctype/system_settings/test_system_settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest + +class TestSystemSettings(unittest.TestCase): + pass diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 13270cc352..4fa1183e74 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -42,6 +42,7 @@ class User(Document): def before_insert(self): self.flags.in_insert = True + throttle_user_creation() def validate(self): self.check_demo() @@ -976,4 +977,10 @@ def reset_otp_secret(user): enqueue(method=frappe.sendmail, queue='short', timeout=300, event=None, async=True, job_name=None, now=False, **email_args) return frappe.msgprint(_("OTP Secret has been reset. Re-registration will be required on next login.")) else: - return frappe.throw(_("OTP secret can only be reset by the Administrator.")) \ No newline at end of file + return frappe.throw(_("OTP secret can only be reset by the Administrator.")) + +def throttle_user_creation(): + if frappe.flags.in_import: + return + if frappe.db.get_creation_count('User', 60) > 60: + frappe.throw(_('Throttled')) \ No newline at end of file diff --git a/frappe/core/page/data_import_tool/data_import_main.html b/frappe/core/page/data_import_tool/data_import_main.html index db44a06e70..549bebcdfd 100644 --- a/frappe/core/page/data_import_tool/data_import_main.html +++ b/frappe/core/page/data_import_tool/data_import_main.html @@ -93,10 +93,16 @@ {%= __("Ignore encoding errors.") %} +
diff --git a/frappe/core/page/data_import_tool/data_import_tool.js b/frappe/core/page/data_import_tool/data_import_tool.js index 9d21c28348..b7e9dab79a 100644 --- a/frappe/core/page/data_import_tool/data_import_tool.js +++ b/frappe/core/page/data_import_tool/data_import_tool.js @@ -114,6 +114,7 @@ frappe.DataImportTool = Class.extend({ return { submit_after_import: me.page.main.find('[name="submit_after_import"]').prop("checked"), ignore_encoding_errors: me.page.main.find('[name="ignore_encoding_errors"]').prop("checked"), + skip_errors: me.page.main.find('[name="skip_errors"]').prop("checked"), overwrite: !me.page.main.find('[name="always_insert"]').prop("checked"), update_only: me.page.main.find('[name="update_only"]').prop("checked"), no_email: me.page.main.find('[name="no_email"]').prop("checked"), diff --git a/frappe/core/page/data_import_tool/importer.py b/frappe/core/page/data_import_tool/importer.py index d26dcfd2b1..083715b24d 100644 --- a/frappe/core/page/data_import_tool/importer.py +++ b/frappe/core/page/data_import_tool/importer.py @@ -21,7 +21,8 @@ from six import text_type, string_types @frappe.whitelist() def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, no_email=True, overwrite=None, - update_only = None, ignore_links=False, pre_process=None, via_console=False, from_data_import="No"): + update_only = None, ignore_links=False, pre_process=None, via_console=False, from_data_import="No", + skip_errors = True): """upload data""" frappe.flags.in_import = True @@ -341,13 +342,14 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, doc.submit() log('Submitted row (#%d) %s' % (row_idx + 1, as_link(doc.doctype, doc.name))) except Exception as e: - error = True - if doc: - frappe.errprint(doc if isinstance(doc, dict) else doc.as_dict()) - err_msg = frappe.local.message_log and "\n\n".join(frappe.local.message_log) or cstr(e) - log('Error for row (#%d) %s : %s' % (row_idx + 1, - len(row)>1 and row[1] or "", err_msg)) - frappe.errprint(frappe.get_traceback()) + if not skip_errors: + error = True + if doc: + frappe.errprint(doc if isinstance(doc, dict) else doc.as_dict()) + err_msg = frappe.local.message_log and "\n\n".join(frappe.local.message_log) or cstr(e) + log('Error for row (#%d) %s : %s' % (row_idx + 1, + len(row)>1 and row[1] or "", err_msg)) + frappe.errprint(frappe.get_traceback()) finally: frappe.local.message_log = [] diff --git a/frappe/core/page/usage_info/usage_info.html b/frappe/core/page/usage_info/usage_info.html index b108f1e7d5..5fd564335c 100644 --- a/frappe/core/page/usage_info/usage_info.html +++ b/frappe/core/page/usage_info/usage_info.html @@ -1,114 +1,75 @@ -
{{ __("Current Users") }} | -{{ __("Max Users") }} | -{{ __("Remaining") }} | -
---|---|---|
{%= enabled_users %} | -{%= limits.users %} | -{%= limits.users - enabled_users %} | -
{%= enabled_users %} out of {%= limits.users %} enabled
+{{ __("Emails Sent") }} | -{{ __("Max Emails") }} | -{{ __("Remaining") }} | -
---|---|---|
{%= emails_sent %} | -{%= limits.emails %} | -{%= emails_remaining %} | -
{%= emails_sent %} out of {%= limits.emails %} sent this month
+{{ __("Type") }} | -{{ __("Size (MB)") }} | -
---|---|
{{ __("Database Size") }} | -{%= limits.space_usage.database_size %} MB | -
{{ __("Files Size") }} | -{%= limits.space_usage.files_size %} MB | -
{{ __("Backup Size") }} | -{%= limits.space_usage.backup_size %} MB | -
{{ __("Total") }} | -{%= limits.space_usage.total %} MB | -
{{ __("Remaining") }} | -- {%= flt(limits.space - limits.space_usage.total, 2) %} MB | -
+ + {%= flt(limits.space - limits.space_usage.total, 2) %} MB + available out of + {%= limits.space %} MB +
+{0}
) and Response Type ({1}
) not allowed".format(self.grant_type, self.response_type)))
diff --git a/frappe/integrations/doctype/s3_backup_settings/__init__.py b/frappe/integrations/doctype/s3_backup_settings/__init__.py
new file mode 100755
index 0000000000..e69de29bb2
diff --git a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.js b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.js
new file mode 100755
index 0000000000..1a1b8a7c67
--- /dev/null
+++ b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.js
@@ -0,0 +1,26 @@
+// Copyright (c) 2017, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('S3 Backup Settings', {
+ refresh: function(frm) {
+ frm.clear_custom_buttons();
+ frm.events.take_backup(frm);
+ },
+
+ take_backup: function(frm) {
+ if (frm.doc.access_key_id && frm.doc.secret_access_key) {
+ frm.add_custom_button(__("Take Backup Now"), function(){
+ frm.dashboard.set_headline_alert("S3 Backup Started!");
+ frappe.call({
+ method: "frappe.integrations.doctype.s3_backup_settings.s3_backup_settings.take_backups_s3",
+ callback: function(r) {
+ if(!r.exc) {
+ frappe.msgprint(__("S3 Backup complete!"));
+ frm.dashboard.clear_headline();
+ }
+ }
+ });
+ }).addClass("btn-primary");
+ }
+ }
+});
diff --git a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json
new file mode 100755
index 0000000000..0cdc8e1dd6
--- /dev/null
+++ b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json
@@ -0,0 +1,273 @@
+{
+ "allow_copy": 0,
+ "allow_guest_to_view": 0,
+ "allow_import": 0,
+ "allow_rename": 0,
+ "beta": 0,
+ "creation": "2017-09-04 20:57:20.129205",
+ "custom": 0,
+ "docstatus": 0,
+ "doctype": "DocType",
+ "document_type": "",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "fields": [
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "enabled",
+ "fieldtype": "Check",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Enable Automatic Backup",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "notify_email",
+ "fieldtype": "Data",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 1,
+ "in_standard_filter": 0,
+ "label": "Send Notifications To",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 1,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "frequency",
+ "fieldtype": "Select",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 1,
+ "in_standard_filter": 0,
+ "label": "Backup Frequency",
+ "length": 0,
+ "no_copy": 0,
+ "options": "Daily\nWeekly\nMonthly\nNone",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 1,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "access_key_id",
+ "fieldtype": "Data",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 1,
+ "in_standard_filter": 0,
+ "label": "Access Key ID",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 1,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "secret_access_key",
+ "fieldtype": "Password",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 1,
+ "in_standard_filter": 0,
+ "label": "Secret Access Key",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 1,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "bucket",
+ "fieldtype": "Data",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Bucket",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 1,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "backup_limit",
+ "fieldtype": "Int",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Backup Limit",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 1,
+ "search_index": 0,
+ "set_only_once": 0,
+ "unique": 0
+ }
+ ],
+ "has_web_view": 0,
+ "hide_heading": 0,
+ "hide_toolbar": 1,
+ "idx": 0,
+ "image_view": 0,
+ "in_create": 0,
+ "is_submittable": 0,
+ "issingle": 1,
+ "istable": 0,
+ "max_attachments": 0,
+ "modified": "2017-10-06 18:27:09.022674",
+ "modified_by": "Administrator",
+ "module": "Integrations",
+ "name": "S3 Backup Settings",
+ "name_case": "",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "amend": 0,
+ "apply_user_permissions": 0,
+ "cancel": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 0,
+ "if_owner": 0,
+ "import": 0,
+ "permlevel": 0,
+ "print": 1,
+ "read": 1,
+ "report": 0,
+ "role": "System Manager",
+ "set_user_permissions": 0,
+ "share": 1,
+ "submit": 0,
+ "write": 1
+ }
+ ],
+ "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/s3_backup_settings/s3_backup_settings.py b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py
new file mode 100755
index 0000000000..c557365e6b
--- /dev/null
+++ b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py
@@ -0,0 +1,153 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2017, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import os
+import os.path
+import frappe
+import boto3
+from frappe import _
+from frappe.model.document import Document
+from frappe.utils import cint, split_emails
+from frappe.utils.background_jobs import enqueue
+from botocore.exceptions import ClientError
+
+class S3BackupSettings(Document):
+
+ def validate(self):
+ conn = boto3.client(
+ 's3',
+ aws_access_key_id=self.access_key_id,
+ aws_secret_access_key=self.get_password('secret_access_key'),
+ )
+
+ bucket_lower = str(self.bucket).lower()
+
+ try:
+ conn.list_buckets()
+
+ except ClientError:
+ frappe.throw(_("Invalid Access Key ID or Secret Access Key."))
+
+ try:
+ conn.create_bucket(Bucket=bucket_lower)
+ except ClientError:
+ frappe.throw(_("Unable to create bucket: {0}. Change it to a more unique name.").format(bucket_lower))
+
+
+@frappe.whitelist()
+def take_backup():
+ "Enqueue longjob for taking backup to s3"
+ enqueue("frappe.integrations.doctype.s3_backup_settings.s3_backup_settings.take_backups_s3", queue='long', timeout=1500)
+ frappe.msgprint(_("Queued for backup. It may take a few minutes to an hour."))
+
+
+def take_backups_daily():
+ take_backups_if("Daily")
+
+
+def take_backups_weekly():
+ take_backups_if("Weekly")
+
+
+def take_backups_monthly():
+ take_backups_if("Monthly")
+
+
+def take_backups_if(freq):
+ if cint(frappe.db.get_value("S3 Backup Settings", None, "enabled")):
+ if frappe.db.get_value("S3 Backup Settings", None, "frequency") == freq:
+ take_backups_s3()
+
+
+@frappe.whitelist()
+def take_backups_s3():
+ try:
+ backup_to_s3()
+ send_email(True, "S3 Backup Settings")
+ except Exception:
+ error_message = frappe.get_traceback()
+ frappe.errprint(error_message)
+ send_email(False, "S3 Backup Settings", error_message)
+
+
+def send_email(success, service_name, error_status=None):
+ if success:
+ subject = "Backup Upload Successful"
+ message = """Hi there, this is just to inform you + that your backup was successfully uploaded to your Amazon S3 bucket. So relax!
""" + + else: + subject = "[Warning] Backup Upload Failed" + message = """Oops, your automated backup to Amazon S3 failed. +
Error message: %s
Please contact your system manager + for more information.
""" % error_status + + if not frappe.db: + frappe.connect() + + if frappe.db.get_value("S3 Backup Settings", None, "notification_email"): + recipients = split_emails(frappe.db.get_value("S3 Backup Settings", None, "notification_email")) + frappe.sendmail(recipients=recipients, subject=subject, message=message) + + +def backup_to_s3(): + from frappe.utils.backups import new_backup + from frappe.utils import get_backups_path + + doc = frappe.get_single("S3 Backup Settings") + bucket = doc.bucket + + conn = boto3.client( + 's3', + aws_access_key_id=doc.access_key_id, + aws_secret_access_key=doc.get_password('secret_access_key'), + ) + + backup = new_backup(ignore_files=False, backup_path_db=None, + backup_path_files=None, backup_path_private_files=None, force=True) + db_filename = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_db)) + files_filename = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_files)) + private_files = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_private_files)) + folder = os.path.basename(db_filename)[:15] + '/' + # for adding datetime to folder name + + upload_file_to_s3(db_filename, folder, conn, bucket) + upload_file_to_s3(private_files, folder, conn, bucket) + upload_file_to_s3(files_filename, folder, conn, bucket) + delete_old_backups(doc.backup_limit, bucket) + +def upload_file_to_s3(filename, folder, conn, bucket): + + destpath = os.path.join(folder, os.path.basename(filename)) + try: + print "Uploading file:", filename + conn.upload_file(filename, bucket, destpath) + + except Exception as e: + print "Error uploading: %s" % (e) + + +def delete_old_backups(limit, bucket): + all_backups = list() + doc = frappe.get_single("S3 Backup Settings") + backup_limit = int(limit) + + s3 = boto3.resource( + 's3', + aws_access_key_id=doc.access_key_id, + aws_secret_access_key=doc.get_password('secret_access_key'), + ) + bucket = s3.Bucket(bucket) + objects = bucket.meta.client.list_objects_v2(Bucket=bucket.name, Delimiter='/') + for obj in objects.get('CommonPrefixes'): + all_backups.append(obj.get('Prefix')) + + oldest_backup = sorted(all_backups)[0] + + if len(all_backups) > backup_limit: + print "Deleting Backup: {0}".format(oldest_backup) + for obj in bucket.objects.filter(Prefix=oldest_backup): + # delete all keys that are inside the oldest_backup + s3.Object(bucket.name, obj.key).delete() diff --git a/frappe/integrations/doctype/s3_backup_settings/test_s3_backup_settings.js b/frappe/integrations/doctype/s3_backup_settings/test_s3_backup_settings.js new file mode 100755 index 0000000000..27e36661f0 --- /dev/null +++ b/frappe/integrations/doctype/s3_backup_settings/test_s3_backup_settings.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: S3 Backup Settings", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially([ + // insert a new S3 Backup Settings + () => frappe.tests.make('S3 Backup Settings', [ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/frappe/integrations/doctype/s3_backup_settings/test_s3_backup_settings.py b/frappe/integrations/doctype/s3_backup_settings/test_s3_backup_settings.py new file mode 100755 index 0000000000..04d90f9b44 --- /dev/null +++ b/frappe/integrations/doctype/s3_backup_settings/test_s3_backup_settings.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +import unittest + +class TestS3BackupSettings(unittest.TestCase): + pass diff --git a/frappe/integrations/doctype/webhook/__init__.py b/frappe/integrations/doctype/webhook/__init__.py index 3c8bf1e2af..b72acc0578 100644 --- a/frappe/integrations/doctype/webhook/__init__.py +++ b/frappe/integrations/doctype/webhook/__init__.py @@ -37,7 +37,8 @@ def run_webhooks(doc, method): 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) + frappe.enqueue("frappe.integrations.doctype.webhook.webhook.enqueue_webhook", + enqueue_after_commit=True, 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 diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 4a5568b238..b3d0bdc277 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -362,39 +362,6 @@ class BaseDocument(object): # this is used to preserve traceback raise frappe.UniqueValidationError(self.doctype, self.name, e) - def db_set(self, fieldname, value=None, update_modified=True): - '''Set a value in the document object, update the timestamp and update the database. - - WARNING: This method does not trigger controller validations and should - be used very carefully. - - :param fieldname: fieldname of the property to be updated, or a {"field":"value"} dictionary - :param value: value of the property to be updated - :param update_modified: default True. updates the `modified` and `modified_by` properties - ''' - if isinstance(fieldname, dict): - self.update(fieldname) - else: - self.set(fieldname, value) - - if update_modified and (self.doctype, self.name) not in frappe.flags.currently_saving: - # don't update modified timestamp if called from post save methods - # like on_update or on_submit - self.set("modified", now()) - self.set("modified_by", frappe.session.user) - - # to trigger email alert on value change - self.run_method('before_change') - - frappe.db.set_value(self.doctype, self.name, fieldname, value, - self.modified, self.modified_by, update_modified=update_modified) - - self.run_method('on_change') - - def db_get(self, fieldname): - '''get database vale for this fieldname''' - return frappe.db.get_value(self.doctype, self.name, fieldname) - def update_modified(self): '''Update modified timestamp''' self.set("modified", now()) diff --git a/frappe/model/db_schema.py b/frappe/model/db_schema.py index e47c678e4c..fb3de07332 100644 --- a/frappe/model/db_schema.py +++ b/frappe/model/db_schema.py @@ -44,6 +44,7 @@ type_map = { ,'Attach Image':('text', '') ,'Signature': ('longtext', '') ,'Color': ('varchar', varchar_len) + ,'Barcode': ('longtext', '') } default_columns = ['name', 'creation', 'modified', 'modified_by', 'owner', diff --git a/frappe/model/document.py b/frappe/model/document.py index 56a14cff8c..6d11a7607d 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -867,6 +867,46 @@ class Document(BaseDocument): not self.meta.get("istable"): frappe.publish_realtime("list_update", {"doctype": self.doctype}, after_commit=True) + def db_set(self, fieldname, value=None, update_modified=True, notify=False, commit=False): + '''Set a value in the document object, update the timestamp and update the database. + + WARNING: This method does not trigger controller validations and should + be used very carefully. + + :param fieldname: fieldname of the property to be updated, or a {"field":"value"} dictionary + :param value: value of the property to be updated + :param update_modified: default True. updates the `modified` and `modified_by` properties + :param notify: default False. run doc.notify_updated() to send updates via socketio + :param commit: default False. run frappe.db.commit() + ''' + if isinstance(fieldname, dict): + self.update(fieldname) + else: + self.set(fieldname, value) + + if update_modified and (self.doctype, self.name) not in frappe.flags.currently_saving: + # don't update modified timestamp if called from post save methods + # like on_update or on_submit + self.set("modified", now()) + self.set("modified_by", frappe.session.user) + + # to trigger email alert on value change + self.run_method('before_change') + + frappe.db.set_value(self.doctype, self.name, fieldname, value, + self.modified, self.modified_by, update_modified=update_modified) + + self.run_method('on_change') + + if notify: + self.notify_update() + + if commit: + frappe.db.commit() + + def db_get(self, fieldname): + '''get database vale for this fieldname''' + return frappe.db.get_value(self.doctype, self.name, fieldname) def check_no_back_links_exist(self): """Check if document links to any active document before Cancel.""" diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 74cce521a5..69182cdeff 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -93,6 +93,9 @@ class Meta(Document): return self.get("fields", {"fieldtype": "Select", "options":["not in", ["[Select]", "Loading..."]]}) + def get_image_fields(self): + return self.get("fields", {"fieldtype": "Attach Image"}) + def get_table_fields(self): if not hasattr(self, "_table_fields"): if self.name!="DocType": @@ -215,7 +218,7 @@ class Meta(Document): title_field = getattr(self, 'title_field', None) if not title_field and self.has_field('title'): title_field = 'title' - else: + if not title_field: title_field = 'name' return title_field diff --git a/frappe/model/sync.py b/frappe/model/sync.py index 9c80a946a1..de2bca903c 100644 --- a/frappe/model/sync.py +++ b/frappe/model/sync.py @@ -56,8 +56,10 @@ def sync_for(app_name, force=0, sync_everything = False, verbose=False, reset_pe def get_doc_files(files, start_path, force=0, sync_everything = False, verbose=False): """walk and sync all doctypes and pages""" + # load in sequence - warning for devs document_types = ['doctype', 'page', 'report', 'print_format', - 'website_theme', 'web_form', 'email_alert', 'print_style'] + 'website_theme', 'web_form', 'email_alert', 'print_style', + 'data_migration_mapping', 'data_migration_plan'] for doctype in document_types: doctype_path = os.path.join(start_path, doctype) if os.path.exists(doctype_path): diff --git a/frappe/modules.txt b/frappe/modules.txt index 0d2e91b35f..a4ceff3d39 100644 --- a/frappe/modules.txt +++ b/frappe/modules.txt @@ -8,3 +8,4 @@ Desk Integrations Printing Contacts +Data Migration \ No newline at end of file diff --git a/frappe/modules/export_file.py b/frappe/modules/export_file.py index 2b7a6cab3b..275db10b13 100644 --- a/frappe/modules/export_file.py +++ b/frappe/modules/export_file.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe, os, json import frappe.model -from frappe.modules import scrub, get_module_path, lower_case_files_for, scrub_dt_dn +from frappe.modules import scrub, get_module_path, scrub_dt_dn def export_doc(doc): export_to_files([[doc.doctype, doc.name]]) @@ -21,7 +21,7 @@ def export_to_files(record_list=None, record_module=None, verbose=0, create_init for record in record_list: write_document_file(frappe.get_doc(record[0], record[1]), record_module, create_init=create_init) -def write_document_file(doc, record_module=None, create_init=None): +def write_document_file(doc, record_module=None, create_init=True): newdoc = doc.as_dict(no_nulls=True) # strip out default fields from children @@ -32,14 +32,12 @@ def write_document_file(doc, record_module=None, create_init=None): del d[fieldname] module = record_module or get_module_name(doc) - if create_init is None: - create_init = doc.doctype in lower_case_files_for # create folder folder = create_folder(module, doc.doctype, doc.name, create_init) # write the data file - fname = (doc.doctype in lower_case_files_for and scrub(doc.name)) or doc.name + fname = scrub(doc.name) with open(os.path.join(folder, fname +".json"),'w+') as txtfile: txtfile.write(frappe.as_json(newdoc)) diff --git a/frappe/modules/utils.py b/frappe/modules/utils.py index 9e43a28c6f..499d2bbb56 100644 --- a/frappe/modules/utils.py +++ b/frappe/modules/utils.py @@ -9,11 +9,6 @@ import frappe, os, json import frappe.utils from frappe import _ -lower_case_files_for = ['DocType', 'Page', 'Report', - "Workflow", 'Module Def', 'Desktop Item', 'Workflow State', - 'Workflow Action', 'Print Format', "Website Theme", 'Web Form', - 'Email Alert', 'Print Style'] - def export_module_json(doc, is_standard, module): """Make a folder for the given doc and add its json file (make it a standard object that will be synced)""" @@ -22,7 +17,8 @@ def export_module_json(doc, is_standard, module): from frappe.modules.export_file import export_to_files # json - export_to_files(record_list=[[doc.doctype, doc.name]], record_module=module) + export_to_files(record_list=[[doc.doctype, doc.name]], record_module=module, + create_init=is_standard) path = os.path.join(frappe.get_module_path(module), scrub(doc.doctype), scrub(doc.name), scrub(doc.name)) @@ -134,11 +130,7 @@ def scrub(txt): def scrub_dt_dn(dt, dn): """Returns in lowercase and code friendly names of doctype and name for certain types""" - ndt, ndn = dt, dn - if dt in lower_case_files_for: - ndt, ndn = scrub(dt), scrub(dn) - - return ndt, ndn + return scrub(dt), scrub(dn) def get_module_path(module): """Returns path of the given module""" diff --git a/frappe/oauth.py b/frappe/oauth.py index 246f771f2a..61b4db5034 100644 --- a/frappe/oauth.py +++ b/frappe/oauth.py @@ -206,7 +206,10 @@ class OAuthWebRequestValidator(RequestValidator): otoken = frappe.new_doc("OAuth Bearer Token") otoken.client = request.client['name'] - otoken.user = request.user if request.user else frappe.db.get_value("OAuth Bearer Token", {"refresh_token":request.body.get("refresh_token")}, "user") + try: + otoken.user = request.user if request.user else frappe.db.get_value("OAuth Bearer Token", {"refresh_token":request.body.get("refresh_token")}, "user") + except Exception as e: + otoken.user = frappe.session.user otoken.scopes = get_url_delimiter().join(request.scopes) otoken.access_token = token['access_token'] otoken.refresh_token = token.get('refresh_token') diff --git a/frappe/patches.txt b/frappe/patches.txt index 7c6156ac3c..38f8fd719e 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -7,10 +7,11 @@ frappe.patches.v7_1.rename_scheduler_log_to_error_log frappe.patches.v6_1.rename_file_data frappe.patches.v7_0.re_route #2016-06-27 frappe.patches.v7_2.remove_in_filter -frappe.patches.v8_0.drop_in_dialog -execute:frappe.reload_doc('core', 'doctype', 'doctype', force=True) #2017-03-09 +frappe.patches.v8_0.drop_in_dialog #2017-09-22 +execute:frappe.reload_doc('core', 'doctype', 'doctype', force=True) #2017-09-22 execute:frappe.reload_doc('core', 'doctype', 'docfield', force=True) #2017-03-03 execute:frappe.reload_doc('core', 'doctype', 'docperm') #2017-03-03 +execute:frappe.reload_doc('core', 'doctype', 'module_def') #2017-09-22 frappe.patches.v8_0.drop_is_custom_from_docperm frappe.patches.v8_0.update_records_in_global_search #11-05-2017 frappe.patches.v8_0.update_published_in_global_search diff --git a/frappe/public/build.json b/frappe/public/build.json index 8d10760cf1..1a418248dd 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -51,7 +51,9 @@ "public/js/frappe/form/controls/read_only.js", "public/js/frappe/form/controls/button.js", "public/js/frappe/form/controls/html.js", - "public/js/frappe/form/controls/heading.js" + "public/js/frappe/form/controls/heading.js", + "public/js/frappe/form/controls/autocomplete.js", + "public/js/frappe/form/controls/barcode.js" ], "js/dialog.min.js": [ "public/js/frappe/dom.js", @@ -101,7 +103,6 @@ "public/css/bootstrap.css", "public/css/font-awesome.css", "public/css/octicons/octicons.css", - "public/css/c3.min.css", "public/css/desk.css", "public/css/indicator.css", "public/css/avatar.css", @@ -113,7 +114,7 @@ "public/css/form.css", "public/css/mobile.css", "public/css/kanban.css", - "public/css/graphs.css" + "public/css/charts.css" ], "css/frappe-rtl.css": [ "public/css/bootstrap-rtl.css", @@ -150,6 +151,7 @@ "public/js/frappe/ui/keyboard.js", "public/js/frappe/ui/emoji.js", "public/js/frappe/ui/colors.js", + "public/js/frappe/ui/sidebar.js", "public/js/frappe/request.js", "public/js/frappe/socketio_client.js", @@ -229,16 +231,11 @@ "public/js/frappe/query_string.js", "public/js/frappe/ui/charts.js", - "public/js/frappe/ui/graphs.js", "public/js/frappe/ui/comment.js", "public/js/frappe/misc/rating_icons.html", "public/js/frappe/feedback.js" ], - "js/d3.min.js": [ - "public/js/lib/d3.min.js", - "public/js/lib/c3.min.js" - ], "css/module.min.css": [ "public/css/module.css" ], @@ -269,7 +266,7 @@ "public/js/frappe/form/linked_with.js", "public/js/frappe/form/workflow.js", "public/js/frappe/form/print.js", - "public/js/frappe/form/sidebar.js", + "public/js/frappe/form/form_sidebar.js", "public/js/frappe/form/user_image.js", "public/js/frappe/form/share.js", "public/js/frappe/form/form_viewers.js", diff --git a/frappe/public/css/c3.min.css b/frappe/public/css/c3.min.css deleted file mode 100644 index 1e20d5b116..0000000000 --- a/frappe/public/css/c3.min.css +++ /dev/null @@ -1 +0,0 @@ -.c3 svg{font:10px sans-serif;-webkit-tap-highlight-color:transparent}.c3 line,.c3 path{fill:none;stroke:#000}.c3 text{-webkit-user-select:none;-moz-user-select:none;user-select:none}.c3-bars path,.c3-event-rect,.c3-legend-item-tile,.c3-xgrid-focus,.c3-ygrid{shape-rendering:crispEdges}.c3-chart-arc path{stroke:#fff}.c3-chart-arc text{fill:#fff;font-size:13px}.c3-grid line{stroke:#aaa}.c3-grid text{fill:#aaa}.c3-xgrid,.c3-ygrid{stroke-dasharray:3 3}.c3-text.c3-empty{fill:gray;font-size:2em}.c3-line{stroke-width:1px}.c3-circle._expanded_{stroke-width:1px;stroke:#fff}.c3-selected-circle{fill:#fff;stroke-width:2px}.c3-bar{stroke-width:0}.c3-bar._expanded_{fill-opacity:.75}.c3-target.c3-focused{opacity:1}.c3-target.c3-focused path.c3-line,.c3-target.c3-focused path.c3-step{stroke-width:2px}.c3-target.c3-defocused{opacity:.3!important}.c3-region{fill:#4682b4;fill-opacity:.1}.c3-brush .extent{fill-opacity:.1}.c3-legend-item{font-size:12px}.c3-legend-item-hidden{opacity:.15}.c3-legend-background{opacity:.75;fill:#fff;stroke:#d3d3d3;stroke-width:1}.c3-title{font:14px sans-serif}.c3-tooltip-container{z-index:10}.c3-tooltip{border-collapse:collapse;border-spacing:0;background-color:#fff;empty-cells:show;-webkit-box-shadow:7px 7px 12px -9px #777;-moz-box-shadow:7px 7px 12px -9px #777;box-shadow:7px 7px 12px -9px #777;opacity:.9}.c3-tooltip tr{border:1px solid #CCC}.c3-tooltip th{background-color:#aaa;font-size:14px;padding:2px 5px;text-align:left;color:#FFF}.c3-tooltip td{font-size:13px;padding:3px 6px;background-color:#fff;border-left:1px dotted #999}.c3-tooltip td>span{display:inline-block;width:10px;height:10px;margin-right:6px}.c3-tooltip td.value{text-align:right}.c3-area{stroke-width:0;opacity:.2}.c3-chart-arcs-title{dominant-baseline:middle;font-size:1.3em}.c3-chart-arcs .c3-chart-arcs-background{fill:#e0e0e0;stroke:none}.c3-chart-arcs .c3-chart-arcs-gauge-unit{fill:#000;font-size:16px}.c3-chart-arcs .c3-chart-arcs-gauge-max,.c3-chart-arcs .c3-chart-arcs-gauge-min{fill:#777}.c3-chart-arc .c3-gauge-value{fill:#000} \ No newline at end of file diff --git a/frappe/public/css/graphs.css b/frappe/public/css/charts.css similarity index 77% rename from frappe/public/css/graphs.css rename to frappe/public/css/charts.css index e0f62b3cd9..f5d279568a 100644 --- a/frappe/public/css/graphs.css +++ b/frappe/public/css/charts.css @@ -1,74 +1,74 @@ -/* graphs */ -.graph-container .graph-focus-margin { +/* charts */ +.chart-container .graph-focus-margin { margin: 0px 5%; } -.graph-container .graphics { +.chart-container .graphics { margin-top: 10px; padding-top: 10px; padding-bottom: 10px; position: relative; } -.graph-container .graph-stats-group { +.chart-container .graph-stats-group { display: flex; justify-content: space-around; flex: 1; } -.graph-container .graph-stats-container { +.chart-container .graph-stats-container { display: flex; justify-content: space-around; padding-top: 10px; } -.graph-container .graph-stats-container .stats { +.chart-container .graph-stats-container .stats { padding-bottom: 15px; } -.graph-container .graph-stats-container .stats-title { +.chart-container .graph-stats-container .stats-title { color: #8D99A6; } -.graph-container .graph-stats-container .stats-value { +.chart-container .graph-stats-container .stats-value { font-size: 20px; font-weight: 300; } -.graph-container .graph-stats-container .stats-description { +.chart-container .graph-stats-container .stats-description { font-size: 12px; color: #8D99A6; } -.graph-container .graph-stats-container .graph-data .stats-value { +.chart-container .graph-stats-container .graph-data .stats-value { color: #98d85b; } -.graph-container .axis, -.graph-container .chart-label { +.chart-container .axis, +.chart-container .chart-label { font-size: 10px; - fill: #959ba1; + fill: #555b51; } -.graph-container .axis line, -.graph-container .chart-label line { - stroke: rgba(27, 31, 35, 0.1); +.chart-container .axis line, +.chart-container .chart-label line { + stroke: rgba(27, 31, 35, 0.2); } -.graph-container .percentage-graph .progress { +.chart-container .percentage-graph .progress { margin-bottom: 0px; } -.graph-container .data-points circle { +.chart-container .data-points circle { stroke: #fff; stroke-width: 2; } -.graph-container .data-points path { +.chart-container .data-points path { fill: none; stroke-opacity: 1; stroke-width: 2px; } -.graph-container line.dashed { +.chart-container line.dashed { stroke-dasharray: 5,3; } -.graph-container .tick.x-axis-label { +.chart-container .tick.x-axis-label { display: block; } -.graph-container .tick .specific-value { +.chart-container .tick .specific-value { text-anchor: start; } -.graph-container .tick .y-value-text { +.chart-container .tick .y-value-text { text-anchor: end; } -.graph-container .tick .x-value-text { +.chart-container .tick .x-value-text { text-anchor: middle; } .graph-svg-tip { @@ -138,6 +138,9 @@ .stroke.light-green { stroke: #98d85b; } +.stroke.lightgreen { + stroke: #98d85b; +} .stroke.green { stroke: #28a745; } @@ -174,6 +177,9 @@ .fill.light-green { fill: #98d85b; } +.fill.lightgreen { + fill: #98d85b; +} .fill.green { fill: #28a745; } @@ -210,6 +216,9 @@ .background.light-green { background: #98d85b; } +.background.lightgreen { + background: #98d85b; +} .background.green { background: #28a745; } @@ -246,6 +255,9 @@ .border-top.light-green { border-top: 3px solid #98d85b; } +.border-top.lightgreen { + border-top: 3px solid #98d85b; +} .border-top.green { border-top: 3px solid #28a745; } diff --git a/frappe/public/css/desk.css b/frappe/public/css/desk.css index 6e7b9768c2..76b38ba164 100644 --- a/frappe/public/css/desk.css +++ b/frappe/public/css/desk.css @@ -440,6 +440,9 @@ fieldset[disabled] .form-control { top: 26px; } } +.barcode-wrapper { + text-align: center; +} @media (min-width: 768px) { .video-modal .modal-dialog { width: 700px; diff --git a/frappe/public/css/desktop.css b/frappe/public/css/desktop.css index 32bbd19c47..8b918ca8fe 100644 --- a/frappe/public/css/desktop.css +++ b/frappe/public/css/desktop.css @@ -59,7 +59,6 @@ body[data-route="desktop"] .navbar-default { width: 32px; } .app-icon path { - fill: #fafbfc; transition: 0.2s; -webkit-transition: 0.2s; } @@ -80,9 +79,6 @@ body[data-route="desktop"] .navbar-default { letter-spacing: normal; cursor: pointer; } -.app-icon:hover path { - fill: #fff; -} .app-icon:hover i, .app-icon:hover { color: #fff; diff --git a/frappe/public/css/form.css b/frappe/public/css/form.css index 255c2afc2a..57cbfb50b3 100644 --- a/frappe/public/css/form.css +++ b/frappe/public/css/form.css @@ -718,3 +718,12 @@ select.form-control { body[data-route^="Form/Communication"] textarea[data-fieldname="subject"] { height: 80px !important; } +.frappe-control[data-fieldtype="Attach"] .attached-file { + position: relative; + margin-top: 5px; +} +.frappe-control[data-fieldtype="Attach"] .attached-file .close { + position: absolute; + top: 0; + right: 0; +} diff --git a/frappe/public/css/page.css b/frappe/public/css/page.css index 5dae59687c..d856222fbc 100644 --- a/frappe/public/css/page.css +++ b/frappe/public/css/page.css @@ -281,6 +281,9 @@ select.input-sm { .setup-state { background-color: #f5f7fa; } +.page-container .page-card-container { + background-color: #fff; +} .page-card-container { padding: 70px; } diff --git a/frappe/public/css/report-rtl.css b/frappe/public/css/report-rtl.css index 26709a55e3..03e986c56b 100644 --- a/frappe/public/css/report-rtl.css +++ b/frappe/public/css/report-rtl.css @@ -2,10 +2,14 @@ direction: ltr; } +.page-form .awesomplete > ul { + left: auto; +} + .chart_area{ direction: ltr; } .grid-report .show-zero{ - direction: rtl ; + direction: rtl; } diff --git a/frappe/public/js/frappe/db.js b/frappe/public/js/frappe/db.js index 101228de97..3cc34e3f79 100644 --- a/frappe/public/js/frappe/db.js +++ b/frappe/public/js/frappe/db.js @@ -2,6 +2,13 @@ // MIT License. See license.txt frappe.db = { + exists: function(doctype, name) { + return new Promise ((resolve) => { + frappe.db.get_value(doctype, {name: name}, 'name').then((r) => { + (r.message && r.message.name) ? resolve(true) : resolve(false); + }); + }); + }, get_value: function(doctype, filters, fieldname, callback) { return frappe.call({ method: "frappe.client.get_value", diff --git a/frappe/public/js/frappe/form/controls/attach.js b/frappe/public/js/frappe/form/controls/attach.js index 99bef4a113..fed5fbe172 100644 --- a/frappe/public/js/frappe/form/controls/attach.js +++ b/frappe/public/js/frappe/form/controls/attach.js @@ -7,12 +7,14 @@ frappe.ui.form.ControlAttach = frappe.ui.form.ControlData.extend({ .on("click", function() { me.onclick(); }); - this.$value = $('${message}
- +=i.length)return n;var r=[],u=a[e++];return n.forEach(function(n,u){r.push({key:n,values:t(u,e)})}),u?r.sort(function(n,t){return u(n.key,t.key)}):r}var e,r,u={},i=[],a=[];return u.map=function(t,e){return n(e,t,0)},u.entries=function(e){return t(n(oa.map,e,0),0)},u.key=function(n){return i.push(n),u},u.sortKeys=function(n){return a[i.length-1]=n,u},u.sortValues=function(n){return e=n,u},u.rollup=function(n){return r=n,u},u},oa.set=function(n){var t=new m;if(n)for(var e=0,r=n.length;r>e;++e)t.add(n[e]);return t},l(m,{has:h,add:function(n){return this._[s(n+="")]=!0,n},remove:g,values:p,size:v,empty:d,forEach:function(n){for(var t in this._)n.call(this,f(t))}}),oa.behavior={},oa.rebind=function(n,t){for(var e,r=1,u=arguments.length;++r=0&&(r=n.slice(e+1),n=n.slice(0,e)),n)return arguments.length<2?this[n].on(r):this[n].on(r,t);if(2===arguments.length){if(null==t)for(n in this)this.hasOwnProperty(n)&&this[n].on(r,null);return this}},oa.event=null,oa.requote=function(n){return n.replace(wa,"\\$&")};var wa=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g,Sa={}.__proto__?function(n,t){n.__proto__=t}:function(n,t){for(var e in t)n[e]=t[e]},ka=function(n,t){return t.querySelector(n)},Na=function(n,t){return t.querySelectorAll(n)},Ea=function(n,t){var e=n.matches||n[x(n,"matchesSelector")];return(Ea=function(n,t){return e.call(n,t)})(n,t)};"function"==typeof Sizzle&&(ka=function(n,t){return Sizzle(n,t)[0]||null},Na=Sizzle,Ea=Sizzle.matchesSelector),oa.selection=function(){return oa.select(sa.documentElement)};var Aa=oa.selection.prototype=[];Aa.select=function(n){var t,e,r,u,i=[];n=A(n);for(var a=-1,o=this.length;++a =0?n.slice(0,t):n,r=t>=0?n.slice(t+1):"in";return e=vl.get(e)||pl,r=dl.get(r)||y,br(r(e.apply(null,la.call(arguments,1))))},oa.interpolateHcl=Rr,oa.interpolateHsl=Dr,oa.interpolateLab=Pr,oa.interpolateRound=Ur,oa.transform=function(n){var t=sa.createElementNS(oa.ns.prefix.svg,"g");return(oa.transform=function(n){if(null!=n){t.setAttribute("transform",n);var e=t.transform.baseVal.consolidate()}return new jr(e?e.matrix:ml)})(n)},jr.prototype.toString=function(){return"translate("+this.translate+")rotate("+this.rotate+")skewX("+this.skew+")scale("+this.scale+")"};var ml={a:1,b:0,c:0,d:1,e:0,f:0};oa.interpolateTransform=$r,oa.layout={},oa.layout.bundle=function(){return function(n){for(var t=[],e=-1,r=n.length;++e {{ _("Edit Auto Email Report Settings") }}: {{edit_report_settings}}t;++t)(r=M[t]).index=t,r.weight=0;for(t=0;c>t;++t)r=x[t],"number"==typeof r.source&&(r.source=M[r.source]),"number"==typeof r.target&&(r.target=M[r.target]),++r.source.weight,++r.target.weight;for(t=0;u>t;++t)r=M[t],isNaN(r.x)&&(r.x=n("x",f)),isNaN(r.y)&&(r.y=n("y",v)),isNaN(r.px)&&(r.px=r.x),isNaN(r.py)&&(r.py=r.y);if(i=[],"function"==typeof h)for(t=0;c>t;++t)i[t]=+h.call(this,x[t],t);else for(t=0;c>t;++t)i[t]=h;if(a=[],"function"==typeof g)for(t=0;c>t;++t)a[t]=+g.call(this,x[t],t);else for(t=0;c>t;++t)a[t]=g;if(o=[],"function"==typeof p)for(t=0;u>t;++t)o[t]=+p.call(this,M[t],t);else for(t=0;u>t;++t)o[t]=p;return l.resume()},l.resume=function(){return l.alpha(.1)},l.stop=function(){return l.alpha(0)},l.drag=function(){return r||(r=oa.behavior.drag().origin(y).on("dragstart.force",Qr).on("drag.force",t).on("dragend.force",nu)),arguments.length?void this.on("mouseover.force",tu).on("mouseout.force",eu).call(r):r},oa.rebind(l,c,"on")};var yl=20,Ml=1,xl=1/0;oa.layout.hierarchy=function(){function n(u){var i,a=[u],o=[];for(u.depth=0;null!=(i=a.pop());)if(o.push(i),(c=e.call(n,i,i.depth))&&(l=c.length)){for(var l,c,s;--l>=0;)a.push(s=c[l]),s.parent=i,s.depth=i.depth+1;r&&(i.value=0),i.children=c}else r&&(i.value=+r.call(n,i,i.depth)||0),delete i.children;return au(u,function(n){var e,u;t&&(e=n.children)&&e.sort(t),r&&(u=n.parent)&&(u.value+=n.value)}),o}var t=cu,e=ou,r=lu;return n.sort=function(e){return arguments.length?(t=e,n):t},n.children=function(t){return arguments.length?(e=t,n):e},n.value=function(t){return arguments.length?(r=t,n):r},n.revalue=function(t){return r&&(iu(t,function(n){n.children&&(n.value=0)}),au(t,function(t){var e;t.children||(t.value=+r.call(n,t,t.depth)||0),(e=t.parent)&&(e.value+=t.value)})),t},n},oa.layout.partition=function(){function n(t,e,r,u){var i=t.children;if(t.x=e,t.y=t.depth*u,t.dx=r,t.dy=u,i&&(a=i.length)){var a,o,l,c=-1;for(r=t.value?r/t.value:0;++cf?-1:1),p=oa.sum(c),v=p?(f-l*g)/p:0,d=oa.range(l),m=[];return null!=e&&d.sort(e===bl?function(n,t){return c[t]-c[n]}:function(n,t){return e(a[n],a[t])}),d.forEach(function(n){m[n]={data:a[n],value:o=c[n],startAngle:s,endAngle:s+=o*v+g,padAngle:h}}),m}var t=Number,e=bl,r=0,u=Fa,i=0;return n.value=function(e){return arguments.length?(t=e,n):t},n.sort=function(t){return arguments.length?(e=t,n):e},n.startAngle=function(t){return arguments.length?(r=t,n):r},n.endAngle=function(t){return arguments.length?(u=t,n):u},n.padAngle=function(t){return arguments.length?(i=t,n):i},n};var bl={};oa.layout.stack=function(){function n(o,l){if(!(h=o.length))return o;var c=o.map(function(e,r){return t.call(n,e,r)}),s=c.map(function(t){return t.map(function(t,e){return[i.call(n,t,e),a.call(n,t,e)]})}),f=e.call(n,s,l);c=oa.permute(c,f),s=oa.permute(s,f);var h,g,p,v,d=r.call(n,s,l),m=c[0].length;for(p=0;m>p;++p)for(u.call(n,c[0][p],v=d[p],s[0][p][1]),g=1;h>g;++g)u.call(n,c[g][p],v+=s[g-1][p][1],s[g][p][1]);return o}var t=y,e=pu,r=vu,u=gu,i=fu,a=hu;return n.values=function(e){return arguments.length?(t=e,n):t},n.order=function(t){return arguments.length?(e="function"==typeof t?t:_l.get(t)||pu,n):e},n.offset=function(t){return arguments.length?(r="function"==typeof t?t:wl.get(t)||vu,n):r},n.x=function(t){return arguments.length?(i=t,n):i},n.y=function(t){return arguments.length?(a=t,n):a},n.out=function(t){return arguments.length?(u=t,n):u},n};var _l=oa.map({"inside-out":function(n){var t,e,r=n.length,u=n.map(du),i=n.map(mu),a=oa.range(r).sort(function(n,t){return u[n]-u[t]}),o=0,l=0,c=[],s=[];for(t=0;r>t;++t)e=a[t],l>o?(o+=i[e],c.push(e)):(l+=i[e],s.push(e));return s.reverse().concat(c)},reverse:function(n){return oa.range(n.length).reverse()},"default":pu}),wl=oa.map({silhouette:function(n){var t,e,r,u=n.length,i=n[0].length,a=[],o=0,l=[];for(e=0;i>e;++e){for(t=0,r=0;u>t;t++)r+=n[t][e][1];r>o&&(o=r),a.push(r)}for(e=0;i>e;++e)l[e]=(o-a[e])/2;return l},wiggle:function(n){var t,e,r,u,i,a,o,l,c,s=n.length,f=n[0],h=f.length,g=[];for(g[0]=l=c=0,e=1;h>e;++e){for(t=0,u=0;s>t;++t)u+=n[t][e][1];for(t=0,i=0,o=f[e][0]-f[e-1][0];s>t;++t){for(r=0,a=(n[t][e][1]-n[t][e-1][1])/(2*o);t>r;++r)a+=(n[r][e][1]-n[r][e-1][1])/o;i+=a*n[t][e][1]}g[e]=l-=u?i/u*o:0,c>l&&(c=l)}for(e=0;h>e;++e)g[e]-=c;return g},expand:function(n){var t,e,r,u=n.length,i=n[0].length,a=1/u,o=[];for(e=0;i>e;++e){for(t=0,r=0;u>t;t++)r+=n[t][e][1];if(r)for(t=0;u>t;t++)n[t][e][1]/=r;else for(t=0;u>t;t++)n[t][e][1]=a}for(e=0;i>e;++e)o[e]=0;return o},zero:vu});oa.layout.histogram=function(){function n(n,i){for(var a,o,l=[],c=n.map(e,this),s=r.call(this,c,i),f=u.call(this,s,c,i),i=-1,h=c.length,g=f.length-1,p=t?1:1/h;++i
\ No newline at end of file
+
+{% endif %}
diff --git a/frappe/test_runner.py b/frappe/test_runner.py
index dd1fa8ac39..22656838eb 100644
--- a/frappe/test_runner.py
+++ b/frappe/test_runner.py
@@ -198,6 +198,11 @@ def _add_test(app, path, filename, verbose, test_suite=None, ui_tests=False):
relative_path=relative_path.replace('/', '.'), module_name=filename[:-3])
module = importlib.import_module(module_name)
+
+ if hasattr(module, "test_dependencies"):
+ for doctype in module.test_dependencies:
+ make_test_records(doctype, verbose=verbose)
+
is_ui_test = True if hasattr(module, 'TestDriver') else False
if is_ui_test != ui_tests:
diff --git a/frappe/tests/test_email.py b/frappe/tests/test_email.py
index fc639e77e9..b35b816e7b 100644
--- a/frappe/tests/test_email.py
+++ b/frappe/tests/test_email.py
@@ -49,7 +49,7 @@ class TestEmail(unittest.TestCase):
self.assertTrue('test@example.com' in queue_recipients)
self.assertTrue('test1@example.com' in queue_recipients)
self.assertEquals(len(queue_recipients), 2)
- self.assertTrue('Unsubscribe' in frappe.flags.sent_mail)
+ self.assertTrue('Unsubscribe' in frappe.flags.sent_mail.decode())
def test_cc_header(self):
#test if sending with cc's makes it into header
@@ -84,7 +84,7 @@ class TestEmail(unittest.TestCase):
self.assertTrue('test@example.com' in queue_recipients)
self.assertTrue('test1@example.com' in queue_recipients)
- self.assertTrue('This email was sent to test@example.com and copied to test1@example.com' in frappe.flags.sent_mail)
+ self.assertTrue('This email was sent to test@example.com and copied to test1@example.com' in frappe.flags.sent_mail.decode())
def test_expose(self):
from frappe.utils.verified_command import verify_request
@@ -104,12 +104,12 @@ class TestEmail(unittest.TestCase):
where status='Sent'""", as_dict=1)[0].message
self.assertTrue('' in message)
- email_obj = email.message_from_string(frappe.flags.sent_mail)
+ email_obj = email.message_from_string(frappe.flags.sent_mail.decode())
for part in email_obj.walk():
content = part.get_payload(decode=True)
if content:
- frappe.local.flags.signed_query_string = re.search('(?<=/api/method/frappe.email.queue.unsubscribe\?).*(?=\n)', content).group(0)
+ frappe.local.flags.signed_query_string = re.search('(?<=/api/method/frappe.email.queue.unsubscribe\?).*(?=\n)', content.decode()).group(0)
self.assertTrue(verify_request())
break
@@ -150,7 +150,7 @@ class TestEmail(unittest.TestCase):
self.assertFalse('test@example.com' in queue_recipients)
self.assertTrue('test1@example.com' in queue_recipients)
self.assertEquals(len(queue_recipients), 1)
- self.assertTrue('Unsubscribe' in frappe.flags.sent_mail)
+ self.assertTrue('Unsubscribe' in frappe.flags.sent_mail.decode())
def test_email_queue_limit(self):
from frappe.email.queue import send, EmailLimitCrossedError
diff --git a/frappe/tests/test_goal.py b/frappe/tests/test_goal.py
index 6e94858785..20c2a9a399 100644
--- a/frappe/tests/test_goal.py
+++ b/frappe/tests/test_goal.py
@@ -31,4 +31,4 @@ class TestGoal(unittest.TestCase):
frappe.db.set_value('Event', docname, 'description', 1)
data = get_monthly_goal_graph_data('Test', 'Event', docname, 'description', 'description', 'description',
'Event', '', 'description', 'creation', 'starts_on = "2014-01-01"', 'count')
- self.assertEquals(float(data['y'][0]['values'][-1]), 1)
+ self.assertEquals(float(data['data']['datasets'][0]['values'][-1]), 1)
diff --git a/frappe/tests/ui/data/test_lib.js b/frappe/tests/ui/data/test_lib.js
index 71ba61efaa..a6ca8a4628 100644
--- a/frappe/tests/ui/data/test_lib.js
+++ b/frappe/tests/ui/data/test_lib.js
@@ -1,15 +1,11 @@
frappe.tests = {
data: {},
- get_fixture_names: (doctype) => {
- return Object.keys(frappe.test_data[doctype]);
- },
make: function(doctype, data) {
return frappe.run_serially([
() => frappe.set_route('List', doctype),
() => frappe.new_doc(doctype),
() => {
- if (frappe.quick_entry)
- {
+ if (frappe.quick_entry) {
frappe.quick_entry.dialog.$wrapper.find('.edit-full').click();
return frappe.timeout(1);
}
@@ -79,13 +75,13 @@ frappe.tests = {
});
return frappe.run_serially(grid_row_tasks);
},
- setup_doctype: (doctype) => {
+ setup_doctype: (doctype, data) => {
return frappe.run_serially([
() => frappe.set_route('List', doctype),
() => frappe.timeout(1),
() => {
frappe.tests.data[doctype] = [];
- let expected = frappe.tests.get_fixture_names(doctype);
+ let expected = Object.keys(data);
cur_list.data.forEach((d) => {
frappe.tests.data[doctype].push(d.name);
if(expected.indexOf(d.name) !== -1) {
@@ -98,7 +94,7 @@ frappe.tests = {
expected.forEach(function(d) {
if(d) {
tasks.push(() => frappe.tests.make(doctype,
- frappe.test_data[doctype][d]));
+ data[d]));
}
});
diff --git a/frappe/tests/ui/setup_wizard.js b/frappe/tests/ui/setup_wizard.js
deleted file mode 100644
index add10883c3..0000000000
--- a/frappe/tests/ui/setup_wizard.js
+++ /dev/null
@@ -1,43 +0,0 @@
-var login = require("./login.js")['Login'];
-
-module.exports = {
- before: browser => {
- browser
- .url(browser.launch_url + '/login')
- .waitForElementVisible('body', 5000);
- },
- 'Login': login,
- 'Welcome': browser => {
- let slide_selector = '[data-slide-name="welcome"]';
- browser
- .assert.title('Frappe Desk')
- .pause(5000)
- .assert.visible(slide_selector, 'Check if welcome slide is visible')
- .assert.value('select[data-fieldname="language"]', 'English')
- .click(slide_selector + ' .next-btn');
- },
- 'Region': browser => {
- let slide_selector = '[data-slide-name="region"]';
- browser
- .waitForElementVisible(slide_selector , 2000)
- .pause(6000)
- .setValue('select[data-fieldname="language"]', "India")
- .pause(4000)
- .assert.containsText('div[data-fieldname="timezone"]', 'India Time - Asia/Kolkata')
- .click(slide_selector + ' .next-btn');
- },
- 'User': browser => {
- let slide_selector = '[data-slide-name="user"]';
- browser
- .waitForElementVisible(slide_selector, 2000)
- .pause(3000)
- .setValue('input[data-fieldname="full_name"]', "John Doe")
- .setValue('input[data-fieldname="email"]', "john@example.com")
- .setValue('input[data-fieldname="password"]', "vbjwearghu")
- .click(slide_selector + ' .next-btn');
- },
-
- after: browser => {
- browser.end();
- },
-};
\ No newline at end of file
diff --git a/frappe/tests/ui/test_oauth20.py b/frappe/tests/ui/test_oauth20.py
index b9b63847e2..c6361fc39d 100644
--- a/frappe/tests/ui/test_oauth20.py
+++ b/frappe/tests/ui/test_oauth20.py
@@ -5,7 +5,7 @@ from __future__ import unicode_literals
import unittest, frappe, requests, time
from frappe.test_runner import make_test_records
from frappe.utils.selenium_testdriver import TestDriver
-from six.moves.urllib.parse import urlparse
+from six.moves.urllib.parse import urlparse, parse_qs
class TestOAuth20(unittest.TestCase):
def setUp(self):
@@ -18,7 +18,7 @@ class TestOAuth20(unittest.TestCase):
frappe.db.set_value("Social Login Keys", None, "frappe_server_url", "http://localhost:8000")
frappe.db.commit()
- def test_login_to_authorize_url(self):
+ def test_login_using_authorization_code(self):
# Go to Authorize url
self.driver.get(
@@ -70,3 +70,46 @@ class TestOAuth20(unittest.TestCase):
self.assertTrue(bearer_token.get("refresh_token"))
self.assertTrue(bearer_token.get("scope"))
self.assertTrue(bearer_token.get("token_type") == "Bearer")
+
+ def test_login_using_implicit_token(self):
+
+ oauth_client = frappe.get_doc("OAuth Client", self.client_id)
+ oauth_client.grant_type = "Implicit"
+ oauth_client.response_type = "Token"
+ oauth_client.save()
+ frappe.db.commit()
+
+ # Go to Authorize url
+ self.driver.get(
+ "api/method/frappe.integrations.oauth2.authorize?client_id=" +
+ self.client_id +
+ "&scope=all%20openid&response_type=token&redirect_uri=http%3A%2F%2Flocalhost"
+ )
+
+ time.sleep(2)
+
+ # Login
+ username = self.driver.find("#login_email")[0]
+ username.send_keys("test@example.com")
+
+ password = self.driver.find("#login_password")[0]
+ password.send_keys("Eastern_43A1W")
+
+ sign_in = self.driver.find(".btn-login")[0]
+ sign_in.submit()
+
+ time.sleep(2)
+
+ # Allow access to resource
+ allow = self.driver.find("#allow")[0]
+ allow.click()
+
+ time.sleep(2)
+
+ # Get token from redirected URL
+ response_url = dict(parse_qs(urlparse(self.driver.driver.current_url).fragment))
+
+ self.assertTrue(response_url.get("access_token"))
+ self.assertTrue(response_url.get("expires_in"))
+ self.assertTrue(response_url.get("scope"))
+ self.assertTrue(response_url.get("token_type"))
diff --git a/frappe/twofactor.py b/frappe/twofactor.py
index a8efc6a98b..9b402a8286 100644
--- a/frappe/twofactor.py
+++ b/frappe/twofactor.py
@@ -9,7 +9,7 @@ import pyotp, os
from frappe.utils.background_jobs import enqueue
from jinja2 import Template
from pyqrcode import create as qrcreate
-from six import StringIO
+from six import BytesIO
from base64 import b64encode, b32encode
from frappe.utils import get_url, get_datetime, time_diff_in_seconds
from six import iteritems, string_types
@@ -317,11 +317,11 @@ def get_qr_svg_code(totp_uri):
'''Get SVG code to display Qrcode for OTP.'''
url = qrcreate(totp_uri)
svg = ''
- stream = StringIO()
+ stream = BytesIO()
try:
url.svg(stream, scale=4, background="#eee", module_color="#222")
- svg = stream.getvalue().replace('\n', '')
- svg = b64encode(bytes(svg))
+ svg = stream.getvalue().decode().replace('\n', '')
+ svg = b64encode(svg.encode())
finally:
stream.close()
return svg
diff --git a/frappe/utils/background_jobs.py b/frappe/utils/background_jobs.py
index 234ba3b929..c540b5bb52 100755
--- a/frappe/utils/background_jobs.py
+++ b/frappe/utils/background_jobs.py
@@ -18,7 +18,7 @@ queue_timeout = {
}
def enqueue(method, queue='default', timeout=300, event=None,
- async=True, job_name=None, now=False, **kwargs):
+ async=True, job_name=None, now=False, enqueue_after_commit=False, **kwargs):
'''
Enqueue method to be executed using a background worker
@@ -37,17 +37,38 @@ def enqueue(method, queue='default', timeout=300, event=None,
q = get_queue(queue, async=async)
if not timeout:
timeout = queue_timeout.get(queue) or 300
-
- return q.enqueue_call(execute_job, timeout=timeout,
- kwargs={
- "site": frappe.local.site,
- "user": frappe.session.user,
- "method": method,
- "event": event,
- "job_name": job_name or cstr(method),
+ queue_args = {
+ "site": frappe.local.site,
+ "user": frappe.session.user,
+ "method": method,
+ "event": event,
+ "job_name": job_name or cstr(method),
+ "async": async,
+ "kwargs": kwargs
+ }
+ if enqueue_after_commit:
+ if not frappe.flags.enqueue_after_commit:
+ frappe.flags.enqueue_after_commit = []
+
+ frappe.flags.enqueue_after_commit.append({
+ "queue": queue,
"async": async,
- "kwargs": kwargs
+ "timeout": timeout,
+ "queue_args":queue_args
})
+ return frappe.flags.enqueue_after_commit
+ else:
+ return q.enqueue_call(execute_job, timeout=timeout,
+ kwargs=queue_args)
+
+def enqueue_doc(doctype, name=None, method=None, queue='default', timeout=300,
+ now=False, **kwargs):
+ '''Enqueue a method to be run on a document'''
+ enqueue('frappe.utils.background_jobs.run_doc_method', doctype=doctype, name=name,
+ doc_method=method, queue=queue, timeout=timeout, now=now, **kwargs)
+
+def run_doc_method(doctype, name, doc_method, **kwargs):
+ getattr(frappe.get_doc(doctype, name), doc_method)(**kwargs)
def execute_job(site, method, event, job_name, kwargs, user=None, async=True, retry=0):
'''Executes job in a worker, performs commit/rollback and logs if there is any error'''
diff --git a/frappe/utils/file_manager.py b/frappe/utils/file_manager.py
index 822b63e240..48fdc82179 100644
--- a/frappe/utils/file_manager.py
+++ b/frappe/utils/file_manager.py
@@ -191,7 +191,9 @@ def write_file(content, fname, is_private=0):
# create directory (if not exists)
frappe.create_folder(file_path)
# write the file
- with open(os.path.join(file_path.encode('utf-8'), fname.encode('utf-8')), 'w+') as f:
+ if isinstance(content, text_type):
+ content = content.encode()
+ with open(os.path.join(file_path.encode('utf-8'), fname.encode('utf-8')), 'wb+') as f:
f.write(content)
return get_files_path(fname, is_private=is_private)
diff --git a/frappe/utils/global_search.py b/frappe/utils/global_search.py
index f9a07e3212..84c3308e96 100644
--- a/frappe/utils/global_search.py
+++ b/frappe/utils/global_search.py
@@ -5,6 +5,7 @@ from __future__ import unicode_literals
import frappe
import re
+import redis
from frappe.utils import cint, strip_html_tags
from frappe.model.base_document import get_controller
from six import text_type
@@ -232,6 +233,18 @@ def update_global_search(doc):
frappe.flags.update_global_search.append(
dict(doctype=doc.doctype, name=doc.name, content=' ||| '.join(content or ''),
published=published, title=doc.get_title(), route=doc.get('route')))
+ enqueue_global_search()
+
+def enqueue_global_search():
+ if frappe.flags.update_global_search:
+ try:
+ frappe.enqueue('frappe.utils.global_search.sync_global_search',
+ now=frappe.flags.in_test or frappe.flags.in_install or frappe.flags.in_migrate,
+ flags=frappe.flags.update_global_search, enqueue_after_commit=True)
+ except redis.exceptions.ConnectionError:
+ sync_global_search()
+
+ frappe.flags.update_global_search = []
def get_formatted_value(value, field):
'''Prepare field from raw data'''
diff --git a/frappe/utils/goal.py b/frappe/utils/goal.py
index a126f161df..801648f249 100644
--- a/frappe/utils/goal.py
+++ b/frappe/utils/goal.py
@@ -97,7 +97,7 @@ def get_monthly_goal_graph_data(title, doctype, docname, goal_value_field, goal_
specific_values = []
summary_values = [
{
- 'name': _("This month"),
+ 'title': _("This month"),
'color': 'green',
'value': formatted_value
}
@@ -106,19 +106,19 @@ def get_monthly_goal_graph_data(title, doctype, docname, goal_value_field, goal_
if float(goal) > 0:
specific_values = [
{
- 'name': _("Goal"),
+ 'title': _("Goal"),
'line_type': "dashed",
'value': goal
},
]
summary_values += [
{
- 'name': _("Goal"),
+ 'title': _("Goal"),
'color': 'blue',
'value': formatted_goal
},
{
- 'name': _("Completed"),
+ 'title': _("Completed"),
'color': 'green',
'value': str(int(round(float(current_month_value)/float(goal)*100))) + "%"
}
@@ -127,16 +127,16 @@ def get_monthly_goal_graph_data(title, doctype, docname, goal_value_field, goal_
data = {
'title': title,
# 'subtitle':
- 'y': [
- {
- 'color': 'green',
- 'values': values,
- 'formatted': values_formatted
- }
- ],
- 'x': {
- 'values': months,
- 'formatted': months_formatted
+
+ 'data': {
+ 'datasets': [
+ {
+ 'color': 'green',
+ 'values': values,
+ 'formatted': values_formatted
+ }
+ ],
+ 'labels': months
},
'specific_values': specific_values,
diff --git a/frappe/utils/pdf.py b/frappe/utils/pdf.py
index 6005346270..57c4051be6 100644
--- a/frappe/utils/pdf.py
+++ b/frappe/utils/pdf.py
@@ -84,6 +84,10 @@ def read_options_from_html(html):
options = {}
soup = BeautifulSoup(html, "html5lib")
+ options.update(prepare_header_footer(soup))
+
+ toggle_visible_pdf(soup)
+
# extract pdfkit options from html
for html_id in ("margin-top", "margin-bottom", "margin-left", "margin-right", "page-size"):
try:
@@ -93,10 +97,6 @@ def read_options_from_html(html):
except:
pass
- options.update(prepare_header_footer(soup))
-
- toggle_visible_pdf(soup)
-
return soup.prettify(), options
def prepare_header_footer(soup):
diff --git a/frappe/utils/selenium_testdriver.py b/frappe/utils/selenium_testdriver.py
index 2c6a224be7..3f3d82fe43 100644
--- a/frappe/utils/selenium_testdriver.py
+++ b/frappe/utils/selenium_testdriver.py
@@ -19,8 +19,8 @@ import frappe
from ast import literal_eval
class TestDriver(object):
- def __init__(self, port='8000'):
- self.port = port
+ def __init__(self, port=None):
+ self.port = port or frappe.get_site_config().webserver_port or '8000'
chrome_options = Options()
capabilities = DesiredCapabilities.CHROME
@@ -84,16 +84,19 @@ class TestDriver(object):
time.sleep(0.2)
def set_field(self, fieldname, text):
- elem = self.find(xpath='//input[@data-fieldname="{0}"]'.format(fieldname))
- elem[0].send_keys(text)
+ elem = self.wait_for(xpath='//input[@data-fieldname="{0}"]'.format(fieldname))
+ time.sleep(0.2)
+ elem.send_keys(text)
def set_select(self, fieldname, text):
- elem = self.find(xpath='//select[@data-fieldname="{0}"]'.format(fieldname))
- elem[0].send_keys(text)
+ elem = self.wait_for(xpath='//select[@data-fieldname="{0}"]'.format(fieldname))
+ time.sleep(0.2)
+ elem.send_keys(text)
def set_text_editor(self, fieldname, text):
- elem = self.find(xpath='//div[@data-fieldname="{0}"]//div[@contenteditable="true"]'.format(fieldname))
- elem[0].send_keys(text)
+ elem = self.wait_for(xpath='//div[@data-fieldname="{0}"]//div[@contenteditable="true"]'.format(fieldname))
+ time.sleep(0.2)
+ elem.send_keys(text)
def find(self, selector=None, everywhere=False, xpath=None):
if xpath:
@@ -164,7 +167,11 @@ class TestDriver(object):
self.wait_for(xpath='//div[@data-page-route="{0}"]'.format('/'.join(args)), timeout=4)
def click(self, css_selector, xpath=None):
- self.wait_till_clickable(css_selector, xpath).click()
+ element = self.wait_till_clickable(css_selector, xpath)
+ self.scroll_to(css_selector)
+ time.sleep(0.5)
+ element.click()
+ return element
def click_primary_action(self):
selector = ".page-actions .primary-action"
@@ -201,6 +208,7 @@ class TestDriver(object):
return self.get_wait().until(EC.element_to_be_clickable(
(by, selector)))
+
def execute_script(self, js):
self.driver.execute_script(js)
diff --git a/frappe/website/doctype/web_form/templates/web_form.html b/frappe/website/doctype/web_form/templates/web_form.html
index a1cbbb0240..df0fa1de3e 100644
--- a/frappe/website/doctype/web_form/templates/web_form.html
+++ b/frappe/website/doctype/web_form/templates/web_form.html
@@ -98,9 +98,9 @@
{% if field.hidden %}
- {% elif field.fieldtype == "HTML" and field.options %}
-
-
@@ -48,4 +49,5 @@