| ${__('Source Text')} | -${__('Translated Text')} | +${__("Source Text")} | +${__("Translated Text")} |
|---|---|---|---|
| ${t.source_text} | ${t.translated_text} |
| '+__('Filter')+' | '+__('Value')+' |
|---|
' + __("Click table to edit") + '
').appendTo(wrapper); + var table = $( + '| ' + + __("Filter") + + " | " + + __("Value") + + " |
|---|
' + __("Click table to edit") + "
").appendTo( + wrapper + ); - var filters = JSON.parse(frm.doc.filters || '{}'); + var filters = JSON.parse(frm.doc.filters || "{}"); let report_filters; - if (frm.doc.report_type === 'Custom Report' - && frappe.query_reports[frm.doc.reference_report] - && frappe.query_reports[frm.doc.reference_report].filters) { + if ( + frm.doc.report_type === "Custom Report" && + frappe.query_reports[frm.doc.reference_report] && + frappe.query_reports[frm.doc.reference_report].filters + ) { report_filters = frappe.query_reports[frm.doc.reference_report].filters; } else { report_filters = frappe.query_reports[frm.doc.report].filters; } - if(report_filters && report_filters.length > 0) { - frm.set_value('filter_meta', JSON.stringify(report_filters)); + if (report_filters && report_filters.length > 0) { + frm.set_value("filter_meta", JSON.stringify(report_filters)); if (frm.is_dirty()) { frm.save(); } } - var report_filters_list = [] - $.each(report_filters, function(key, val){ + var report_filters_list = []; + $.each(report_filters, function (key, val) { // Remove break fieldtype from the filters - if(val.fieldtype != 'Break') { - report_filters_list.push(val) + if (val.fieldtype != "Break") { + report_filters_list.push(val); } - }) + }); report_filters = report_filters_list; const mandatory_css = { "background-color": "var(--error-bg)", - "font-weight": "bold" + "font-weight": "bold", }; - report_filters.forEach(f => { + report_filters.forEach((f) => { const css = f.reqd ? mandatory_css : {}; const row = $("- ' + links.map(link => `
- ${link} `).join('') + '
- " +
+ links.map((link) => `
- ${link} `).join("") + + "
{0}. {1}.
- +{}. {}.
+ """.format( *translatable_content ) @@ -282,7 +288,7 @@ def subscribe(email, email_group=_("Website")): @frappe.whitelist(allow_guest=True) -def confirm_subscription(email, email_group=_("Website")): +def confirm_subscription(email, email_group=_("Website")): # noqa """API endpoint to confirm email subscription. This endpoint is called when user clicks on the link sent to their mail. """ @@ -329,19 +335,17 @@ def send_scheduled_email(): pluck="name", ) - for newsletter in scheduled_newsletter: + for newsletter_name in scheduled_newsletter: try: - frappe.get_doc("Newsletter", newsletter).queue_all() + newsletter = frappe.get_doc("Newsletter", newsletter_name) + newsletter.queue_all() except Exception: frappe.db.rollback() # wasn't able to send emails :( - frappe.db.set_value("Newsletter", newsletter, "email_sent", 0) - message = ( - f"Newsletter {newsletter} failed to send" "\n\n" f"Traceback: {frappe.get_traceback()}" - ) - frappe.log_error(title="Send Newsletter", message=message) + frappe.db.set_value("Newsletter", newsletter_name, "email_sent", 0) + newsletter.log_error("Failed to send newsletter") if not frappe.flags.in_test: frappe.db.commit() diff --git a/frappe/email/doctype/newsletter/newsletter_list.js b/frappe/email/doctype/newsletter/newsletter_list.js index 9ded6148e0..0921de02b4 100644 --- a/frappe/email/doctype/newsletter/newsletter_list.js +++ b/frappe/email/doctype/newsletter/newsletter_list.js @@ -1,12 +1,12 @@ -frappe.listview_settings['Newsletter'] = { +frappe.listview_settings["Newsletter"] = { add_fields: ["subject", "email_sent", "schedule_sending"], - get_indicator: function(doc) { + get_indicator: function (doc) { if (doc.email_sent) { return [__("Sent"), "green", "email_sent,=,Yes"]; } else if (doc.schedule_sending) { - return [__("Scheduled"), "orange", "email_sent,=,No|schedule_sending,=,Yes"]; + return [__("Scheduled"), "purple", "email_sent,=,No|schedule_sending,=,Yes"]; } else { - return [__("Not Sent"), "orange", "email_sent,=,No"]; + return [__("Not Sent"), "gray", "email_sent,=,No"]; } - } + }, }; diff --git a/frappe/email/doctype/newsletter/test_newsletter.py b/frappe/email/doctype/newsletter/test_newsletter.py index c62b7e84aa..524289db7f 100644 --- a/frappe/email/doctype/newsletter/test_newsletter.py +++ b/frappe/email/doctype/newsletter/test_newsletter.py @@ -1,13 +1,10 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # MIT License. See LICENSE -import unittest from random import choice -from typing import Union from unittest.mock import MagicMock, PropertyMock, patch import frappe -from frappe.desk.form.load import run_onload from frappe.email.doctype.newsletter.exceptions import ( NewsletterAlreadySentError, NoRecipientFoundError, @@ -18,9 +15,9 @@ from frappe.email.doctype.newsletter.newsletter import ( send_scheduled_email, ) from frappe.email.queue import flush +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, getdate -test_dependencies = ["Email Group"] emails = [ "test_subscriber1@example.com", "test_subscriber2@example.com", @@ -64,17 +61,24 @@ class TestNewsletterMixin: for email in emails: doctype = "Email Group Member" email_filters = {"email": email, "email_group": "_Test Email Group"} + + savepoint = "setup_email_group" + frappe.db.savepoint(savepoint) + try: frappe.get_doc( { "doctype": doctype, **email_filters, } - ).insert() + ).insert(ignore_if_duplicate=True) except Exception: + frappe.db.rollback(save_point=savepoint) frappe.db.update(doctype, email_filters, "unsubscribed", 0) - def send_newsletter(self, published=0, schedule_send=None) -> Union[str, None]: + frappe.db.release_savepoint(savepoint) + + def send_newsletter(self, published=0, schedule_send=None) -> str | None: frappe.db.delete("Email Queue") frappe.db.delete("Email Queue Recipient") frappe.db.delete("Newsletter") @@ -128,7 +132,7 @@ class TestNewsletterMixin: return newsletter -class TestNewsletter(TestNewsletterMixin, unittest.TestCase): +class TestNewsletter(TestNewsletterMixin, FrappeTestCase): def test_send(self): self.send_newsletter() @@ -221,3 +225,24 @@ class TestNewsletter(TestNewsletterMixin, unittest.TestCase): newsletter.reload() self.assertEqual(newsletter.email_sent, 0) + + def test_retry_partially_sent_newsletter(self): + frappe.db.delete("Email Queue") + frappe.db.delete("Email Queue Recipient") + frappe.db.delete("Newsletter") + + newsletter = self.get_newsletter() + newsletter.send_emails() + email_queue_list = [frappe.get_doc("Email Queue", e.name) for e in frappe.get_all("Email Queue")] + self.assertEqual(len(email_queue_list), 4) + + # emulate partial send + email_queue_list[0].status = "Error" + email_queue_list[0].recipients[0].status = "Error" + email_queue_list[0].save() + newsletter.email_sent = False + + # retry + newsletter.send_emails() + email_queue_list = [frappe.get_doc("Email Queue", e.name) for e in frappe.get_all("Email Queue")] + self.assertEqual(len(email_queue_list), 5) diff --git a/frappe/email/doctype/newsletter_email_group/newsletter_email_group.py b/frappe/email/doctype/newsletter_email_group/newsletter_email_group.py index b7a00ac7d2..41ada8a491 100644 --- a/frappe/email/doctype/newsletter_email_group/newsletter_email_group.py +++ b/frappe/email/doctype/newsletter_email_group/newsletter_email_group.py @@ -1,8 +1,6 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2015, Frappe Technologies and contributors # License: MIT. See LICENSE -import frappe from frappe.model.document import Document diff --git a/frappe/email/doctype/notification/notification.js b/frappe/email/doctype/notification/notification.js index f14447707f..4e3b1eae53 100644 --- a/frappe/email/doctype/notification/notification.js +++ b/frappe/email/doctype/notification/notification.js @@ -1,100 +1,98 @@ // Copyright (c) 2018, Frappe Technologies and contributors // For license information, please see license.txt -this.frm.add_fetch('sender', 'email_id', 'sender_email'); +this.frm.add_fetch("sender", "email_id", "sender_email"); -this.frm.fields_dict.sender.get_query = function() { +this.frm.fields_dict.sender.get_query = function () { return { filters: { - enable_outgoing: 1 - } + enable_outgoing: 1, + }, }; }; frappe.notification = { - setup_fieldname_select: function(frm) { + setup_fieldname_select: function (frm) { // get the doctype to update fields if (!frm.doc.document_type) { return; } - frappe.model.with_doctype(frm.doc.document_type, function() { - let get_select_options = function(df, parent_field) { + frappe.model.with_doctype(frm.doc.document_type, function () { + let get_select_options = function (df, parent_field) { // Append parent_field name along with fieldname for child table fields - let select_value = parent_field ? df.fieldname + ',' + parent_field : df.fieldname; + let select_value = parent_field ? df.fieldname + "," + parent_field : df.fieldname; return { value: select_value, - label: df.fieldname + ' (' + __(df.label) + ')' + label: df.fieldname + " (" + __(df.label) + ")", }; }; - let get_date_change_options = function() { - let date_options = $.map(fields, function(d) { - return d.fieldtype == 'Date' || d.fieldtype == 'Datetime' + let get_date_change_options = function () { + let date_options = $.map(fields, function (d) { + return d.fieldtype == "Date" || d.fieldtype == "Datetime" ? get_select_options(d) : null; }); // append creation and modified date to Date Change field return date_options.concat([ - { value: 'creation', label: `creation (${__('Created On')})` }, - { value: 'modified', label: `modified (${__('Last Modified Date')})` } + { value: "creation", label: `creation (${__("Created On")})` }, + { value: "modified", label: `modified (${__("Last Modified Date")})` }, ]); }; - let fields = frappe.get_doc('DocType', frm.doc.document_type).fields; - let options = $.map(fields, function(d) { + let fields = frappe.get_doc("DocType", frm.doc.document_type).fields; + let options = $.map(fields, function (d) { return in_list(frappe.model.no_value_type, d.fieldtype) - ? null : get_select_options(d); + ? null + : get_select_options(d); }); // set value changed options - frm.set_df_property('value_changed', 'options', [''].concat(options)); - frm.set_df_property( - 'set_property_after_alert', - 'options', - [''].concat(options) - ); + frm.set_df_property("value_changed", "options", [""].concat(options)); + frm.set_df_property("set_property_after_alert", "options", [""].concat(options)); // set date changed options - frm.set_df_property('date_changed', 'options', get_date_change_options()); + frm.set_df_property("date_changed", "options", get_date_change_options()); let receiver_fields = []; - if (frm.doc.channel === 'Email') { - receiver_fields = $.map(fields, function(d) { - + if (frm.doc.channel === "Email") { + receiver_fields = $.map(fields, function (d) { // Add User and Email fields from child into select dropdown - if (d.fieldtype == 'Table') { - let child_fields = frappe.get_doc('DocType', d.options).fields; - return $.map(child_fields, function(df) { - return df.options == 'Email' || - (df.options == 'User' && df.fieldtype == 'Link') - ? get_select_options(df, d.fieldname) : null; + if (d.fieldtype == "Table") { + let child_fields = frappe.get_doc("DocType", d.options).fields; + return $.map(child_fields, function (df) { + return df.options == "Email" || + (df.options == "User" && df.fieldtype == "Link") + ? get_select_options(df, d.fieldname) + : null; }); - // Add User and Email fields from parent into select dropdown + // Add User and Email fields from parent into select dropdown } else { - return d.options == 'Email' || - (d.options == 'User' && d.fieldtype == 'Link') - ? get_select_options(d) : null; + return d.options == "Email" || + (d.options == "User" && d.fieldtype == "Link") + ? get_select_options(d) + : null; } }); - } else if (in_list(['WhatsApp', 'SMS'], frm.doc.channel)) { - receiver_fields = $.map(fields, function(d) { - return d.options == 'Phone' ? get_select_options(d) : null; + } else if (in_list(["WhatsApp", "SMS"], frm.doc.channel)) { + receiver_fields = $.map(fields, function (d) { + return d.options == "Phone" ? get_select_options(d) : null; }); } // set email recipient options frm.fields_dict.recipients.grid.update_docfield_property( - 'receiver_by_document_field', - 'options', - [''].concat(["owner"]).concat(receiver_fields) + "receiver_by_document_field", + "options", + [""].concat(["owner"]).concat(receiver_fields) ); }); }, - setup_example_message: function(frm) { - let template = ''; - if (frm.doc.channel === 'Email') { + setup_example_message: function (frm) { + let template = ""; + if (frm.doc.channel === "Email") { template = `Message Example
<h3>Order Overdue</h3>
@@ -114,7 +112,7 @@ Last comment: {{ comments[-1].comment }} by {{ comments[-1].by }}
</ul>
`;
- } else if (in_list(['Slack', 'System Notification', 'SMS'], frm.doc.channel)) {
+ } else if (in_list(["Slack", "System Notification", "SMS"], frm.doc.channel)) {
template = `Message Example
*Order Overdue*
@@ -133,71 +131,72 @@ Last comment: {{ comments[-1].comment }} by {{ comments[-1].by }}
`;
}
if (template) {
- frm.set_df_property('message_examples', 'options', template);
+ frm.set_df_property("message_examples", "options", template);
}
-
- }
+ },
};
-frappe.ui.form.on('Notification', {
- onload: function(frm) {
- frm.set_query('document_type', function() {
+frappe.ui.form.on("Notification", {
+ onload: function (frm) {
+ frm.set_query("document_type", function () {
return {
filters: {
- istable: 0
- }
+ istable: 0,
+ },
};
});
- frm.set_query('print_format', function() {
+ frm.set_query("print_format", function () {
return {
filters: {
- doc_type: frm.doc.document_type
- }
+ doc_type: frm.doc.document_type,
+ },
};
});
},
- refresh: function(frm) {
+ refresh: function (frm) {
frappe.notification.setup_fieldname_select(frm);
frappe.notification.setup_example_message(frm);
- frm.get_field('is_standard').toggle(frappe.boot.developer_mode);
- frm.trigger('event');
+ frm.get_field("is_standard").toggle(frappe.boot.developer_mode);
+ frm.trigger("event");
},
- document_type: function(frm) {
+ document_type: function (frm) {
frappe.notification.setup_fieldname_select(frm);
},
- view_properties: function(frm) {
+ view_properties: function (frm) {
frappe.route_options = { doc_type: frm.doc.document_type };
- frappe.set_route('Form', 'Customize Form');
+ frappe.set_route("Form", "Customize Form");
},
- event: function(frm) {
- if (in_list(['Days Before', 'Days After'], frm.doc.event)) {
- frm.add_custom_button(__('Get Alerts for Today'), function() {
+ event: function (frm) {
+ if (in_list(["Days Before", "Days After"], frm.doc.event)) {
+ frm.add_custom_button(__("Get Alerts for Today"), function () {
frappe.call({
- method:
- 'frappe.email.doctype.notification.notification.get_documents_for_today',
+ method: "frappe.email.doctype.notification.notification.get_documents_for_today",
args: {
- notification: frm.doc.name
+ notification: frm.doc.name,
},
- callback: function(r) {
+ callback: function (r) {
if (r.message) {
frappe.msgprint(r.message);
} else {
- frappe.msgprint(__('No alerts for today'));
+ frappe.msgprint(__("No alerts for today"));
}
- }
+ },
});
});
}
},
- channel: function(frm) {
- frm.toggle_reqd('recipients', frm.doc.channel == 'Email');
+ channel: function (frm) {
+ frm.toggle_reqd("recipients", frm.doc.channel == "Email");
frappe.notification.setup_fieldname_select(frm);
frappe.notification.setup_example_message(frm);
- if (frm.doc.channel === 'SMS' && frm.doc.__islocal) {
- frm.set_df_property('channel',
- 'description', `To use SMS Channel, initialize SMS Settings.`);
+ if (frm.doc.channel === "SMS" && frm.doc.__islocal) {
+ frm.set_df_property(
+ "channel",
+ "description",
+ `To use SMS Channel, initialize SMS Settings.`
+ );
} else {
- frm.set_df_property('channel', 'description', ` `);
+ frm.set_df_property("channel", "description", ` `);
}
- }
+ },
});
diff --git a/frappe/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py
index 5c27eb95eb..8d0857ac60 100644
--- a/frappe/email/doctype/notification/notification.py
+++ b/frappe/email/doctype/notification/notification.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and contributors
# License: MIT. See LICENSE
@@ -13,7 +12,7 @@ from frappe.desk.doctype.notification_log.notification_log import enqueue_create
from frappe.integrations.doctype.slack_webhook_url.slack_webhook_url import send_slack_message
from frappe.model.document import Document
from frappe.modules.utils import export_module_json, get_doc_module
-from frappe.utils import add_to_date, is_html, nowdate, parse_val, validate_email_address
+from frappe.utils import add_to_date, cast, is_html, nowdate, validate_email_address
from frappe.utils.jinja import validate_template
from frappe.utils.safe_exec import get_safe_globals
@@ -140,8 +139,8 @@ def get_context(context):
if self.channel == "System Notification" or self.send_system_notification:
self.create_system_notification(doc, context)
- except:
- frappe.log_error(title="Failed to send notification", message=frappe.get_traceback())
+ except Exception:
+ self.log_error("Failed to send Notification")
if self.set_property_after_alert:
allow_update = True
@@ -168,7 +167,7 @@ def get_context(context):
doc.save(ignore_permissions=True)
doc.flags.in_notification_update = False
except Exception:
- frappe.log_error(title="Document update failed", message=frappe.get_traceback())
+ self.log_error("Document update failed")
def create_system_notification(self, doc, context):
subject = self.subject
@@ -368,7 +367,7 @@ def get_context(context):
template = ""
template_path = os.path.join(os.path.dirname(module.__file__), frappe.scrub(self.name) + extn)
if os.path.exists(template_path):
- with open(template_path, "r") as f:
+ with open(template_path) as f:
template = f.read()
return template
@@ -417,7 +416,7 @@ def trigger_notifications(doc, method=None):
frappe.db.commit()
-def evaluate_alert(doc, alert, event):
+def evaluate_alert(doc: Document, alert, event):
from jinja2 import TemplateError
try:
@@ -433,14 +432,14 @@ def evaluate_alert(doc, alert, event):
if event == "Value Change" and not doc.is_new():
if not frappe.db.has_column(doc.doctype, alert.value_changed):
alert.db_set("enabled", 0)
- frappe.log_error("Notification {0} has been disabled due to missing field".format(alert.name))
+ alert.log_error(f"Notification {alert.name} has been disabled due to missing field")
return
doc_before_save = doc.get_doc_before_save()
field_value_before_save = doc_before_save.get(alert.value_changed) if doc_before_save else None
- field_value_before_save = parse_val(field_value_before_save)
- if doc.get(alert.value_changed) == field_value_before_save:
+ fieldtype = doc.meta.get_field(alert.value_changed).fieldtype
+ if cast(fieldtype, doc.get(alert.value_changed)) == cast(fieldtype, field_value_before_save):
# value not changed
return
diff --git a/frappe/email/doctype/notification/test_notification.py b/frappe/email/doctype/notification/test_notification.py
index 4d8b26c559..0f570b1fd3 100644
--- a/frappe/email/doctype/notification/test_notification.py
+++ b/frappe/email/doctype/notification/test_notification.py
@@ -1,7 +1,7 @@
-# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and Contributors
# License: MIT. See LICENSE
import unittest
+from contextlib import contextmanager
import frappe
import frappe.utils
@@ -11,6 +11,15 @@ from frappe.desk.form import assign_to
test_dependencies = ["User", "Notification"]
+@contextmanager
+def get_test_notification(config):
+ try:
+ notification = frappe.get_doc(doctype="Notification", **config).insert()
+ yield notification
+ finally:
+ notification.delete()
+
+
class TestNotification(unittest.TestCase):
def setUp(self):
frappe.db.delete("Email Queue")
@@ -84,7 +93,7 @@ class TestNotification(unittest.TestCase):
def test_condition(self):
"""Check notification is triggered based on a condition."""
event = frappe.new_doc("Event")
- event.subject = ("test",)
+ event.subject = "test"
event.event_type = "Private"
event.starts_on = "2014-06-06 12:00:00"
event.insert()
@@ -137,7 +146,7 @@ class TestNotification(unittest.TestCase):
def test_value_changed(self):
event = frappe.new_doc("Event")
- event.subject = ("test",)
+ event.subject = "test"
event.event_type = "Private"
event.starts_on = "2014-06-06 12:00:00"
event.insert()
@@ -186,7 +195,7 @@ class TestNotification(unittest.TestCase):
frappe.db.commit()
event = frappe.new_doc("Event")
- event.subject = ("test-2",)
+ event.subject = "test-2"
event.event_type = "Private"
event.starts_on = "2014-06-06 12:00:00"
event.insert()
@@ -200,9 +209,8 @@ class TestNotification(unittest.TestCase):
event.delete()
def test_date_changed(self):
-
event = frappe.new_doc("Event")
- event.subject = ("test",)
+ event.subject = "test"
event.event_type = "Private"
event.starts_on = "2014-01-01 12:00:00"
event.insert()
@@ -345,6 +353,31 @@ class TestNotification(unittest.TestCase):
self.assertTrue("test2@example.com" in recipients)
self.assertTrue("test1@example.com" in recipients)
+ def test_notification_value_change_casted_types(self):
+ """Make sure value change event dont fire because of incorrect type comparisons."""
+ frappe.set_user("Administrator")
+
+ notification = {
+ "document_type": "User",
+ "subject": "User changed birthdate",
+ "event": "Value Change",
+ "channel": "System Notification",
+ "value_changed": "birth_date",
+ "recipients": [{"receiver_by_document_field": "email"}],
+ }
+
+ with get_test_notification(notification) as n:
+ frappe.db.delete("Notification Log", {"subject": n.subject})
+
+ user = frappe.get_doc("User", "test@example.com")
+ user.birth_date = frappe.utils.add_days(user.birth_date, 1)
+ user.save()
+
+ user.reload()
+ user.birth_date = frappe.utils.getdate(user.birth_date)
+ user.save()
+ self.assertEqual(1, frappe.db.count("Notification Log", {"subject": n.subject}))
+
@classmethod
def tearDownClass(cls):
frappe.delete_doc_if_exists("Notification", "ToDo Status Update")
diff --git a/frappe/email/doctype/notification_recipient/notification_recipient.py b/frappe/email/doctype/notification_recipient/notification_recipient.py
index 9de15f46c0..75bb274599 100644
--- a/frappe/email/doctype/notification_recipient/notification_recipient.py
+++ b/frappe/email/doctype/notification_recipient/notification_recipient.py
@@ -1,8 +1,6 @@
-# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies and contributors
# License: MIT. See LICENSE
-import frappe
from frappe.model.document import Document
diff --git a/frappe/email/doctype/unhandled_email/test_unhandled_email.py b/frappe/email/doctype/unhandled_email/test_unhandled_email.py
index 694b3e03a6..debc52d685 100644
--- a/frappe/email/doctype/unhandled_email/test_unhandled_email.py
+++ b/frappe/email/doctype/unhandled_email/test_unhandled_email.py
@@ -1,10 +1,7 @@
-# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import unittest
-import frappe
-
# test_records = frappe.get_test_records('Unhandled Emails')
diff --git a/frappe/email/doctype/unhandled_email/unhandled_email.json b/frappe/email/doctype/unhandled_email/unhandled_email.json
index de4407f38f..d904536936 100644
--- a/frappe/email/doctype/unhandled_email/unhandled_email.json
+++ b/frappe/email/doctype/unhandled_email/unhandled_email.json
@@ -1,212 +1,60 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2016-04-14 09:41:45.892975",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Setup",
- "editable_grid": 0,
+ "actions": [],
+ "creation": "2016-04-14 09:41:45.892975",
+ "doctype": "DocType",
+ "document_type": "Setup",
+ "engine": "InnoDB",
+ "field_order": [
+ "email_account",
+ "uid",
+ "reason",
+ "message_id",
+ "raw"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "email_account",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Email Account",
- "length": 0,
- "no_copy": 0,
- "options": "Email Account",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "email_account",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Email Account",
+ "options": "Email Account"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "uid",
- "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": "UID",
- "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
- },
+ "fieldname": "uid",
+ "fieldtype": "Data",
+ "label": "UID"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "reason",
- "fieldtype": "Long Text",
- "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": "Reason",
- "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
- },
+ "fieldname": "reason",
+ "fieldtype": "Long Text",
+ "in_list_view": 1,
+ "label": "Reason"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "message_id",
- "fieldtype": "Code",
- "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": "Message-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": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "message_id",
+ "fieldtype": "Code",
+ "label": "Message-id"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "raw",
- "fieldtype": "Code",
- "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": "Raw Email",
- "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
+ "fieldname": "raw",
+ "fieldtype": "Code",
+ "label": "Raw Email"
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 1,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2017-09-19 16:28:00.042256",
- "modified_by": "Administrator",
- "module": "Email",
- "name": "Unhandled Email",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "in_create": 1,
+ "links": [],
+ "modified": "2022-08-03 12:20:51.822287",
+ "modified_by": "Administrator",
+ "module": "Email",
+ "name": "Unhandled Email",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 0,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 0,
- "read": 1,
- "report": 0,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
- "write": 0
+ "read": 1,
+ "role": "System Manager"
}
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 0,
- "track_seen": 0
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
}
\ No newline at end of file
diff --git a/frappe/email/doctype/unhandled_email/unhandled_email.py b/frappe/email/doctype/unhandled_email/unhandled_email.py
index e703f1ec97..1c315e2423 100644
--- a/frappe/email/doctype/unhandled_email/unhandled_email.py
+++ b/frappe/email/doctype/unhandled_email/unhandled_email.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
# License: MIT. See LICENSE
diff --git a/frappe/email/email_body.py b/frappe/email/email_body.py
index 07f698f740..20f81cb89b 100755
--- a/frappe/email/email_body.py
+++ b/frappe/email/email_body.py
@@ -24,6 +24,8 @@ from frappe.utils import (
)
from frappe.utils.pdf import get_pdf
+EMBED_PATTERN = re.compile("""embed=["'](.*?)["']""")
+
def get_email(
recipients,
@@ -190,7 +192,7 @@ class EMail:
def set_part_html(self, message, inline_images):
from email.mime.text import MIMEText
- has_inline_images = re.search("""embed=['"].*?['"]""", message)
+ has_inline_images = EMBED_PATTERN.search(message)
if has_inline_images:
# process inline images
@@ -265,28 +267,27 @@ class EMail:
validate_email_address(strip(self.sender), True)
self.reply_to = validate_email_address(strip(self.reply_to) or self.sender, True)
+ self.set_header("X-Original-From", self.sender)
self.replace_sender()
self.replace_sender_name()
- self.recipients = [strip(r) for r in self.recipients]
- self.cc = [strip(r) for r in self.cc]
- self.bcc = [strip(r) for r in self.bcc]
+ self.recipients = [strip(r) for r in self.recipients if r not in frappe.STANDARD_USERS]
+ self.cc = [strip(r) for r in self.cc if r not in frappe.STANDARD_USERS]
+ self.bcc = [strip(r) for r in self.bcc if r not in frappe.STANDARD_USERS]
for e in self.recipients + (self.cc or []) + (self.bcc or []):
validate_email_address(e, True)
def replace_sender(self):
if cint(self.email_account.always_use_account_email_id_as_sender):
- self.set_header("X-Original-From", self.sender)
- sender_name, sender_email = parse_addr(self.sender)
+ sender_name, _ = parse_addr(self.sender)
self.sender = email.utils.formataddr(
(str(Header(sender_name or self.email_account.name, "utf-8")), self.email_account.email_id)
)
def replace_sender_name(self):
if cint(self.email_account.always_use_account_name_as_sender_name):
- self.set_header("X-Original-From", self.sender)
- sender_name, sender_email = parse_addr(self.sender)
+ _, sender_email = parse_addr(self.sender)
self.sender = email.utils.formataddr(
(str(Header(self.email_account.name, "utf-8")), sender_email)
)
@@ -330,12 +331,12 @@ class EMail:
def set_header(self, key, value):
if key in self.msg_root:
+ # delete key if found
+ # this is done because adding the same key doesn't override
+ # the existing key, rather appends another header with same key.
del self.msg_root[key]
- try:
- self.msg_root[key] = value
- except ValueError:
- self.msg_root[key] = sanitize_email_header(value)
+ self.msg_root[key] = sanitize_email_header(value)
def as_string(self):
"""validate, build message and convert to string"""
@@ -351,7 +352,7 @@ def get_formatted_html(
print_html=None,
email_account=None,
header=None,
- unsubscribe_link=None,
+ unsubscribe_link: frappe._dict | None = None,
sender=None,
with_container=False,
):
@@ -451,7 +452,7 @@ def add_attachment(fname, fcontent, content_type=None, parent=None, content_id=N
attachment_type = "inline" if inline else "attachment"
part.add_header("Content-Disposition", attachment_type, filename=str(fname))
if content_id:
- part.add_header("Content-ID", "<{0}>".format(content_id))
+ part.add_header("Content-ID", f"<{content_id}>")
parent.attach(part)
@@ -499,7 +500,7 @@ def replace_filename_with_cid(message):
inline_images = []
while True:
- matches = re.search("""embed=["'](.*?)["']""", message)
+ matches = EMBED_PATTERN.search(message)
if not matches:
break
groups = matches.groups()
@@ -510,7 +511,7 @@ def replace_filename_with_cid(message):
filecontent = get_filecontent_from_path(img_path)
if not filecontent:
- message = re.sub("""embed=['"]{0}['"]""".format(img_path), "", message)
+ message = re.sub(f"""embed=['"]{img_path}['"]""", "", message)
continue
content_id = random_string(10)
@@ -519,9 +520,7 @@ def replace_filename_with_cid(message):
{"filename": filename, "filecontent": filecontent, "content_id": content_id}
)
- message = re.sub(
- """embed=['"]{0}['"]""".format(img_path), 'src="cid:{0}"'.format(content_id), message
- )
+ message = re.sub(f"""embed=['"]{img_path}['"]""", f'src="cid:{content_id}"', message)
return (message, inline_images)
@@ -580,8 +579,17 @@ def get_header(header=None):
return email_header
-def sanitize_email_header(str):
- return str.replace("\r", "").replace("\n", "")
+def sanitize_email_header(header: str):
+ """
+ Removes all line boundaries in the headers.
+
+ Email Policy (python's std) has some bugs in it which uses splitlines
+ and raises ValueError (ref: https://github.com/python/cpython/blob/main/Lib/email/policy.py#L143).
+ Hence removing all line boundaries while sanitization of headers to prevent such faliures.
+ The line boundaries which are removed can be found here: https://docs.python.org/3/library/stdtypes.html#str.splitlines
+ """
+
+ return "".join(header.splitlines())
def get_brand_logo(email_account):
diff --git a/frappe/email/oauth.py b/frappe/email/oauth.py
new file mode 100644
index 0000000000..89b6df15d8
--- /dev/null
+++ b/frappe/email/oauth.py
@@ -0,0 +1,168 @@
+import base64
+from imaplib import IMAP4
+from poplib import POP3
+from smtplib import SMTP
+from urllib.parse import quote
+
+import frappe
+from frappe.integrations.google_oauth import GoogleOAuth
+from frappe.utils.password import encrypt
+
+
+class OAuthenticationError(Exception):
+ pass
+
+
+class Oauth:
+ def __init__(
+ self,
+ conn: IMAP4 | POP3 | SMTP,
+ email_account: str,
+ email: str,
+ access_token: str,
+ refresh_token: str,
+ service: str,
+ mechanism: str = "XOAUTH2",
+ ) -> None:
+
+ self.email_account = email_account
+ self.email = email
+ self.service = service
+ self._mechanism = mechanism
+ self._conn = conn
+ self._access_token = access_token
+ self._refresh_token = refresh_token
+
+ self._validate()
+
+ def _validate(self) -> None:
+ if self.service != "GMail":
+ raise NotImplementedError(
+ f"Service {self.service} currently doesn't have oauth implementation."
+ )
+
+ if not self._refresh_token:
+ frappe.throw(
+ frappe._("Please Authorize OAuth."),
+ OAuthenticationError,
+ frappe._("OAuth Error"),
+ )
+
+ @property
+ def _auth_string(self) -> str:
+ return f"user={self.email}\1auth=Bearer {self._access_token}\1\1"
+
+ def connect(self, _retry: int = 0) -> None:
+ """Connection method with retry on exception for Oauth"""
+ try:
+ if isinstance(self._conn, POP3):
+ res = self._connect_pop()
+
+ if not res.startswith(b"+OK"):
+ raise
+
+ elif isinstance(self._conn, IMAP4):
+ self._connect_imap()
+
+ else:
+ # SMTP
+ self._connect_smtp()
+
+ except Exception as e:
+ # maybe the access token expired - refreshing
+ access_token = self._refresh_access_token()
+
+ if not access_token or _retry > 0:
+ frappe.log_error(
+ "OAuth Error - Authentication Failed", str(e), "Email Account", self.email_account
+ )
+ # raising a bare exception here as we have a lot of exception handling present
+ # where the connect method is called from - hence just logging and raising.
+ raise
+
+ self._access_token = access_token
+ self.connect(_retry + 1)
+
+ def _connect_pop(self) -> bytes:
+ # poplib doesn't have AUTH command implementation
+ res = self._conn._shortcmd(
+ "AUTH {} {}".format(
+ self._mechanism, base64.b64encode(bytes(self._auth_string, "utf-8")).decode("utf-8")
+ )
+ )
+
+ return res
+
+ def _connect_imap(self) -> None:
+ self._conn.authenticate(self._mechanism, lambda x: self._auth_string)
+
+ def _connect_smtp(self) -> None:
+ self._conn.auth(self._mechanism, lambda x: self._auth_string, initial_response_ok=False)
+
+ def _refresh_access_token(self) -> str:
+ """Refreshes access token via calling `refresh_access_token` method of oauth service object"""
+ service_obj = self._get_service_object()
+ access_token = service_obj.refresh_access_token(self._refresh_token).get("access_token")
+
+ if access_token:
+ # set the new access token in db
+ frappe.db.set_value(
+ "Email Account",
+ self.email_account,
+ "access_token",
+ encrypt(access_token),
+ update_modified=False,
+ )
+
+ return access_token
+
+ def _get_service_object(self):
+ """Get Oauth service object"""
+
+ return {
+ "GMail": GoogleOAuth("mail", validate=False),
+ }[self.service]
+
+
+@frappe.whitelist(methods=["POST"])
+def oauth_access(email_account: str, service: str):
+ """Used as a default endpoint/caller for all oauth services.
+ Returns authorization url for redirection"""
+
+ if not service:
+ frappe.throw(frappe._("No Service is selected. Please select one and try again!"))
+
+ doctype = "Email Account"
+
+ if service == "GMail":
+ return authorize_google_access(email_account, doctype)
+
+ raise NotImplementedError(f"Service {service} currently doesn't have oauth implementation.")
+
+
+def authorize_google_access(email_account, doctype: str = "Email Account", code: str = None):
+ """Facilitates google oauth for email.
+ This is invoked 2 times - first time when user clicks `Authorze API Access` for getting the authorization url
+ and second time for setting the refresh and access token in db when google redirects back with oauth code."""
+
+ oauth_obj = GoogleOAuth("mail")
+
+ if not code:
+ return oauth_obj.get_authentication_url(
+ {
+ "redirect": f"/app/Form/{quote(doctype)}/{quote(email_account)}",
+ "success_query_param": "successful_authorization=1",
+ "email_account": email_account,
+ },
+ )
+
+ res = oauth_obj.authorize(code)
+ frappe.db.set_value(
+ doctype,
+ email_account,
+ {
+ "refresh_token": encrypt(res.get("refresh_token")),
+ "access_token": encrypt(res.get("access_token")),
+ },
+ update_modified=False,
+ )
diff --git a/frappe/email/queue.py b/frappe/email/queue.py
index b0a3b0583b..bc02c6be32 100755
--- a/frappe/email/queue.py
+++ b/frappe/email/queue.py
@@ -67,37 +67,24 @@ def get_emails_sent_today(email_account=None):
return frappe.db.sql(q, q_args)[0][0]
-def get_unsubscribe_message(unsubscribe_message, expose_recipients):
- if unsubscribe_message:
- unsubscribe_html = """{0}""".format(
- unsubscribe_message
- )
- else:
- unsubscribe_link = """{0}""".format(
- _("Unsubscribe")
- )
- unsubscribe_html = _("{0} to stop receiving emails of this type").format(unsubscribe_link)
-
- html = """" + "\n".join("
{0}
".format(h) for h in headers) + self.html_content += "" + "\n".join(f"
{h}
" for h in headers) if not message.is_multipart() and message.get_content_type() == "text/plain": # email.parser didn't parse it! @@ -566,7 +597,7 @@ class Email: try: fname = fname.replace("\n", " ").replace("\r", "") fname = cstr(decode_header(fname)[0][0]) - except: + except Exception: fname = get_random_filename(content_type=content_type) else: fname = get_random_filename(content_type=content_type) @@ -618,7 +649,7 @@ class Email: def get_thread_id(self): """Extract thread ID from `[]`""" - l = re.findall(r"(?<=\[)[\w/-]+", self.subject) + l = THREAD_ID_PATTERN.findall(self.subject) return l and l[0] or None def is_reply(self): @@ -704,7 +735,7 @@ class InboundMail(Email): content = self.content for file in attachments: if file.name in self.cid_map and self.cid_map[file.name]: - content = content.replace("cid:{0}".format(self.cid_map[file.name]), file.file_url) + content = content.replace(f"cid:{self.cid_map[file.name]}", file.file_url) return content def is_notification(self): @@ -889,7 +920,7 @@ class InboundMail(Email): users = frappe.get_all( "User Email", filters={"email_account": email_account.name}, fields=["parent"] ) - return list(set([user.get("parent") for user in users])) + return list({user.get("parent") for user in users}) @staticmethod def clean_subject(subject): @@ -940,7 +971,7 @@ class InboundMail(Email): } -class TimerMixin(object): +class TimerMixin: def __init__(self, *args, **kwargs): self.timeout = kwargs.pop("timeout", 0.0) self.elapsed_time = 0.0 diff --git a/frappe/email/smtp.py b/frappe/email/smtp.py index 1c91356506..10eb2f7681 100644 --- a/frappe/email/smtp.py +++ b/frappe/email/smtp.py @@ -1,25 +1,12 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE -import email.utils import smtplib -import sys - -import _socket import frappe from frappe import _ -from frappe.utils import cint, cstr, parse_addr - -CONNECTION_FAILED = _("Could not connect to outgoing email server") -AUTH_ERROR_TITLE = _("Invalid Credentials") -AUTH_ERROR = _("Incorrect email or password. Please check your login credentials.") -SOCKET_ERROR_TITLE = _("Incorrect Configuration") -SOCKET_ERROR = _("Invalid Outgoing Mail Server or Port") -SEND_MAIL_FAILED = _("Unable to send emails at this time") -EMAIL_ACCOUNT_MISSING = _( - "Email Account not setup. Please create a new Email Account from Setup > Email > Email Account" -) +from frappe.email.oauth import Oauth +from frappe.utils import cint, cstr class InvalidEmailCredentials(frappe.ValidationError): @@ -57,17 +44,40 @@ def send(email, append_to=None, retry=1): class SMTPServer: - def __init__(self, server, login=None, password=None, port=None, use_tls=None, use_ssl=None): + def __init__( + self, + server, + login=None, + email_account=None, + password=None, + port=None, + use_tls=None, + use_ssl=None, + use_oauth=0, + refresh_token=None, + access_token=None, + service=None, + ): self.login = login + self.email_account = email_account self.password = password self._server = server self._port = port self.use_tls = use_tls self.use_ssl = use_ssl + self.use_oauth = use_oauth + self.refresh_token = refresh_token + self.access_token = access_token + self.service = service self._session = None if not self.server: - frappe.msgprint(EMAIL_ACCOUNT_MISSING, raise_exception=frappe.OutgoingEmailError) + frappe.msgprint( + _( + "Email Account not setup. Please create a new Email Account from Setup > Email > Email Account" + ), + raise_exception=frappe.OutgoingEmailError, + ) @property def port(self): @@ -95,10 +105,18 @@ class SMTPServer: try: _session = SMTP(self.server, self.port) if not _session: - frappe.msgprint(CONNECTION_FAILED, raise_exception=frappe.OutgoingEmailError) + frappe.msgprint( + _("Could not connect to outgoing email server"), raise_exception=frappe.OutgoingEmailError + ) self.secure_session(_session) - if self.login and self.password: + + if self.use_oauth: + Oauth( + _session, self.email_account, self.login, self.access_token, self.refresh_token, self.service + ).connect() + + elif self.password: res = _session.login(str(self.login or ""), str(self.password or "")) # check if logged correctly @@ -108,16 +126,12 @@ class SMTPServer: self._session = _session return self._session - except smtplib.SMTPAuthenticationError as e: + except smtplib.SMTPAuthenticationError: self.throw_invalid_credentials_exception() - except _socket.error as e: + except OSError: # Invalid mail server -- due to refusing connection - frappe.throw(SOCKET_ERROR, title=SOCKET_ERROR_TITLE) - - except smtplib.SMTPException: - frappe.msgprint(SEND_MAIL_FAILED) - raise + frappe.throw(_("Invalid Outgoing Mail Server or Port"), title=_("Incorrect Configuration")) def is_session_active(self): if self._session: @@ -132,4 +146,8 @@ class SMTPServer: @classmethod def throw_invalid_credentials_exception(cls): - frappe.throw(AUTH_ERROR, title=AUTH_ERROR_TITLE, exc=InvalidEmailCredentials) + frappe.throw( + _("Please check your email login credentials."), + title=_("Invalid Credentials"), + exc=InvalidEmailCredentials, + ) diff --git a/frappe/email/test_email_body.py b/frappe/email/test_email_body.py index 3de21f64ce..d5b1013a73 100644 --- a/frappe/email/test_email_body.py +++ b/frappe/email/test_email_body.py @@ -5,6 +5,7 @@ import base64 import os import unittest +import frappe from frappe import safe_decode from frappe.email.doctype.email_queue.email_queue import QueueBuilder, SendMailContext from frappe.email.email_body import ( @@ -54,26 +55,27 @@ This is the text version of this email uni_chr1 = chr(40960) uni_chr2 = chr(1972) - queue_doc = QueueBuilder( + QueueBuilder( recipients=["test@example.com"], sender="me@example.com", subject="Test Subject", - message="" + uni_chr1 + "abcd" + uni_chr2 + "
", + message=f"{uni_chr1}abcd{uni_chr2}
", text_content="whatever", - ).process()[0] + ).process() + queue_doc = frappe.get_last_doc("Email Queue") mail_ctx = SendMailContext(queue_doc=queue_doc) result = mail_ctx.build_message(recipient_email="test@test.com") self.assertTrue(b"=EA=80=80abcd=DE=B4
" in result) def test_prepare_message_returns_cr_lf(self): - queue_doc = QueueBuilder( + QueueBuilder( recipients=["test@example.com"], sender="me@example.com", subject="Test Subject", message="\n this is a test of newlines\n" + "
", text_content="whatever", - ).process()[0] - + ).process() + queue_doc = frappe.get_last_doc("Email Queue") mail_ctx = SendMailContext(queue_doc=queue_doc) result = safe_decode(mail_ctx.build_message(recipient_email="test@test.com")) @@ -128,7 +130,7 @@ w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> processed_message = """Hey John Doe!
This is embedded image you asked for
""" - email_string = ( - get_email( - recipients=["test@example.com"], - sender="me@example.com", - subject="Test Subject", - content=email_html, - header=["Email Title", "green"], - ) - .as_string() - .replace("\r\n", "\n") - ) + email_string = get_email( + recipients=["test@example.com"], + sender="me@example.com", + subject="Test Subject\u2028, with line break, \nand Line feed \rand carriage return.", + content=email_html, + header=["Email Title", "green"], + ).as_string() # REDESIGN-TODO: Add style for indicators in email self.assertTrue("""""" in email_string) self.assertTrue("Email Title" in email_string) + self.assertIn( + "Subject: Test Subject, with line break, and Line feed and carriage return.", email_string + ) def test_get_email_header(self): html = get_header(["This is test", "orange"]) diff --git a/frappe/email/utils.py b/frappe/email/utils.py index 147284a625..7fc2e0ff89 100644 --- a/frappe/email/utils.py +++ b/frappe/email/utils.py @@ -1,5 +1,6 @@ # Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors # License: MIT. See LICENSE + import imaplib import poplib diff --git a/frappe/event_streaming/doctype/document_type_field_mapping/document_type_field_mapping.py b/frappe/event_streaming/doctype/document_type_field_mapping/document_type_field_mapping.py index 3019d70035..96d9e0fcb3 100644 --- a/frappe/event_streaming/doctype/document_type_field_mapping/document_type_field_mapping.py +++ b/frappe/event_streaming/doctype/document_type_field_mapping/document_type_field_mapping.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2019, Frappe Technologies and contributors # License: MIT. See LICENSE diff --git a/frappe/event_streaming/doctype/document_type_mapping/document_type_mapping.js b/frappe/event_streaming/doctype/document_type_mapping/document_type_mapping.js index 4653bf4d03..ad9ab0f51d 100644 --- a/frappe/event_streaming/doctype/document_type_mapping/document_type_mapping.js +++ b/frappe/event_streaming/doctype/document_type_mapping/document_type_mapping.js @@ -1,28 +1,37 @@ // Copyright (c) 2019, Frappe Technologies and contributors // For license information, please see license.txt -frappe.ui.form.on('Document Type Mapping', { - local_doctype: function(frm) { +frappe.ui.form.on("Document Type Mapping", { + local_doctype: function (frm) { if (frm.doc.local_doctype) { - frappe.model.clear_table(frm.doc, 'field_mapping'); + frappe.model.clear_table(frm.doc, "field_mapping"); let fields = frm.events.get_fields(frm); - $.each(fields, function(i, data) { - let row = frappe.model.add_child(frm.doc, 'Document Type Mapping', 'field_mapping'); + $.each(fields, function (i, data) { + let row = frappe.model.add_child( + frm.doc, + "Document Type Field Mapping", + "field_mapping" + ); row.local_fieldname = data; }); - refresh_field('field_mapping'); + refresh_field("field_mapping"); } }, - get_fields: function(frm) { + get_fields: function (frm) { let filtered_fields = []; - frappe.model.with_doctype(frm.doc.local_doctype, ()=> { - frappe.get_meta(frm.doc.local_doctype).fields.map( field => { - if (field.fieldname !== 'remote_docname' && field.fieldname !== 'remote_site_name' && frappe.model.is_value_type(field) && !field.hidden) { + frappe.model.with_doctype(frm.doc.local_doctype, () => { + frappe.get_meta(frm.doc.local_doctype).fields.map((field) => { + if ( + field.fieldname !== "remote_docname" && + field.fieldname !== "remote_site_name" && + frappe.model.is_value_type(field) && + !field.hidden + ) { filtered_fields.push(field.fieldname); } }); }); return filtered_fields; - } + }, }); diff --git a/frappe/event_streaming/doctype/document_type_mapping/document_type_mapping.py b/frappe/event_streaming/doctype/document_type_mapping/document_type_mapping.py index bcd2b275d1..04b5015296 100644 --- a/frappe/event_streaming/doctype/document_type_mapping/document_type_mapping.py +++ b/frappe/event_streaming/doctype/document_type_mapping/document_type_mapping.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2019, Frappe Technologies and contributors # License: MIT. See LICENSE import json diff --git a/frappe/event_streaming/doctype/document_type_mapping/test_document_type_mapping.py b/frappe/event_streaming/doctype/document_type_mapping/test_document_type_mapping.py index 1d5c4862de..676d5040ff 100644 --- a/frappe/event_streaming/doctype/document_type_mapping/test_document_type_mapping.py +++ b/frappe/event_streaming/doctype/document_type_mapping/test_document_type_mapping.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2019, Frappe Technologies and Contributors # License: MIT. See LICENSE # import frappe diff --git a/frappe/event_streaming/doctype/event_consumer/event_consumer.js b/frappe/event_streaming/doctype/event_consumer/event_consumer.js index 66d92699fa..2bcf96f9f3 100644 --- a/frappe/event_streaming/doctype/event_consumer/event_consumer.js +++ b/frappe/event_streaming/doctype/event_consumer/event_consumer.js @@ -1,19 +1,17 @@ // Copyright (c) 2019, Frappe Technologies and contributors // For license information, please see license.txt -frappe.ui.form.on('Event Consumer', { - refresh: function(frm) { +frappe.ui.form.on("Event Consumer", { + refresh: function (frm) { // formatter for subscribed doctype approval status - frm.set_indicator_formatter('status', - function(doc) { - let indicator = 'orange'; - if (doc.status == 'Approved') { - indicator = 'green'; - } else if (doc.status == 'Rejected') { - indicator = 'red'; - } - return indicator; + frm.set_indicator_formatter("status", function (doc) { + let indicator = "orange"; + if (doc.status == "Approved") { + indicator = "green"; + } else if (doc.status == "Rejected") { + indicator = "red"; } - ); - } + return indicator; + }); + }, }); diff --git a/frappe/event_streaming/doctype/event_consumer/event_consumer.py b/frappe/event_streaming/doctype/event_consumer/event_consumer.py index 287a1fca03..a2ae6f6651 100644 --- a/frappe/event_streaming/doctype/event_consumer/event_consumer.py +++ b/frappe/event_streaming/doctype/event_consumer/event_consumer.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2019, Frappe Technologies and contributors # License: MIT. See LICENSE @@ -213,5 +212,5 @@ def has_consumer_access(consumer, update_log): else: return frappe.safe_eval(condition, frappe._dict(doc=doc)) except Exception as e: - frappe.log_error(title="has_consumer_access error", message=e) + consumer.log_error("has_consumer_access error") return False diff --git a/frappe/event_streaming/doctype/event_consumer/test_event_consumer.py b/frappe/event_streaming/doctype/event_consumer/test_event_consumer.py index 605fc7982a..6f04af643e 100644 --- a/frappe/event_streaming/doctype/event_consumer/test_event_consumer.py +++ b/frappe/event_streaming/doctype/event_consumer/test_event_consumer.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2019, Frappe Technologies and Contributors # License: MIT. See LICENSE # import frappe diff --git a/frappe/event_streaming/doctype/event_consumer_document_type/event_consumer_document_type.py b/frappe/event_streaming/doctype/event_consumer_document_type/event_consumer_document_type.py index b33313087f..1ed15c5a75 100644 --- a/frappe/event_streaming/doctype/event_consumer_document_type/event_consumer_document_type.py +++ b/frappe/event_streaming/doctype/event_consumer_document_type/event_consumer_document_type.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2019, Frappe Technologies and contributors # License: MIT. See LICENSE diff --git a/frappe/event_streaming/doctype/event_producer/event_producer.js b/frappe/event_streaming/doctype/event_producer/event_producer.js index c2c3389e92..23ca482433 100644 --- a/frappe/event_streaming/doctype/event_producer/event_producer.js +++ b/frappe/event_streaming/doctype/event_producer/event_producer.js @@ -1,27 +1,25 @@ // Copyright (c) 2019, Frappe Technologies and contributors // For license information, please see license.txt -frappe.ui.form.on('Event Producer', { - refresh: function(frm) { - frm.set_query('ref_doctype', 'producer_doctypes', function() { +frappe.ui.form.on("Event Producer", { + refresh: function (frm) { + frm.set_query("ref_doctype", "producer_doctypes", function () { return { filters: { issingle: 0, - istable: 0 - } + istable: 0, + }, }; }); - frm.set_indicator_formatter('status', - function(doc) { - let indicator = 'orange'; - if (doc.status == 'Approved') { - indicator = 'green'; - } else if (doc.status == 'Rejected') { - indicator = 'red'; - } - return indicator; + frm.set_indicator_formatter("status", function (doc) { + let indicator = "orange"; + if (doc.status == "Approved") { + indicator = "green"; + } else if (doc.status == "Rejected") { + indicator = "red"; } - ); - } + return indicator; + }); + }, }); diff --git a/frappe/event_streaming/doctype/event_producer/event_producer.py b/frappe/event_streaming/doctype/event_producer/event_producer.py index f639e48b50..f91c8a4fd4 100644 --- a/frappe/event_streaming/doctype/event_producer/event_producer.py +++ b/frappe/event_streaming/doctype/event_producer/event_producer.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2019, Frappe Technologies and contributors # License: MIT. See LICENSE @@ -315,8 +314,9 @@ def set_insert(update, producer_site, event_producer): else: # if event consumer is not saving documents with the same name as the producer # store the remote docname in a custom field for future updates - local_doc = doc.insert(set_child_names=False) - set_custom_fields(local_doc, update.docname, event_producer) + doc.remote_docname = update.docname + doc.remote_site_name = event_producer + doc.insert(set_child_names=False) def set_update(update, producer_site): @@ -567,9 +567,3 @@ def resync(update): update = get_mapped_update(update, producer_site) update.data = json.loads(update.data) return sync(update, producer_site, event_producer, in_retry=True) - - -def set_custom_fields(local_doc, remote_docname, remote_site_name): - """sets custom field in doc for storing remote docname""" - frappe.db.set_value(local_doc.doctype, local_doc.name, "remote_docname", remote_docname) - frappe.db.set_value(local_doc.doctype, local_doc.name, "remote_site_name", remote_site_name) diff --git a/frappe/event_streaming/doctype/event_producer/test_event_producer.py b/frappe/event_streaming/doctype/event_producer/test_event_producer.py index 4464b0a434..168c9a61cf 100644 --- a/frappe/event_streaming/doctype/event_producer/test_event_producer.py +++ b/frappe/event_streaming/doctype/event_producer/test_event_producer.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2019, Frappe Technologies and Contributors # License: MIT. See LICENSE import json @@ -8,6 +7,8 @@ import frappe from frappe.core.doctype.user.user import generate_keys from frappe.event_streaming.doctype.event_producer.event_producer import pull_from_node from frappe.frappeclient import FrappeClient +from frappe.query_builder.utils import db_type_is +from frappe.tests.test_query_builder import run_only_if producer_url = "http://test_site_producer:8000" @@ -51,7 +52,9 @@ class TestEventProducer(unittest.TestCase): self.pull_producer_data() self.assertFalse(frappe.db.exists("ToDo", producer_doc.name)) + @run_only_if(db_type_is.MARIADB) def test_multiple_doctypes_sync(self): + # TODO: This test is extremely flaky with Postgres. Rewrite this! producer = get_remote_site() # insert todo and note in producer diff --git a/frappe/event_streaming/doctype/event_producer_document_type/event_producer_document_type.py b/frappe/event_streaming/doctype/event_producer_document_type/event_producer_document_type.py index 3e9623f56f..8f4c936792 100644 --- a/frappe/event_streaming/doctype/event_producer_document_type/event_producer_document_type.py +++ b/frappe/event_streaming/doctype/event_producer_document_type/event_producer_document_type.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2019, Frappe Technologies and contributors # License: MIT. See LICENSE diff --git a/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.js b/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.js index 15730e4c5f..6d18be43e3 100644 --- a/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.js +++ b/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.js @@ -1,8 +1,7 @@ // Copyright (c) 2020, Frappe Technologies and contributors // For license information, please see license.txt -frappe.ui.form.on('Event Producer Last Update', { +frappe.ui.form.on("Event Producer Last Update", { // refresh: function(frm) { - // } }); diff --git a/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.py b/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.py index 8e32e6fe6f..ec5cee7e78 100644 --- a/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.py +++ b/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2020, Frappe Technologies and contributors # License: MIT. See LICENSE diff --git a/frappe/event_streaming/doctype/event_producer_last_update/test_event_producer_last_update.py b/frappe/event_streaming/doctype/event_producer_last_update/test_event_producer_last_update.py index 6054ec873f..ccdea6c694 100644 --- a/frappe/event_streaming/doctype/event_producer_last_update/test_event_producer_last_update.py +++ b/frappe/event_streaming/doctype/event_producer_last_update/test_event_producer_last_update.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2020, Frappe Technologies and Contributors # License: MIT. See LICENSE # import frappe diff --git a/frappe/event_streaming/doctype/event_sync_log/event_sync_log.js b/frappe/event_streaming/doctype/event_sync_log/event_sync_log.js index 5199e3f02d..7cc3198bae 100644 --- a/frappe/event_streaming/doctype/event_sync_log/event_sync_log.js +++ b/frappe/event_streaming/doctype/event_sync_log/event_sync_log.js @@ -1,24 +1,24 @@ // Copyright (c) 2019, Frappe Technologies and contributors // For license information, please see license.txt -frappe.ui.form.on('Event Sync Log', { - refresh: function(frm) { - if (frm.doc.status == 'Failed') { - frm.add_custom_button(__('Resync'), function() { +frappe.ui.form.on("Event Sync Log", { + refresh: function (frm) { + if (frm.doc.status == "Failed") { + frm.add_custom_button(__("Resync"), function () { frappe.call({ method: "frappe.event_streaming.doctype.event_producer.event_producer.resync", args: { update: frm.doc, }, - callback: function(r) { + callback: function (r) { if (r.message) { frappe.msgprint(r.message); - frm.set_value('status', r.message); + frm.set_value("status", r.message); frm.save(); } - } + }, }); }); } - } + }, }); diff --git a/frappe/event_streaming/doctype/event_sync_log/event_sync_log.py b/frappe/event_streaming/doctype/event_sync_log/event_sync_log.py index c26ca46e05..a1d82ad08f 100644 --- a/frappe/event_streaming/doctype/event_sync_log/event_sync_log.py +++ b/frappe/event_streaming/doctype/event_sync_log/event_sync_log.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2019, Frappe Technologies and contributors # License: MIT. See LICENSE diff --git a/frappe/event_streaming/doctype/event_sync_log/event_sync_log_list.js b/frappe/event_streaming/doctype/event_sync_log/event_sync_log_list.js index 75d67003c4..97d2ee0a1d 100644 --- a/frappe/event_streaming/doctype/event_sync_log/event_sync_log_list.js +++ b/frappe/event_streaming/doctype/event_sync_log/event_sync_log_list.js @@ -1,9 +1,9 @@ -frappe.listview_settings['Event Sync Log'] = { - get_indicator: function(doc) { +frappe.listview_settings["Event Sync Log"] = { + get_indicator: function (doc) { var colors = { - "Failed": "red", - "Synced": "green" + Failed: "red", + Synced: "green", }; return [__(doc.status), colors[doc.status], "status,=," + doc.status]; - } + }, }; diff --git a/frappe/event_streaming/doctype/event_sync_log/test_event_sync_log.py b/frappe/event_streaming/doctype/event_sync_log/test_event_sync_log.py index da90c8e634..13028cbac7 100644 --- a/frappe/event_streaming/doctype/event_sync_log/test_event_sync_log.py +++ b/frappe/event_streaming/doctype/event_sync_log/test_event_sync_log.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2019, Frappe Technologies and Contributors # License: MIT. See LICENSE # import frappe diff --git a/frappe/event_streaming/doctype/event_update_log/event_update_log.js b/frappe/event_streaming/doctype/event_update_log/event_update_log.js index c5e8ed5915..d901799780 100644 --- a/frappe/event_streaming/doctype/event_update_log/event_update_log.js +++ b/frappe/event_streaming/doctype/event_update_log/event_update_log.js @@ -1,8 +1,7 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Event Update Log', { +frappe.ui.form.on("Event Update Log", { // refresh: function(frm) { - // } }); diff --git a/frappe/event_streaming/doctype/event_update_log/event_update_log.py b/frappe/event_streaming/doctype/event_update_log/event_update_log.py index 658a3b47cc..e40f600484 100644 --- a/frappe/event_streaming/doctype/event_update_log/event_update_log.py +++ b/frappe/event_streaming/doctype/event_update_log/event_update_log.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors # License: MIT. See LICENSE diff --git a/frappe/event_streaming/doctype/event_update_log/test_event_update_log.py b/frappe/event_streaming/doctype/event_update_log/test_event_update_log.py index 673164b8d7..0cbff47912 100644 --- a/frappe/event_streaming/doctype/event_update_log/test_event_update_log.py +++ b/frappe/event_streaming/doctype/event_update_log/test_event_update_log.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE # import frappe diff --git a/frappe/event_streaming/doctype/event_update_log_consumer/event_update_log_consumer.py b/frappe/event_streaming/doctype/event_update_log_consumer/event_update_log_consumer.py index 4f00504538..69da7db92e 100644 --- a/frappe/event_streaming/doctype/event_update_log_consumer/event_update_log_consumer.py +++ b/frappe/event_streaming/doctype/event_update_log_consumer/event_update_log_consumer.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2020, Frappe Technologies and contributors # License: MIT. See LICENSE diff --git a/frappe/exceptions.py b/frappe/exceptions.py index a8569481d3..c3bb45caea 100644 --- a/frappe/exceptions.py +++ b/frappe/exceptions.py @@ -76,7 +76,7 @@ class ImproperDBConfigurationError(Exception): def __init__(self, reason, msg=None): if not msg: msg = "MariaDb is not properly configured" - super(ImproperDBConfigurationError, self).__init__(msg) + super().__init__(msg) self.reason = reason @@ -263,3 +263,15 @@ class ExecutableNotFound(FileNotFoundError): class InvalidRemoteException(Exception): pass + + +class LinkExpired(ValidationError): + http_status_code = 410 + title = "Link Expired" + message = "The link has expired" + + +class InvalidKeyError(ValidationError): + http_status_code = 401 + title = "Invalid Key" + message = "The document key is invalid" diff --git a/frappe/frappeclient.py b/frappe/frappeclient.py index 04087463bc..474a5b06c4 100644 --- a/frappe/frappeclient.py +++ b/frappe/frappeclient.py @@ -26,7 +26,7 @@ class FrappeException(Exception): pass -class FrappeClient(object): +class FrappeClient: def __init__( self, url, @@ -85,11 +85,9 @@ class FrappeClient(object): def setup_key_authentication_headers(self): if self.api_key and self.api_secret: - token = base64.b64encode( - ("{}:{}".format(self.api_key, self.api_secret)).encode("utf-8") - ).decode("utf-8") + token = base64.b64encode((f"{self.api_key}:{self.api_secret}").encode()).decode("utf-8") auth_header = { - "Authorization": "Basic {}".format(token), + "Authorization": f"Basic {token}", } self.headers.update(auth_header) @@ -266,7 +264,7 @@ class FrappeClient(object): # build - attach children to parents if tables: docs = [frappe._dict(doc) for doc in docs] - docs_map = dict((doc.name, doc) for doc in docs) + docs_map = {doc.name: doc for doc in docs} for fieldname in tables: for child in tables[fieldname]: diff --git a/frappe/geo/country_info.json b/frappe/geo/country_info.json index 7ffdf0a8bf..ed44b1c7f8 100644 --- a/frappe/geo/country_info.json +++ b/frappe/geo/country_info.json @@ -8,7 +8,8 @@ "number_format": "#,###.##", "timezones": [ "Asia/Kabul" - ] + ], + "isd": "+93" }, "Albania": { "code": "al", @@ -20,7 +21,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Tirane" - ] + ], + "isd": "+355" }, "Algeria": { "code": "dz", @@ -32,11 +34,13 @@ "number_format": "#,###.##", "timezones": [ "Africa/Algiers" - ] + ], + "isd": "+213" }, "American Samoa": { "code": "as", - "number_format": "#,###.##" + "number_format": "#,###.##", + "isd": "+1684" }, "Andorra": { "code": "ad", @@ -48,7 +52,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Andorra" - ] + ], + "isd": "+376" }, "Angola": { "code": "ao", @@ -60,7 +65,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Luanda" - ] + ], + "isd": "+244" }, "Anguilla": { "code": "ai", @@ -72,7 +78,8 @@ "number_format": "#,###.##", "timezones": [ "America/Anguilla" - ] + ], + "isd": "+1264" }, "Antarctica": { "code": "aq", @@ -88,7 +95,8 @@ "Antarctica/Rothera", "Antarctica/Syowa", "Antarctica/Vostok" - ] + ], + "isd": "+672" }, "Antigua and Barbuda": { "code": "ag", @@ -100,7 +108,8 @@ "number_format": "#,###.##", "timezones": [ "America/Antigua" - ] + ], + "isd": "+1268" }, "Argentina": { "code": "ar", @@ -123,7 +132,8 @@ "America/Argentina/San_Luis", "America/Argentina/Tucuman", "America/Argentina/Ushuaia" - ] + ], + "isd": "+54" }, "Armenia": { "code": "am", @@ -135,7 +145,8 @@ "number_format": "#,###.##", "timezones": [ "Asia/Yerevan" - ] + ], + "isd": "+374" }, "Aruba": { "code": "aw", @@ -147,7 +158,8 @@ "number_format": "#,###.##", "timezones": [ "America/Aruba" - ] + ], + "isd": "+297" }, "Australia": { "code": "au", @@ -170,7 +182,8 @@ "Australia/Melbourne", "Australia/Perth", "Australia/Sydney" - ] + ], + "isd": "+61" }, "Austria": { "code": "at", @@ -182,7 +195,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Vienna" - ] + ], + "isd": "+43" }, "Azerbaijan": { "code": "az", @@ -192,7 +206,8 @@ "number_format": "#,###.##", "timezones": [ "Asia/Baku" - ] + ], + "isd": "+994" }, "Bahamas": { "code": "bs", @@ -201,7 +216,8 @@ "number_format": "#,###.##", "timezones": [ "America/Nassau" - ] + ], + "isd": "+1242" }, "Bahrain": { "code": "bh", @@ -213,7 +229,8 @@ "number_format": "#,###.###", "timezones": [ "Asia/Bahrain" - ] + ], + "isd": "+973" }, "Bangladesh": { "code": "bd", @@ -225,7 +242,8 @@ "number_format": "#,###.##", "timezones": [ "Asia/Dhaka" - ] + ], + "isd": "+880" }, "Barbados": { "code": "bb", @@ -237,7 +255,8 @@ "number_format": "#,###.##", "timezones": [ "America/Barbados" - ] + ], + "isd": "+1246" }, "Belarus": { "code": "by", @@ -247,7 +266,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Minsk" - ] + ], + "isd": "+375" }, "Belgium": { "code": "be", @@ -259,7 +279,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Brussels" - ] + ], + "isd": "+32" }, "Belize": { "code": "bz", @@ -272,7 +293,8 @@ "number_format": "#,###.##", "timezones": [ "America/Belize" - ] + ], + "isd": "+501" }, "Benin": { "code": "bj", @@ -284,7 +306,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Porto-Novo" - ] + ], + "isd": "+229" }, "Bermuda": { "code": "bm", @@ -296,7 +319,8 @@ "number_format": "#,###.##", "timezones": [ "Atlantic/Bermuda" - ] + ], + "isd": "+1441" }, "Bhutan": { "code": "bt", @@ -308,13 +332,15 @@ "number_format": "#,###.##", "timezones": [ "Asia/Thimphu" - ] + ], + "isd": "+975" }, "Bolivia, Plurinational State of": { "code": "bo", "currency": "BOB", "currency_name": "Boliviano", - "number_format": "#,###.##" + "number_format": "#,###.##", + "isd": "+591" }, "Bonaire, Sint Eustatius and Saba": { "code": "bq", @@ -329,7 +355,8 @@ "number_format": "#.###,##", "timezones": [ "Europe/Sarajevo" - ] + ], + "isd": "+387" }, "Botswana": { "code": "bw", @@ -341,11 +368,13 @@ "number_format": "#,###.##", "timezones": [ "Africa/Gaborone" - ] + ], + "isd": "+267" }, "Bouvet Island": { "code": "bv", - "number_format": "#,###.##" + "number_format": "#,###.##", + "isd": "+47" }, "Brazil": { "code": "br", @@ -372,7 +401,8 @@ "America/Rio_Branco", "America/Santarem", "America/Sao_Paulo" - ] + ], + "isd": "+55" }, "British Indian Ocean Territory": { "code": "io", @@ -382,7 +412,8 @@ "number_format": "#,###.##", "timezones": [ "Indian/Chagos" - ] + ], + "isd": "+246" }, "Brunei Darussalam": { "code": "bn", @@ -391,7 +422,8 @@ "number_format": "#,###.##", "timezones": [ "Asia/Brunei" - ] + ], + "isd": "+673" }, "Bulgaria": { "code": "bg", @@ -403,7 +435,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Sofia" - ] + ], + "isd": "+359" }, "Burkina Faso": { "code": "bf", @@ -415,7 +448,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Ouagadougou" - ] + ], + "isd": "+226" }, "Burundi": { "code": "bi", @@ -427,7 +461,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Bujumbura" - ] + ], + "isd": "+257" }, "Cambodia": { "code": "kh", @@ -439,7 +474,8 @@ "number_format": "#,###.##", "timezones": [ "Asia/Phnom_Penh" - ] + ], + "isd": "+855" }, "Cameroon": { "code": "cm", @@ -451,7 +487,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Douala" - ] + ], + "isd": "+237" }, "Canada": { "code": "ca", @@ -491,7 +528,8 @@ "America/Whitehorse", "America/Winnipeg", "America/Yellowknife" - ] + ], + "isd": "+1" }, "Cape Verde": { "code": "cv", @@ -503,7 +541,8 @@ "number_format": "#,###.##", "timezones": [ "Atlantic/Cape_Verde" - ] + ], + "isd": "+238" }, "Cayman Islands": { "code": "ky", @@ -515,7 +554,8 @@ "number_format": "#,###.##", "timezones": [ "America/Cayman" - ] + ], + "isd": "+ 345" }, "Central African Republic": { "code": "cf", @@ -527,7 +567,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Bangui" - ] + ], + "isd": "+236" }, "Chad": { "code": "td", @@ -539,7 +580,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Ndjamena" - ] + ], + "isd": "+235" }, "Chile": { "code": "cl", @@ -552,7 +594,8 @@ "timezones": [ "America/Santiago", "Pacific/Easter" - ] + ], + "isd": "+56" }, "China": { "code": "cn", @@ -568,14 +611,16 @@ "Asia/Kashgar", "Asia/Shanghai", "Asia/Urumqi" - ] + ], + "isd": "+86" }, "Christmas Island": { "code": "cx", "number_format": "#,###.##", "timezones": [ "Indian/Christmas" - ] + ], + "isd": "+61" }, "Cocos (Keeling) Islands": { "code": "cc", @@ -585,7 +630,8 @@ "number_format": "#,###.##", "timezones": [ "Indian/Cocos" - ] + ], + "isd": "+61" }, "Colombia": { "code": "co", @@ -597,7 +643,8 @@ "number_format": "#.###,##", "timezones": [ "America/Bogota" - ] + ], + "isd": "+57" }, "Comoros": { "code": "km", @@ -609,7 +656,8 @@ "number_format": "#,###.##", "timezones": [ "Indian/Comoro" - ] + ], + "isd": "+269" }, "Congo": { "code": "cg", @@ -618,7 +666,8 @@ "currency_name": "Central African CFA Franc", "currency_symbol": "FCFA", "currency_fraction": "Centime", - "currency_fraction_units": 100 + "currency_fraction_units": 100, + "isd": "+242" }, "Congo, The Democratic Republic of the": { "code": "cd", @@ -627,7 +676,8 @@ "currency_name": "Congolese franc", "currency_symbol": "FC", "currency_fraction": "Centime", - "currency_fraction_units": 100 + "currency_fraction_units": 100, + "isd": "+243" }, "Cook Islands": { "code": "ck", @@ -637,7 +687,8 @@ "number_format": "#,###.##", "timezones": [ "Pacific/Rarotonga" - ] + ], + "isd": "+682" }, "Costa Rica": { "code": "cr", @@ -649,7 +700,8 @@ "number_format": "#.###,##", "timezones": [ "America/Costa_Rica" - ] + ], + "isd": "+506" }, "Croatia": { "code": "hr", @@ -661,7 +713,8 @@ "number_format": "#.###,##", "timezones": [ "Europe/Zagreb" - ] + ], + "isd": "+385" }, "Cuba": { "code": "cu", @@ -673,7 +726,8 @@ "number_format": "#,###.##", "timezones": [ "America/Havana" - ] + ], + "isd": "+53" }, "Cura\u00e7ao": { "code": "cw", @@ -692,7 +746,8 @@ "number_format": "#.###,##", "timezones": [ "Asia/Nicosia" - ] + ], + "isd": "+357" }, "Czech Republic": { "code": "cz", @@ -704,7 +759,8 @@ "number_format": "#.###,##", "timezones": [ "Europe/Prague" - ] + ], + "isd": "+420" }, "Denmark": { "code": "dk", @@ -716,7 +772,8 @@ "number_format": "#.###,##", "timezones": [ "Europe/Copenhagen" - ] + ], + "isd": "+45" }, "Djibouti": { "code": "dj", @@ -728,7 +785,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Djibouti" - ] + ], + "isd": "+253" }, "Dominica": { "code": "dm", @@ -740,7 +798,8 @@ "number_format": "#,###.##", "timezones": [ "America/Dominica" - ] + ], + "isd": "+1767" }, "Dominican Republic": { "code": "do", @@ -752,7 +811,8 @@ "number_format": "#,###.##", "timezones": [ "America/Santo_Domingo" - ] + ], + "isd": "+1849" }, "Ecuador": { "code": "ec", @@ -763,7 +823,8 @@ "timezones": [ "America/Guayaquil", "Pacific/Galapagos" - ] + ], + "isd": "+593" }, "Egypt": { "code": "eg", @@ -775,12 +836,13 @@ "number_format": "#,###.##", "timezones": [ "Africa/Cairo" - ] + ], + "isd": "+20" }, "El Salvador": { "code": "sv", "currency": "USD", - "currency_fraction": "Centavo", + "currency_fraction": "Cent", "currency_fraction_units": 100, "smallest_currency_fraction_value": 0.01, "currency_name": "Dolar estadounidense", @@ -789,7 +851,8 @@ "number_format": "#,###.##", "timezones": [ "America/El_Salvador" - ] + ], + "isd": "+503" }, "Equatorial Guinea": { "code": "gq", @@ -801,7 +864,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Malabo" - ] + ], + "isd": "+240" }, "Eritrea": { "code": "er", @@ -813,7 +877,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Asmara" - ] + ], + "isd": "+291" }, "Estonia": { "code": "ee", @@ -825,7 +890,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Tallinn" - ] + ], + "isd": "+372" }, "Ethiopia": { "code": "et", @@ -837,13 +903,15 @@ "number_format": "#,###.##", "timezones": [ "Africa/Addis_Ababa" - ] + ], + "isd": "+251" }, "Falkland Islands (Malvinas)": { "code": "fk", "currency": "FKP", "currency_name": "Falkland Islands Pound", - "number_format": "#,###.##" + "number_format": "#,###.##", + "isd": "+500" }, "Faroe Islands": { "code": "fo", @@ -853,7 +921,8 @@ "number_format": "#,###.##", "timezones": [ "Atlantic/Faroe" - ] + ], + "isd": "+298" }, "Fiji": { "code": "fj", @@ -865,7 +934,8 @@ "number_format": "#,###.##", "timezones": [ "Pacific/Fiji" - ] + ], + "isd": "+679" }, "Finland": { "code": "fi", @@ -877,7 +947,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Helsinki" - ] + ], + "isd": "+358" }, "France": { "code": "fr", @@ -890,14 +961,16 @@ "date_format": "dd/mm/yyyy", "timezones": [ "Europe/Paris" - ] + ], + "isd": "+33" }, "French Guiana": { "code": "gf", "number_format": "#,###.##", "timezones": [ "America/Cayenne" - ] + ], + "isd": "+594" }, "French Polynesia": { "code": "pf", @@ -909,11 +982,13 @@ "Pacific/Gambier", "Pacific/Marquesas", "Pacific/Tahiti" - ] + ], + "isd": "+689" }, "French Southern Territories": { "code": "tf", - "number_format": "#,###.##" + "number_format": "#,###.##", + "isd": "+262" }, "Gabon": { "code": "ga", @@ -925,7 +1000,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Libreville" - ] + ], + "isd": "+241" }, "Gambia": { "code": "gm", @@ -934,7 +1010,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Banjul" - ] + ], + "isd": "+220" }, "Georgia": { "code": "ge", @@ -944,7 +1021,8 @@ "number_format": "#,###.##", "timezones": [ "Asia/Tbilisi" - ] + ], + "isd": "+995" }, "Germany": { "code": "de", @@ -954,9 +1032,12 @@ "smallest_currency_fraction_value": 0.01, "currency_symbol": "\u20ac", "number_format": "#.###,##", + "date_format": "dd.mm.yyyy", + "time_format": "HH:mm", "timezones": [ "Europe/Berlin" - ] + ], + "isd": "+49" }, "Ghana": { "code": "gh", @@ -967,7 +1048,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Accra" - ] + ], + "isd": "+233" }, "Gibraltar": { "code": "gi", @@ -979,7 +1061,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Gibraltar" - ] + ], + "isd": "+350" }, "Greece": { "code": "gr", @@ -991,7 +1074,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Athens" - ] + ], + "isd": "+30" }, "Greenland": { "code": "gl", @@ -1001,7 +1085,8 @@ "America/Godthab", "America/Scoresbysund", "America/Thule" - ] + ], + "isd": "+299" }, "Grenada": { "code": "gd", @@ -1013,21 +1098,24 @@ "number_format": "#,###.##", "timezones": [ "America/Grenada" - ] + ], + "isd": "+1473" }, "Guadeloupe": { "code": "gp", "number_format": "#,###.##", "timezones": [ "America/Guadeloupe" - ] + ], + "isd": "+590" }, "Guam": { "code": "gu", "number_format": "#,###.##", "timezones": [ "Pacific/Guam" - ] + ], + "isd": "+1671" }, "Guatemala": { "code": "gt", @@ -1039,7 +1127,8 @@ "number_format": "#,###.##", "timezones": [ "America/Guatemala" - ] + ], + "isd": "+502" }, "Guernsey": { "code": "gg", @@ -1049,7 +1138,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/London" - ] + ], + "isd": "+44" }, "Guinea": { "code": "gn", @@ -1061,7 +1151,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Conakry" - ] + ], + "isd": "+224" }, "Guinea-Bissau": { "code": "gw", @@ -1073,7 +1164,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Bissau" - ] + ], + "isd": "+245" }, "Guyana": { "code": "gy", @@ -1085,7 +1177,8 @@ "number_format": "#,###.##", "timezones": [ "America/Guyana" - ] + ], + "isd": "+592" }, "Haiti": { "code": "ht", @@ -1098,15 +1191,18 @@ "timezones": [ "America/Guatemala", "America/Port-au-Prince" - ] + ], + "isd": "+509" }, "Heard Island and McDonald Islands": { "code": "hm", - "number_format": "#,###.##" + "number_format": "#,###.##", + "isd": "+0" }, "Holy See (Vatican City State)": { "code": "va", - "number_format": "#,###.##" + "number_format": "#,###.##", + "isd": "+379" }, "Honduras": { "code": "hn", @@ -1118,7 +1214,8 @@ "number_format": "#,###.##", "timezones": [ "America/Tegucigalpa" - ] + ], + "isd": "+504" }, "Hong Kong": { "code": "hk", @@ -1130,7 +1227,8 @@ "number_format": "#,###.##", "timezones": [ "Asia/Hong_Kong" - ] + ], + "isd": "+852" }, "Hungary": { "code": "hu", @@ -1143,7 +1241,8 @@ "number_format": "#.###", "timezones": [ "Europe/Budapest" - ] + ], + "isd": "+36" }, "Iceland": { "code": "is", @@ -1155,7 +1254,8 @@ "number_format": "#.###", "timezones": [ "Atlantic/Reykjavik" - ] + ], + "isd": "+354" }, "India": { "code": "in", @@ -1167,7 +1267,8 @@ "number_format": "#,##,###.##", "timezones": [ "Asia/Kolkata" - ] + ], + "isd": "+91" }, "Indonesia": { "code": "id", @@ -1182,7 +1283,8 @@ "Asia/Jayapura", "Asia/Makassar", "Asia/Pontianak" - ] + ], + "isd": "+62" }, "Iran": { "code": "ir", @@ -1192,7 +1294,8 @@ "number_format": "#,###.##", "timezones": [ "Asia/Tehran" - ] + ], + "isd": "+98" }, "Iraq": { "code": "iq", @@ -1204,7 +1307,8 @@ "number_format": "#,###.###", "timezones": [ "Asia/Baghdad" - ] + ], + "isd": "+964" }, "Ireland": { "code": "ie", @@ -1216,7 +1320,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Dublin" - ] + ], + "isd": "+353" }, "Isle of Man": { "code": "im", @@ -1226,7 +1331,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/London" - ] + ], + "isd": "+44" }, "Israel": { "code": "il", @@ -1238,7 +1344,8 @@ "number_format": "#,###.##", "timezones": [ "Asia/Jerusalem" - ] + ], + "isd": "+972" }, "Italy": { "code": "it", @@ -1251,7 +1358,8 @@ "date_format": "dd/mm/yyyy", "timezones": [ "Europe/Rome" - ] + ], + "isd": "+39" }, "Ivory Coast": { "code": "ci", @@ -1263,7 +1371,8 @@ "number_format": "#,###.##", "timeszones": [ "Africa/Abidjan" - ] + ], + "isd": "+225" }, "Jamaica": { "code": "jm", @@ -1275,7 +1384,8 @@ "number_format": "#,###.##", "timezones": [ "America/Jamaica" - ] + ], + "isd": "+1876" }, "Japan": { "code": "jp", @@ -1287,7 +1397,8 @@ "number_format": "#,###", "timezones": [ "Asia/Tokyo" - ] + ], + "isd": "+81" }, "Jersey": { "code": "je", @@ -1297,7 +1408,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/London" - ] + ], + "isd": "+44" }, "Jordan": { "code": "jo", @@ -1309,7 +1421,8 @@ "number_format": "#,###.###", "timezones": [ "Asia/Amman" - ] + ], + "isd": "+962" }, "Kazakhstan": { "code": "kz", @@ -1325,7 +1438,8 @@ "Asia/Aqtobe", "Asia/Oral", "Asia/Qyzylorda" - ] + ], + "isd": "+7" }, "Kenya": { "code": "ke", @@ -1337,7 +1451,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Nairobi" - ] + ], + "isd": "+254" }, "Kiribati": { "code": "ki", @@ -1349,19 +1464,22 @@ "Pacific/Enderbury", "Pacific/Kiritimati", "Pacific/Tarawa" - ] + ], + "isd": "+686" }, "Korea, Democratic Peoples Republic of": { "code": "kp", "currency": "KPW", "currency_name": "North Korean Won", - "number_format": "#,###.##" + "number_format": "#,###.##", + "isd": "+850" }, "Korea, Republic of": { "code": "kr", "currency": "KRW", "currency_name": "Won", - "number_format": "#,###" + "number_format": "#,###", + "isd": "+82" }, "Kuwait": { "code": "kw", @@ -1373,7 +1491,8 @@ "number_format": "#,###.###", "timezones": [ "Asia/Kuwait" - ] + ], + "isd": "+965" }, "Kyrgyzstan": { "code": "kg", @@ -1385,16 +1504,18 @@ "number_format": "#,###.##", "timezones": [ "Asia/Bishkek" - ] + ], + "isd": "+996" }, "Lao Peoples Democratic Republic": { "code": "la", "currency": "LAK", "currency_name": "Kip", "number_format": "#,###.##", - "timezones":[ - "Asia/Vientiane" - ] + "timezones": [ + "Asia/Vientiane" + ], + "isd": "+856" }, "Latvia": { "code": "lv", @@ -1406,7 +1527,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Riga" - ] + ], + "isd": "+371" }, "Lebanon": { "code": "lb", @@ -1418,7 +1540,8 @@ "number_format": "#,###.##", "timezones": [ "Asia/Beirut" - ] + ], + "isd": "+961" }, "Lesotho": { "code": "ls", @@ -1430,7 +1553,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Maseru" - ] + ], + "isd": "+266" }, "Liberia": { "code": "lr", @@ -1442,7 +1566,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Monrovia" - ] + ], + "isd": "+231" }, "Libya": { "code": "ly", @@ -1454,7 +1579,8 @@ "number_format": "#,###.###", "timezones": [ "Africa/Tripoli" - ] + ], + "isd": "+218" }, "Liechtenstein": { "code": "li", @@ -1464,7 +1590,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Vaduz" - ] + ], + "isd": "+423" }, "Lithuania": { "code": "lt", @@ -1477,7 +1604,8 @@ "number_format": "# ###,##", "timezones": [ "Europe/Vilnius" - ] + ], + "isd": "+370" }, "Luxembourg": { "code": "lu", @@ -1489,13 +1617,15 @@ "number_format": "#,###.##", "timezones": [ "Europe/Luxembourg" - ] + ], + "isd": "+352" }, "Macao": { "code": "mo", "currency": "MOP", "currency_name": "Pataca", - "number_format": "#,###.##" + "number_format": "#,###.##", + "isd": "+853" }, "Macedonia": { "code": "mk", @@ -1504,7 +1634,8 @@ "currency_fraction_units": 100, "currency_name": "Denar", "currency_symbol": "\u0434\u0435\u043d", - "number_format": "#,###.##" + "number_format": "#,###.##", + "isd": "+389" }, "Madagascar": { "code": "mg", @@ -1514,7 +1645,8 @@ "number_format": "#,###.##", "timezones": [ "Indian/Antananarivo" - ] + ], + "isd": "+261" }, "Malawi": { "code": "mw", @@ -1526,7 +1658,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Blantyre" - ] + ], + "isd": "+265" }, "Malaysia": { "code": "my", @@ -1539,7 +1672,8 @@ "timezones": [ "Asia/Kuala_Lumpur", "Asia/Kuching" - ] + ], + "isd": "+60" }, "Maldives": { "code": "mv", @@ -1551,7 +1685,8 @@ "number_format": "#,###.##", "timezones": [ "Indian/Maldives" - ] + ], + "isd": "+960" }, "Mali": { "code": "ml", @@ -1563,19 +1698,22 @@ "number_format": "#,###.##", "timezones": [ "Africa/Bamako" - ] + ], + "isd": "+223" }, "Malta": { "code": "mt", - "currency": "MTL", + "currency": "EUR", "currency_fraction": "Cent", "currency_fraction_units": 100, - "currency_name": "Maltese Lira", + "smallest_currency_fraction_value": 0.01, "currency_symbol": "\u20ac", "number_format": "#,###.##", + "date_format": "dd/mm/yyyy", "timezones": [ "Europe/Malta" - ] + ], + "isd": "+356" }, "Marshall Islands": { "code": "mh", @@ -1586,14 +1724,16 @@ "timezones": [ "Pacific/Kwajalein", "Pacific/Majuro" - ] + ], + "isd": "+692" }, "Martinique": { "code": "mq", "number_format": "#,###.##", "timezones": [ "America/Martinique" - ] + ], + "isd": "+596" }, "Mauritania": { "code": "mr", @@ -1605,7 +1745,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Nouakchott" - ] + ], + "isd": "+222" }, "Mauritius": { "code": "mu", @@ -1617,14 +1758,16 @@ "number_format": "#,###", "timezones": [ "Indian/Mauritius" - ] + ], + "isd": "+230" }, "Mayotte": { "code": "yt", "number_format": "#,###.##", "timezones": [ "Indian/Mayotte" - ] + ], + "isd": "+262" }, "Mexico": { "code": "mx", @@ -1647,17 +1790,20 @@ "America/Ojinaga", "America/Santa_Isabel", "America/Tijuana" - ] + ], + "isd": "+52" }, "Micronesia, Federated States of": { "code": "fm", - "number_format": "#,###.##" + "number_format": "#,###.##", + "isd": "+691" }, "Moldova, Republic of": { "code": "md", "currency": "MDL", "currency_name": "Moldovan Leu", - "number_format": "#,###.##" + "number_format": "#,###.##", + "isd": "+373" }, "Monaco": { "code": "mc", @@ -1669,7 +1815,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Monaco" - ] + ], + "isd": "+377" }, "Mongolia": { "code": "mn", @@ -1684,7 +1831,8 @@ "Asia/Choibalsan", "Asia/Hovd", "Asia/Ulaanbaatar" - ] + ], + "isd": "+976" }, "Montenegro": { "code": "me", @@ -1696,7 +1844,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Belgrade" - ] + ], + "isd": "+382" }, "Montserrat": { "code": "ms", @@ -1708,7 +1857,8 @@ "number_format": "#,###.##", "timezones": [ "America/Montserrat" - ] + ], + "isd": "+1664" }, "Morocco": { "code": "ma", @@ -1720,7 +1870,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Casablanca" - ] + ], + "isd": "+212" }, "Mozambique": { "code": "mz", @@ -1731,7 +1882,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Maputo" - ] + ], + "isd": "+258" }, "Myanmar": { "code": "mm", @@ -1740,7 +1892,8 @@ "number_format": "#,###.##", "timezones": [ "Asia/Rangoon" - ] + ], + "isd": "+95" }, "Namibia": { "code": "na", @@ -1752,7 +1905,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Windhoek" - ] + ], + "isd": "+264" }, "Nauru": { "code": "nr", @@ -1762,7 +1916,8 @@ "number_format": "#,###.##", "timezones": [ "Pacific/Nauru" - ] + ], + "isd": "+674" }, "Nepal": { "code": "np", @@ -1774,7 +1929,8 @@ "number_format": "#,###.##", "timezones": [ "Asia/Kathmandu" - ] + ], + "isd": "+977" }, "Netherlands": { "code": "nl", @@ -1786,7 +1942,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Amsterdam" - ] + ], + "isd": "+31" }, "New Caledonia": { "code": "nc", @@ -1796,7 +1953,8 @@ "number_format": "#,###.##", "timezones": [ "Pacific/Noumea" - ] + ], + "isd": "+687" }, "New Zealand": { "code": "nz", @@ -1809,7 +1967,8 @@ "timezones": [ "Pacific/Auckland", "Pacific/Chatham" - ] + ], + "isd": "+64" }, "Nicaragua": { "code": "ni", @@ -1821,7 +1980,8 @@ "number_format": "#,###.##", "timezones": [ "America/Managua" - ] + ], + "isd": "+505" }, "Niger": { "code": "ne", @@ -1833,7 +1993,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Niamey" - ] + ], + "isd": "+227" }, "Nigeria": { "code": "ng", @@ -1845,7 +2006,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Lagos" - ] + ], + "isd": "+234" }, "Niue": { "code": "nu", @@ -1855,21 +2017,24 @@ "number_format": "#,###.##", "timezones": [ "Pacific/Niue" - ] + ], + "isd": "+683" }, "Norfolk Island": { "code": "nf", "number_format": "#,###.##", "timezones": [ "Pacific/Norfolk" - ] + ], + "isd": "+672" }, "Northern Mariana Islands": { "code": "mp", "number_format": "#,###.##", "timezones": [ "Pacific/Saipan" - ] + ], + "isd": "+1670" }, "Norway": { "code": "no", @@ -1881,7 +2046,8 @@ "number_format": "#.###,##", "timezones": [ "Europe/Oslo" - ] + ], + "isd": "+47" }, "Oman": { "code": "om", @@ -1893,7 +2059,8 @@ "number_format": "#,###.###", "timezones": [ "Asia/Muscat" - ] + ], + "isd": "+968" }, "Pakistan": { "code": "pk", @@ -1905,7 +2072,8 @@ "number_format": "#,###.##", "timezones": [ "Asia/Karachi" - ] + ], + "isd": "+92" }, "Palau": { "code": "pw", @@ -1916,11 +2084,13 @@ "number_format": "#,###.##", "timezones": [ "Pacific/Palau" - ] + ], + "isd": "+680" }, "Palestinian Territory, Occupied": { "code": "ps", - "number_format": "#,###.##" + "number_format": "#,###.##", + "isd": "+970" }, "Panama": { "code": "pa", @@ -1930,7 +2100,8 @@ "number_format": "#,###.##", "timezones": [ "America/Panama" - ] + ], + "isd": "+507" }, "Papua New Guinea": { "code": "pg", @@ -1942,7 +2113,8 @@ "number_format": "#,###.##", "timezones": [ "Pacific/Port_Moresby" - ] + ], + "isd": "+675" }, "Paraguay": { "code": "py", @@ -1954,7 +2126,8 @@ "number_format": "#,###.##", "timezones": [ "America/Asuncion" - ] + ], + "isd": "+595" }, "Peru": { "code": "pe", @@ -1966,7 +2139,8 @@ "number_format": "#,###.##", "timezones": [ "America/Lima" - ] + ], + "isd": "+51" }, "Philippines": { "code": "ph", @@ -1979,14 +2153,16 @@ "number_format": "#,###.##", "timezones": [ "Asia/Manila" - ] + ], + "isd": "+63" }, "Pitcairn": { "code": "pn", "number_format": "#,###.##", "timezones": [ "Pacific/Pitcairn" - ] + ], + "isd": "+64" }, "Poland": { "code": "pl", @@ -1997,7 +2173,8 @@ "number_format": "#.###,##", "timezones": [ "Europe/Warsaw" - ] + ], + "isd": "+48" }, "Portugal": { "code": "pt", @@ -2011,14 +2188,16 @@ "Atlantic/Azores", "Atlantic/Madeira", "Europe/Lisbon" - ] + ], + "isd": "+351" }, "Puerto Rico": { "code": "pr", "number_format": "#,###.##", "timezones": [ "America/Puerto_Rico" - ] + ], + "isd": "+1939" }, "Qatar": { "code": "qa", @@ -2030,7 +2209,8 @@ "number_format": "#,###.##", "timezones": [ "Asia/Qatar" - ] + ], + "isd": "+974" }, "Romania": { "code": "ro", @@ -2042,13 +2222,15 @@ "number_format": "#,###.##", "timezones": [ "Europe/Bucharest" - ] + ], + "isd": "+40" }, "Russian Federation": { "code": "ru", "currency": "RUB", "currency_name": "Russian Ruble", - "number_format": "#.###,##" + "number_format": "#.###,##", + "isd": "+7" }, "Rwanda": { "code": "rw", @@ -2060,21 +2242,25 @@ "number_format": "#,###.##", "timezones": [ "Africa/Kigali" - ] + ], + "isd": "+250" }, "R\u00e9union": { "code": "re", - "number_format": "#,###.##" + "number_format": "#,###.##", + "isd": "+262" }, "Saint Barth\u00e9lemy": { "code": "bl", - "number_format": "#,###.##" + "number_format": "#,###.##", + "isd": "+590" }, "Saint Helena, Ascension and Tristan da Cunha": { "code": "sh", "currency": "SHP", "currency_name": "Saint Helena Pound", - "number_format": "#,###.##" + "number_format": "#,###.##", + "isd": "+290" }, "Saint Kitts and Nevis": { "code": "kn", @@ -2086,7 +2272,8 @@ "number_format": "#,###.##", "timezones": [ "America/St_Kitts" - ] + ], + "isd": "+1869" }, "Saint Lucia": { "code": "lc", @@ -2098,15 +2285,18 @@ "number_format": "#,###.##", "timezones": [ "America/St_Lucia" - ] + ], + "isd": "+1758" }, "Saint Martin (French part)": { "code": "mf", - "number_format": "#,###.##" + "number_format": "#,###.##", + "isd": "+590" }, "Saint Pierre and Miquelon": { "code": "pm", - "number_format": "#,###.##" + "number_format": "#,###.##", + "isd": "+508" }, "Saint Vincent and the Grenadines": { "code": "vc", @@ -2118,7 +2308,8 @@ "number_format": "#,###.##", "timezones": [ "America/St_Vincent" - ] + ], + "isd": "+1784" }, "Samoa": { "code": "ws", @@ -2130,7 +2321,8 @@ "number_format": "#,###.##", "timezones": [ "Pacific/Apia" - ] + ], + "isd": "+685" }, "San Marino": { "code": "sm", @@ -2142,13 +2334,15 @@ "number_format": "#,###.##", "timezones": [ "Europe/Rome" - ] + ], + "isd": "+378" }, "Sao Tome and Principe": { "code": "st", "currency": "STD", "currency_name": "Dobra", - "number_format": "#,###.##" + "number_format": "#,###.##", + "isd": "+239" }, "Saudi Arabia": { "code": "sa", @@ -2160,7 +2354,8 @@ "number_format": "#,###.##", "timezones": [ "Asia/Riyadh" - ] + ], + "isd": "+966" }, "Senegal": { "code": "sn", @@ -2172,7 +2367,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Dakar" - ] + ], + "isd": "+221" }, "Serbia": { "code": "rs", @@ -2184,7 +2380,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Belgrade" - ] + ], + "isd": "+381" }, "Seychelles": { "code": "sc", @@ -2196,7 +2393,8 @@ "number_format": "#,###.##", "timezones": [ "Indian/Mahe" - ] + ], + "isd": "+248" }, "Sierra Leone": { "code": "sl", @@ -2208,19 +2406,21 @@ "number_format": "#,###.##", "timezones": [ "Africa/Freetown" - ] + ], + "isd": "+232" }, "Singapore": { "code": "sg", "currency": "SGD", - "currency_fraction": "Sen", + "currency_fraction": "Cent", "currency_fraction_units": 100, "currency_name": "Singapore Dollar", "currency_symbol": "$", "number_format": "#,###.##", "timezones": [ "Asia/Singapore" - ] + ], + "isd": "+65" }, "Sint Maarten (Dutch part)": { "code": "sx", @@ -2236,7 +2436,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Bratislava" - ] + ], + "isd": "+421" }, "Slovenia": { "code": "si", @@ -2248,7 +2449,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Belgrade" - ] + ], + "isd": "+386" }, "Solomon Islands": { "code": "sb", @@ -2260,7 +2462,8 @@ "number_format": "#,###.##", "timezones": [ "Pacific/Guadalcanal" - ] + ], + "isd": "+677" }, "Somalia": { "code": "so", @@ -2272,7 +2475,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Mogadishu" - ] + ], + "isd": "+252" }, "South Africa": { "code": "za", @@ -2285,14 +2489,16 @@ "number_format": "# ###.##", "timezones": [ "Africa/Johannesburg" - ] + ], + "isd": "+27" }, "South Georgia and the South Sandwich Islands": { "code": "gs", "currency_fraction": "Penny", "currency_fraction_units": 100, "currency_symbol": "\u00a3", - "number_format": "#,###.##" + "number_format": "#,###.##", + "isd": "+500" }, "South Sudan": { "code": "ss", @@ -2302,7 +2508,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Juba" - ] + ], + "isd": "+211" }, "Spain": { "code": "es", @@ -2316,7 +2523,8 @@ "Africa/Ceuta", "Atlantic/Canary", "Europe/Madrid" - ] + ], + "isd": "+34" }, "Sri Lanka": { "code": "lk", @@ -2328,7 +2536,8 @@ "number_format": "#,###.##", "timezones": [ "Asia/Colombo" - ] + ], + "isd": "+94" }, "Sudan": { "code": "sd", @@ -2338,7 +2547,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Khartoum" - ] + ], + "isd": "+249" }, "Suriname": { "code": "sr", @@ -2349,11 +2559,13 @@ "number_format": "#,###.##", "timezones": [ "America/Paramaribo" - ] + ], + "isd": "+597" }, "Svalbard and Jan Mayen": { "code": "sj", - "number_format": "#,###.##" + "number_format": "#,###.##", + "isd": "+47" }, "Swaziland": { "code": "sz", @@ -2365,7 +2577,8 @@ "number_format": "#, ###.##", "timezones": [ "Africa/Mbabane" - ] + ], + "isd": "+268" }, "Sweden": { "code": "se", @@ -2377,7 +2590,8 @@ "number_format": "#.###,##", "timezones": [ "Europe/Stockholm" - ] + ], + "isd": "+46" }, "Switzerland": { "code": "ch", @@ -2390,19 +2604,22 @@ "number_format": "#'###.##", "timezones": [ "Europe/Zurich" - ] + ], + "isd": "+41" }, "Syria": { "code": "sy", "currency": "SYP", "currency_name": "Syrian Pound", - "number_format": "#,###.##" + "number_format": "#,###.##", + "isd": "+963" }, "Taiwan": { "code": "tw", "currency": "TWD", "date_format": "yyyy-mm-dd", - "number_format": "#,###.##" + "number_format": "#,###.##", + "isd": "+886" }, "Tajikistan": { "code": "tj", @@ -2412,13 +2629,15 @@ "number_format": "#,###.##", "timezones": [ "Asia/Dushanbe" - ] + ], + "isd": "+992" }, "Tanzania": { "code": "tz", "currency": "TZS", "currency_name": "Tanzanian Shilling", - "number_format": "#,###.##" + "number_format": "#,###.##", + "isd": "+255" }, "Thailand": { "code": "th", @@ -2430,11 +2649,13 @@ "number_format": "#,###.##", "timezones": [ "Asia/Bangkok" - ] + ], + "isd": "+66" }, "Timor-Leste": { "code": "tl", - "number_format": "#,###.##" + "number_format": "#,###.##", + "isd": "+670" }, "Togo": { "code": "tg", @@ -2446,14 +2667,16 @@ "number_format": "#,###.##", "timezones": [ "Africa/Lome" - ] + ], + "isd": "+228" }, "Tokelau": { "code": "tk", "number_format": "#,###.##", "timezones": [ "Pacific/Fakaofo" - ] + ], + "isd": "+690" }, "Tonga": { "code": "to", @@ -2465,7 +2688,8 @@ "number_format": "#,###.##", "timezones": [ "Pacific/Tongatapu" - ] + ], + "isd": "+676" }, "Trinidad and Tobago": { "code": "tt", @@ -2477,7 +2701,8 @@ "number_format": "#,###.##", "timezones": [ "America/Port_of_Spain" - ] + ], + "isd": "+1868" }, "Tunisia": { "code": "tn", @@ -2489,7 +2714,8 @@ "number_format": "#,###.###", "timezones": [ "Africa/Tunis" - ] + ], + "isd": "+216" }, "Turkey": { "code": "tr", @@ -2500,7 +2726,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/Istanbul" - ] + ], + "isd": "+90" }, "Turkmenistan": { "code": "tm", @@ -2512,14 +2739,16 @@ "number_format": "#,###.##", "timezones": [ "Asia/Ashgabat" - ] + ], + "isd": "+993" }, "Turks and Caicos Islands": { "code": "tc", "currency_fraction": "Cent", "currency_fraction_units": 100, "currency_symbol": "$", - "number_format": "#,###.##" + "number_format": "#,###.##", + "isd": "+1649" }, "Tuvalu": { "code": "tv", @@ -2529,7 +2758,8 @@ "number_format": "#,###.##", "timezones": [ "Pacific/Funafuti" - ] + ], + "isd": "+688" }, "Uganda": { "code": "ug", @@ -2541,7 +2771,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Kampala" - ] + ], + "isd": "+256" }, "Ukraine": { "code": "ua", @@ -2556,7 +2787,8 @@ "Europe/Simferopol", "Europe/Uzhgorod", "Europe/Zaporozhye" - ] + ], + "isd": "+380" }, "United Arab Emirates": { "code": "ae", @@ -2568,7 +2800,8 @@ "number_format": "#,###.##", "timezones": [ "Asia/Dubai" - ] + ], + "isd": "+971" }, "United Kingdom": { "code": "gb", @@ -2580,7 +2813,8 @@ "number_format": "#,###.##", "timezones": [ "Europe/London" - ] + ], + "isd": "+44" }, "United States": { "code": "us", @@ -2623,7 +2857,8 @@ "America/Sitka", "America/Yakutat", "Pacific/Honolulu" - ] + ], + "isd": "+1" }, "United States Minor Outlying Islands": { "code": "um", @@ -2639,7 +2874,8 @@ "number_format": "#.###,##", "timezones": [ "America/Montevideo" - ] + ], + "isd": "+598" }, "Uzbekistan": { "code": "uz", @@ -2652,7 +2888,8 @@ "timezones": [ "Asia/Samarkand", "Asia/Tashkent" - ] + ], + "isd": "+998" }, "Vanuatu": { "code": "vu", @@ -2664,7 +2901,8 @@ "number_format": "#,###", "timezones": [ "Pacific/Efate" - ] + ], + "isd": "+678" }, "Venezuela, Bolivarian Republic of": { "code": "ve", @@ -2672,28 +2910,33 @@ "currency": "VEF", "currency_symbol": "Bs.", "currency_fraction": "Centimos", - "currency_fraction_units": 100 + "currency_fraction_units": 100, + "isd": "+58" }, "Vietnam": { "code": "vn", "currency": "VND", "currency_name": "Dong", - "number_format": "#.###" + "number_format": "#.###", + "isd": "+84" }, "Virgin Islands, British": { "code": "vg", - "number_format": "#,###.##" + "number_format": "#,###.##", + "isd": "+1284" }, "Virgin Islands, U.S.": { "code": "vi", - "number_format": "#,###.##" + "number_format": "#,###.##", + "isd": "+1340" }, "Wallis and Futuna": { "code": "wf", "currency_fraction": "Centime", "currency_fraction_units": 100, "currency_symbol": "Fr", - "number_format": "#,###.##" + "number_format": "#,###.##", + "isd": "+681" }, "Western Sahara": { "code": "eh", @@ -2713,7 +2956,8 @@ "number_format": "#,###.##", "timezones": [ "Asia/Aden" - ] + ], + "isd": "+967" }, "Zambia": { "code": "zm", @@ -2725,7 +2969,8 @@ "number_format": "#,###.##", "timezones": [ "Africa/Lusaka" - ] + ], + "isd": "+260" }, "Zimbabwe": { "code": "zw", @@ -2737,10 +2982,12 @@ "number_format": "# ###.##", "timezones": [ "Africa/Harare" - ] + ], + "isd": "+263" }, "\u00c5land Islands": { "code": "ax", - "number_format": "#,###.##" + "number_format": "#,###.##", + "isd": "+358" } } diff --git a/frappe/geo/country_info.py b/frappe/geo/country_info.py index 1de8467a6a..2aefa27170 100644 --- a/frappe/geo/country_info.py +++ b/frappe/geo/country_info.py @@ -22,7 +22,7 @@ def get_country_info(country=None): def get_all(): - with open(os.path.join(os.path.dirname(__file__), "country_info.json"), "r") as local_info: + with open(os.path.join(os.path.dirname(__file__), "country_info.json")) as local_info: all_data = json.loads(local_info.read()) return all_data @@ -59,7 +59,7 @@ def get_translated_dict(): def update(): - with open(os.path.join(os.path.dirname(__file__), "currency_info.json"), "r") as nformats: + with open(os.path.join(os.path.dirname(__file__), "currency_info.json")) as nformats: nformats = json.loads(nformats.read()) all_data = get_all() diff --git a/frappe/geo/doctype/country/__init__.py b/frappe/geo/doctype/country/__init__.py index 8b13789179..e69de29bb2 100644 --- a/frappe/geo/doctype/country/__init__.py +++ b/frappe/geo/doctype/country/__init__.py @@ -1 +0,0 @@ - diff --git a/frappe/geo/doctype/country/country.js b/frappe/geo/doctype/country/country.js index 62159a1fe7..75bb3f46d5 100644 --- a/frappe/geo/doctype/country/country.js +++ b/frappe/geo/doctype/country/country.js @@ -1,8 +1,6 @@ // Copyright (c) 2016, Frappe Technologies and contributors // For license information, please see license.txt -frappe.ui.form.on('Country', { - refresh: function(frm) { - - } +frappe.ui.form.on("Country", { + refresh: function (frm) {}, }); diff --git a/frappe/geo/doctype/country/country.py b/frappe/geo/doctype/country/country.py index b3ba1b7127..f6be7a078d 100644 --- a/frappe/geo/doctype/country/country.py +++ b/frappe/geo/doctype/country/country.py @@ -1,7 +1,6 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE -import frappe from frappe.model.document import Document diff --git a/frappe/geo/doctype/currency/__init__.py b/frappe/geo/doctype/currency/__init__.py index 8b13789179..e69de29bb2 100644 --- a/frappe/geo/doctype/currency/__init__.py +++ b/frappe/geo/doctype/currency/__init__.py @@ -1 +0,0 @@ - diff --git a/frappe/geo/doctype/currency/currency.js b/frappe/geo/doctype/currency/currency.js index af2d6ebc4e..08915893a5 100644 --- a/frappe/geo/doctype/currency/currency.js +++ b/frappe/geo/doctype/currency/currency.js @@ -1,11 +1,11 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: See license.txt -frappe.ui.form.on('Currency', { +frappe.ui.form.on("Currency", { refresh(frm) { frm.set_intro(""); - if(!frm.doc.enabled) { + if (!frm.doc.enabled) { frm.set_intro(__("This Currency is disabled. Enable to use in transactions")); } - } + }, }); diff --git a/frappe/geo/doctype/currency/currency.json b/frappe/geo/doctype/currency/currency.json index db3fa5a19f..c51ab7f063 100644 --- a/frappe/geo/doctype/currency/currency.json +++ b/frappe/geo/doctype/currency/currency.json @@ -15,6 +15,7 @@ "fraction_units", "smallest_currency_fraction_value", "symbol", + "symbol_on_right", "number_format" ], "fields": [ @@ -69,16 +70,23 @@ "in_list_view": 1, "label": "Number Format", "options": "\n#,###.##\n#.###,##\n# ###.##\n# ###,##\n#'###.##\n#, ###.##\n#,##,###.##\n#,###.###\n#.###\n#,###" + }, + { + "default": "0", + "fieldname": "symbol_on_right", + "fieldtype": "Check", + "label": "Show Currency Symbol on Right Side" } ], "icon": "fa fa-bitcoin", "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2020-10-29 06:33:12.879978", + "modified": "2022-07-04 09:42:52.425440", "modified_by": "Administrator", "module": "Geo", "name": "Currency", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { @@ -109,5 +117,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/frappe/geo/doctype/currency/currency.py b/frappe/geo/doctype/currency/currency.py index dd5df57bab..93bcc063f8 100644 --- a/frappe/geo/doctype/currency/currency.py +++ b/frappe/geo/doctype/currency/currency.py @@ -2,7 +2,6 @@ # License: MIT. See LICENSE import frappe -from frappe import _, throw from frappe.model.document import Document diff --git a/frappe/geo/doctype/currency/test_currency.py b/frappe/geo/doctype/currency/test_currency.py index f93a452462..b02dd4258c 100644 --- a/frappe/geo/doctype/currency/test_currency.py +++ b/frappe/geo/doctype/currency/test_currency.py @@ -4,5 +4,10 @@ # pre loaded import frappe +from frappe.tests.utils import FrappeTestCase -test_records = frappe.get_test_records("Currency") + +class TestUser(FrappeTestCase): + def test_default_currency_on_setup(self): + usd = frappe.get_doc("Currency", "USD") + self.assertDocumentEqual({"enabled": 1, "fraction": "Cent"}, usd) diff --git a/frappe/geo/utils.py b/frappe/geo/utils.py index 577c5de2ff..53caed452d 100644 --- a/frappe/geo/utils.py +++ b/frappe/geo/utils.py @@ -1,9 +1,6 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2020, Frappe Technologies and contributors # License: MIT. See LICENSE -from pymysql import InternalError - import frappe @@ -65,9 +62,9 @@ def return_location(doctype, filters_sql): if filters_sql: try: coords = frappe.db.sql( - """SELECT name, location FROM `tab{}` WHERE {}""".format(doctype, filters_sql), as_dict=True + f"""SELECT name, location FROM `tab{doctype}` WHERE {filters_sql}""", as_dict=True ) - except InternalError: + except frappe.db.InternalError: frappe.msgprint(frappe._("This Doctype does not contain location fields"), raise_exception=True) return else: @@ -80,10 +77,10 @@ def return_coordinates(doctype, filters_sql): if filters_sql: try: coords = frappe.db.sql( - """SELECT name, latitude, longitude FROM `tab{}` WHERE {}""".format(doctype, filters_sql), + f"""SELECT name, latitude, longitude FROM `tab{doctype}` WHERE {filters_sql}""", as_dict=True, ) - except InternalError: + except frappe.db.InternalError: frappe.msgprint( frappe._("This Doctype does not contain latitude and longitude fields"), raise_exception=True ) diff --git a/frappe/handler.py b/frappe/handler.py index 7b010eb716..cee6d3fbde 100644 --- a/frappe/handler.py +++ b/frappe/handler.py @@ -1,7 +1,9 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE +import os from mimetypes import guess_type +from typing import TYPE_CHECKING from werkzeug.wrappers import Response @@ -15,6 +17,10 @@ from frappe.utils.csvutils import build_csv_response from frappe.utils.image import optimize_image from frappe.utils.response import build_response +if TYPE_CHECKING: + from frappe.core.doctype.file.file import File + from frappe.core.doctype.user.user import User + ALLOWED_MIMETYPES = ( "image/png", "image/jpeg", @@ -25,6 +31,7 @@ ALLOWED_MIMETYPES = ( "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/vnd.oasis.opendocument.text", "application/vnd.oasis.opendocument.spreadsheet", + "text/plain", ) @@ -165,9 +172,9 @@ def upload_file(): if frappe.get_system_settings("allow_guests_to_upload_files"): ignore_permissions = True else: - return + raise frappe.PermissionError else: - user = frappe.get_doc("User", frappe.session.user) + user: "User" = frappe.get_doc("User", frappe.session.user) ignore_permissions = False files = frappe.request.files @@ -199,17 +206,19 @@ def upload_file(): frappe.local.uploaded_file = content frappe.local.uploaded_filename = filename - if not file_url and (frappe.session.user == "Guest" or (user and not user.has_desk_access())): + if content is not None and ( + frappe.session.user == "Guest" or (user and not user.has_desk_access()) + ): filetype = guess_type(filename)[0] if filetype not in ALLOWED_MIMETYPES: - frappe.throw(_("You can only upload JPG, PNG, PDF, or Microsoft documents.")) + frappe.throw(_("You can only upload JPG, PNG, PDF, TXT or Microsoft documents.")) if method: method = frappe.get_attr(method) is_whitelisted(method) return method() else: - ret = frappe.get_doc( + return frappe.get_doc( { "doctype": "File", "attached_to_doctype": doctype, @@ -221,9 +230,26 @@ def upload_file(): "is_private": cint(is_private), "content": content, } - ) - ret.save(ignore_permissions=ignore_permissions) - return ret + ).save(ignore_permissions=ignore_permissions) + + +@frappe.whitelist(allow_guest=True) +def download_file(file_url: str): + """ + Download file using token and REST API. Valid session or + token is required to download private files. + + Method : GET + Endpoints : download_file, frappe.core.doctype.file.file.download_file + URL Params : file_name = /path/to/file relative to site path + """ + file: "File" = frappe.get_doc("File", {"file_url": file_url}) + if not file.is_downloadable(): + raise frappe.PermissionError + + frappe.local.response.filename = os.path.basename(file_url) + frappe.local.response.filecontent = file.get_content() + frappe.local.response.type = "download" def get_attr(cmd): diff --git a/frappe/hooks.py b/frappe/hooks.py index d3de3877ba..14e76adc22 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -4,8 +4,6 @@ app_name = "frappe" app_title = "Frappe Framework" app_publisher = "Frappe Technologies" app_description = "Full stack web framework with Python, Javascript, MariaDB, Redis, Node" -app_icon = "octicon octicon-circuit-board" -app_color = "orange" source_link = "https://github.com/frappe/frappe" app_license = "MIT" app_logo_url = "/assets/frappe/images/frappe-framework-logo.svg" @@ -148,7 +146,7 @@ doc_events = { "frappe.core.doctype.activity_log.feed.update_feed", "frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions", "frappe.automation.doctype.assignment_rule.assignment_rule.apply", - "frappe.core.doctype.file.file.attach_files_to_document", + "frappe.core.doctype.file.utils.attach_files_to_document", "frappe.event_streaming.doctype.event_update_log.event_update_log.notify_consumers", "frappe.automation.doctype.assignment_rule.assignment_rule.update_due_date", "frappe.core.doctype.user_type.user_type.apply_permissions_for_non_standard_user_type", @@ -182,12 +180,10 @@ doc_events = { "on_update": "frappe.integrations.doctype.google_contacts.google_contacts.update_contacts_to_google_contacts", }, "DocType": { - "after_insert": "frappe.cache_manager.build_domain_restriced_doctype_cache", - "after_save": "frappe.cache_manager.build_domain_restriced_doctype_cache", + "on_update": "frappe.cache_manager.build_domain_restriced_doctype_cache", }, "Page": { - "after_insert": "frappe.cache_manager.build_domain_restriced_page_cache", - "after_save": "frappe.cache_manager.build_domain_restriced_page_cache", + "on_update": "frappe.cache_manager.build_domain_restriced_page_cache", }, } @@ -203,7 +199,6 @@ scheduler_events = { "frappe.email.queue.flush", "frappe.email.doctype.email_account.email_account.pull", "frappe.email.doctype.email_account.email_account.notify_unreplied", - "frappe.integrations.doctype.razorpay_settings.razorpay_settings.capture_payment", "frappe.utils.global_search.sync_global_search", "frappe.monitor.flush", ], @@ -221,12 +216,9 @@ scheduler_events = { "daily": [ "frappe.email.queue.set_expiry_for_email_queue", "frappe.desk.notifications.clear_notifications", - "frappe.core.doctype.error_log.error_log.set_old_logs_as_seen", "frappe.desk.doctype.event.event.send_event_digest", "frappe.sessions.clear_expired_sessions", "frappe.email.doctype.notification.notification.trigger_daily_alerts", - "frappe.utils.scheduler.restrict_scheduler_events_if_dormant", - "frappe.email.doctype.auto_email_report.auto_email_report.send_daily", "frappe.website.doctype.personal_data_deletion_request.personal_data_deletion_request.remove_unverified_record", "frappe.desk.form.document_follow.send_daily_updates", "frappe.social.doctype.energy_point_settings.energy_point_settings.allocate_review_points", @@ -241,12 +233,12 @@ scheduler_events = { "frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_daily", "frappe.utils.change_log.check_for_update", "frappe.integrations.doctype.s3_backup_settings.s3_backup_settings.take_backups_daily", + "frappe.email.doctype.auto_email_report.auto_email_report.send_daily", "frappe.integrations.doctype.google_drive.google_drive.daily_backup", ], "weekly_long": [ "frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_weekly", "frappe.integrations.doctype.s3_backup_settings.s3_backup_settings.take_backups_weekly", - "frappe.desk.doctype.route_history.route_history.flush_old_route_records", "frappe.desk.form.document_follow.send_weekly_updates", "frappe.social.doctype.energy_point_log.energy_point_log.send_weekly_summary", "frappe.integrations.doctype.google_drive.google_drive.weekly_backup", @@ -281,7 +273,7 @@ setup_wizard_exception = [ "frappe.desk.page.setup_wizard.setup_wizard.log_setup_wizard_exception", ] -before_migrate = ["frappe.patches.v11_0.sync_user_permission_doctype_before_migrate.execute"] +before_migrate = [] after_migrate = ["frappe.website.doctype.website_theme.website_theme.after_migrate"] otp_methods = ["OTP App", "Email", "SMS"] @@ -370,4 +362,16 @@ global_search_doctypes = { ] } +override_whitelisted_methods = { + "frappe.core.doctype.file.file.download_file": "download_file", + "frappe.core.doctype.file.file.unzip_file": "frappe.core.api.file.unzip_file", + "frappe.core.doctype.file.file.get_attached_images": "frappe.core.api.file.get_attached_images", + "frappe.core.doctype.file.file.get_files_in_folder": "frappe.core.api.file.get_files_in_folder", + "frappe.core.doctype.file.file.get_files_by_search_text": "frappe.core.api.file.get_files_by_search_text", + "frappe.core.doctype.file.file.get_max_file_size": "frappe.core.api.file.get_max_file_size", + "frappe.core.doctype.file.file.create_new_folder": "frappe.core.api.file.create_new_folder", + "frappe.core.doctype.file.file.move_file": "frappe.core.api.file.move_file", + "frappe.core.doctype.file.file.zip_files": "frappe.core.api.file.zip_files", +} + translated_search_doctypes = ["DocType", "Role", "Country", "Gender", "Salutation"] diff --git a/frappe/installer.py b/frappe/installer.py index 634d6287f8..32ab45e383 100644 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -1,15 +1,29 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE import json import os import sys from collections import OrderedDict -from typing import Dict, List, Tuple + +import click import frappe from frappe.defaults import _clear_cache -from frappe.utils import is_git_url +from frappe.utils import cint, is_git_url + + +def _is_scheduler_enabled() -> bool: + enable_scheduler = False + try: + frappe.connect() + enable_scheduler = cint(frappe.db.get_single_value("System Settings", "enable_scheduler")) + except Exception: + pass + finally: + frappe.db.close() + + return bool(enable_scheduler) def _new_site( @@ -28,15 +42,13 @@ def _new_site( db_type=None, db_host=None, db_port=None, - new_site=False, ): """Install a new Frappe site""" - from frappe.commands.scheduler import _is_scheduler_enabled from frappe.utils import get_site_path, scheduler, touch_file if not force and os.path.exists(site): - print("Site {0} already exists".format(site)) + print(f"Site {site} already exists") sys.exit(1) if no_mariadb_socket and not db_type == "mariadb": @@ -80,7 +92,13 @@ def _new_site( ) for app in apps_to_install: - install_app(app, verbose=verbose, set_as_patched=not source_sql) + # NOTE: not using force here for 2 reasons: + # 1. It's not really needed here as we've freshly installed a new db + # 2. If someone uses a sql file to do restore and that file already had + # installed_apps then it might cause problems as that sql file can be of any previous version(s) + # which might be incompatible with the current version and using force might cause problems. + # Example: the DocType DocType might not have `migration_hash` column which will cause failure in the restore. + install_app(app, verbose=verbose, set_as_patched=not source_sql, force=False) os.remove(installing) @@ -143,7 +161,7 @@ def install_db( frappe.flags.in_install_db = False -def find_org(org_repo: str) -> Tuple[str, str]: +def find_org(org_repo: str) -> tuple[str, str]: """find the org a repo is in find_org() @@ -171,7 +189,7 @@ def find_org(org_repo: str) -> Tuple[str, str]: raise InvalidRemoteException -def fetch_details_from_tag(_tag: str) -> Tuple[str, str, str]: +def fetch_details_from_tag(_tag: str) -> tuple[str, str, str]: """parse org, repo, tag from string fetch_details_from_tag() @@ -226,7 +244,7 @@ def parse_app_name(name: str) -> str: return repo -def install_app(name, verbose=False, set_as_patched=True): +def install_app(name, verbose=False, set_as_patched=True, force=False): from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs from frappe.model.sync import sync_for from frappe.modules.utils import sync_customizations @@ -243,7 +261,7 @@ def install_app(name, verbose=False, set_as_patched=True): if app_hooks.required_apps: for app in app_hooks.required_apps: required_app = parse_app_name(app) - install_app(required_app, verbose=verbose) + install_app(required_app, verbose=verbose, force=force) frappe.flags.in_install = name frappe.clear_cache() @@ -251,11 +269,11 @@ def install_app(name, verbose=False, set_as_patched=True): if name not in frappe.get_all_apps(): raise Exception("App not in apps.txt") - if name in installed_apps: - frappe.msgprint(frappe._("App {0} already installed").format(name)) + if not force and name in installed_apps: + click.secho(f"App {name} already installed", fg="yellow") return - print("\nInstalling {0}...".format(name)) + print(f"\nInstalling {name}...") if name != "frappe": frappe.only_for("System Manager") @@ -266,9 +284,9 @@ def install_app(name, verbose=False, set_as_patched=True): return if name != "frappe": - add_module_defs(name) + add_module_defs(name, ignore_if_duplicate=force) - sync_for(name, force=True, reset_permissions=True) + sync_for(name, force=force, reset_permissions=True) add_to_installed_apps(name) @@ -315,7 +333,6 @@ def remove_from_installed_apps(app_name): def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False): """Remove app and all linked to the app's module with the app from a site.""" - import click site = frappe.local.site app_hooks = frappe.get_hooks(app_name=app_name) @@ -364,7 +381,7 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False) frappe.flags.in_uninstall = False -def _delete_modules(modules: List[str], dry_run: bool) -> List[str]: +def _delete_modules(modules: list[str], dry_run: bool) -> list[str]: """Delete modules belonging to the app and all related doctypes. Note: All record linked linked to Module Def are also deleted. @@ -397,7 +414,7 @@ def _delete_modules(modules: List[str], dry_run: bool) -> List[str]: def _delete_linked_documents( - module_name: str, doctype_linkfield_map: Dict[str, str], dry_run: bool + module_name: str, doctype_linkfield_map: dict[str, str], dry_run: bool ) -> None: """Deleted all records linked with module def""" @@ -408,7 +425,7 @@ def _delete_linked_documents( frappe.delete_doc(doctype, record, ignore_on_trash=True, force=True) -def _get_module_linked_doctype_field_map() -> Dict[str, str]: +def _get_module_linked_doctype_field_map() -> dict[str, str]: """Get all the doctypes which have module linked with them. returns ordered dictionary with doctype->link field mapping.""" @@ -437,7 +454,7 @@ def _get_module_linked_doctype_field_map() -> Dict[str, str]: return doctype_to_field_map -def _delete_doctypes(doctypes: List[str], dry_run: bool) -> None: +def _delete_doctypes(doctypes: list[str], dry_run: bool) -> None: for doctype in set(doctypes): print(f"* dropping Table for '{doctype}'...") if not dry_run: @@ -466,13 +483,20 @@ def set_all_patches_as_completed(app): def init_singles(): - singles = [single["name"] for single in frappe.get_all("DocType", filters={"issingle": True})] + singles = frappe.get_all("DocType", filters={"issingle": True}, pluck="name") for single in singles: - if not frappe.db.get_singles_dict(single): + if frappe.db.get_singles_dict(single): + continue + + try: doc = frappe.new_doc(single) doc.flags.ignore_mandatory = True doc.flags.ignore_validate = True doc.save() + except ImportError: + # The doctype exists, but controller is deleted, + # no need to attempt to init such single, ref: #16917 + continue def make_conf( @@ -515,7 +539,7 @@ def update_site_config(key, value, validate=True, site_config_path=None): if not site_config_path: site_config_path = get_site_config_path() - with open(site_config_path, "r") as f: + with open(site_config_path) as f: site_config = json.loads(f.read()) # In case of non-int value @@ -573,13 +597,13 @@ def make_site_dirs(): os.makedirs(path, exist_ok=True) -def add_module_defs(app): +def add_module_defs(app, ignore_if_duplicate=False): modules = frappe.get_module_list(app) for module in modules: d = frappe.new_doc("Module Def") d.app_name = app d.module_name = module - d.insert(ignore_permissions=True, ignore_if_duplicate=True) + d.insert(ignore_permissions=True, ignore_if_duplicate=ignore_if_duplicate) def remove_missing_apps(): @@ -650,7 +674,7 @@ def extract_sql_gzip(sql_gz_path): try: original_file = sql_gz_path decompressed_file = original_file.rstrip(".gz") - cmd = "gzip --decompress --force < {0} > {1}".format(original_file, decompressed_file) + cmd = f"gzip --decompress --force < {original_file} > {decompressed_file}" subprocess.check_call(cmd, shell=True) except Exception: raise @@ -682,7 +706,7 @@ def extract_files(site_name, file_path): subprocess.check_output(["tar", "xvf", tar_path, "--strip", "2"], cwd=abs_site_path) elif file_path.endswith(".tgz"): subprocess.check_output(["tar", "zxvf", tar_path, "--strip", "2"], cwd=abs_site_path) - except: + except Exception: raise finally: frappe.destroy() @@ -752,11 +776,9 @@ def partial_restore(sql_file_path, verbose=False): elif frappe.conf.db_type == "postgres": import warnings - from click import style - from frappe.database.postgres.setup_db import import_db_from_sql - warn = style( + warn = click.style( "Delete the tables you want to restore manually before attempting" " partial restore operation for PostreSQL databases", fg="yellow", @@ -788,7 +810,7 @@ def validate_database_sql(path, _raise=True): # dont bother checking if empty file if not empty_file: - with open(path, "r") as f: + with open(path) as f: for line in f: if "tabDefaultValue" in line: missing_table = False @@ -798,8 +820,6 @@ def validate_database_sql(path, _raise=True): error_message = "Table `tabDefaultValue` not found in file." if error_message: - import click - click.secho(error_message, fg="red") if _raise and (missing_table or empty_file): diff --git a/frappe/integrations/doctype/braintree_settings/__init__.py b/frappe/integrations/doctype/braintree_settings/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/integrations/doctype/braintree_settings/braintree_settings.js b/frappe/integrations/doctype/braintree_settings/braintree_settings.js deleted file mode 100644 index c844022cec..0000000000 --- a/frappe/integrations/doctype/braintree_settings/braintree_settings.js +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) 2018, Frappe Technologies and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Braintree Settings', { - -}); diff --git a/frappe/integrations/doctype/braintree_settings/braintree_settings.json b/frappe/integrations/doctype/braintree_settings/braintree_settings.json deleted file mode 100644 index eebf64dfd1..0000000000 --- a/frappe/integrations/doctype/braintree_settings/braintree_settings.json +++ /dev/null @@ -1,273 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "field:gateway_name", - "beta": 0, - "creation": "2018-02-05 13:46:12.101852", - "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": "gateway_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Payment Gateway Name", - "length": 0, - "no_copy": 0, - "options": "Company", - "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": "section_break_2", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "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": "merchant_id", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Merchant 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": "public_key", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Public 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": "private_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": "Private 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": "use_sandbox", - "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 Sandbox", - "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": "header_img", - "fieldtype": "Attach Image", - "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": "Header Image", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-02-05 14:33:06.050377", - "modified_by": "Administrator", - "module": "Integrations", - "name": "Braintree 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": 0, - "track_seen": 0 -} \ No newline at end of file diff --git a/frappe/integrations/doctype/braintree_settings/braintree_settings.py b/frappe/integrations/doctype/braintree_settings/braintree_settings.py deleted file mode 100644 index ca7ab0cfdf..0000000000 --- a/frappe/integrations/doctype/braintree_settings/braintree_settings.py +++ /dev/null @@ -1,288 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2018, Frappe Technologies and contributors -# License: MIT. See LICENSE - -from urllib.parse import urlencode - -import braintree - -import frappe -from frappe import _ -from frappe.integrations.utils import create_payment_gateway, create_request_log -from frappe.model.document import Document -from frappe.utils import call_hook_method, get_url - - -class BraintreeSettings(Document): - supported_currencies = [ - "AED", - "AMD", - "AOA", - "ARS", - "AUD", - "AWG", - "AZN", - "BAM", - "BBD", - "BDT", - "BGN", - "BIF", - "BMD", - "BND", - "BOB", - "BRL", - "BSD", - "BWP", - "BYN", - "BZD", - "CAD", - "CHF", - "CLP", - "CNY", - "COP", - "CRC", - "CVE", - "CZK", - "DJF", - "DKK", - "DOP", - "DZD", - "EGP", - "ETB", - "EUR", - "FJD", - "FKP", - "GBP", - "GEL", - "GHS", - "GIP", - "GMD", - "GNF", - "GTQ", - "GYD", - "HKD", - "HNL", - "HRK", - "HTG", - "HUF", - "IDR", - "ILS", - "INR", - "ISK", - "JMD", - "JPY", - "KES", - "KGS", - "KHR", - "KMF", - "KRW", - "KYD", - "KZT", - "LAK", - "LBP", - "LKR", - "LRD", - "LSL", - "LTL", - "MAD", - "MDL", - "MKD", - "MNT", - "MOP", - "MUR", - "MVR", - "MWK", - "MXN", - "MYR", - "MZN", - "NAD", - "NGN", - "NIO", - "NOK", - "NPR", - "NZD", - "PAB", - "PEN", - "PGK", - "PHP", - "PKR", - "PLN", - "PYG", - "QAR", - "RON", - "RSD", - "RUB", - "RWF", - "SAR", - "SBD", - "SCR", - "SEK", - "SGD", - "SHP", - "SLL", - "SOS", - "SRD", - "STD", - "SVC", - "SYP", - "SZL", - "THB", - "TJS", - "TOP", - "TRY", - "TTD", - "TWD", - "TZS", - "UAH", - "UGX", - "USD", - "UYU", - "UZS", - "VEF", - "VND", - "VUV", - "WST", - "XAF", - "XCD", - "XOF", - "XPF", - "YER", - "ZAR", - "ZMK", - "ZWD", - ] - - def validate(self): - if not self.flags.ignore_mandatory: - self.configure_braintree() - - def on_update(self): - create_payment_gateway( - "Braintree-" + self.gateway_name, settings="Braintree Settings", controller=self.gateway_name - ) - call_hook_method("payment_gateway_enabled", gateway="Braintree-" + self.gateway_name) - - def configure_braintree(self): - if self.use_sandbox: - environment = "sandbox" - else: - environment = "production" - - braintree.Configuration.configure( - environment=environment, - merchant_id=self.merchant_id, - public_key=self.public_key, - private_key=self.get_password(fieldname="private_key", raise_exception=False), - ) - - def validate_transaction_currency(self, currency): - if currency not in self.supported_currencies: - frappe.throw( - _( - "Please select another payment method. Stripe does not support transactions in currency '{0}'" - ).format(currency) - ) - - def get_payment_url(self, **kwargs): - return get_url("./integrations/braintree_checkout?{0}".format(urlencode(kwargs))) - - def create_payment_request(self, data): - self.data = frappe._dict(data) - - try: - self.integration_request = create_request_log(self.data, "Host", "Braintree") - return self.create_charge_on_braintree() - - except Exception: - frappe.log_error(frappe.get_traceback()) - return { - "redirect_to": frappe.redirect_to_message( - _("Server Error"), - _( - "There seems to be an issue with the server's braintree configuration. Don't worry, in case of failure, the amount will get refunded to your account." - ), - ), - "status": 401, - } - - def create_charge_on_braintree(self): - self.configure_braintree() - - redirect_to = self.data.get("redirect_to") or None - redirect_message = self.data.get("redirect_message") or None - - result = braintree.Transaction.sale( - { - "amount": self.data.amount, - "payment_method_nonce": self.data.payload_nonce, - "options": {"submit_for_settlement": True}, - } - ) - - if result.is_success: - self.integration_request.db_set("status", "Completed", update_modified=False) - self.flags.status_changed_to = "Completed" - self.integration_request.db_set("output", result.transaction.status, update_modified=False) - - elif result.transaction: - self.integration_request.db_set("status", "Failed", update_modified=False) - error_log = frappe.log_error( - "code: " - + str(result.transaction.processor_response_code) - + " | text: " - + str(result.transaction.processor_response_text), - "Braintree Payment Error", - ) - self.integration_request.db_set("error", error_log.error, update_modified=False) - else: - self.integration_request.db_set("status", "Failed", update_modified=False) - for error in result.errors.deep_errors: - error_log = frappe.log_error( - "code: " + str(error.code) + " | message: " + str(error.message), "Braintree Payment Error" - ) - self.integration_request.db_set("error", error_log.error, update_modified=False) - - if self.flags.status_changed_to == "Completed": - status = "Completed" - if self.data.reference_doctype and self.data.reference_docname: - custom_redirect_to = None - try: - custom_redirect_to = frappe.get_doc( - self.data.reference_doctype, self.data.reference_docname - ).run_method("on_payment_authorized", self.flags.status_changed_to) - braintree_success_page = frappe.get_hooks("braintree_success_page") - if braintree_success_page: - custom_redirect_to = frappe.get_attr(braintree_success_page[-1])(self.data) - except Exception: - frappe.log_error(frappe.get_traceback()) - - if custom_redirect_to: - redirect_to = custom_redirect_to - - redirect_url = "payment-success" - else: - status = "Error" - redirect_url = "payment-failed" - - if redirect_to: - redirect_url += "?" + urlencode({"redirect_to": redirect_to}) - if redirect_message: - redirect_url += "&" + urlencode({"redirect_message": redirect_message}) - - return {"redirect_to": redirect_url, "status": status} - - -def get_gateway_controller(doc): - payment_request = frappe.get_doc("Payment Request", doc) - gateway_controller = frappe.db.get_value( - "Payment Gateway", payment_request.payment_gateway, "gateway_controller" - ) - return gateway_controller - - -def get_client_token(doc): - gateway_controller = get_gateway_controller(doc) - settings = frappe.get_doc("Braintree Settings", gateway_controller) - settings.configure_braintree() - - return braintree.ClientToken.generate() diff --git a/frappe/integrations/doctype/braintree_settings/test_braintree_settings.py b/frappe/integrations/doctype/braintree_settings/test_braintree_settings.py deleted file mode 100644 index 475a62be79..0000000000 --- a/frappe/integrations/doctype/braintree_settings/test_braintree_settings.py +++ /dev/null @@ -1,8 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2018, Frappe Technologies and Contributors -# License: MIT. See LICENSE -import unittest - - -class TestBraintreeSettings(unittest.TestCase): - pass diff --git a/frappe/integrations/doctype/connected_app/connected_app.js b/frappe/integrations/doctype/connected_app/connected_app.js index 4d20f65559..11dcda235e 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.js +++ b/frappe/integrations/doctype/connected_app/connected_app.js @@ -1,38 +1,38 @@ // Copyright (c) 2019, Frappe Technologies and contributors // For license information, please see license.txt -frappe.ui.form.on('Connected App', { - refresh: frm => { - frm.add_custom_button(__('Get OpenID Configuration'), async () => { +frappe.ui.form.on("Connected App", { + refresh: (frm) => { + frm.add_custom_button(__("Get OpenID Configuration"), async () => { if (!frm.doc.openid_configuration) { - frappe.msgprint(__('Please enter OpenID Configuration URL')); + frappe.msgprint(__("Please enter OpenID Configuration URL")); } else { try { const response = await fetch(frm.doc.openid_configuration); const oidc = await response.json(); - frm.set_value('authorization_uri', oidc.authorization_endpoint); - frm.set_value('token_uri', oidc.token_endpoint); - frm.set_value('userinfo_uri', oidc.userinfo_endpoint); - frm.set_value('introspection_uri', oidc.introspection_endpoint); - frm.set_value('revocation_uri', oidc.revocation_endpoint); + frm.set_value("authorization_uri", oidc.authorization_endpoint); + frm.set_value("token_uri", oidc.token_endpoint); + frm.set_value("userinfo_uri", oidc.userinfo_endpoint); + frm.set_value("introspection_uri", oidc.introspection_endpoint); + frm.set_value("revocation_uri", oidc.revocation_endpoint); } catch (error) { - frappe.msgprint(__('Please check OpenID Configuration URL')); + frappe.msgprint(__("Please check OpenID Configuration URL")); } } }); if (!frm.is_new()) { - frm.add_custom_button(__('Connect to {}', [frm.doc.provider_name]), async () => { + frm.add_custom_button(__("Connect to {}", [frm.doc.provider_name]), async () => { frappe.call({ - method: 'initiate_web_application_flow', + method: "initiate_web_application_flow", doc: frm.doc, - callback: function(r) { - window.open(r.message, '_blank'); - } + callback: function (r) { + window.open(r.message, "_blank"); + }, }); }); } - frm.toggle_display('sb_client_credentials_section', !frm.is_new()); - } + frm.toggle_display("sb_client_credentials_section", !frm.is_new()); + }, }); diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index e472193da8..308d1ca84a 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2019, Frappe Technologies and contributors # License: MIT. See LICENSE diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.py b/frappe/integrations/doctype/connected_app/test_connected_app.py index 1597ec75bf..1acedff160 100644 --- a/frappe/integrations/doctype/connected_app/test_connected_app.py +++ b/frappe/integrations/doctype/connected_app/test_connected_app.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2019, Frappe Technologies and contributors # License: MIT. See LICENSE import unittest diff --git a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.js b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.js index ea731fafc2..9a5e9a4dc7 100644 --- a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.js +++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.js @@ -1,51 +1,54 @@ // Copyright (c) 2016, Frappe Technologies and contributors // For license information, please see license.txt -frappe.ui.form.on('Dropbox Settings', { - refresh: function(frm) { - frm.toggle_display(["app_access_key", "app_secret_key"], !(frm.doc.__onload && frm.doc.__onload.dropbox_setup_via_site_config)); +frappe.ui.form.on("Dropbox Settings", { + refresh: function (frm) { + frm.toggle_display( + ["app_access_key", "app_secret_key"], + !(frm.doc.__onload && frm.doc.__onload.dropbox_setup_via_site_config) + ); frm.clear_custom_buttons(); frm.events.take_backup(frm); }, - allow_dropbox_access: function(frm) { + allow_dropbox_access: function (frm) { if (frm.doc.app_access_key && frm.doc.app_secret_key) { frappe.call({ method: "frappe.integrations.doctype.dropbox_settings.dropbox_settings.get_dropbox_authorize_url", freeze: true, - callback: function(r) { - if(!r.exc) { + callback: function (r) { + if (!r.exc) { window.open(r.message.auth_url); } - } - }) - } - else if (frm.doc.__onload && frm.doc.__onload.dropbox_setup_via_site_config) { + }, + }); + } else if (frm.doc.__onload && frm.doc.__onload.dropbox_setup_via_site_config) { frappe.call({ method: "frappe.integrations.doctype.dropbox_settings.dropbox_settings.get_redirect_url", freeze: true, - callback: function(r) { - if(!r.exc) { + callback: function (r) { + if (!r.exc) { window.open(r.message.auth_url); } - } - }) - } - else { - frappe.msgprint(__("Please enter values for App Access Key and App Secret Key")) + }, + }); + } else { + frappe.msgprint(__("Please enter values for App Access Key and App Secret Key")); } }, - take_backup: function(frm) { - if (frm.doc.enabled && ((frm.doc.app_access_key && frm.doc.app_secret_key) - || (frm.doc.__onload && frm.doc.__onload.dropbox_setup_via_site_config))) { - frm.add_custom_button(__("Take Backup Now"), function(frm){ + take_backup: function (frm) { + if ( + frm.doc.enabled && + ((frm.doc.app_access_key && frm.doc.app_secret_key) || + (frm.doc.__onload && frm.doc.__onload.dropbox_setup_via_site_config)) + ) { + frm.add_custom_button(__("Take Backup Now"), function (frm) { frappe.call({ method: "frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backup", - freeze: true - }) - }).addClass("btn-primary") + freeze: true, + }); + }).addClass("btn-primary"); } - } + }, }); - diff --git a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py index 8a6a7a4bfb..dc9db2ccda 100644 --- a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py +++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2015, Frappe Technologies and contributors # License: MIT. See LICENSE @@ -63,21 +62,21 @@ def take_backups_weekly(): def take_backups_if(freq): - if frappe.db.get_value("Dropbox Settings", None, "backup_frequency") == freq: + if frappe.db.get_single_value("Dropbox Settings", "backup_frequency") == freq: take_backup_to_dropbox() def take_backup_to_dropbox(retry_count=0, upload_db_backup=True): did_not_upload, error_log = [], [] try: - if cint(frappe.db.get_value("Dropbox Settings", None, "enabled")): + if cint(frappe.db.get_single_value("Dropbox Settings", "enabled")): validate_file_size() did_not_upload, error_log = backup_to_dropbox(upload_db_backup) if did_not_upload: raise Exception - if cint(frappe.db.get_value("Dropbox Settings", None, "send_email_for_successful_backup")): + if cint(frappe.db.get_single_value("Dropbox Settings", "send_email_for_successful_backup")): send_email(True, "Dropbox", "Dropbox Settings", "send_notifications_to") except JobTimeoutException: if retry_count < 2: @@ -89,7 +88,7 @@ def take_backup_to_dropbox(retry_count=0, upload_db_backup=True): "frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backup_to_dropbox", queue="long", timeout=1500, - **args + **args, ) except Exception: if isinstance(error_log, str): @@ -212,7 +211,7 @@ def upload_file_to_dropbox(filename, folder, dropbox_client): mode = dropbox.files.WriteMode.overwrite f = open(encode(filename), "rb") - path = "{0}/{1}".format(folder, os.path.basename(filename)) + path = f"{folder}/{os.path.basename(filename)}" try: if file_size <= chunk_size: @@ -234,7 +233,7 @@ def upload_file_to_dropbox(filename, folder, dropbox_client): cursor.offset = f.tell() except dropbox.exceptions.ApiError as e: if isinstance(e.error, dropbox.files.UploadError): - error = "File Path: {path}\n".format(path=path) + error = f"File Path: {path}\n" error += frappe.get_traceback() frappe.log_error(error) else: @@ -326,7 +325,7 @@ def delete_older_backups(dropbox_client, folder_path, to_keep): def get_redirect_url(): if not frappe.conf.dropbox_broker_site: frappe.conf.dropbox_broker_site = "https://dropbox.erpnext.com" - url = "{0}/api/method/dropbox_erpnext_broker.www.setup_dropbox.get_authotize_url".format( + url = "{}/api/method/dropbox_erpnext_broker.www.setup_dropbox.get_authotize_url".format( frappe.conf.dropbox_broker_site ) diff --git a/frappe/integrations/doctype/dropbox_settings/test_dropbox_settings.py b/frappe/integrations/doctype/dropbox_settings/test_dropbox_settings.py index e73cf03268..b165e03780 100644 --- a/frappe/integrations/doctype/dropbox_settings/test_dropbox_settings.py +++ b/frappe/integrations/doctype/dropbox_settings/test_dropbox_settings.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2019, Frappe Technologies and Contributors # License: MIT. See LICENSE # import frappe diff --git a/frappe/integrations/doctype/google_calendar/google_calendar.js b/frappe/integrations/doctype/google_calendar/google_calendar.js index f30c52b2f2..977dee8dfe 100644 --- a/frappe/integrations/doctype/google_calendar/google_calendar.js +++ b/frappe/integrations/doctype/google_calendar/google_calendar.js @@ -2,15 +2,22 @@ // For license information, please see license.txt frappe.ui.form.on("Google Calendar", { - refresh: function(frm) { + refresh: function (frm) { if (frm.is_new()) { - frm.dashboard.set_headline(__("To use Google Calendar, enable {0}.", [`${__('Google Settings')}`])); + frm.dashboard.set_headline( + __("To use Google Calendar, enable {0}.", [ + `${__("Google Settings")}`, + ]) + ); } frappe.realtime.on("import_google_calendar", (data) => { if (data.progress) { - frm.dashboard.show_progress("Syncing Google Calendar", data.progress / data.total * 100, - __("Syncing {0} of {1}", [data.progress, data.total])); + frm.dashboard.show_progress( + "Syncing Google Calendar", + (data.progress / data.total) * 100, + __("Syncing {0} of {1}", [data.progress, data.total]) + ); if (data.progress === data.total) { frm.dashboard.hide_progress("Syncing Google Calendar"); } @@ -21,38 +28,40 @@ frappe.ui.form.on("Google Calendar", { frm.add_custom_button(__("Sync Calendar"), function () { frappe.show_alert({ indicator: "green", - message: __("Syncing") - }); - frappe.call({ - method: "frappe.integrations.doctype.google_calendar.google_calendar.sync", - args: { - "g_calendar": frm.doc.name - }, - }).then((r) => { - frappe.hide_progress(); - frappe.msgprint(r.message); + message: __("Syncing"), }); + frappe + .call({ + method: "frappe.integrations.doctype.google_calendar.google_calendar.sync", + args: { + g_calendar: frm.doc.name, + }, + }) + .then((r) => { + frappe.hide_progress(); + frappe.msgprint(r.message); + }); }); } }, - authorize_google_calendar_access: function(frm) { + authorize_google_calendar_access: function (frm) { let reauthorize = 0; - if(frm.doc.authorization_code) { + if (frm.doc.authorization_code) { reauthorize = 1; } frappe.call({ method: "frappe.integrations.doctype.google_calendar.google_calendar.authorize_access", args: { - "g_calendar": frm.doc.name, - "reauthorize": reauthorize + g_calendar: frm.doc.name, + reauthorize: reauthorize, }, - callback: function(r) { - if(!r.exc) { + callback: function (r) { + if (!r.exc) { frm.save(); window.open(r.message.url); } - } + }, }); - } + }, }); diff --git a/frappe/integrations/doctype/google_calendar/google_calendar.py b/frappe/integrations/doctype/google_calendar/google_calendar.py index 71f0e83f80..09ed012454 100644 --- a/frappe/integrations/doctype/google_calendar/google_calendar.py +++ b/frappe/integrations/doctype/google_calendar/google_calendar.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2019, Frappe Technologies and contributors # License: MIT. See LICENSE @@ -14,7 +13,7 @@ from googleapiclient.errors import HttpError import frappe from frappe import _ -from frappe.integrations.doctype.google_settings.google_settings import get_auth_url +from frappe.integrations.google_oauth import GoogleOAuth from frappe.model.document import Document from frappe.utils import ( add_days, @@ -91,7 +90,7 @@ class GoogleCalendar(Document): } try: - r = requests.post(get_auth_url(), data=data).json() + r = requests.post(GoogleOAuth.OAUTH_URL, data=data).json() except requests.exceptions.HTTPError: button_label = frappe.bold(_("Allow Google Calendar Access")) frappe.throw( @@ -131,7 +130,7 @@ def authorize_access(g_calendar, reauthorize=None): "redirect_uri": redirect_uri, "grant_type": "authorization_code", } - r = requests.post(get_auth_url(), data=data).json() + r = requests.post(GoogleOAuth.OAUTH_URL, data=data).json() if "refresh_token" in r: frappe.db.set_value( @@ -140,7 +139,7 @@ def authorize_access(g_calendar, reauthorize=None): frappe.db.commit() frappe.local.response["type"] = "redirect" - frappe.local.response["location"] = "/app/Form/{0}/{1}".format( + frappe.local.response["location"] = "/app/Form/{}/{}".format( quote("Google Calendar"), quote(google_calendar.name) ) @@ -192,7 +191,7 @@ def get_google_calendar_object(g_calendar): credentials_dict = { "token": account.get_access_token(), "refresh_token": account.get_password(fieldname="refresh_token", raise_exception=False), - "token_uri": get_auth_url(), + "token_uri": GoogleOAuth.OAUTH_URL, "client_id": google_settings.client_id, "client_secret": google_settings.get_password(fieldname="client_secret", raise_exception=False), "scopes": "https://www.googleapis.com/auth/calendar/v3", diff --git a/frappe/integrations/doctype/google_contacts/google_contacts.js b/frappe/integrations/doctype/google_contacts/google_contacts.js index 7cbef46699..06289b0ca5 100644 --- a/frappe/integrations/doctype/google_contacts/google_contacts.js +++ b/frappe/integrations/doctype/google_contacts/google_contacts.js @@ -1,59 +1,63 @@ // Copyright (c) 2019, Frappe Technologies and contributors // For license information, please see license.txt -frappe.ui.form.on('Google Contacts', { - refresh: function(frm) { +frappe.ui.form.on("Google Contacts", { + refresh: function (frm) { if (!frm.doc.enable) { - frm.dashboard.set_headline(__("To use Google Contacts, enable {0}.", [`${__('Google Settings')}`])); + frm.dashboard.set_headline( + __("To use Google Contacts, enable {0}.", [ + `${__("Google Settings")}`, + ]) + ); } - frappe.realtime.on('import_google_contacts', (data) => { + frappe.realtime.on("import_google_contacts", (data) => { if (data.progress) { - frm.dashboard.show_progress('Import Google Contacts', data.progress / data.total * 100, - __('Importing {0} of {1}', [data.progress, data.total])); + frm.dashboard.show_progress( + "Import Google Contacts", + (data.progress / data.total) * 100, + __("Importing {0} of {1}", [data.progress, data.total]) + ); if (data.progress === data.total) { - frm.dashboard.hide_progress('Import Google Contacts'); + frm.dashboard.hide_progress("Import Google Contacts"); } } }); if (frm.doc.refresh_token) { - let sync_button = frm.add_custom_button(__('Sync Contacts'), function () { + let sync_button = frm.add_custom_button(__("Sync Contacts"), function () { frappe.show_alert({ - indicator: 'green', - message: __('Syncing') - }); - frappe.call({ - method: "frappe.integrations.doctype.google_contacts.google_contacts.sync", - args: { - "g_contact": frm.doc.name - }, - btn: sync_button - }).then((r) => { - frappe.hide_progress(); - frappe.msgprint(r.message); + indicator: "green", + message: __("Syncing"), }); + frappe + .call({ + method: "frappe.integrations.doctype.google_contacts.google_contacts.sync", + args: { + g_contact: frm.doc.name, + }, + btn: sync_button, + }) + .then((r) => { + frappe.hide_progress(); + frappe.msgprint(r.message); + }); }); } }, - authorize_google_contacts_access: function(frm) { - let reauthorize = 0; - if(frm.doc.authorization_code) { - reauthorize = 1; - } - + authorize_google_contacts_access: function (frm) { frappe.call({ method: "frappe.integrations.doctype.google_contacts.google_contacts.authorize_access", args: { - "g_contact": frm.doc.name, - "reauthorize": reauthorize + g_contact: frm.doc.name, + reauthorize: frm.doc.authorization_code ? 1 : 0, }, - callback: function(r) { - if(!r.exc) { + callback: function (r) { + if (!r.exc) { frm.save(); window.open(r.message.url); } - } + }, }); - } + }, }); diff --git a/frappe/integrations/doctype/google_contacts/google_contacts.py b/frappe/integrations/doctype/google_contacts/google_contacts.py index c26366f71a..9a20d5e905 100644 --- a/frappe/integrations/doctype/google_contacts/google_contacts.py +++ b/frappe/integrations/doctype/google_contacts/google_contacts.py @@ -1,20 +1,15 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2019, Frappe Technologies and contributors # License: MIT. See LICENSE -import google.oauth2.credentials -import requests -from googleapiclient.discovery import build +from urllib.parse import quote + from googleapiclient.errors import HttpError import frappe from frappe import _ -from frappe.integrations.doctype.google_settings.google_settings import get_auth_url +from frappe.integrations.google_oauth import GoogleOAuth from frappe.model.document import Document -from frappe.utils import get_request_site_address - -SCOPES = "https://www.googleapis.com/auth/contacts" class GoogleContacts(Document): @@ -23,120 +18,56 @@ class GoogleContacts(Document): frappe.throw(_("Enable Google API in Google Settings.")) def get_access_token(self): - google_settings = frappe.get_doc("Google Settings") - - if not google_settings.enable: - frappe.throw(_("Google Contacts Integration is disabled.")) - if not self.refresh_token: button_label = frappe.bold(_("Allow Google Contacts Access")) raise frappe.ValidationError(_("Click on {0} to generate Refresh Token.").format(button_label)) - data = { - "client_id": google_settings.client_id, - "client_secret": google_settings.get_password(fieldname="client_secret", raise_exception=False), - "refresh_token": self.get_password(fieldname="refresh_token", raise_exception=False), - "grant_type": "refresh_token", - "scope": SCOPES, - } - - try: - r = requests.post(get_auth_url(), data=data).json() - except requests.exceptions.HTTPError: - button_label = frappe.bold(_("Allow Google Contacts Access")) - frappe.throw( - _( - "Something went wrong during the token generation. Click on {0} to generate a new one." - ).format(button_label) - ) + oauth_obj = GoogleOAuth("contacts") + r = oauth_obj.refresh_access_token( + self.get_password(fieldname="refresh_token", raise_exception=False) + ) return r.get("access_token") -@frappe.whitelist() -def authorize_access(g_contact, reauthorize=None): +@frappe.whitelist(methods=["POST"]) +def authorize_access(g_contact, reauthorize=False, code=None): """ If no Authorization code get it from Google and then request for Refresh Token. Google Contact Name is set to flags to set_value after Authorization Code is obtained. """ - google_settings = frappe.get_doc("Google Settings") - google_contact = frappe.get_doc("Google Contacts", g_contact) - - redirect_uri = ( - get_request_site_address(True) - + "?cmd=frappe.integrations.doctype.google_contacts.google_contacts.google_callback" + oauth_code = ( + frappe.db.get_value("Google Contacts", g_contact, "authorization_code") if not code else code ) + oauth_obj = GoogleOAuth("contacts") - if not google_contact.authorization_code or reauthorize: - frappe.cache().hset("google_contacts", "google_contact", google_contact.name) - return get_authentication_url(client_id=google_settings.client_id, redirect_uri=redirect_uri) - else: - try: - data = { - "code": google_contact.authorization_code, - "client_id": google_settings.client_id, - "client_secret": google_settings.get_password( - fieldname="client_secret", raise_exception=False - ), - "redirect_uri": redirect_uri, - "grant_type": "authorization_code", - } - r = requests.post(get_auth_url(), data=data).json() - - if "refresh_token" in r: - frappe.db.set_value( - "Google Contacts", google_contact.name, "refresh_token", r.get("refresh_token") - ) - frappe.db.commit() - - frappe.local.response["type"] = "redirect" - frappe.local.response["location"] = "/app/Form/Google%20Contacts/{}".format(google_contact.name) - - frappe.msgprint(_("Google Contacts has been configured.")) - except Exception as e: - frappe.throw(e) - - -def get_authentication_url(client_id=None, redirect_uri=None): - return { - "url": "https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&response_type=code&prompt=consent&client_id={}&include_granted_scopes=true&scope={}&redirect_uri={}".format( - client_id, SCOPES, redirect_uri + if not oauth_code or reauthorize: + return oauth_obj.get_authentication_url( + { + "g_contact": g_contact, + "redirect": f"/app/Form/{quote('Google Contacts')}/{quote(g_contact)}", + }, ) - } - -@frappe.whitelist() -def google_callback(code=None): - """ - Authorization code is sent to callback as per the API configuration - """ - google_contact = frappe.cache().hget("google_contacts", "google_contact") - frappe.db.set_value("Google Contacts", google_contact, "authorization_code", code) - frappe.db.commit() - - authorize_access(google_contact) + r = oauth_obj.authorize(oauth_code) + frappe.db.set_value( + "Google Contacts", + g_contact, + {"authorization_code": oauth_code, "refresh_token": r.get("refresh_token")}, + ) def get_google_contacts_object(g_contact): """ Returns an object of Google Calendar along with Google Calendar doc. """ - google_settings = frappe.get_doc("Google Settings") account = frappe.get_doc("Google Contacts", g_contact) + oauth_obj = GoogleOAuth("contacts") - credentials_dict = { - "token": account.get_access_token(), - "refresh_token": account.get_password(fieldname="refresh_token", raise_exception=False), - "token_uri": get_auth_url(), - "client_id": google_settings.client_id, - "client_secret": google_settings.get_password(fieldname="client_secret", raise_exception=False), - "scopes": "https://www.googleapis.com/auth/contacts", - } - - credentials = google.oauth2.credentials.Credentials(**credentials_dict) - google_contacts = build( - serviceName="people", version="v1", credentials=credentials, static_discovery=False + google_contacts = oauth_obj.get_google_service_object( + account.get_access_token(), + account.get_password(fieldname="indexing_refresh_token", raise_exception=False), ) return google_contacts, account diff --git a/frappe/integrations/doctype/google_drive/google_drive.js b/frappe/integrations/doctype/google_drive/google_drive.js index c314d02e7e..208c1e5e1a 100644 --- a/frappe/integrations/doctype/google_drive/google_drive.js +++ b/frappe/integrations/doctype/google_drive/google_drive.js @@ -1,16 +1,23 @@ // Copyright (c) 2019, Frappe Technologies and contributors // For license information, please see license.txt -frappe.ui.form.on('Google Drive', { - refresh: function(frm) { +frappe.ui.form.on("Google Drive", { + refresh: function (frm) { if (!frm.doc.enable) { - frm.dashboard.set_headline(__("To use Google Drive, enable {0}.", [`${__('Google Settings')}`])); + frm.dashboard.set_headline( + __("To use Google Drive, enable {0}.", [ + `${__("Google Settings")}`, + ]) + ); } frappe.realtime.on("upload_to_google_drive", (data) => { if (data.progress) { - frm.dashboard.show_progress("Uploading to Google Drive", data.progress / data.total * 100, - __("{0}", [data.message])); + frm.dashboard.show_progress( + "Uploading to Google Drive", + (data.progress / data.total) * 100, + __("{0}", [data.message]) + ); if (data.progress === data.total) { frm.dashboard.hide_progress("Uploading to Google Drive"); } @@ -21,42 +28,43 @@ frappe.ui.form.on('Google Drive', { let sync_button = frm.add_custom_button(__("Take Backup"), function () { frappe.show_alert({ indicator: "green", - message: __("Backing up to Google Drive.") - }); - frappe.call({ - method: "frappe.integrations.doctype.google_drive.google_drive.take_backup", - btn: sync_button - }).then((r) => { - frappe.msgprint(r.message); + message: __("Backing up to Google Drive."), }); + frappe + .call({ + method: "frappe.integrations.doctype.google_drive.google_drive.take_backup", + btn: sync_button, + }) + .then((r) => { + frappe.msgprint(r.message); + }); }); } if (frm.doc.enable && frm.doc.backup_folder_name && !frm.doc.refresh_token) { - frm.dashboard.set_headline(__("Click on Authorize Google Drive Access to authorize Google Drive Access.")); + frm.dashboard.set_headline( + __( + "Click on Authorize Google Drive Access to authorize Google Drive Access." + ) + ); } if (frm.doc.enable && frm.doc.refresh_token && frm.doc.authorization_code) { frm.page.set_indicator("Authorized", "green"); } }, - authorize_google_drive_access: function(frm) { - let reauthorize = 0; - if (frm.doc.authorization_code) { - reauthorize = 1; - } - + authorize_google_drive_access: function (frm) { frappe.call({ method: "frappe.integrations.doctype.google_drive.google_drive.authorize_access", args: { - "reauthorize": reauthorize + reauthorize: frm.doc.authorization_code ? 1 : 0, }, - callback: function(r) { + callback: function (r) { if (!r.exc) { frm.save(); window.open(r.message.url); } - } + }, }); - } + }, }); diff --git a/frappe/integrations/doctype/google_drive/google_drive.py b/frappe/integrations/doctype/google_drive/google_drive.py index bbb1e8485e..6ea1294cb0 100644 --- a/frappe/integrations/doctype/google_drive/google_drive.py +++ b/frappe/integrations/doctype/google_drive/google_drive.py @@ -1,31 +1,25 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2019, Frappe Technologies and contributors # License: MIT. See LICENSE import os from urllib.parse import quote -import google.oauth2.credentials -import requests from apiclient.http import MediaFileUpload -from googleapiclient.discovery import build from googleapiclient.errors import HttpError import frappe from frappe import _ -from frappe.integrations.doctype.google_settings.google_settings import get_auth_url +from frappe.integrations.google_oauth import GoogleOAuth from frappe.integrations.offsite_backup_utils import ( get_latest_backup_file, send_email, validate_file_size, ) from frappe.model.document import Document -from frappe.utils import get_backups_path, get_bench_path, get_request_site_address +from frappe.utils import get_backups_path, get_bench_path from frappe.utils.background_jobs import enqueue from frappe.utils.backups import new_backup -SCOPES = "https://www.googleapis.com/auth/drive" - class GoogleDrive(Document): def validate(self): @@ -34,118 +28,57 @@ class GoogleDrive(Document): self.backup_folder_id = "" def get_access_token(self): - google_settings = frappe.get_doc("Google Settings") - - if not google_settings.enable: - frappe.throw(_("Google Integration is disabled.")) - if not self.refresh_token: button_label = frappe.bold(_("Allow Google Drive Access")) raise frappe.ValidationError(_("Click on {0} to generate Refresh Token.").format(button_label)) - data = { - "client_id": google_settings.client_id, - "client_secret": google_settings.get_password(fieldname="client_secret", raise_exception=False), - "refresh_token": self.get_password(fieldname="refresh_token", raise_exception=False), - "grant_type": "refresh_token", - "scope": SCOPES, - } - - try: - r = requests.post(get_auth_url(), data=data).json() - except requests.exceptions.HTTPError: - button_label = frappe.bold(_("Allow Google Drive Access")) - frappe.throw( - _( - "Something went wrong during the token generation. Click on {0} to generate a new one." - ).format(button_label) - ) + oauth_obj = GoogleOAuth("drive") + r = oauth_obj.refresh_access_token( + self.get_password(fieldname="refresh_token", raise_exception=False) + ) return r.get("access_token") -@frappe.whitelist() -def authorize_access(reauthorize=None): +@frappe.whitelist(methods=["POST"]) +def authorize_access(reauthorize=False, code=None): """ If no Authorization code get it from Google and then request for Refresh Token. Google Contact Name is set to flags to set_value after Authorization Code is obtained. """ - google_settings = frappe.get_doc("Google Settings") - google_drive = frappe.get_doc("Google Drive") - - redirect_uri = ( - get_request_site_address(True) - + "?cmd=frappe.integrations.doctype.google_drive.google_drive.google_callback" + oauth_code = ( + frappe.db.get_single_value("Google Drive", "authorization_code") if not code else code ) + oauth_obj = GoogleOAuth("drive") - if not google_drive.authorization_code or reauthorize: + if not oauth_code or reauthorize: if reauthorize: frappe.db.set_value("Google Drive", None, "backup_folder_id", "") - return get_authentication_url(client_id=google_settings.client_id, redirect_uri=redirect_uri) - else: - try: - data = { - "code": google_drive.authorization_code, - "client_id": google_settings.client_id, - "client_secret": google_settings.get_password( - fieldname="client_secret", raise_exception=False - ), - "redirect_uri": redirect_uri, - "grant_type": "authorization_code", - } - r = requests.post(get_auth_url(), data=data).json() - - if "refresh_token" in r: - frappe.db.set_value("Google Drive", google_drive.name, "refresh_token", r.get("refresh_token")) - frappe.db.commit() - - frappe.local.response["type"] = "redirect" - frappe.local.response["location"] = "/app/Form/{0}".format(quote("Google Drive")) - - frappe.msgprint(_("Google Drive has been configured.")) - except Exception as e: - frappe.throw(e) - - -def get_authentication_url(client_id, redirect_uri): - return { - "url": "https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&response_type=code&prompt=consent&client_id={}&include_granted_scopes=true&scope={}&redirect_uri={}".format( - client_id, SCOPES, redirect_uri + return oauth_obj.get_authentication_url( + { + "redirect": f"/app/Form/{quote('Google Drive')}", + }, ) - } - -@frappe.whitelist() -def google_callback(code=None): - """ - Authorization code is sent to callback as per the API configuration - """ - frappe.db.set_value("Google Drive", None, "authorization_code", code) - frappe.db.commit() - - authorize_access() + r = oauth_obj.authorize(oauth_code) + frappe.db.set_value( + "Google Drive", + "Google Drive", + {"authorization_code": oauth_code, "refresh_token": r.get("refresh_token")}, + ) def get_google_drive_object(): """ Returns an object of Google Drive. """ - google_settings = frappe.get_doc("Google Settings") account = frappe.get_doc("Google Drive") + oauth_obj = GoogleOAuth("drive") - credentials_dict = { - "token": account.get_access_token(), - "refresh_token": account.get_password(fieldname="refresh_token", raise_exception=False), - "token_uri": get_auth_url(), - "client_id": google_settings.client_id, - "client_secret": google_settings.get_password(fieldname="client_secret", raise_exception=False), - "scopes": "https://www.googleapis.com/auth/drive/v3", - } - - credentials = google.oauth2.credentials.Credentials(**credentials_dict) - google_drive = build( - serviceName="drive", version="v3", credentials=credentials, static_discovery=False + google_drive = oauth_obj.get_google_service_object( + account.get_access_token(), + account.get_password(fieldname="indexing_refresh_token", raise_exception=False), ) return google_drive, account @@ -243,7 +176,7 @@ def upload_system_backup_to_google_drive(): media = MediaFileUpload( get_absolute_path(filename=fileurl), mimetype="application/gzip", resumable=True ) - except IOError as e: + except OSError as e: frappe.throw(_("Google Drive - Could not locate - {0}").format(e)) try: @@ -259,20 +192,20 @@ def upload_system_backup_to_google_drive(): def daily_backup(): - drive_settings = frappe.db.get_singles_dict("Google Drive") + drive_settings = frappe.db.get_singles_dict("Google Drive", cast=True) if drive_settings.enable and drive_settings.frequency == "Daily": upload_system_backup_to_google_drive() def weekly_backup(): - drive_settings = frappe.db.get_singles_dict("Google Drive") + drive_settings = frappe.db.get_singles_dict("Google Drive", cast=True) if drive_settings.enable and drive_settings.frequency == "Weekly": upload_system_backup_to_google_drive() def get_absolute_path(filename): file_path = os.path.join(get_backups_path()[2:], os.path.basename(filename)) - return "{0}/sites/{1}".format(get_bench_path(), file_path) + return f"{get_bench_path()}/sites/{file_path}" def set_progress(progress, message): diff --git a/frappe/integrations/doctype/google_drive/test_google_drive.py b/frappe/integrations/doctype/google_drive/test_google_drive.py index 17f5b152ca..4dcc79afd6 100644 --- a/frappe/integrations/doctype/google_drive/test_google_drive.py +++ b/frappe/integrations/doctype/google_drive/test_google_drive.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2019, Frappe Technologies and Contributors # License: MIT. See LICENSE # import frappe diff --git a/frappe/integrations/doctype/google_settings/google_settings.js b/frappe/integrations/doctype/google_settings/google_settings.js index 01a127db7f..58093034b5 100644 --- a/frappe/integrations/doctype/google_settings/google_settings.js +++ b/frappe/integrations/doctype/google_settings/google_settings.js @@ -1,8 +1,14 @@ // Copyright (c) 2019, Frappe Technologies and contributors // For license information, please see license.txt -frappe.ui.form.on('Google Settings', { - refresh: function(frm) { - frm.dashboard.set_headline(__("For more information, {0}.", [`${__('Click here')}`])); - } +frappe.ui.form.on("Google Settings", { + refresh: function (frm) { + frm.dashboard.set_headline( + __("For more information, {0}.", [ + `${__( + "Click here" + )}`, + ]) + ); + }, }); diff --git a/frappe/integrations/doctype/google_settings/google_settings.py b/frappe/integrations/doctype/google_settings/google_settings.py index 0d5f9cb00d..e464e0d090 100644 --- a/frappe/integrations/doctype/google_settings/google_settings.py +++ b/frappe/integrations/doctype/google_settings/google_settings.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2019, Frappe Technologies and contributors # License: MIT. See LICENSE @@ -10,10 +9,6 @@ class GoogleSettings(Document): pass -def get_auth_url(): - return "https://www.googleapis.com/oauth2/v4/token" - - @frappe.whitelist() def get_file_picker_settings(): """Return all the data FileUploader needs to start the Google Drive Picker.""" diff --git a/frappe/integrations/doctype/google_settings/test_google_settings.py b/frappe/integrations/doctype/google_settings/test_google_settings.py index 53d59b1be0..8d07ffa54f 100644 --- a/frappe/integrations/doctype/google_settings/test_google_settings.py +++ b/frappe/integrations/doctype/google_settings/test_google_settings.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2021, Frappe Technologies and Contributors # License: MIT. See LICENSE -from __future__ import unicode_literals import unittest diff --git a/frappe/integrations/doctype/integration_request/integration_request.js b/frappe/integrations/doctype/integration_request/integration_request.js index 4b3b9a2de7..ac810f4d73 100644 --- a/frappe/integrations/doctype/integration_request/integration_request.js +++ b/frappe/integrations/doctype/integration_request/integration_request.js @@ -1,8 +1,6 @@ // Copyright (c) 2016, Frappe Technologies and contributors // For license information, please see license.txt -frappe.ui.form.on('Integration Request', { - refresh: function(frm) { - - } +frappe.ui.form.on("Integration Request", { + refresh: function (frm) {}, }); diff --git a/frappe/integrations/doctype/integration_request/integration_request.json b/frappe/integrations/doctype/integration_request/integration_request.json index 8a3fbc41ba..98db8ea748 100644 --- a/frappe/integrations/doctype/integration_request/integration_request.json +++ b/frappe/integrations/doctype/integration_request/integration_request.json @@ -1,334 +1,154 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2016-08-04 04:58:40.457416", - "custom": 0, - "docstatus": 0, + "actions": [], + "creation": "2022-03-28 12:25:29.929952", "doctype": "DocType", - "document_type": "", "editable_grid": 1, "engine": "InnoDB", + "field_order": [ + "request_id", + "integration_request_service", + "is_remote_request", + "column_break_5", + "request_description", + "status", + "section_break_8", + "url", + "request_headers", + "data", + "response_section", + "output", + "error", + "reference_section", + "reference_doctype", + "column_break_16", + "reference_docname" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "integration_type", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Integration Type", - "length": 0, - "no_copy": 0, - "options": "\nHost\nRemote\nSubscription Notification", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "integration_request_service", "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": "Integration Request Service", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Service", + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "Queued", - "fetch_if_empty": 0, "fieldname": "status", "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, "in_standard_filter": 1, "label": "Status", - "length": 0, - "no_copy": 0, "options": "\nQueued\nAuthorized\nCompleted\nCancelled\nFailed", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "data", "fieldtype": "Code", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Data", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Request Data", + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "output", "fieldtype": "Code", - "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": "Output", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "error", "fieldtype": "Code", - "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": "Error", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "reference_doctype", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Reference Document Type", - "length": 0, - "no_copy": 0, "options": "DocType", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "reference_docname", "fieldtype": "Dynamic Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Reference Docname", - "length": 0, - "no_copy": 0, + "label": "Reference Document Name", "options": "reference_doctype", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 + }, + { + "default": "0", + "fieldname": "is_remote_request", + "fieldtype": "Check", + "label": "Is Remote Request?", + "read_only": 1 + }, + { + "fieldname": "request_description", + "fieldtype": "Data", + "label": "Request Description", + "read_only": 1 + }, + { + "fieldname": "request_id", + "fieldtype": "Data", + "label": "Request ID", + "read_only": 1 + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_8", + "fieldtype": "Section Break" + }, + { + "fieldname": "url", + "fieldtype": "Data", + "label": "URL", + "read_only": 1 + }, + { + "fieldname": "response_section", + "fieldtype": "Section Break", + "label": "Response" + }, + { + "depends_on": "eval:doc.reference_doctype", + "fieldname": "reference_section", + "fieldtype": "Section Break", + "label": "Reference" + }, + { + "fieldname": "column_break_16", + "fieldtype": "Column Break" + }, + { + "fieldname": "request_headers", + "fieldtype": "Code", + "label": "Request Headers", + "read_only": 1 } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, "in_create": 1, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-09-05 14:22:27.664645", + "links": [], + "modified": "2022-04-07 11:32:27.557548", "modified_by": "Administrator", "module": "Integrations", "name": "Integration Request", - "name_case": "", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 + "share": 1 } ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "integration_request_service", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + "track_changes": 1 } \ No newline at end of file diff --git a/frappe/integrations/doctype/integration_request/integration_request.py b/frappe/integrations/doctype/integration_request/integration_request.py index 4c99613161..334736bc9b 100644 --- a/frappe/integrations/doctype/integration_request/integration_request.py +++ b/frappe/integrations/doctype/integration_request/integration_request.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2015, Frappe Technologies and contributors # License: MIT. See LICENSE diff --git a/frappe/integrations/doctype/integration_request/test_integration_request.py b/frappe/integrations/doctype/integration_request/test_integration_request.py index d14af481e8..45963d5096 100644 --- a/frappe/integrations/doctype/integration_request/test_integration_request.py +++ b/frappe/integrations/doctype/integration_request/test_integration_request.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2015, Frappe Technologies and Contributors # License: MIT. See LICENSE import unittest diff --git a/frappe/integrations/doctype/ldap_group_mapping/ldap_group_mapping.json b/frappe/integrations/doctype/ldap_group_mapping/ldap_group_mapping.json index 92db68e962..9bfe1eac56 100644 --- a/frappe/integrations/doctype/ldap_group_mapping/ldap_group_mapping.json +++ b/frappe/integrations/doctype/ldap_group_mapping/ldap_group_mapping.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2019-05-29 01:24:29.585060", "doctype": "DocType", "editable_grid": 1, @@ -19,13 +20,14 @@ "fieldname": "erpnext_role", "fieldtype": "Link", "in_list_view": 1, - "label": "ERPNext Role", + "label": "User Role", "options": "Role", "reqd": 1 } ], "istable": 1, - "modified": "2019-07-15 06:46:38.050408", + "links": [], + "modified": "2022-07-07 16:28:44.828514", "modified_by": "Administrator", "module": "Integrations", "name": "LDAP Group Mapping", @@ -34,5 +36,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "ASC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/frappe/integrations/doctype/ldap_group_mapping/ldap_group_mapping.py b/frappe/integrations/doctype/ldap_group_mapping/ldap_group_mapping.py index f1b242e4bb..853cfc96a1 100644 --- a/frappe/integrations/doctype/ldap_group_mapping/ldap_group_mapping.py +++ b/frappe/integrations/doctype/ldap_group_mapping/ldap_group_mapping.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2019, Frappe Technologies and contributors # License: MIT. See LICENSE diff --git a/frappe/integrations/doctype/ldap_settings/ldap_settings.js b/frappe/integrations/doctype/ldap_settings/ldap_settings.js index 9ac95883b7..2ca7370ecf 100644 --- a/frappe/integrations/doctype/ldap_settings/ldap_settings.js +++ b/frappe/integrations/doctype/ldap_settings/ldap_settings.js @@ -1,8 +1,6 @@ // Copyright (c) 2016, Frappe Technologies and contributors // For license information, please see license.txt -frappe.ui.form.on('LDAP Settings', { - refresh: function(frm) { - - } +frappe.ui.form.on("LDAP Settings", { + refresh: function (frm) {}, }); diff --git a/frappe/integrations/doctype/ldap_settings/ldap_settings.json b/frappe/integrations/doctype/ldap_settings/ldap_settings.json index fd45a71538..f5472a5097 100644 --- a/frappe/integrations/doctype/ldap_settings/ldap_settings.json +++ b/frappe/integrations/doctype/ldap_settings/ldap_settings.json @@ -42,7 +42,10 @@ "column_break_33", "ldap_group_member_attribute", "ldap_group_mappings_section", + "default_user_type", + "column_break_38", "default_role", + "section_break_40", "ldap_groups", "ldap_group_field" ], @@ -79,9 +82,11 @@ "reqd": 1 }, { + "depends_on": "eval: doc.default_user_type == \"System User\"", "fieldname": "default_role", "fieldtype": "Link", - "label": "Default Role on Creation", + "label": "Default User Role", + "mandatory_depends_on": "eval: doc.default_user_type == \"System User\"", "options": "Role", "reqd": 1 }, @@ -249,10 +254,10 @@ "label": "Group Object Class" }, { - "description": "string value, i.e. {0} or uid={0},ou=users,dc=example,dc=com", - "fieldname": "ldap_custom_group_search", - "fieldtype": "Data", - "label": "Custom Group Search" + "description": "string value, i.e. {0} or uid={0},ou=users,dc=example,dc=com", + "fieldname": "ldap_custom_group_search", + "fieldtype": "Data", + "label": "Custom Group Search" }, { "description": "Requires any valid fdn path. i.e. ou=users,dc=example,dc=com", @@ -268,12 +273,28 @@ "fieldtype": "Data", "label": "LDAP search path for Groups", "reqd": 1 + }, + { + "fieldname": "default_user_type", + "fieldtype": "Link", + "label": "Default User Type", + "options": "User Type", + "reqd": 1 + }, + { + "fieldname": "column_break_38", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_40", + "fieldtype": "Section Break", + "hide_border": 1 } ], "in_create": 1, "issingle": 1, "links": [], - "modified": "2021-07-27 11:51:43.328271", + "modified": "2022-07-07 16:51:46.230793", "modified_by": "Administrator", "module": "Integrations", "name": "LDAP Settings", @@ -294,5 +315,6 @@ "read_only": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/frappe/integrations/doctype/ldap_settings/ldap_settings.py b/frappe/integrations/doctype/ldap_settings/ldap_settings.py index a14124234f..735b96968c 100644 --- a/frappe/integrations/doctype/ldap_settings/ldap_settings.py +++ b/frappe/integrations/doctype/ldap_settings/ldap_settings.py @@ -1,20 +1,37 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2015, Frappe Technologies and contributors +# Copyright (c) 2022, Frappe Technologies and contributors # License: MIT. See LICENSE +import ssl +from typing import TYPE_CHECKING + +import ldap3 +from ldap3 import AUTO_BIND_TLS_BEFORE_BIND, HASHED_SALTED_SHA, MODIFY_REPLACE +from ldap3.abstract.entry import Entry +from ldap3.core.exceptions import ( + LDAPAttributeError, + LDAPInvalidCredentialsResult, + LDAPInvalidFilterError, + LDAPNoSuchObjectResult, +) +from ldap3.utils.hashed import hashed + import frappe from frappe import _, safe_encode from frappe.model.document import Document from frappe.twofactor import authenticate_for_2factor, confirm_otp_token, should_run_2fa +if TYPE_CHECKING: + from frappe.core.doctype.user.user import User + class LDAPSettings(Document): def validate(self): + self.default_user_type = self.default_user_type or "System User" + if not self.enabled: return if not self.flags.ignore_mandatory: - if ( self.ldap_search_string.count("(") == self.ldap_search_string.count(")") and self.ldap_search_string.startswith("(") @@ -29,8 +46,6 @@ class LDAPSettings(Document): try: if conn.result["type"] == "bindResponse" and self.base_dn: - import ldap3 - conn.search( search_base=self.ldap_search_path_user, search_filter="(objectClass=*)", @@ -41,13 +56,13 @@ class LDAPSettings(Document): search_base=self.ldap_search_path_group, search_filter="(objectClass=*)", attributes=["cn"] ) - except ldap3.core.exceptions.LDAPAttributeError as ex: + except LDAPAttributeError as ex: frappe.throw( _("LDAP settings incorrect. validation response was: {0}").format(ex), title=_("Misconfigured"), ) - except ldap3.core.exceptions.LDAPNoSuchObjectResult: + except LDAPNoSuchObjectResult: frappe.throw( _("Ensure the user and group search paths are correct."), title=_("Misconfigured") ) @@ -76,12 +91,8 @@ class LDAPSettings(Document): ) ) - def connect_to_ldap(self, base_dn, password, read_only=True): + def connect_to_ldap(self, base_dn, password, read_only=True) -> ldap3.Connection: try: - import ssl - - import ldap3 - if self.require_trusted_certificate == "Yes": tls_configuration = ldap3.Tls(validate=ssl.CERT_REQUIRED, version=ssl.PROTOCOL_TLS_CLIENT) else: @@ -95,9 +106,9 @@ class LDAPSettings(Document): tls_configuration.ca_certs_file = self.local_ca_certs_file server = ldap3.Server(host=self.ldap_server_url, tls=tls_configuration) - bind_type = ldap3.AUTO_BIND_TLS_BEFORE_BIND if self.ssl_tls_mode == "StartTLS" else True + bind_type = AUTO_BIND_TLS_BEFORE_BIND if self.ssl_tls_mode == "StartTLS" else True - conn = ldap3.Connection( + return ldap3.Connection( server=server, user=base_dn, password=password, @@ -106,42 +117,38 @@ class LDAPSettings(Document): raise_exceptions=True, ) - return conn - except ImportError: msg = _("Please Install the ldap3 library via pip to use ldap functionality.") frappe.throw(msg, title=_("LDAP Not Installed")) - except ldap3.core.exceptions.LDAPInvalidCredentialsResult: + except LDAPInvalidCredentialsResult: frappe.throw(_("Invalid username or password")) except Exception as ex: frappe.throw(_(str(ex))) @staticmethod - def get_ldap_client_settings(): + def get_ldap_client_settings() -> dict: # return the settings to be used on the client side. result = {"enabled": False} - ldap = frappe.get_doc("LDAP Settings") + ldap = frappe.get_cached_doc("LDAP Settings") if ldap.enabled: result["enabled"] = True result["method"] = "frappe.integrations.doctype.ldap_settings.ldap_settings.login" return result @classmethod - def update_user_fields(cls, user, user_data): - + def update_user_fields(cls, user: "User", user_data: dict): updatable_data = {key: value for key, value in user_data.items() if key != "email"} for key, value in updatable_data.items(): setattr(user, key, value) user.save(ignore_permissions=True) - def sync_roles(self, user, additional_groups=None): - - current_roles = set(d.role for d in user.get("roles")) - - needed_roles = set() - needed_roles.add(self.default_role) - + def sync_roles(self, user: "User", additional_groups: list = None): + current_roles = {d.role for d in user.get("roles")} + if self.default_user_type == "System User": + needed_roles = {self.default_role} + else: + needed_roles = set() lower_groups = [g.lower() for g in additional_groups or []] all_mapped_roles = {r.erpnext_role for r in self.ldap_groups} @@ -158,28 +165,31 @@ class LDAPSettings(Document): user.remove_roles(*roles_to_remove) - def create_or_update_user(self, user_data, groups=None): - user = None + def create_or_update_user(self, user_data: dict, groups: list = None): + user: "User" = None + role: str = None + if frappe.db.exists("User", user_data["email"]): user = frappe.get_doc("User", user_data["email"]) LDAPSettings.update_user_fields(user=user, user_data=user_data) else: - doc = user_data - doc.update( - { - "doctype": "User", - "send_welcome_email": 0, - "language": "", - "user_type": "System User", - # "roles": [{ - # "role": self.default_role - # }] - } - ) + doc = user_data | { + "doctype": "User", + "send_welcome_email": 0, + "language": "", + "user_type": self.default_user_type, + } user = frappe.get_doc(doc) user.insert(ignore_permissions=True) - # always add default role. - user.add_roles(self.default_role) + + if self.default_user_type == "System User": + role = self.default_role + else: + role = frappe.db.get_value("User Type", user.user_type, "role") + + if role: + user.add_roles(role) + self.sync_roles(user, groups) return user @@ -204,40 +214,28 @@ class LDAPSettings(Document): return ldap_attributes - def fetch_ldap_groups(self, user, conn): - import ldap3 + def fetch_ldap_groups(self, user: Entry, conn: ldap3.Connection) -> list: + if not isinstance(user, Entry): + raise TypeError("Invalid type, attribute 'user' must be of type 'ldap3.abstract.entry.Entry'") - if type(user) is not ldap3.abstract.entry.Entry: - raise TypeError( - "Invalid type, attribute {0} must be of type '{1}'".format( - "user", "ldap3.abstract.entry.Entry" - ) - ) - - if type(conn) is not ldap3.core.connection.Connection: - raise TypeError( - "Invalid type, attribute {0} must be of type '{1}'".format("conn", "ldap3.Connection") - ) + if not isinstance(conn, ldap3.Connection): + raise TypeError("Invalid type, attribute 'conn' must be of type 'ldap3.Connection'") fetch_ldap_groups = None - ldap_object_class = None ldap_group_members_attribute = None if self.ldap_directory_server.lower() == "active directory": - ldap_object_class = "Group" ldap_group_members_attribute = "member" user_search_str = user.entry_dn elif self.ldap_directory_server.lower() == "openldap": - ldap_object_class = "posixgroup" ldap_group_members_attribute = "memberuid" user_search_str = getattr(user, self.ldap_username_field).value elif self.ldap_directory_server.lower() == "custom": - ldap_object_class = self.ldap_group_objectclass ldap_group_members_attribute = self.ldap_group_member_attribute ldap_custom_group_search = self.ldap_custom_group_search or "{0}" @@ -248,68 +246,55 @@ class LDAPSettings(Document): # this path will be hit for everyone with preconfigured ldap settings. this must be taken into account so as not to break ldap for those users. if self.ldap_group_field: - fetch_ldap_groups = getattr(user, self.ldap_group_field).values if ldap_object_class is not None: conn.search( search_base=self.ldap_search_path_group, - search_filter="(&(objectClass={0})({1}={2}))".format( - ldap_object_class, ldap_group_members_attribute, user_search_str - ), + search_filter=f"(&(objectClass={ldap_object_class})({ldap_group_members_attribute}={user_search_str}))", attributes=["cn"], ) # Build search query if len(conn.entries) >= 1: - fetch_ldap_groups = [] for group in conn.entries: fetch_ldap_groups.append(group["cn"].value) return fetch_ldap_groups - def authenticate(self, username, password): - + def authenticate(self, username: str, password: str): if not self.enabled: frappe.throw(_("LDAP is not enabled.")) user_filter = self.ldap_search_string.format(username) ldap_attributes = self.get_ldap_attributes() - conn = self.connect_to_ldap(self.base_dn, self.get_password(raise_exception=False)) try: - import ldap3 - conn.search( search_base=self.ldap_search_path_user, - search_filter="{0}".format(user_filter), + search_filter=f"{user_filter}", attributes=ldap_attributes, ) if len(conn.entries) == 1 and conn.entries[0]: user = conn.entries[0] - groups = self.fetch_ldap_groups(user, conn) # only try and connect as the user, once we have their fqdn entry. if user.entry_dn and password and conn.rebind(user=user.entry_dn, password=password): - return self.create_or_update_user(self.convert_ldap_entry_to_dict(user), groups=groups) - raise ldap3.core.exceptions.LDAPInvalidCredentialsResult # even though nothing foundor failed authentication raise invalid credentials + raise LDAPInvalidCredentialsResult # even though nothing foundor failed authentication raise invalid credentials - except ldap3.core.exceptions.LDAPInvalidFilterError: + except LDAPInvalidFilterError: frappe.throw(_("Please use a valid LDAP search filter"), title=_("Misconfigured")) - except ldap3.core.exceptions.LDAPInvalidCredentialsResult: + except LDAPInvalidCredentialsResult: frappe.throw(_("Invalid username or password")) def reset_password(self, user, password, logout_sessions=False): - from ldap3 import HASHED_SALTED_SHA, MODIFY_REPLACE - from ldap3.utils.hashed import hashed - - search_filter = "({0}={1})".format(self.ldap_email_field, user) + search_filter = f"({self.ldap_email_field}={user})" conn = self.connect_to_ldap( self.base_dn, self.get_password(raise_exception=False), read_only=False @@ -337,8 +322,7 @@ class LDAPSettings(Document): else: frappe.throw(_("No LDAP User found for email: {0}").format(user)) - def convert_ldap_entry_to_dict(self, user_entry): - + def convert_ldap_entry_to_dict(self, user_entry: Entry): # support multiple email values email = user_entry[self.ldap_email_field] @@ -349,7 +333,6 @@ class LDAPSettings(Document): } # optional fields - if self.ldap_middle_name_field: data["middle_name"] = user_entry[self.ldap_middle_name_field].value @@ -369,7 +352,7 @@ class LDAPSettings(Document): def login(): # LDAP LOGIN LOGIC args = frappe.form_dict - ldap = frappe.get_doc("LDAP Settings") + ldap: LDAPSettings = frappe.get_doc("LDAP Settings") user = ldap.authenticate(frappe.as_unicode(args.usr), frappe.as_unicode(args.pwd)) @@ -386,7 +369,7 @@ def login(): @frappe.whitelist() def reset_password(user, password, logout): - ldap = frappe.get_doc("LDAP Settings") + ldap: LDAPSettings = frappe.get_doc("LDAP Settings") if not ldap.enabled: frappe.throw(_("LDAP is not enabled.")) ldap.reset_password(user, password, logout_sessions=int(logout)) diff --git a/frappe/integrations/doctype/ldap_settings/test_ldap_settings.py b/frappe/integrations/doctype/ldap_settings/test_ldap_settings.py index 0651932843..9080e0c82a 100644 --- a/frappe/integrations/doctype/ldap_settings/test_ldap_settings.py +++ b/frappe/integrations/doctype/ldap_settings/test_ldap_settings.py @@ -1,16 +1,16 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2019, Frappe Technologies and Contributors +# Copyright (c) 2022, Frappe Technologies and Contributors # License: MIT. See LICENSE +import contextlib import functools import os import ssl -import unittest -from unittest import mock +from unittest import TestCase, mock import ldap3 from ldap3 import MOCK_SYNC, OFFLINE_AD_2012_R2, OFFLINE_SLAPD_2_4, Connection, Server import frappe +from frappe.exceptions import MandatoryError, ValidationError from frappe.integrations.doctype.ldap_settings.ldap_settings import LDAPSettings @@ -23,15 +23,19 @@ class LDAP_TestCase: LDAP_LDIF_JSON = None TEST_VALUES_LDAP_COMPLEX_SEARCH_STRING = None + # for adding type hints during development ^_^ + assertTrue = TestCase.assertTrue + assertEqual = TestCase.assertEqual + assertIn = TestCase.assertIn + def mock_ldap_connection(f): @functools.wraps(f) def wrapped(self, *args, **kwargs): with mock.patch( - "frappe.integrations.doctype.ldap_settings.ldap_settings.LDAPSettings.connect_to_ldap" - ) as mock_connection: - mock_connection.return_value = self.connection - + "frappe.integrations.doctype.ldap_settings.ldap_settings.LDAPSettings.connect_to_ldap", + return_value=self.connection, + ): self.test_class = LDAPSettings(self.doc) # Create a clean doc @@ -48,80 +52,66 @@ class LDAP_TestCase: return wrapped def clean_test_users(): - try: # clean up test user 1 + with contextlib.suppress(Exception): frappe.get_doc("User", "posix.user1@unit.testing").delete() - except Exception: - pass - - try: # clean up test user 2 + with contextlib.suppress(Exception): frappe.get_doc("User", "posix.user2@unit.testing").delete() - except Exception: - pass + with contextlib.suppress(Exception): + frappe.get_doc("User", "website_ldap_user@test.com").delete() @classmethod - def setUpClass(self, ldapServer="OpenLDAP"): - - self.clean_test_users() + def setUpClass(cls): + cls.clean_test_users() # Save user data for restoration in tearDownClass() - self.user_ldap_settings = frappe.get_doc("LDAP Settings") + cls.user_ldap_settings = frappe.get_doc("LDAP Settings") # Create test user1 - self.user1doc = { + cls.user1doc = { "username": "posix.user", "email": "posix.user1@unit.testing", "first_name": "posix", + "doctype": "User", + "send_welcome_email": 0, + "language": "", + "user_type": "System User", } - self.user1doc.update( - { - "doctype": "User", - "send_welcome_email": 0, - "language": "", - "user_type": "System User", - } - ) - user = frappe.get_doc(self.user1doc) + user = frappe.get_doc(cls.user1doc) user.insert(ignore_permissions=True) - # Create test user1 - self.user2doc = { + cls.user2doc = { "username": "posix.user2", "email": "posix.user2@unit.testing", "first_name": "posix", + "doctype": "User", + "send_welcome_email": 0, + "language": "", + "user_type": "System User", } - self.user2doc.update( - { - "doctype": "User", - "send_welcome_email": 0, - "language": "", - "user_type": "System User", - } - ) - - user = frappe.get_doc(self.user2doc) + user = frappe.get_doc(cls.user2doc) user.insert(ignore_permissions=True) # Setup Mock OpenLDAP Directory - self.ldap_dc_path = "dc=unit,dc=testing" - self.ldap_user_path = "ou=users," + self.ldap_dc_path - self.ldap_group_path = "ou=groups," + self.ldap_dc_path - self.base_dn = "cn=base_dn_user," + self.ldap_dc_path - self.base_password = "my_password" - self.ldap_server = "ldap://my_fake_server:389" + cls.ldap_dc_path = "dc=unit,dc=testing" + cls.ldap_user_path = f"ou=users,{cls.ldap_dc_path}" + cls.ldap_group_path = f"ou=groups,{cls.ldap_dc_path}" + cls.base_dn = f"cn=base_dn_user,{cls.ldap_dc_path}" + cls.base_password = "my_password" + cls.ldap_server = "ldap://my_fake_server:389" - self.doc = { + cls.doc = { "doctype": "LDAP Settings", "enabled": True, - "ldap_directory_server": self.TEST_LDAP_SERVER, - "ldap_server_url": self.ldap_server, - "base_dn": self.base_dn, - "password": self.base_password, - "ldap_search_path_user": self.ldap_user_path, - "ldap_search_string": self.TEST_LDAP_SEARCH_STRING, - "ldap_search_path_group": self.ldap_group_path, + "ldap_directory_server": cls.TEST_LDAP_SERVER, + "ldap_server_url": cls.ldap_server, + "base_dn": cls.base_dn, + "password": cls.base_password, + "ldap_search_path_user": cls.ldap_user_path, + "ldap_search_string": cls.TEST_LDAP_SEARCH_STRING, + "ldap_search_path_group": cls.ldap_group_path, "ldap_user_creation_and_mapping_section": "", "ldap_email_field": "mail", - "ldap_username_field": self.LDAP_USERNAME_FIELD, + "ldap_username_field": cls.LDAP_USERNAME_FIELD, "ldap_first_name_field": "givenname", "ldap_middle_name_field": "", "ldap_last_name_field": "sn", @@ -136,50 +126,41 @@ class LDAP_TestCase: "ldap_group_objectclass": "", "ldap_group_member_attribute": "", "default_role": "Newsletter Manager", - "ldap_groups": self.DOCUMENT_GROUP_MAPPINGS, + "ldap_groups": cls.DOCUMENT_GROUP_MAPPINGS, "ldap_group_field": "", + "default_user_type": "System User", } - self.server = Server(host=self.ldap_server, port=389, get_info=self.LDAP_SCHEMA) - - self.connection = Connection( - self.server, - user=self.base_dn, - password=self.base_password, + cls.server = Server(host=cls.ldap_server, port=389, get_info=cls.LDAP_SCHEMA) + cls.connection = Connection( + cls.server, + user=cls.base_dn, + password=cls.base_password, read_only=True, client_strategy=MOCK_SYNC, ) - - self.connection.strategy.entries_from_json( - os.path.abspath(os.path.dirname(__file__)) + "/" + self.LDAP_LDIF_JSON + cls.connection.strategy.entries_from_json( + f"{os.path.abspath(os.path.dirname(__file__))}/{cls.LDAP_LDIF_JSON}" ) - - self.connection.bind() + cls.connection.bind() @classmethod - def tearDownClass(self): - try: + def tearDownClass(cls): + with contextlib.suppress(Exception): frappe.get_doc("LDAP Settings").delete() - except Exception: - pass - - try: - # return doc back to user data - self.user_ldap_settings.save() - - except Exception: - pass + # return doc back to user data + with contextlib.suppress(Exception): + cls.user_ldap_settings.save() # Clean-up test users - self.clean_test_users() + cls.clean_test_users() # Clear OpenLDAP connection - self.connection = None + cls.connection = None @mock_ldap_connection def test_mandatory_fields(self): - mandatory_fields = [ "ldap_server_url", "ldap_directory_server", @@ -196,26 +177,14 @@ class LDAP_TestCase: ] # fields that are required to have ldap functioning need to be mandatory for mandatory_field in mandatory_fields: - localdoc = self.doc.copy() localdoc[mandatory_field] = "" - try: - + with contextlib.suppress(MandatoryError, ValidationError): frappe.get_doc(localdoc).save() - - self.fail("Document LDAP Settings field [{0}] is not mandatory".format(mandatory_field)) - - except frappe.exceptions.MandatoryError: - pass - - except frappe.exceptions.ValidationError: - if mandatory_field == "ldap_search_string": - # additional validation is done on this field, pass in this instance - pass + self.fail(f"Document LDAP Settings field [{mandatory_field}] is not mandatory") for non_mandatory_field in self.doc: # Ensure remaining fields have not been made mandatory - if non_mandatory_field == "doctype" or non_mandatory_field in mandatory_fields: continue @@ -223,17 +192,12 @@ class LDAP_TestCase: localdoc[non_mandatory_field] = "" try: - frappe.get_doc(localdoc).save() - - except frappe.exceptions.MandatoryError: - self.fail( - "Document LDAP Settings field [{0}] should not be mandatory".format(non_mandatory_field) - ) + except MandatoryError: + self.fail(f"Document LDAP Settings field [{non_mandatory_field}] should not be mandatory") @mock_ldap_connection def test_validation_ldap_search_string(self): - invalid_ldap_search_strings = [ "", "uid={0}", @@ -245,19 +209,26 @@ class LDAP_TestCase: ] # ldap search string must be enclosed in '()' for ldap search to work for finding user and have the same number of opening and closing brackets. for invalid_search_string in invalid_ldap_search_strings: - localdoc = self.doc.copy() localdoc["ldap_search_string"] = invalid_search_string - try: + with contextlib.suppress(ValidationError): frappe.get_doc(localdoc).save() - - self.fail("LDAP search string [{0}] should not validate".format(invalid_search_string)) - - except frappe.exceptions.ValidationError: - pass + self.fail(f"LDAP search string [{invalid_search_string}] should not validate") def test_connect_to_ldap(self): + # prevent these parameters for security or lack of the und user from being able to configure + prevent_connection_parameters = { + "mode": { + "IP_V4_ONLY": "Locks the user to IPv4 without frappe providing a way to configure", + "IP_V6_ONLY": "Locks the user to IPv6 without frappe providing a way to configure", + }, + "auto_bind": { + "NONE": "ldap3.Connection must autobind with base_dn", + "NO_TLS": "ldap3.Connection must have TLS", + "TLS_AFTER_BIND": "[Security] ldap3.Connection TLS bind must occur before bind", + }, + } # setup a clean doc with ldap disabled so no validation occurs (this is tested seperatly) local_doc = self.doc.copy() @@ -265,48 +236,25 @@ class LDAP_TestCase: self.test_class = LDAPSettings(self.doc) with mock.patch("ldap3.Server") as ldap3_server_method: - - with mock.patch("ldap3.Connection") as ldap3_connection_method: - ldap3_connection_method.return_value = self.connection - + with mock.patch("ldap3.Connection", return_value=self.connection) as ldap3_connection_method: with mock.patch("ldap3.Tls") as ldap3_Tls_method: - function_return = self.test_class.connect_to_ldap( base_dn=self.base_dn, password=self.base_password ) - args, kwargs = ldap3_connection_method.call_args - prevent_connection_parameters = { - # prevent these parameters for security or lack of the und user from being able to configure - "mode": { - "IP_V4_ONLY": "Locks the user to IPv4 without frappe providing a way to configure", - "IP_V6_ONLY": "Locks the user to IPv6 without frappe providing a way to configure", - }, - "auto_bind": { - "NONE": "ldap3.Connection must autobind with base_dn", - "NO_TLS": "ldap3.Connection must have TLS", - "TLS_AFTER_BIND": "[Security] ldap3.Connection TLS bind must occur before bind", - }, - } - for connection_arg in kwargs: - if ( connection_arg in prevent_connection_parameters and kwargs[connection_arg] in prevent_connection_parameters[connection_arg] ): - self.fail( - "ldap3.Connection was called with {0}, failed reason: [{1}]".format( - kwargs[connection_arg], - prevent_connection_parameters[connection_arg][kwargs[connection_arg]], - ) + f"ldap3.Connection was called with {kwargs[connection_arg]}, failed reason: [{prevent_connection_parameters[connection_arg][kwargs[connection_arg]]}]" ) + tls_version = ssl.PROTOCOL_TLS_CLIENT if local_doc["require_trusted_certificate"] == "Yes": tls_validate = ssl.CERT_REQUIRED - tls_version = ssl.PROTOCOL_TLS_CLIENT tls_configuration = ldap3.Tls(validate=tls_validate, version=tls_version) self.assertTrue( @@ -316,7 +264,6 @@ class LDAP_TestCase: else: tls_validate = ssl.CERT_NONE - tls_version = ssl.PROTOCOL_TLS_CLIENT tls_configuration = ldap3.Tls(validate=tls_validate, version=tls_version) self.assertTrue(kwargs["auto_bind"], "ldap3.Connection must autobind") @@ -350,7 +297,7 @@ class LDAP_TestCase: ) self.assertTrue( - type(function_return) is ldap3.core.connection.Connection, + type(function_return) is Connection, "The return type must be of ldap3.Connection", ) @@ -367,24 +314,20 @@ class LDAP_TestCase: @mock_ldap_connection def test_get_ldap_client_settings(self): - result = self.test_class.get_ldap_client_settings() self.assertIsInstance(result, dict) - self.assertTrue(result["enabled"] == self.doc["enabled"]) # settings should match doc localdoc = self.doc.copy() localdoc["enabled"] = False frappe.get_doc(localdoc).save() - result = self.test_class.get_ldap_client_settings() self.assertFalse(result["enabled"]) # must match the edited doc @mock_ldap_connection def test_update_user_fields(self): - test_user_data = { "username": "posix.user", "email": "posix.user1@unit.testing", @@ -394,11 +337,8 @@ class LDAP_TestCase: "phone": "08 1234 5678", "mobile_no": "0421 123 456", } - test_user = frappe.get_doc("User", test_user_data["email"]) - self.test_class.update_user_fields(test_user, test_user_data) - updated_user = frappe.get_doc("User", test_user_data["email"]) self.assertTrue(updated_user.middle_name == test_user_data["middle_name"]) @@ -406,9 +346,23 @@ class LDAP_TestCase: self.assertTrue(updated_user.phone == test_user_data["phone"]) self.assertTrue(updated_user.mobile_no == test_user_data["mobile_no"]) + self.assertEqual(updated_user.user_type, self.test_class.default_user_type) + self.assertIn(self.test_class.default_role, frappe.get_roles(updated_user.name)) + + @mock_ldap_connection + def test_create_website_user(self): + new_test_user_data = { + "username": "website_ldap_user.test", + "email": "website_ldap_user@test.com", + "first_name": "Website User - LDAP Test", + } + self.test_class.default_user_type = "Website User" + self.test_class.create_or_update_user(user_data=new_test_user_data, groups=[]) + new_user = frappe.get_doc("User", new_test_user_data["email"]) + self.assertEqual(new_user.user_type, "Website User") + @mock_ldap_connection def test_sync_roles(self): - if self.TEST_LDAP_SERVER.lower() == "openldap": test_user_data = { "posix.user1": [ @@ -460,9 +414,8 @@ class LDAP_TestCase: user.insert(ignore_permissions=True) for test_user in test_user_data: - - test_user_doc = frappe.get_doc("User", test_user + "@unit.testing") - test_user_roles = frappe.get_roles(test_user + "@unit.testing") + test_user_doc = frappe.get_doc("User", f"{test_user}@unit.testing") + test_user_roles = frappe.get_roles(f"{test_user}@unit.testing") self.assertTrue( len(test_user_roles) == 2, "User should only be a part of the All and Guest roles" @@ -470,28 +423,22 @@ class LDAP_TestCase: self.test_class.sync_roles(test_user_doc, test_user_data[test_user]) # update user roles - frappe.get_doc("User", test_user + "@unit.testing") - updated_user_roles = frappe.get_roles(test_user + "@unit.testing") + frappe.get_doc("User", f"{test_user}@unit.testing") + updated_user_roles = frappe.get_roles(f"{test_user}@unit.testing") self.assertTrue( len(updated_user_roles) == len(test_user_data[test_user]), - "syncing of the user roles failed. {0} != {1} for user {2}".format( - len(updated_user_roles), len(test_user_data[test_user]), test_user - ), + f"syncing of the user roles failed. {len(updated_user_roles)} != {len(test_user_data[test_user])} for user {test_user}", ) for user_role in updated_user_roles: # match each users role mapped to ldap groups - self.assertTrue( role_to_group_map[user_role] in test_user_data[test_user], - "during sync_roles(), the user was given role {0} which should not have occured".format( - user_role - ), + f"during sync_roles(), the user was given role {user_role} which should not have occured", ) @mock_ldap_connection def test_create_or_update_user(self): - test_user_data = { "posix.user1": [ "Users", @@ -501,28 +448,21 @@ class LDAP_TestCase: "frappe_default_guest", ], } - test_user = "posix.user1" - frappe.get_doc("User", test_user + "@unit.testing").delete() # remove user 1 + frappe.get_doc("User", f"{test_user}@unit.testing").delete() with self.assertRaises( frappe.exceptions.DoesNotExistError ): # ensure user deleted so function can be tested - frappe.get_doc("User", test_user + "@unit.testing") + frappe.get_doc("User", f"{test_user}@unit.testing") with mock.patch( "frappe.integrations.doctype.ldap_settings.ldap_settings.LDAPSettings.update_user_fields" ) as update_user_fields_method: - - update_user_fields_method.return_value = None - with mock.patch( "frappe.integrations.doctype.ldap_settings.ldap_settings.LDAPSettings.sync_roles" ) as sync_roles_method: - - sync_roles_method.return_value = None - # New user self.test_class.create_or_update_user(self.user1doc, test_user_data[test_user]) @@ -542,14 +482,11 @@ class LDAP_TestCase: @mock_ldap_connection def test_get_ldap_attributes(self): - method_return = self.test_class.get_ldap_attributes() - self.assertTrue(type(method_return) is list) @mock_ldap_connection def test_fetch_ldap_groups(self): - if self.TEST_LDAP_SERVER.lower() == "openldap": test_users = {"posix.user": ["Users", "Administrators"], "posix.user2": ["Users", "Group3"]} elif self.TEST_LDAP_SERVER.lower() == "active directory": @@ -559,7 +496,6 @@ class LDAP_TestCase: } for test_user in test_users: - self.connection.search( search_base=self.ldap_user_path, search_filter=self.TEST_LDAP_SEARCH_STRING.format(test_user), @@ -572,18 +508,13 @@ class LDAP_TestCase: self.assertTrue(len(method_return) == len(test_users[test_user])) for returned_group in method_return: - self.assertTrue(returned_group in test_users[test_user]) @mock_ldap_connection def test_authenticate(self): - with mock.patch( "frappe.integrations.doctype.ldap_settings.ldap_settings.LDAPSettings.fetch_ldap_groups" ) as fetch_ldap_groups_function: - - fetch_ldap_groups_function.return_value = None - self.assertTrue(self.test_class.authenticate("posix.user", "posix_user_password")) self.assertTrue( @@ -602,25 +533,19 @@ class LDAP_TestCase: ] # All invalid users should return 'invalid username or password' for username, password in enumerate(invalid_users): - with self.assertRaises(frappe.exceptions.ValidationError) as display_massage: - self.test_class.authenticate(username, password) self.assertTrue( str(display_massage.exception).lower() == "invalid username or password", - "invalid credentials passed authentication [user: {0}, password: {1}]".format( - username, password - ), + f"invalid credentials passed authentication [user: {username}, password: {password}]", ) @mock_ldap_connection def test_complex_ldap_search_filter(self): - ldap_search_filters = self.TEST_VALUES_LDAP_COMPLEX_SEARCH_STRING for search_filter in ldap_search_filters: - self.test_class.ldap_search_string = search_filter if ( @@ -637,55 +562,44 @@ class LDAP_TestCase: self.assertTrue(self.test_class.authenticate("posix.user", "posix_user_password")) def test_reset_password(self): - self.test_class = LDAPSettings(self.doc) # Create a clean doc localdoc = self.doc.copy() - localdoc["enabled"] = False frappe.get_doc(localdoc).save() with mock.patch( - "frappe.integrations.doctype.ldap_settings.ldap_settings.LDAPSettings.connect_to_ldap" + "frappe.integrations.doctype.ldap_settings.ldap_settings.LDAPSettings.connect_to_ldap", + return_value=self.connection, ) as connect_to_ldap: - connect_to_ldap.return_value = self.connection - with self.assertRaises( frappe.exceptions.ValidationError ) as validation: # Fail if username string used self.test_class.reset_password("posix.user", "posix_user_password") - self.assertTrue(str(validation.exception) == "No LDAP User found for email: posix.user") - try: + with contextlib.suppress(Exception): self.test_class.reset_password( "posix.user1@unit.testing", "posix_user_password" ) # Change Password - - except Exception: # An exception from the tested class is ok, as long as the connection to LDAP was made writeable - pass - connect_to_ldap.assert_called_with(self.base_dn, self.base_password, read_only=False) @mock_ldap_connection def test_convert_ldap_entry_to_dict(self): - self.connection.search( search_base=self.ldap_user_path, search_filter=self.TEST_LDAP_SEARCH_STRING.format("posix.user"), attributes=self.test_class.get_ldap_attributes(), ) - test_ldap_entry = self.connection.entries[0] - method_return = self.test_class.convert_ldap_entry_to_dict(test_ldap_entry) self.assertTrue(type(method_return) is dict) # must be dict self.assertTrue(len(method_return) == 6) # there are 6 fields in mock_ldap for use -class Test_OpenLDAP(LDAP_TestCase, unittest.TestCase): +class Test_OpenLDAP(LDAP_TestCase, TestCase): TEST_LDAP_SERVER = "OpenLDAP" TEST_LDAP_SEARCH_STRING = "(uid={0})" DOCUMENT_GROUP_MAPPINGS = [ @@ -709,7 +623,7 @@ class Test_OpenLDAP(LDAP_TestCase, unittest.TestCase): ] -class Test_ActiveDirectory(LDAP_TestCase, unittest.TestCase): +class Test_ActiveDirectory(LDAP_TestCase, TestCase): TEST_LDAP_SERVER = "Active Directory" TEST_LDAP_SEARCH_STRING = "(samaccountname={0})" DOCUMENT_GROUP_MAPPINGS = [ diff --git a/frappe/integrations/doctype/oauth_authorization_code/oauth_authorization_code.js b/frappe/integrations/doctype/oauth_authorization_code/oauth_authorization_code.js index 32746e6752..83ad1b3ee5 100644 --- a/frappe/integrations/doctype/oauth_authorization_code/oauth_authorization_code.js +++ b/frappe/integrations/doctype/oauth_authorization_code/oauth_authorization_code.js @@ -1,8 +1,6 @@ // Copyright (c) 2016, Frappe Technologies and contributors // For license information, please see license.txt -frappe.ui.form.on('OAuth Authorization Code', { - refresh: function(frm) { - - } +frappe.ui.form.on("OAuth Authorization Code", { + refresh: function (frm) {}, }); diff --git a/frappe/integrations/doctype/oauth_authorization_code/oauth_authorization_code.py b/frappe/integrations/doctype/oauth_authorization_code/oauth_authorization_code.py index 4ef6f65dc7..431d27bc04 100644 --- a/frappe/integrations/doctype/oauth_authorization_code/oauth_authorization_code.py +++ b/frappe/integrations/doctype/oauth_authorization_code/oauth_authorization_code.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2015, Frappe Technologies and contributors # License: MIT. See LICENSE diff --git a/frappe/integrations/doctype/oauth_authorization_code/test_oauth_authorization_code.py b/frappe/integrations/doctype/oauth_authorization_code/test_oauth_authorization_code.py index 72cb789ebb..2036a42f15 100644 --- a/frappe/integrations/doctype/oauth_authorization_code/test_oauth_authorization_code.py +++ b/frappe/integrations/doctype/oauth_authorization_code/test_oauth_authorization_code.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2015, Frappe Technologies and Contributors # License: MIT. See LICENSE import unittest diff --git a/frappe/integrations/doctype/oauth_bearer_token/oauth_bearer_token.js b/frappe/integrations/doctype/oauth_bearer_token/oauth_bearer_token.js index da69753903..7794f2fb70 100644 --- a/frappe/integrations/doctype/oauth_bearer_token/oauth_bearer_token.js +++ b/frappe/integrations/doctype/oauth_bearer_token/oauth_bearer_token.js @@ -1,8 +1,6 @@ // Copyright (c) 2016, Frappe Technologies and contributors // For license information, please see license.txt -frappe.ui.form.on('OAuth Bearer Token', { - refresh: function(frm) { - - } +frappe.ui.form.on("OAuth Bearer Token", { + refresh: function (frm) {}, }); diff --git a/frappe/integrations/doctype/oauth_bearer_token/oauth_bearer_token.py b/frappe/integrations/doctype/oauth_bearer_token/oauth_bearer_token.py index 515d3d2ba3..2a17035571 100644 --- a/frappe/integrations/doctype/oauth_bearer_token/oauth_bearer_token.py +++ b/frappe/integrations/doctype/oauth_bearer_token/oauth_bearer_token.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2015, Frappe Technologies and contributors # License: MIT. See LICENSE diff --git a/frappe/integrations/doctype/oauth_bearer_token/test_oauth_bearer_token.py b/frappe/integrations/doctype/oauth_bearer_token/test_oauth_bearer_token.py index 9dea8f482a..3439096809 100644 --- a/frappe/integrations/doctype/oauth_bearer_token/test_oauth_bearer_token.py +++ b/frappe/integrations/doctype/oauth_bearer_token/test_oauth_bearer_token.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2015, Frappe Technologies and Contributors # License: MIT. See LICENSE import unittest diff --git a/frappe/integrations/doctype/oauth_client/oauth_client.js b/frappe/integrations/doctype/oauth_client/oauth_client.js index b0caa562b1..3ddd1a046b 100644 --- a/frappe/integrations/doctype/oauth_client/oauth_client.js +++ b/frappe/integrations/doctype/oauth_client/oauth_client.js @@ -1,8 +1,6 @@ // Copyright (c) 2016, Frappe Technologies and contributors // For license information, please see license.txt -frappe.ui.form.on('OAuth Client', { - refresh: function(frm) { - - } +frappe.ui.form.on("OAuth Client", { + refresh: function (frm) {}, }); diff --git a/frappe/integrations/doctype/oauth_client/oauth_client.json b/frappe/integrations/doctype/oauth_client/oauth_client.json index d0d45c36ab..f4ccde8174 100644 --- a/frappe/integrations/doctype/oauth_client/oauth_client.json +++ b/frappe/integrations/doctype/oauth_client/oauth_client.json @@ -1,517 +1,144 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "", - "beta": 0, + "actions": [], "creation": "2016-08-24 14:07:21.955052", - "custom": 0, - "docstatus": 0, "doctype": "DocType", "document_type": "Document", "editable_grid": 1, "engine": "InnoDB", + "field_order": [ + "client_id", + "app_name", + "user", + "cb_1", + "client_secret", + "skip_authorization", + "sb_1", + "scopes", + "cb_3", + "redirect_uris", + "default_redirect_uri", + "sb_advanced", + "grant_type", + "cb_2", + "response_type" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", "fieldname": "client_id", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "App Client ID", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "app_name", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "App Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "user", "fieldtype": "Link", "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "User", - "length": 0, - "no_copy": 0, - "options": "User", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "options": "User" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "cb_1", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "client_secret", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "App Client Secret", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "description": "If checked, users will not see the Confirm Access dialog.", "fieldname": "skip_authorization", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Skip Authorization", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "label": "Skip Authorization" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", "fieldname": "sb_1", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldtype": "Section Break" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "all openid", "description": "A list of resources which the Client App will have access to after the user allows it.e.g. project", "fieldname": "scopes", "fieldtype": "Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Scopes", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "cb_3", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "description": "URIs for receiving authorization code once the user allows access, as well as failure responses. Typically a REST endpoint exposed by the Client App.\n
e.g. http://hostname//api/method/frappe.www.login.login_via_facebook", "fieldname": "redirect_uris", "fieldtype": "Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Redirect URIs", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "label": "Redirect URIs" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "default_redirect_uri", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Default Redirect URI", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, "collapsible": 1, "collapsible_depends_on": "1", - "columns": 0, "fieldname": "sb_advanced", "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Advanced Settings", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "label": "Advanced Settings" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "grant_type", "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, "in_standard_filter": 1, "label": "Grant Type", - "length": 0, - "no_copy": 0, - "options": "Authorization Code\nImplicit", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "options": "Authorization Code\nImplicit" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "cb_2", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "Code", "fieldname": "response_type", "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, "in_standard_filter": 1, "label": "Response Type", - "length": 0, - "no_copy": 0, - "options": "Code\nToken", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "options": "Code\nToken" } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2020-04-07 21:07:39.476360", + "links": [], + "modified": "2022-08-03 12:21:52.062755", "modified_by": "Administrator", "module": "Integrations", "name": "OAuth Client", - "name_case": "", "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "System Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 } ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "app_name", - "track_changes": 1, - "track_seen": 0 + "track_changes": 1 } \ No newline at end of file diff --git a/frappe/integrations/doctype/oauth_client/oauth_client.py b/frappe/integrations/doctype/oauth_client/oauth_client.py index 09f6e3aced..ab40467751 100644 --- a/frappe/integrations/doctype/oauth_client/oauth_client.py +++ b/frappe/integrations/doctype/oauth_client/oauth_client.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2015, Frappe Technologies and contributors # License: MIT. See LICENSE diff --git a/frappe/integrations/doctype/oauth_client/test_oauth_client.py b/frappe/integrations/doctype/oauth_client/test_oauth_client.py index dd1b25239a..8fd732673e 100644 --- a/frappe/integrations/doctype/oauth_client/test_oauth_client.py +++ b/frappe/integrations/doctype/oauth_client/test_oauth_client.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2015, Frappe Technologies and Contributors # License: MIT. See LICENSE import unittest diff --git a/frappe/integrations/doctype/oauth_client/test_records.json b/frappe/integrations/doctype/oauth_client/test_records.json deleted file mode 100644 index 11e6338a87..0000000000 --- a/frappe/integrations/doctype/oauth_client/test_records.json +++ /dev/null @@ -1,15 +0,0 @@ -[ - { - "app_name": "_Test OAuth Client", - "client_secret": "test_client_secret", - "default_redirect_uri": "http://localhost", - "docstatus": 0, - "doctype": "OAuth Client", - "grant_type": "Authorization Code", - "name": "test_client_id", - "redirect_uris": "http://localhost", - "response_type": "Code", - "scopes": "all openid", - "skip_authorization": 1 - } -] diff --git a/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.js b/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.js index 6d7d071934..0071b4e977 100644 --- a/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.js +++ b/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.js @@ -1,8 +1,6 @@ // Copyright (c) 2016, Frappe Technologies and contributors // For license information, please see license.txt -frappe.ui.form.on('OAuth Provider Settings', { - refresh: function(frm) { - - } +frappe.ui.form.on("OAuth Provider Settings", { + refresh: function (frm) {}, }); diff --git a/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.json b/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.json index bf19eee6b1..219a87f2f4 100644 --- a/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.json +++ b/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.json @@ -1,90 +1,43 @@ { - "allow_copy": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2016-09-03 11:42:42.575525", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "creation": "2016-09-03 11:42:42.575525", + "doctype": "DocType", + "document_type": "Setup", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "skip_authorization" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "skip_authorization", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Skip Authorization", - "length": 0, - "no_copy": 0, - "options": "Force\nAuto", - "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 + "fieldname": "skip_authorization", + "fieldtype": "Select", + "label": "Skip Authorization", + "options": "Force\nAuto" } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 1, - "istable": 0, - "max_attachments": 0, - "modified": "2016-12-29 14:40:30.718685", - "modified_by": "Administrator", - "module": "Integrations", - "name": "OAuth Provider Settings", - "name_case": "", - "owner": "Administrator", + ], + "issingle": 1, + "links": [], + "modified": "2022-08-03 12:20:52.328415", + "modified_by": "Administrator", + "module": "Integrations", + "name": "OAuth Provider Settings", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "is_custom": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 0, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 } \ No newline at end of file diff --git a/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.py b/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.py index 2aefd591a1..5a918db587 100644 --- a/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.py +++ b/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2015, Frappe Technologies and contributors # License: MIT. See LICENSE @@ -15,7 +14,9 @@ def get_oauth_settings(): """Returns oauth settings""" out = frappe._dict( { - "skip_authorization": frappe.db.get_value("OAuth Provider Settings", None, "skip_authorization") + "skip_authorization": frappe.db.get_single_value( + "OAuth Provider Settings", "skip_authorization" + ) } ) diff --git a/frappe/integrations/doctype/oauth_scope/oauth_scope.py b/frappe/integrations/doctype/oauth_scope/oauth_scope.py index a30d087cc0..1db49a3818 100644 --- a/frappe/integrations/doctype/oauth_scope/oauth_scope.py +++ b/frappe/integrations/doctype/oauth_scope/oauth_scope.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2020, Frappe Technologies and contributors # License: MIT. See LICENSE diff --git a/frappe/integrations/doctype/paypal_settings/__init__.py b/frappe/integrations/doctype/paypal_settings/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/integrations/doctype/paypal_settings/paypal_settings.js b/frappe/integrations/doctype/paypal_settings/paypal_settings.js deleted file mode 100644 index 63480bc927..0000000000 --- a/frappe/integrations/doctype/paypal_settings/paypal_settings.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2016, Frappe Technologies and contributors -// For license information, please see license.txt - -frappe.ui.form.on('PayPal Settings', { - refresh: function(frm) { - - } -}); diff --git a/frappe/integrations/doctype/paypal_settings/paypal_settings.json b/frappe/integrations/doctype/paypal_settings/paypal_settings.json deleted file mode 100644 index 8d48496a4c..0000000000 --- a/frappe/integrations/doctype/paypal_settings/paypal_settings.json +++ /dev/null @@ -1,202 +0,0 @@ -{ - "allow_copy": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2016-09-21 08:03:01.009852", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "System", - "editable_grid": 1, - "fields": [ - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "api_username", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "API Username", - "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_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "api_password", - "fieldtype": "Password", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "API Password", - "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_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "signature", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Signature", - "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_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Check this if you are testing your payment using the Sandbox API", - "fieldname": "paypal_sandbox", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Use Sandbox", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Mention transaction completion page URL", - "fieldname": "redirect_to", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Redirect 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": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 1, - - "is_submittable": 0, - "issingle": 1, - "istable": 0, - "max_attachments": 0, - "modified": "2016-12-29 14:40:31.574789", - "modified_by": "Administrator", - "module": "Integrations", - "name": "PayPal Settings", - "name_case": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "is_custom": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 0, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "quick_entry": 0, - "read_only": 1, - "read_only_onload": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 -} \ No newline at end of file diff --git a/frappe/integrations/doctype/paypal_settings/paypal_settings.py b/frappe/integrations/doctype/paypal_settings/paypal_settings.py deleted file mode 100644 index ab7512f403..0000000000 --- a/frappe/integrations/doctype/paypal_settings/paypal_settings.py +++ /dev/null @@ -1,508 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2015, Frappe Technologies and contributors -# License: MIT. See LICENSE - -""" -# Integrating PayPal - -### 1. Validate Currency Support - -Example: - - from frappe.integrations.utils import get_payment_gateway_controller - - controller = get_payment_gateway_controller("PayPal") - controller().validate_transaction_currency(currency) - -### 2. Redirect for payment - -Example: - - payment_details = { - "amount": 600, - "title": "Payment for bill : 111", - "description": "payment via cart", - "reference_doctype": "Payment Request", - "reference_docname": "PR0001", - "payer_email": "NuranVerkleij@example.com", - "payer_name": "Nuran Verkleij", - "order_id": "111", - "currency": "USD", - "payment_gateway": "Razorpay", - "subscription_details": { - "plan_id": "plan_12313", # if Required - "start_date": "2018-08-30", - "billing_period": "Month" #(Day, Week, SemiMonth, Month, Year), - "billing_frequency": 1, - "customer_notify": 1, - "upfront_amount": 1000 - } - } - - # redirect the user to this url - url = controller().get_payment_url(**payment_details) - - -### 3. On Completion of Payment - -Write a method for `on_payment_authorized` in the reference doctype - -Example: - - def on_payment_authorized(payment_status): - # your code to handle callback - -##### Note: - -payment_status - payment gateway will put payment status on callback. -For paypal payment status parameter is one from: [Completed, Cancelled, Failed] - - -More Details: -
Backup Uploaded Successfully!
-Hi there, this is just to inform you that your backup was successfully uploaded to your {0} bucket. So relax!
""".format( +Hi there, this is just to inform you that your backup was successfully uploaded to your {} bucket. So relax!
""".format( service_name ) else: subject = "[Warning] Backup Upload Failed" message = """Backup Upload Failed!
-Oops, your automated backup to {0} failed.
-Error message: {1}
+Oops, your automated backup to {} failed.
+Error message: {}
Please contact your system manager for more information.
""".format( service_name, error_status ) diff --git a/frappe/integrations/utils.py b/frappe/integrations/utils.py index 191cd1f23b..5ae8965c83 100644 --- a/frappe/integrations/utils.py +++ b/frappe/integrations/utils.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2019, Frappe Technologies and contributors # License: MIT. See LICENSE @@ -42,22 +41,45 @@ def make_put_request(url, **kwargs): return make_request("PUT", url, **kwargs) -def create_request_log(data, integration_type, service_name, name=None, error=None): - if isinstance(data, str): - data = json.loads(data) +def create_request_log( + data, + integration_type=None, + service_name=None, + name=None, + error=None, + request_headers=None, + output=None, + **kwargs, +): + """ + DEPRECATED: The parameter integration_type will be removed in the next major release. + Use is_remote_request instead. + """ + if integration_type == "Remote": + kwargs["is_remote_request"] = 1 - if isinstance(error, str): - error = json.loads(error) + elif integration_type == "Subscription Notification": + kwargs["request_description"] = integration_type + + reference_doctype = reference_docname = None + if "reference_doctype" not in kwargs: + if isinstance(data, str): + data = json.loads(data) + + reference_doctype = data.get("reference_doctype") + reference_docname = data.get("reference_docname") integration_request = frappe.get_doc( { "doctype": "Integration Request", - "integration_type": integration_type, "integration_request_service": service_name, - "reference_doctype": data.get("reference_doctype"), - "reference_docname": data.get("reference_docname"), - "error": json.dumps(error, default=json_handler), - "data": json.dumps(data, default=json_handler), + "request_headers": get_json(request_headers), + "data": get_json(data), + "output": get_json(output), + "error": get_json(error), + "reference_doctype": reference_doctype, + "reference_docname": reference_docname, + **kwargs, } ) @@ -70,52 +92,8 @@ def create_request_log(data, integration_type, service_name, name=None, error=No return integration_request -def get_payment_gateway_controller(payment_gateway): - """Return payment gateway controller""" - gateway = frappe.get_doc("Payment Gateway", payment_gateway) - if gateway.gateway_controller is None: - try: - return frappe.get_doc("{0} Settings".format(payment_gateway)) - except Exception: - frappe.throw(_("{0} Settings not found").format(payment_gateway)) - else: - try: - return frappe.get_doc(gateway.gateway_settings, gateway.gateway_controller) - except Exception: - frappe.throw(_("{0} Settings not found").format(payment_gateway)) - - -@frappe.whitelist(allow_guest=True, xss_safe=True) -def get_checkout_url(**kwargs): - try: - if kwargs.get("payment_gateway"): - doc = frappe.get_doc("{0} Settings".format(kwargs.get("payment_gateway"))) - return doc.get_payment_url(**kwargs) - else: - raise Exception - except Exception: - frappe.respond_as_web_page( - _("Something went wrong"), - _( - "Looks like something is wrong with this site's payment gateway configuration. No payment has been made." - ), - indicator_color="red", - http_status_code=frappe.ValidationError.http_status_code, - ) - - -def create_payment_gateway(gateway, settings=None, controller=None): - # NOTE: we don't translate Payment Gateway name because it is an internal doctype - if not frappe.db.exists("Payment Gateway", gateway): - payment_gateway = frappe.get_doc( - { - "doctype": "Payment Gateway", - "gateway": gateway, - "gateway_settings": settings, - "gateway_controller": controller, - } - ) - payment_gateway.insert(ignore_permissions=True) +def get_json(obj): + return obj if isinstance(obj, str) else frappe.as_json(obj, indent=1) def json_handler(obj): diff --git a/frappe/integrations/workspace/integrations/integrations.json b/frappe/integrations/workspace/integrations/integrations.json index bbd2e1199f..8d1dfd64af 100644 --- a/frappe/integrations/workspace/integrations/integrations.json +++ b/frappe/integrations/workspace/integrations/integrations.json @@ -1,6 +1,6 @@ { "charts": [], - "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Backup\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Google Services\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Authentication\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Payments\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]", + "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Backup\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Google Services\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Authentication\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]", "creation": "2020-03-02 15:16:18.714190", "docstatus": 0, "doctype": "Workspace", @@ -106,11 +106,52 @@ { "hidden": 0, "is_query_report": 0, - "label": "Authentication", + "label": "Settings", "link_count": 0, "onboard": 0, "type": "Card Break" }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Webhook", + "link_count": 0, + "link_to": "Webhook", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Slack Webhook URL", + "link_count": 0, + "link_to": "Slack Webhook URL", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "SMS Settings", + "link_count": 0, + "link_to": "SMS Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Authentication", + "link_count": 4, + "onboard": 0, + "type": "Card Break" + }, { "dependencies": "", "hidden": 0, @@ -154,119 +195,16 @@ "link_type": "DocType", "onboard": 0, "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Payments", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Braintree Settings", - "link_count": 0, - "link_to": "Braintree Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "PayPal Settings", - "link_count": 0, - "link_to": "PayPal Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Razorpay Settings", - "link_count": 0, - "link_to": "Razorpay Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Stripe Settings", - "link_count": 0, - "link_to": "Stripe Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Paytm Settings", - "link_count": 0, - "link_to": "Paytm Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Settings", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Webhook", - "link_count": 0, - "link_to": "Webhook", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Slack Webhook URL", - "link_count": 0, - "link_to": "Slack Webhook URL", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "SMS Settings", - "link_count": 0, - "link_to": "SMS Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" } ], - "modified": "2022-01-13 17:39:01.292154", + "modified": "2022-07-23 18:00:28.805405", "modified_by": "Administrator", "module": "Integrations", "name": "Integrations", "owner": "Administrator", "parent_page": "", "public": 1, + "quick_lists": [], "restrict_to_domain": "", "roles": [], "sequence_id": 15.0, diff --git a/frappe/middlewares.py b/frappe/middlewares.py index cd47b7210f..168d129ebe 100644 --- a/frappe/middlewares.py +++ b/frappe/middlewares.py @@ -13,7 +13,7 @@ from frappe.utils import cstr, get_site_name class StaticDataMiddleware(SharedDataMiddleware): def __call__(self, environ, start_response): self.environ = environ - return super(StaticDataMiddleware, self).__call__(environ, start_response) + return super().__call__(environ, start_response) def get_directory_loader(self, directory): def loader(path): diff --git a/frappe/migrate.py b/frappe/migrate.py index bb83fa5b6d..1c249dfdb1 100644 --- a/frappe/migrate.py +++ b/frappe/migrate.py @@ -159,13 +159,13 @@ class SiteMigration: """Run Migrate operation on site specified. This method initializes and destroys connections to the site database. """ - if not self.required_services_running(): - raise SystemExit(1) - if site: frappe.init(site=site) frappe.connect() + if not self.required_services_running(): + raise SystemExit(1) + self.setUp() try: self.pre_schema_updates() diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py index bd607e7119..29991fa403 100644 --- a/frappe/model/__init__.py +++ b/frappe/model/__init__.py @@ -36,10 +36,14 @@ data_fieldtypes = ( "Geolocation", "Duration", "Icon", + "Phone", "Autocomplete", "JSON", ) +float_like_fields = {"Float", "Currency", "Percent"} +datetime_fields = {"Datetime", "Date", "Time"} + attachment_fieldtypes = ( "Attach", "Attach Image", diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 4f2ddd3bb6..1162ceacd3 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -4,8 +4,15 @@ import datetime import json import frappe -from frappe import _ -from frappe.model import child_table_fields, default_fields, display_fieldtypes, table_fields +from frappe import _, _dict +from frappe.model import ( + child_table_fields, + datetime_fields, + default_fields, + display_fieldtypes, + float_like_fields, + table_fields, +) from frappe.model.docstatus import DocStatus from frappe.model.naming import set_new_name from frappe.model.utils.link_count import notify_link_count @@ -13,9 +20,18 @@ from frappe.modules import load_doctype_module from frappe.utils import cast_fieldtype, cint, cstr, flt, now, sanitize_html, strip_html from frappe.utils.html_utils import unescape_html -max_positive_value = {"smallint": 2**15, "int": 2**31, "bigint": 2**63} +max_positive_value = {"smallint": 2**15 - 1, "int": 2**31 - 1, "bigint": 2**63 - 1} -DOCTYPES_FOR_DOCTYPE = ("DocType", "DocField", "DocPerm", "DocType Action", "DocType Link") +DOCTYPE_TABLE_FIELDS = [ + _dict(fieldname="fields", options="DocField"), + _dict(fieldname="permissions", options="DocPerm"), + _dict(fieldname="actions", options="DocType Action"), + _dict(fieldname="links", options="DocType Link"), + _dict(fieldname="states", options="DocType State"), +] + +TABLE_DOCTYPES_FOR_DOCTYPE = {df["fieldname"]: df["options"] for df in DOCTYPE_TABLE_FIELDS} +DOCTYPES_FOR_DOCTYPE = {"DocType", *TABLE_DOCTYPES_FOR_DOCTYPE.values()} def get_controller(doctype): @@ -42,9 +58,7 @@ def get_controller(doctype): module_path, classname = import_path.rsplit(".", 1) module = frappe.get_module(module_path) if not hasattr(module, classname): - raise ImportError( - "{0}: {1} does not exist in module {2}".format(doctype, classname, module_path) - ) + raise ImportError(f"{doctype}: {classname} does not exist in module {module_path}") else: module = load_doctype_module(doctype, module_name) classname = doctype.replace(" ", "").replace("-", "") @@ -69,13 +83,29 @@ def get_controller(doctype): return site_controllers[doctype] -class BaseDocument(object): - ignore_in_setter = ("doctype", "_meta", "meta", "_table_fields", "_valid_columns") +class BaseDocument: + _reserved_keywords = { + "doctype", + "meta", + "_meta", + "flags", + "_table_fields", + "_valid_columns", + "_table_fieldnames", + "_reserved_keywords", + "dont_update_if_missing", + } def __init__(self, d): if d.get("doctype"): self.doctype = d["doctype"] + self._table_fieldnames = ( + d["_table_fieldnames"] # from cache + if "_table_fieldnames" in d + else {df.fieldname for df in self._get_table_fields()} + ) + self.update(d) self.dont_update_if_missing = [] @@ -84,14 +114,29 @@ class BaseDocument(object): @property def meta(self): - if not getattr(self, "_meta", None): - self._meta = frappe.get_meta(self.doctype) + if not (meta := getattr(self, "_meta", None)): + self._meta = meta = frappe.get_meta(self.doctype) - return self._meta + return meta def __getstate__(self): - self._meta = None - return self.__dict__ + """ + Called when pickling. + Returns a copy of `__dict__` excluding unpicklable values like `_meta`. + + More info: https://docs.python.org/3/library/pickle.html#handling-stateful-objects + """ + + # Always use the dict.copy() method to avoid modifying the original state + state = self.__dict__.copy() + self.remove_unpicklable_values(state) + + return state + + def remove_unpicklable_values(self, state): + """Remove unpicklable values before pickling""" + + state.pop("_meta", None) def update(self, d): """Update multiple fields of a doctype using a dictionary of key-value pairs. @@ -136,17 +181,12 @@ class BaseDocument(object): if filters: if isinstance(filters, dict): - value = _filter(self.__dict__.get(key, []), filters, limit=limit) - else: - default = filters - filters = None - value = self.__dict__.get(key, default) - else: - value = self.__dict__.get(key, default) + return _filter(self.__dict__.get(key, []), filters, limit=limit) - if value is None and key in (d.fieldname for d in self.meta.get_table_fields()): - value = [] - self.set(key, value) + # perhaps you wanted to set a default instead + default = filters + + value = self.__dict__.get(key, default) if limit and isinstance(value, (list, tuple)) and len(value) > limit: value = value[:limit] @@ -157,14 +197,19 @@ class BaseDocument(object): return self.get(key, filters=filters, limit=1)[0] def set(self, key, value, as_value=False): - if key in self.ignore_in_setter: + if key in self._reserved_keywords: return - if isinstance(value, list) and not as_value: + if not as_value and key in self._table_fieldnames: self.__dict__[key] = [] - self.extend(key, value) - else: - self.__dict__[key] = value + + # if value is falsy, just init to an empty list + if value: + self.extend(key, value) + + return + + self.__dict__[key] = value def delete_key(self, key): if key in self.__dict__: @@ -182,41 +227,27 @@ class BaseDocument(object): """ if value is None: value = {} - if isinstance(value, (dict, BaseDocument)): - if not self.__dict__.get(key): - self.__dict__[key] = [] - value = self._init_child(value, key) - self.__dict__[key].append(value) + if (table := self.__dict__.get(key)) is None: + self.__dict__[key] = table = [] - # reference parent document - value.parent_doc = self + value = self._init_child(value, key) + table.append(value) - return value - else: + # reference parent document + value.parent_doc = self - # metaclasses may have arbitrary lists - # which we can ignore - if getattr(self, "_metaclass", None) or self.__class__.__name__ in ( - "Meta", - "FormMeta", - "DocField", - ): - return value - - raise ValueError( - 'Document for field "{0}" attached to child table of "{1}" must be a dict or BaseDocument, not {2} ({3})'.format( - key, self.name, str(type(value))[1:-1], value - ) - ) + return value def extend(self, key, value): - if isinstance(value, list): - for v in value: - self.append(key, v) - else: + try: + value = iter(value) + except TypeError: raise ValueError + for v in value: + self.append(key, v) + def remove(self, doc): # Usage: from the parent doc, pass the child table doc # to remove that child doc from the child table, thus removing it from the parent doc @@ -224,16 +255,12 @@ class BaseDocument(object): self.get(doc.parentfield).remove(doc) def _init_child(self, value, key): - if not self.doctype: - return value - if not isinstance(value, BaseDocument): - value["doctype"] = self.get_table_field_doctype(key) - if not value["doctype"]: + if not (doctype := self.get_table_field_doctype(key)): raise AttributeError(key) - value = get_controller(value["doctype"])(value) - value.init_valid_columns() + value["doctype"] = doctype + value = get_controller(doctype)(value) value.parent = self.name value.parenttype = self.doctype @@ -243,19 +270,38 @@ class BaseDocument(object): value.docstatus = DocStatus.draft() if not getattr(value, "idx", None): - value.idx = len(self.get(key) or []) + 1 + if table := getattr(self, key, None): + value.idx = len(table) + 1 + else: + value.idx = 1 if not getattr(value, "name", None): value.__dict__["__islocal"] = 1 return value + def _get_table_fields(self): + """ + To get table fields during Document init + Meta.get_table_fields goes into recursion for special doctypes + """ + + if self.doctype == "DocType": + return DOCTYPE_TABLE_FIELDS + + # child tables don't have child tables + if self.doctype in DOCTYPES_FOR_DOCTYPE or getattr(self, "parentfield", None): + return () + + return self.meta.get_table_fields() + def get_valid_dict( self, sanitize=True, convert_dates_to_str=False, ignore_nulls=False, ignore_virtual=False - ): - d = frappe._dict() + ) -> dict: + d = _dict() for fieldname in self.meta.get_valid_columns(): - d[fieldname] = self.get(fieldname) + # column is valid, we can use getattr + d[fieldname] = getattr(self, fieldname, None) # if no need for sanitization and value is None, continue if not sanitize and d[fieldname] is None: @@ -263,25 +309,24 @@ class BaseDocument(object): df = self.meta.get_field(fieldname) - if df and df.get("is_virtual"): - if ignore_virtual: - del d[fieldname] - continue + if df: + if getattr(df, "is_virtual", False): + if ignore_virtual: + del d[fieldname] + continue - from frappe.utils.safe_exec import get_safe_globals + if d[fieldname] is None and (options := getattr(df, "options", None)): + from frappe.utils.safe_exec import get_safe_globals - if d[fieldname] is None: - if df.get("options"): d[fieldname] = frappe.safe_eval( - code=df.get("options"), + code=options, eval_globals=get_safe_globals(), eval_locals={"doc": self}, ) - else: - _val = getattr(self, fieldname, None) - if _val and not callable(_val): - d[fieldname] = _val - elif df: + + if isinstance(d[fieldname], list) and df.fieldtype not in table_fields: + frappe.throw(_("Value for {0} cannot be a list").format(_(df.label))) + if df.fieldtype == "Check": d[fieldname] = 1 if cint(d[fieldname]) else 0 @@ -291,29 +336,34 @@ class BaseDocument(object): elif df.fieldtype == "JSON" and isinstance(d[fieldname], dict): d[fieldname] = json.dumps(d[fieldname], sort_keys=True, indent=4, separators=(",", ": ")) - elif df.fieldtype in ("Currency", "Float", "Percent") and not isinstance(d[fieldname], float): + elif df.fieldtype in float_like_fields and not isinstance(d[fieldname], float): d[fieldname] = flt(d[fieldname]) - elif df.fieldtype in ("Datetime", "Date", "Time") and d[fieldname] == "": + elif (df.fieldtype in datetime_fields and d[fieldname] == "") or ( + getattr(df, "unique", False) and cstr(d[fieldname]).strip() == "" + ): d[fieldname] = None - elif df.get("unique") and cstr(d[fieldname]).strip() == "": - # unique empty field should be set to None - d[fieldname] = None - - if isinstance(d[fieldname], list) and df.fieldtype not in table_fields: - frappe.throw(_("Value for {0} cannot be a list").format(_(df.label))) - if convert_dates_to_str and isinstance( d[fieldname], (datetime.datetime, datetime.date, datetime.time, datetime.timedelta) ): d[fieldname] = str(d[fieldname]) - if d[fieldname] is None and ignore_nulls: + if ignore_nulls and d[fieldname] is None: del d[fieldname] return d + def init_child_tables(self): + """ + This is needed so that one can loop over child table properties + without worrying about whether or not they have values + """ + + for fieldname in self._table_fieldnames: + if self.__dict__.get(fieldname) is None: + self.__dict__[fieldname] = [] + def init_valid_columns(self): for key in default_fields: if key not in self.__dict__: @@ -329,7 +379,7 @@ class BaseDocument(object): if key not in self.__dict__: self.__dict__[key] = None - def get_valid_columns(self): + def get_valid_columns(self) -> list[str]: if self.doctype not in frappe.local.valid_columns: if self.doctype in DOCTYPES_FOR_DOCTYPE: from frappe.model.meta import get_table_columns @@ -342,12 +392,12 @@ class BaseDocument(object): return frappe.local.valid_columns[self.doctype] - def is_new(self): + def is_new(self) -> bool: return self.get("__islocal") @property def docstatus(self): - return DocStatus(self.get("docstatus")) + return DocStatus(cint(self.get("docstatus"))) @docstatus.setter def docstatus(self, value): @@ -359,13 +409,13 @@ class BaseDocument(object): no_default_fields=False, convert_dates_to_str=False, no_child_table_fields=False, - ): - doc = self.get_valid_dict(convert_dates_to_str=convert_dates_to_str) + ) -> dict: + doc = self.get_valid_dict(convert_dates_to_str=convert_dates_to_str, ignore_nulls=no_nulls) doc["doctype"] = self.doctype - for df in self.meta.get_table_fields(): - children = self.get(df.fieldname) or [] - doc[df.fieldname] = [ + for fieldname in self._table_fieldnames: + children = self.get(fieldname) or [] + doc[fieldname] = [ d.as_dict( convert_dates_to_str=convert_dates_to_str, no_nulls=no_nulls, @@ -375,20 +425,15 @@ class BaseDocument(object): for d in children ] - if no_nulls: - for k in list(doc): - if doc[k] is None: - del doc[k] - if no_default_fields: - for k in list(doc): - if k in default_fields: - del doc[k] + for key in default_fields: + if key in doc: + del doc[key] if no_child_table_fields: - for k in list(doc): - if k in child_table_fields: - del doc[k] + for key in child_table_fields: + if key in doc: + del doc[key] for key in ( "_user_tags", @@ -398,8 +443,8 @@ class BaseDocument(object): "__run_link_triggers", "__unsaved", ): - if self.get(key): - doc[key] = self.get(key) + if value := getattr(self, key, None): + doc[key] = value return doc @@ -410,10 +455,9 @@ class BaseDocument(object): try: return self.meta.get_field(fieldname).options except AttributeError: - if self.doctype == "DocType": - return dict(links="DocType Link", actions="DocType Action", states="DocType State").get( - fieldname - ) + if self.doctype == "DocType" and (table_doctype := TABLE_DOCTYPES_FOR_DOCTYPE.get(fieldname)): + return table_doctype + raise def get_parentfield_of_doctype(self, doctype): @@ -495,7 +539,9 @@ class BaseDocument(object): return d = self.get_valid_dict( - convert_dates_to_str=True, ignore_nulls=self.doctype in DOCTYPES_FOR_DOCTYPE + convert_dates_to_str=True, + ignore_nulls=self.doctype in DOCTYPES_FOR_DOCTYPE, + ignore_virtual=True, ) # don't update name, as case might've been changed @@ -522,8 +568,8 @@ class BaseDocument(object): """Raw update parent + children DOES NOT VALIDATE AND CALL TRIGGERS""" self.db_update() - for df in self.meta.get_table_fields(): - for doc in self.get(df.fieldname): + for fieldname in self._table_fieldnames: + for doc in self.get(fieldname): doc.db_update() def show_unique_validation_message(self, e): @@ -635,7 +681,7 @@ class BaseDocument(object): if self.meta.istable: for fieldname in ("parent", "parenttype"): if not self.get(fieldname): - missing.append((fieldname, get_msg(frappe._dict(label=fieldname)))) + missing.append((fieldname, get_msg(_dict(label=fieldname)))) return missing @@ -647,7 +693,7 @@ class BaseDocument(object): if self.get("parentfield"): return "{} #{}: {}: {}".format(_("Row"), self.idx, _(df.label), docname) - return "{}: {}".format(_(df.label), docname) + return f"{_(df.label)}: {docname}" invalid_links = [] cancelled_links = [] @@ -682,7 +728,7 @@ class BaseDocument(object): if not frappe.get_meta(doctype).get("is_virtual"): if not fields_to_fetch: # cache a single value type - values = frappe._dict(name=frappe.db.get_value(doctype, docname, "name", cache=True)) + values = _dict(name=frappe.db.get_value(doctype, docname, "name", cache=True)) else: values_to_fetch = ["name"] + [_df.fetch_from.split(".")[-1] for _df in fields_to_fetch] @@ -771,6 +817,10 @@ class BaseDocument(object): def _validate_data_fields(self): # data_field options defined in frappe.model.data_field_options + for phone_field in self.meta.get_phone_fields(): + phone = self.get(phone_field.fieldname) + frappe.utils.validate_phone_number_with_country_code(phone, phone_field.fieldname) + for data_field in self.meta.get_data_fields(): data = self.get(data_field.fieldname) data_field_options = data_field.get("options") @@ -869,7 +919,7 @@ class BaseDocument(object): autoname = self.meta.autoname or "" _empty, _field_specifier, fieldname = autoname.partition("field:") - if fieldname and self.name and self.name != self.get("fieldname"): + if fieldname and self.name and self.name != self.get(fieldname): self.set(fieldname, self.name) def throw_length_exceeded_error(self, df, max_length, value): @@ -877,7 +927,7 @@ class BaseDocument(object): if self.get("parentfield"): reference = _("{0}, Row {1}").format(_(self.doctype), self.idx) else: - reference = "{0} {1}".format(_(self.doctype), self.name) + reference = f"{_(self.doctype)} {self.name}" frappe.throw( _("{0}: '{1}' ({3}) will get truncated, as max characters allowed is {2}").format( @@ -1008,10 +1058,10 @@ class BaseDocument(object): cache_key = parentfield or "main" if not hasattr(self, "_precision"): - self._precision = frappe._dict() + self._precision = _dict() if cache_key not in self._precision: - self._precision[cache_key] = frappe._dict() + self._precision[cache_key] = _dict() if fieldname not in self._precision[cache_key]: self._precision[cache_key][fieldname] = None @@ -1132,7 +1182,7 @@ class BaseDocument(object): return cast_fieldtype(df.fieldtype, value, show_warning=False) def _extract_images_from_text_editor(self): - from frappe.core.doctype.file.file import extract_images_from_doc + from frappe.core.doctype.file.utils import extract_images_from_doc if self.doctype != "DocType": for df in self.meta.get("fields", {"fieldtype": ("=", "Text Editor")}): diff --git a/frappe/model/create_new.py b/frappe/model/create_new.py index 8671008f82..51810c3e18 100644 --- a/frappe/model/create_new.py +++ b/frappe/model/create_new.py @@ -23,8 +23,6 @@ def get_new_doc(doctype, parent_doc=None, parentfield=None, as_dict=False): doc = copy.deepcopy(frappe.local.new_doc_templates[doctype]) - # doc = make_new_doc(doctype) - set_dynamic_default_values(doc, parent_doc, parentfield) if as_dict: diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index a7d9536ebc..a29ede37bf 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -6,7 +6,6 @@ import copy import json import re from datetime import datetime -from typing import List import frappe import frappe.defaults @@ -14,6 +13,7 @@ import frappe.permissions import frappe.share from frappe import _ from frappe.core.doctype.server_script.server_script_utils import get_server_script_map +from frappe.database.utils import FallBackDateTimeStr from frappe.model import optional_fields from frappe.model.meta import get_table_columns from frappe.model.utils.user_settings import get_user_settings, update_user_settings @@ -29,11 +29,34 @@ from frappe.utils import ( make_filter_tuple, ) +LOCATE_PATTERN = re.compile(r"locate\([^,]+,\s*[`\"]?name[`\"]?\s*\)", flags=re.IGNORECASE) +LOCATE_CAST_PATTERN = re.compile( + r"locate\(([^,]+),\s*([`\"]?name[`\"]?)\s*\)", flags=re.IGNORECASE +) +FUNC_IFNULL_PATTERN = re.compile( + r"(strpos|ifnull|coalesce)\(\s*[`\"]?name[`\"]?\s*,", flags=re.IGNORECASE +) +CAST_VARCHAR_PATTERN = re.compile( + r"([`\"]?tab[\w`\" -]+\.[`\"]?name[`\"]?)(?!\w)", flags=re.IGNORECASE +) +ORDER_BY_PATTERN = re.compile(r"\ order\ by\ |\ asc|\ ASC|\ desc|\ DESC", flags=re.IGNORECASE) +SUB_QUERY_PATTERN = re.compile("^.*[,();@].*") +IS_QUERY_PATTERN = re.compile(r"^(select|delete|update|drop|create)\s") +IS_QUERY_PREDICATE_PATTERN = re.compile( + r"\s*[0-9a-zA-z]*\s*( from | group by | order by | where | join )" +) +FIELD_QUOTE_PATTERN = re.compile(r"[0-9a-zA-Z]+\s*'") +FIELD_COMMA_PATTERN = re.compile(r"[0-9a-zA-Z]+\s*,") +STRICT_FIELD_PATTERN = re.compile(r".*/\*.*") +STRICT_UNION_PATTERN = re.compile(r".*\s(union).*\s") +ORDER_GROUP_PATTERN = re.compile(r".*[^a-z0-9-_ ,`'\"\.\(\)].*") -class DatabaseQuery(object): + +class DatabaseQuery: def __init__(self, doctype, user=None): self.doctype = doctype self.tables = [] + self.link_tables = [] self.conditions = [] self.or_conditions = [] self.fields = None @@ -75,7 +98,7 @@ class DatabaseQuery(object): pluck=None, ignore_ddl=False, parent_doctype=None, - ) -> List: + ) -> list: if ( not ignore_permissions @@ -213,9 +236,13 @@ class DatabaseQuery(object): # left join parent, child tables for child in self.tables[1:]: - parent_name = self.cast_name(f"{self.tables[0]}.name") + parent_name = cast_name(f"{self.tables[0]}.name") args.tables += f" {self.join} {child} on ({child}.parent = {parent_name})" + # left join link tables + for link in self.link_tables: + args.tables += f" {self.join} `tab{link.doctype}` on (`tab{link.doctype}`.`name` = {self.tables[0]}.`{link.fieldname}`)" + if self.grouped_or_conditions: self.conditions.append(f"({' or '.join(self.grouped_or_conditions)})") @@ -225,6 +252,7 @@ class DatabaseQuery(object): args.conditions += (" or " if args.conditions else "") + " or ".join(self.or_conditions) self.set_field_tables() + self.cast_name_fields() fields = [] @@ -260,7 +288,7 @@ class DatabaseQuery(object): return args def prepare_select_args(self, args): - order_field = re.sub(r"\ order\ by\ |\ asc|\ ASC|\ desc|\ DESC", "", args.order_by) + order_field = ORDER_BY_PATTERN.sub("", args.order_by) if order_field not in args.fields: extracted_column = order_column = order_field.replace("`", "") @@ -286,6 +314,23 @@ class DatabaseQuery(object): # remove empty strings / nulls in fields self.fields = [f for f in self.fields if f] + # convert child_table.fieldname to `tabChild DocType`.`fieldname` + for field in self.fields: + if "." in field and "tab" not in field: + original_field = field + alias = None + if " as " in field: + field, alias = field.split(" as ") + linked_fieldname, fieldname = field.split(".") + linked_field = frappe.get_meta(self.doctype).get_field(linked_fieldname) + linked_doctype = linked_field.options + if linked_field.fieldtype == "Link": + self.append_link_table(linked_doctype, linked_fieldname) + field = f"`tab{linked_doctype}`.`{fieldname}`" + if alias: + field = f"{field} as {alias}" + self.fields[self.fields.index(original_field)] = field + for filter_name in ["filters", "or_filters"]: filters = getattr(self, filter_name) if isinstance(filters, str): @@ -308,8 +353,6 @@ class DatabaseQuery(object): As field contains `,` and mysql function `version()`, with the help of regex the system will filter out this field. """ - - sub_query_regex = re.compile("^.*[,();@].*") blacklisted_keywords = ["select", "create", "insert", "delete", "drop", "update", "case", "show"] blacklisted_functions = [ "concat", @@ -333,19 +376,14 @@ class DatabaseQuery(object): frappe.throw(_("Use of sub-query or function is restricted"), frappe.DataError) def _is_query(field): - if re.compile(r"^(select|delete|update|drop|create)\s").match(field): + if IS_QUERY_PATTERN.match(field): _raise_exception() - elif re.compile(r"\s*[0-9a-zA-z]*\s*( from | group by | order by | where | join )").match( - field - ): + elif IS_QUERY_PREDICATE_PATTERN.match(field): _raise_exception() for field in self.fields: - if sub_query_regex.match(field): - if any(keyword in field.lower().split() for keyword in blacklisted_keywords): - _raise_exception() - + if SUB_QUERY_PATTERN.match(field): if any(f"({keyword}" in field.lower() for keyword in blacklisted_keywords): _raise_exception() @@ -356,19 +394,19 @@ class DatabaseQuery(object): # prevent access to global variables _raise_exception() - if re.compile(r"[0-9a-zA-Z]+\s*'").match(field): + if FIELD_QUOTE_PATTERN.match(field): _raise_exception() - if re.compile(r"[0-9a-zA-Z]+\s*,").match(field): + if FIELD_COMMA_PATTERN.match(field): _raise_exception() _is_query(field) if self.strict: - if re.compile(r".*/\*.*").match(field): + if STRICT_FIELD_PATTERN.match(field): frappe.throw(_("Illegal SQL Query")) - if re.compile(r".*\s(union).*\s").match(field.lower()): + if STRICT_UNION_PATTERN.match(field.lower()): frappe.throw(_("Illegal SQL Query")) def extract_tables(self): @@ -385,16 +423,8 @@ class DatabaseQuery(object): ] # add tables from fields if self.fields: - for i, field in enumerate(self.fields): - # add cast in locate/strpos - func_found = False - for func in sql_functions: - if func in field.lower(): - self.fields[i] = self.cast_name(field, func) - func_found = True - break - - if func_found or not ("tab" in field and "." in field): + for field in self.fields: + if not ("tab" in field and "." in field) or any(x for x in sql_functions if x in field): continue table_name = field.split(".")[0] @@ -403,44 +433,27 @@ class DatabaseQuery(object): table_name = table_name[13:] if not table_name[0] == "`": table_name = f"`{table_name}`" - if table_name not in self.tables: + if table_name not in self.tables and table_name not in ( + d.table_name for d in self.link_tables + ): self.append_table(table_name) - def cast_name( - self, - column: str, - sql_function: str = "", - ) -> str: - if frappe.db.db_type == "postgres": - if "name" in column.lower(): - if "cast(" not in column.lower() or "::" not in column: - if not sql_function: - return f"cast({column} as varchar)" - - elif sql_function == "locate(": - return re.sub( - r"locate\(([^,]+),([^)]+)\)", - r"locate(\1, cast(\2 as varchar))", - column, - flags=re.IGNORECASE, - ) - - elif sql_function == "strpos(": - return re.sub( - r"strpos\(([^,]+),([^)]+)\)", - r"strpos(cast(\1 as varchar), \2)", - column, - flags=re.IGNORECASE, - ) - - elif sql_function == "ifnull(": - return re.sub(r"ifnull\(([^,]+)", r"ifnull(cast(\1 as varchar)", column, flags=re.IGNORECASE) - - return column - def append_table(self, table_name): self.tables.append(table_name) doctype = table_name[4:-1] + self.check_read_permission(doctype) + + def append_link_table(self, doctype, fieldname): + for d in self.link_tables: + if d.doctype == doctype and d.fieldname == fieldname: + return + + self.check_read_permission(doctype) + self.link_tables.append( + frappe._dict(doctype=doctype, fieldname=fieldname, table_name=f"`tab{doctype}`") + ) + + def check_read_permission(self, doctype): ptype = "select" if frappe.only_has_select_perm(doctype) else "read" if not self.flags.ignore_permissions and not frappe.has_permission( @@ -457,11 +470,15 @@ class DatabaseQuery(object): methods = ("count(", "avg(", "sum(", "extract(", "dayofyear(") return field.lower().startswith(methods) - if len(self.tables) > 1: + if len(self.tables) > 1 or len(self.link_tables) > 0: for idx, field in enumerate(self.fields): if "." not in field and not _in_standard_sql_methods(field): self.fields[idx] = f"{self.tables[0]}.{field}" + def cast_name_fields(self): + for i, field in enumerate(self.fields): + self.fields[i] = cast_name(field) + def get_table_columns(self): try: return get_table_columns(self.doctype) @@ -541,10 +558,7 @@ class DatabaseQuery(object): if tname not in self.tables: self.append_table(tname) - if "ifnull(" in f.fieldname: - column_name = self.cast_name(f.fieldname, "ifnull(") - else: - column_name = self.cast_name(f"{tname}.`{f.fieldname}`") + column_name = cast_name(f.fieldname if "ifnull(" in f.fieldname else f"{tname}.`{f.fieldname}`") if f.operator.lower() in additional_filters_config: f.update(get_additional_filter_field(additional_filters_config, f, f.value)) @@ -619,11 +633,11 @@ class DatabaseQuery(object): date_range = get_date_range(f.operator.lower(), f.value) f.operator = "Between" f.value = date_range - fallback = "'0001-01-01 00:00:00'" + fallback = f"'{FallBackDateTimeStr}'" if f.operator in (">", "<") and (f.fieldname in ("creation", "modified")): value = cstr(f.value) - fallback = "'0001-01-01 00:00:00'" + fallback = f"'{FallBackDateTimeStr}'" elif f.operator.lower() in ("between") and ( f.fieldname in ("creation", "modified") @@ -631,7 +645,7 @@ class DatabaseQuery(object): ): value = get_between_date_filter(f.value, df) - fallback = "'0001-01-01 00:00:00'" + fallback = f"'{FallBackDateTimeStr}'" elif f.operator.lower() == "is": if f.value == "set": @@ -652,7 +666,7 @@ class DatabaseQuery(object): elif (df and df.fieldtype == "Datetime") or isinstance(f.value, datetime): value = frappe.db.format_datetime(f.value) - fallback = "'0001-01-01 00:00:00'" + fallback = f"'{FallBackDateTimeStr}'" elif df and df.fieldtype == "Time": value = get_time(f.value).strftime("%H:%M:%S.%f") @@ -706,7 +720,7 @@ class DatabaseQuery(object): return condition - def build_match_conditions(self, as_condition=True): + def build_match_conditions(self, as_condition=True) -> str | list: """add match conditions if applicable""" self.match_filters = [] self.match_conditions = [] @@ -729,7 +743,7 @@ class DatabaseQuery(object): ): only_if_shared = True if not self.shared: - frappe.throw(_("No permission to read {0}").format(self.doctype), frappe.PermissionError) + frappe.throw(_("No permission to read {0}").format(_(self.doctype)), frappe.PermissionError) else: self.conditions.append(self.get_share_condition()) @@ -766,7 +780,10 @@ class DatabaseQuery(object): return self.match_filters def get_share_condition(self): - return f"`tab{self.doctype}`.name in ({', '.join(frappe.db.escape(s, percent=False) for s in self.shared)})" + return ( + cast_name(f"`tab{self.doctype}`.name") + + f" in ({', '.join(frappe.db.escape(s, percent=False) for s in self.shared)})" + ) def add_user_permissions(self, user_permissions): meta = frappe.get_meta(self.doctype) @@ -794,7 +811,9 @@ class DatabaseQuery(object): if frappe.get_system_settings("apply_strict_user_permissions"): condition = "" else: - empty_value_condition = f"ifnull(`tab{self.doctype}`.`{df.get('fieldname')}`, '')=''" + empty_value_condition = cast_name( + f"ifnull(`tab{self.doctype}`.`{df.get('fieldname')}`, '')=''" + ) condition = empty_value_condition + " or " for permission in user_permission_values: @@ -815,7 +834,7 @@ class DatabaseQuery(object): if docs: values = ", ".join(frappe.db.escape(doc, percent=False) for doc in docs) - condition += f"`tab{self.doctype}`.`{df.get('fieldname')}` in ({values})" + condition += cast_name(f"`tab{self.doctype}`.`{df.get('fieldname')}`") + f" in ({values})" match_conditions.append(f"({condition})") match_filters[df.get("options")] = docs @@ -894,7 +913,7 @@ class DatabaseQuery(object): if "select" in _lower and "from" in _lower: frappe.throw(_("Cannot use sub-query in order by")) - if re.compile(r".*[^a-z0-9-_ ,`'\"\.\(\)].*").match(_lower): + if ORDER_GROUP_PATTERN.match(_lower): frappe.throw(_("Illegal SQL Query")) for field in parameters.split(","): @@ -907,7 +926,7 @@ class DatabaseQuery(object): def add_limit(self): if self.limit_page_length: - return "limit %s offset %s" % (self.limit_page_length, self.limit_start) + return f"limit {self.limit_page_length} offset {self.limit_start}" else: return "" @@ -933,6 +952,38 @@ class DatabaseQuery(object): update_user_settings(self.doctype, user_settings) +def cast_name(column: str) -> str: + """Casts name field to varchar for postgres + + Handles majorly 4 cases: + 1. locate + 2. strpos + 3. ifnull + 4. coalesce + + Uses regex substitution. + + Example: + input - "ifnull(`tabBlog Post`.`name`, '')=''" + output - "ifnull(cast(`tabBlog Post`.`name` as varchar), '')=''" """ + + if frappe.db.db_type == "mariadb": + return column + + kwargs = {"string": column} + if "cast(" not in column.lower() and "::" not in column: + if LOCATE_PATTERN.search(**kwargs): + return LOCATE_CAST_PATTERN.sub(r"locate(\1, cast(\2 as varchar))", **kwargs) + + elif match := FUNC_IFNULL_PATTERN.search(**kwargs): + func = match.groups()[0] + return re.sub(rf"{func}\(\s*([`\"]?name[`\"]?)\s*,", rf"{func}(cast(\1 as varchar),", **kwargs) + + return CAST_VARCHAR_PATTERN.sub(r"cast(\1 as varchar)", **kwargs) + + return column + + def check_parent_permission(parent, child_doctype): if parent: # User may pass fake parent and get the information from the child table @@ -981,11 +1032,11 @@ def is_parent_only_filter(doctype, filters): only_parent_doctype = True if isinstance(filters, list): - for flt in filters: - if doctype not in flt: + for filter in filters: + if doctype not in filter: only_parent_doctype = False - if "Between" in flt: - flt[3] = get_between_date_filter(flt[3]) + if "Between" in filter: + filter[3] = get_between_date_filter(flt[3]) return only_parent_doctype @@ -1019,12 +1070,12 @@ def get_between_date_filter(value, df=None): to_date = add_to_date(to_date, days=1) if df and df.fieldtype == "Datetime": - data = "'%s' AND '%s'" % ( + data = "'{}' AND '{}'".format( frappe.db.format_datetime(from_date), frappe.db.format_datetime(to_date), ) else: - data = "'%s' AND '%s'" % (frappe.db.format_date(from_date), frappe.db.format_date(to_date)) + data = f"'{frappe.db.format_date(from_date)}' AND '{frappe.db.format_date(to_date)}'" return data @@ -1040,7 +1091,7 @@ def get_additional_filter_field(additional_filters_config, f, value): return f -def get_date_range(operator, value): +def get_date_range(operator: str, value: str): timespan_map = { "1 week": "week", "1 month": "month", @@ -1053,7 +1104,10 @@ def get_date_range(operator, value): "next": "next", } - timespan = period_map[operator] + " " + timespan_map[value] if operator != "timespan" else value + if operator != "timespan": + timespan = f"{period_map[operator]} {timespan_map[value]}" + else: + timespan = value return get_timespan_date_range(timespan) diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index 733e8ca367..332a4337e2 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -11,6 +11,7 @@ from frappe import _, get_module_path from frappe.desk.doctype.tag.tag import delete_tags_for_document from frappe.model.dynamic_links import get_dynamic_link_map from frappe.model.naming import revert_series_if_last +from frappe.model.utils import is_virtual_doctype from frappe.utils.file_manager import remove_all from frappe.utils.global_search import delete_for_document from frappe.utils.password import delete_all_passwords_for @@ -29,6 +30,8 @@ doctypes_to_skip = ( "Tag Link", "Notification Log", "Email Queue", + "Document Share Key", + "Integration Request", ) @@ -55,11 +58,16 @@ def delete_doc( doctype = frappe.form_dict.get("dt") name = frappe.form_dict.get("dn") + is_virtual = is_virtual_doctype(doctype) + names = name if isinstance(name, str) or isinstance(name, int): names = [name] for name in names or []: + if is_virtual: + frappe.get_doc(doctype, name).delete() + continue # already deleted..? if not frappe.db.exists(doctype, name): @@ -87,12 +95,6 @@ def delete_doc( update_flags(doc, flags, ignore_permissions) check_permission_and_not_submitted(doc) - - frappe.db.delete("Custom Field", {"dt": name}) - frappe.db.delete("Client Script", {"dt": name}) - frappe.db.delete("Property Setter", {"doc_type": name}) - frappe.db.delete("Report", {"ref_doctype": name}) - frappe.db.delete("Custom DocPerm", {"parent": name}) frappe.db.delete("__global_search", {"doctype": name}) delete_from_table(doctype, name, ignore_doctypes, None) @@ -106,7 +108,7 @@ def delete_doc( ): try: delete_controllers(name, doc.module) - except (FileNotFoundError, OSError, KeyError): + except (OSError, KeyError): # in case a doctype doesnt have any controller code nor any app and module pass @@ -192,39 +194,27 @@ def update_naming_series(doc): revert_series_if_last(doc.meta.autoname, doc.name, doc) -def delete_from_table(doctype, name, ignore_doctypes, doc): +def delete_from_table(doctype: str, name: str, ignore_doctypes: list[str], doc): if doctype != "DocType" and doctype == name: frappe.db.delete("Singles", {"doctype": name}) else: frappe.db.delete(doctype, {"name": name}) - # get child tables if doc: - tables = [d.options for d in doc.meta.get_table_fields()] + child_doctypes = [ + d.options for d in doc.meta.get_table_fields() if frappe.get_meta(d.options).is_virtual == 0 + ] else: + child_doctypes = frappe.get_all( + "DocField", + fields="options", + filters={"fieldtype": ["in", frappe.model.table_fields], "parent": doctype}, + pluck="options", + ) - def get_table_fields(field_doctype): - if field_doctype == "Custom Field": - return [] - - return [ - r[0] - for r in frappe.get_all( - field_doctype, - fields="options", - filters={"fieldtype": ["in", frappe.model.table_fields], "parent": doctype}, - as_list=1, - ) - ] - - tables = get_table_fields("DocField") - if not frappe.flags.in_install == "frappe": - tables += get_table_fields("Custom Field") - - # delete from child tables - for t in list(set(tables)): - if t not in ignore_doctypes: - frappe.db.delete(t, {"parenttype": doctype, "parent": name}) + child_doctypes_to_delete = set(child_doctypes) - set(ignore_doctypes) + for child_doctype in child_doctypes_to_delete: + frappe.db.delete(child_doctype, {"parenttype": doctype, "parent": name}) def update_flags(doc, flags=None, ignore_permissions=False): @@ -354,7 +344,7 @@ def check_if_doc_is_dynamically_linked(doc, method="Delete"): reference_doctype = refdoc.parenttype if meta.istable else df.parent reference_docname = refdoc.parent if meta.istable else refdoc.name - at_position = "at Row: {0}".format(refdoc.idx) if meta.istable else "" + at_position = f"at Row: {refdoc.idx}" if meta.istable else "" raise_link_exists_exception(doc, reference_doctype, reference_docname, at_position) @@ -447,7 +437,7 @@ def insert_feed(doc): "doctype": "Comment", "comment_type": "Deleted", "reference_doctype": doc.doctype, - "subject": "{0} {1}".format(_(doc.doctype), doc.name), + "subject": f"{_(doc.doctype)} {doc.name}", "full_name": get_fullname(doc.owner), } ).insert(ignore_permissions=True) diff --git a/frappe/model/docfield.py b/frappe/model/docfield.py index 195385a2e1..c54a3855cb 100644 --- a/frappe/model/docfield.py +++ b/frappe/model/docfield.py @@ -45,7 +45,7 @@ def update_parent_field(f, new): if f["fieldtype"] in frappe.model.table_fields: frappe.db.begin() frappe.db.sql( - """update `tab%s` set parentfield=%s where parentfield=%s""" % (f["options"], "%s", "%s"), + """update `tab{}` set parentfield={} where parentfield={}""".format(f["options"], "%s", "%s"), (new, f["fieldname"]), ) frappe.db.commit() @@ -56,7 +56,7 @@ def get_change_column_query(f, new): desc = frappe.db.sql("desc `tab%s`" % f["parent"]) for d in desc: if d[0] == f["fieldname"]: - return "alter table `tab%s` change `%s` `%s` %s" % (f["parent"], f["fieldname"], new, d[1]) + return "alter table `tab{}` change `{}` `{}` {}".format(f["parent"], f["fieldname"], new, d[1]) def supports_translation(fieldtype): diff --git a/frappe/model/document.py b/frappe/model/document.py index 07ea58d8e9..c5b6607da6 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -3,7 +3,6 @@ import hashlib import json import time -from typing import List from werkzeug.exceptions import NotFound @@ -112,7 +111,8 @@ class Document(BaseDocument): if kwargs: # init base document - super(Document, self).__init__(kwargs) + super().__init__(kwargs) + self.init_child_tables() self.init_valid_columns() else: @@ -135,7 +135,7 @@ class Document(BaseDocument): single_doc["name"] = self.doctype del single_doc["__islocal"] - super(Document, self).__init__(single_doc) + super().__init__(single_doc) self.init_valid_columns() self._fix_numeric_types() @@ -148,33 +148,40 @@ class Document(BaseDocument): _("{0} {1} not found").format(_(self.doctype), self.name), frappe.DoesNotExistError ) - super(Document, self).__init__(d) + super().__init__(d) - if self.name == "DocType" and self.doctype == "DocType": - from frappe.model.meta import DOCTYPE_TABLE_FIELDS - - table_fields = DOCTYPE_TABLE_FIELDS - else: - table_fields = self.meta.get_table_fields() - - for df in table_fields: - children = frappe.db.get_values( - df.options, - {"parent": self.name, "parenttype": self.doctype, "parentfield": df.fieldname}, - "*", - as_dict=True, - order_by="idx asc", - ) - if children: - self.set(df.fieldname, children) - else: + for df in self._get_table_fields(): + # Make sure not to query the DB for a child table, if it is a virtual one. + # During frappe is installed, the property "is_virtual" is not available in tabDocType, so + # we need to filter those cases for the access to frappe.db.get_value() as it would crash otherwise. + if ( + hasattr(self, "doctype") + and not hasattr(self, "module") + and frappe.db.get_value("DocType", df.options, "is_virtual", cache=True) + ): self.set(df.fieldname, []) + continue + + children = ( + frappe.db.get_values( + df.options, + {"parent": self.name, "parenttype": self.doctype, "parentfield": df.fieldname}, + "*", + as_dict=True, + order_by="idx asc", + ) + or [] + ) + + self.set(df.fieldname, children) # sometimes __setup__ can depend on child values, hence calling again at the end if hasattr(self, "__setup__"): self.__setup__() - reload = load_from_db + def reload(self): + """Reload document from database""" + self.load_from_db() def get_latest(self): if not getattr(self, "latest", None): @@ -384,7 +391,10 @@ class Document(BaseDocument): d.db_update() rows.append(d.name) - if df.options in (self.flags.ignore_children_type or []): + if ( + df.options in (self.flags.ignore_children_type or []) + or frappe.get_meta(df.options).is_virtual == 1 + ): # do not delete rows for this because of flags # hack for docperm :( return @@ -392,9 +402,9 @@ class Document(BaseDocument): if rows: # select rows that do not match the ones in the document deleted_rows = frappe.db.sql( - """select name from `tab{0}` where parent=%s + """select name from `tab{}` where parent=%s and parenttype=%s and parentfield=%s - and name not in ({1})""".format( + and name not in ({})""".format( df.options, ",".join(["%s"] * len(rows)) ), [self.name, self.doctype, fieldname] + rows, @@ -409,7 +419,7 @@ class Document(BaseDocument): df.options, {"parent": self.name, "parenttype": self.doctype, "parentfield": fieldname} ) - def get_doc_before_save(self): + def get_doc_before_save(self) -> "Document": return getattr(self, "_doc_before_save", None) def has_value_changed(self, fieldname): @@ -443,7 +453,7 @@ class Document(BaseDocument): def get_title(self): """Get the document title based on title_field or `title` or `name`""" - return self.get(self.meta.get_title_field()) + return self.get(self.meta.get_title_field()) or "" def set_title_field(self): """Set title field based on template""" @@ -528,6 +538,7 @@ class Document(BaseDocument): d._validate_non_negative() d._validate_length() d._validate_code_fields() + d._sync_autoname_field() d._extract_images_from_text_editor() d._sanitize_content() d._save_passwords() @@ -746,7 +757,7 @@ class Document(BaseDocument): conflict = True else: tmp = frappe.db.sql( - """select modified, docstatus from `tab{0}` + """select modified, docstatus from `tab{}` where name = %s for update""".format( self.doctype ), @@ -769,7 +780,7 @@ class Document(BaseDocument): if conflict: frappe.msgprint( _("Error: Document has been modified after you have opened it") - + (" (%s, %s). " % (modified, self.modified)) + + (f" ({modified}, {self.modified}). ") + _("Please refresh to get the latest document."), raise_exception=frappe.TimestampMismatchError, ) @@ -864,7 +875,7 @@ class Document(BaseDocument): raise frappe.MandatoryError( "[{doctype}, {name}]: {fields}".format( - fields=", ".join((each[0] for each in missing)), doctype=self.doctype, name=self.name + fields=", ".join(each[0] for each in missing), doctype=self.doctype, name=self.name ) ) @@ -880,14 +891,14 @@ class Document(BaseDocument): cancelled_links.extend(result[1]) if invalid_links: - msg = ", ".join((each[2] for each in invalid_links)) + msg = ", ".join(each[2] for each in invalid_links) frappe.throw(_("Could not find {0}").format(msg), frappe.LinkValidationError) if cancelled_links: - msg = ", ".join((each[2] for each in cancelled_links)) + msg = ", ".join(each[2] for each in cancelled_links) frappe.throw(_("Cannot link cancelled document: {0}").format(msg), frappe.CancelledLinkError) - def get_all_children(self, parenttype=None) -> List["Document"]: + def get_all_children(self, parenttype=None) -> list["Document"]: """Returns all children documents from **Table** type fields in a list.""" children = [] @@ -896,8 +907,7 @@ class Document(BaseDocument): if parenttype and df.options != parenttype: continue - value = self.get(df.fieldname) - if isinstance(value, list): + if value := self.get(df.fieldname): children.extend(value) return children @@ -988,6 +998,16 @@ class Document(BaseDocument): self.docstatus = DocStatus.cancelled() return self.save() + @whitelist.__func__ + def _rename( + self, name: str, merge: bool = False, force: bool = False, validate_rename: bool = True + ): + """Rename the document. Triggers frappe.rename_doc, then reloads.""" + from frappe.model.rename_doc import rename_doc + + self.name = rename_doc(doc=self, new=name, merge=merge, force=force, validate=validate_rename) + self.reload() + @whitelist.__func__ def submit(self): """Submit the document. Sets `docstatus` = 1, then saves.""" @@ -998,10 +1018,21 @@ class Document(BaseDocument): """Cancel the document. Sets `docstatus` = 2, then saves.""" return self._cancel() - def delete(self, ignore_permissions=False): + @whitelist.__func__ + def rename( + self, name: str, merge: bool = False, force: bool = False, validate_rename: bool = True + ): + """Rename the document to `name`. This transforms the current object.""" + return self._rename(name=name, merge=merge, force=force, validate_rename=validate_rename) + + def delete(self, ignore_permissions=False, force=False): """Delete document.""" - frappe.delete_doc( - self.doctype, self.name, ignore_permissions=ignore_permissions, flags=self.flags + return frappe.delete_doc( + self.doctype, + self.name, + ignore_permissions=ignore_permissions, + flags=self.flags, + force=force, ) def run_before_save_methods(self): @@ -1067,7 +1098,9 @@ class Document(BaseDocument): self.run_method("on_update_after_submit") self.clear_cache() - self.notify_update() + + if self.flags.get("notify_update", True): + self.notify_update() update_global_search(self) @@ -1120,7 +1153,7 @@ class Document(BaseDocument): :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 notify: default False. run doc.notify_update() to send updates via socketio :param commit: default False. run frappe.db.commit() """ if isinstance(fieldname, dict): @@ -1186,11 +1219,10 @@ class Document(BaseDocument): return version = frappe.new_doc("Version") - if not self._doc_before_save: - version.for_insert(self) - version.insert(ignore_permissions=True) - elif version.set_diff(self._doc_before_save, self): + + if is_useful_diff := version.update_version_info(self._doc_before_save, self): version.insert(ignore_permissions=True) + if not frappe.flags.in_migrate: # follow since you made a change? if frappe.get_cached_value("User", frappe.session.user, "follow_created_documents"): @@ -1243,7 +1275,7 @@ class Document(BaseDocument): def is_whitelisted(self, method_name): method = getattr(self, method_name, None) if not method: - raise NotFound("Method {0} not found".format(method_name)) + raise NotFound(f"Method {method_name} not found") is_whitelisted(getattr(method, "__func__", method)) @@ -1361,10 +1393,40 @@ class Document(BaseDocument): ).insert(ignore_permissions=True) frappe.local.flags.commit = True + def log_error(self, title=None, message=None): + """Helper function to create an Error Log""" + return frappe.log_error( + message=message, title=title, reference_doctype=self.doctype, reference_name=self.name + ) + def get_signature(self): """Returns signature (hash) for private URL.""" return hashlib.sha224(get_datetime_str(self.creation).encode()).hexdigest() + def get_document_share_key(self, expires_on=None, no_expiry=False): + if no_expiry: + expires_on = None + + existing_key = frappe.db.exists( + "Document Share Key", + { + "reference_doctype": self.doctype, + "reference_docname": self.name, + "expires_on": expires_on, + }, + ) + if existing_key: + doc = frappe.get_doc("Document Share Key", existing_key) + else: + doc = frappe.new_doc("Document Share Key") + doc.reference_doctype = self.doctype + doc.reference_docname = self.name + doc.expires_on = expires_on + doc.flags.no_expiry = no_expiry + doc.insert(ignore_permissions=True) + + return doc.key + def get_liked_by(self): liked_by = getattr(self, "_liked_by", None) if liked_by: @@ -1391,21 +1453,22 @@ class Document(BaseDocument): # See: Stock Reconciliation from frappe.utils.background_jobs import enqueue - if hasattr(self, "_" + action): - action = "_" + action + if hasattr(self, f"_{action}"): + action = f"_{action}" - if file_lock.lock_exists(self.get_signature()): + try: + self.lock() + except frappe.DocumentLockedError: frappe.throw( _("This document is currently queued for execution. Please try again"), title=_("Document Queued"), ) - self.lock() - enqueue( + return enqueue( "frappe.model.document.execute_action", - doctype=self.doctype, - name=self.name, - action=action, + __doctype=self.doctype, + __name=self.name, + __action=action, **kwargs, ) @@ -1426,10 +1489,13 @@ class Document(BaseDocument): if lock_exists: raise frappe.DocumentLockedError file_lock.create_lock(signature) + frappe.local.locked_documents.append(self) def unlock(self): """Delete the lock file for this document""" file_lock.delete_lock(self.get_signature()) + if self in frappe.local.locked_documents: + frappe.local.locked_documents.remove(self) # validation helpers def validate_from_to_dates(self, from_date_field, to_date_field): @@ -1488,12 +1554,12 @@ class Document(BaseDocument): return f"{doctype}({name})" -def execute_action(doctype, name, action, **kwargs): +def execute_action(__doctype, __name, __action, **kwargs): """Execute an action on a document (called by background worker)""" - doc = frappe.get_doc(doctype, name) + doc = frappe.get_doc(__doctype, __name) doc.unlock() try: - getattr(doc, action)(**kwargs) + getattr(doc, __action)(**kwargs) except Exception: frappe.db.rollback() @@ -1504,4 +1570,4 @@ def execute_action(doctype, name, action, **kwargs): msg = "" + frappe.get_traceback() + ""
doc.add_comment("Comment", _("Action Failed") + "" + msg) - doc.notify_update() + doc.notify_update() diff --git a/frappe/model/mapper.py b/frappe/model/mapper.py index 6a6522ad07..9df79ef276 100644 --- a/frappe/model/mapper.py +++ b/frappe/model/mapper.py @@ -231,7 +231,7 @@ def map_fetch_fields(target_doc, df, no_copy_fields): linked_doc = None # options should be like "link_fieldname.fieldname_in_liked_doc" - for fetch_df in target_doc.meta.get("fields", {"fetch_from": "^{0}.".format(df.fieldname)}): + for fetch_df in target_doc.meta.get("fields", {"fetch_from": f"^{df.fieldname}."}): if not (fetch_df.fieldtype == "Read Only" or fetch_df.read_only): continue @@ -243,7 +243,7 @@ def map_fetch_fields(target_doc, df, no_copy_fields): if not linked_doc: try: linked_doc = frappe.get_doc(df.options, target_doc.get(df.fieldname)) - except: + except Exception: return val = linked_doc.get(source_fieldname) diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 407f7d0811..014dd5faf1 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -30,14 +30,18 @@ from frappe.model import ( optional_fields, table_fields, ) -from frappe.model.base_document import BaseDocument +from frappe.model.base_document import ( + DOCTYPE_TABLE_FIELDS, + TABLE_DOCTYPES_FOR_DOCTYPE, + BaseDocument, +) from frappe.model.document import Document from frappe.model.workflow import get_workflow_name from frappe.modules import load_doctype_module from frappe.utils import cast, cint, cstr -def get_meta(doctype, cached=True): +def get_meta(doctype, cached=True) -> "Meta": if cached: if not frappe.local.meta_cache.get(doctype): meta = frappe.cache().hget("meta", doctype) @@ -63,7 +67,7 @@ def get_table_columns(doctype): def load_doctype_from_file(doctype): fname = frappe.scrub(doctype) - with open(frappe.get_app_path("frappe", "core", "doctype", fname, fname + ".json"), "r") as f: + with open(frappe.get_app_path("frappe", "core", "doctype", fname, fname + ".json")) as f: txt = json.loads(f.read()) for d in txt.get("fields", []): @@ -99,19 +103,19 @@ class Meta(Document): def __init__(self, doctype): self._fields = {} if isinstance(doctype, dict): - super(Meta, self).__init__(doctype) + super().__init__(doctype) elif isinstance(doctype, Document): - super(Meta, self).__init__(doctype.as_dict()) + super().__init__(doctype.as_dict()) self.process() else: - super(Meta, self).__init__("DocType", doctype) + super().__init__("DocType", doctype) self.process() def load_from_db(self): try: - super(Meta, self).load_from_db() + super().load_from_db() except frappe.DoesNotExistError: if self.doctype == "DocType" and self.name in self.special_doctypes: self.__dict__.update(load_doctype_from_file(self.name)) @@ -148,9 +152,9 @@ class Meta(Document): out[key] = value # set empty lists for unset table fields - for table_field in DOCTYPE_TABLE_FIELDS: - if out.get(table_field.fieldname) is None: - out[table_field.fieldname] = [] + for fieldname in TABLE_DOCTYPES_FOR_DOCTYPE.keys(): + if out.get(fieldname) is None: + out[fieldname] = [] return out @@ -162,6 +166,9 @@ class Meta(Document): def get_data_fields(self): return self.get("fields", {"fieldtype": "Data"}) + def get_phone_fields(self): + return self.get("fields", {"fieldtype": "Phone"}) + def get_dynamic_link_fields(self): if not hasattr(self, "_dynamic_link_fields"): self._dynamic_link_fields = self.get("fields", {"fieldtype": "Dynamic Link"}) @@ -222,13 +229,7 @@ class Meta(Document): return self._valid_columns def get_table_field_doctype(self, fieldname): - return { - "fields": "DocField", - "permissions": "DocPerm", - "actions": "DocType Action", - "links": "DocType Link", - "states": "DocType State", - }.get(fieldname) + return TABLE_DOCTYPES_FOR_DOCTYPE.get(fieldname) def get_field(self, fieldname): """Return docfield from meta""" @@ -250,10 +251,15 @@ class Meta(Document): else: label = { "name": _("ID"), - "owner": _("Created By"), - "modified_by": _("Modified By"), "creation": _("Created On"), - "modified": _("Last Modified On"), + "docstatus": _("Document Status"), + "idx": _("Index"), + "modified": _("Last Updated On"), + "modified_by": _("Last Updated By"), + "owner": _("Created By"), + "_user_tags": _("Tags"), + "_liked_by": _("Liked By"), + "_comments": _("Comments"), "_assign": _("Assigned To"), }.get(fieldname) or _("No Label") return label @@ -340,6 +346,16 @@ class Meta(Document): def get_workflow(self): return get_workflow_name(self.name) + def get_naming_series_options(self) -> list[str]: + """Get list naming series options.""" + + field = self.get_field("naming_series") + if field: + options = field.options or "" + + return options.split("\n") + return [] + def add_custom_fields(self): if not frappe.db.table_exists("Custom Field"): return @@ -417,7 +433,7 @@ class Meta(Document): # set the fields in order if specified # order is saved as `links_order` - order = json.loads(self.get("{}_order".format(fieldname)) or "[]") + order = json.loads(self.get(f"{fieldname}_order") or "[]") if order: name_map = {d.name: d for d in self.get(fieldname)} new_list = [] @@ -639,14 +655,6 @@ class Meta(Document): return self.has_field("lft") and self.has_field("rgt") -DOCTYPE_TABLE_FIELDS = [ - frappe._dict({"fieldname": "fields", "options": "DocField"}), - frappe._dict({"fieldname": "permissions", "options": "DocPerm"}), - frappe._dict({"fieldname": "actions", "options": "DocType Action"}), - frappe._dict({"fieldname": "links", "options": "DocType Link"}), - frappe._dict({"fieldname": "states", "options": "DocType State"}), -] - ####### @@ -781,7 +789,10 @@ def trim_table(doctype, dry_run=True): ignore_fields = default_fields + optional_fields + child_table_fields columns = frappe.db.get_table_columns(doctype) fields = frappe.get_meta(doctype, cached=False).get_fieldnames_with_value() - is_internal = lambda f: f not in ignore_fields and not f.startswith("_") + + def is_internal(field): + return field not in ignore_fields and not field.startswith("_") + columns_to_remove = [f for f in list(set(columns) - set(fields)) if is_internal(f)] DROPPED_COLUMNS = columns_to_remove[:] diff --git a/frappe/model/naming.py b/frappe/model/naming.py index 9d1079d995..49a58da314 100644 --- a/frappe/model/naming.py +++ b/frappe/model/naming.py @@ -1,20 +1,130 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE +import datetime import re -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING, Callable, Optional import frappe from frappe import _ -from frappe.database.sequence import get_next_val, set_next_val from frappe.model import log_types from frappe.query_builder import DocType from frappe.utils import cint, cstr, now_datetime if TYPE_CHECKING: + from frappe.model.document import Document from frappe.model.meta import Meta +# NOTE: This is used to keep track of status of sites +# whether `log_types` have autoincremented naming set for the site or not. +autoincremented_site_status_map = {} + +NAMING_SERIES_PATTERN = re.compile(r"^[\w\- \/.#{}]+$", re.UNICODE) +BRACED_PARAMS_PATTERN = re.compile(r"(\{[\w | #]+\})") + + +# Types that can be using in naming series fields +NAMING_SERIES_PART_TYPES = ( + int, + str, + datetime.datetime, + datetime.date, + datetime.time, + datetime.timedelta, +) + + +class InvalidNamingSeriesError(frappe.ValidationError): + pass + + +class NamingSeries: + __slots__ = ("series",) + + def __init__(self, series: str): + self.series = series + + # Add default number part if missing + if "#" not in self.series: + self.series += ".#####" + + def validate(self): + if "." not in self.series: + frappe.throw( + _("Invalid naming series {}: dot (.) missing").format(frappe.bold(self.series)), + exc=InvalidNamingSeriesError, + ) + + if not NAMING_SERIES_PATTERN.match(self.series): + frappe.throw( + _( + 'Special Characters except "-", "#", ".", "/", "{" and "}" not allowed in naming series', + ), + exc=InvalidNamingSeriesError, + ) + + def generate_next_name(self, doc: "Document") -> str: + self.validate() + parts = self.series.split(".") + return parse_naming_series(parts, doc=doc) + + def get_prefix(self) -> str: + """Naming series stores prefix to maintain a counter in DB. This prefix can be used to update counter or validations. + + e.g. `SINV-.YY.-.####` has prefix of `SINV-22-` in database for year 2022. + """ + + prefix = None + + def fake_counter_backend(partial_series, digits): + nonlocal prefix + prefix = partial_series + return "#" * digits + + # This function evaluates all parts till we hit numerical parts and then + # sends prefix + digits to DB to find next number. + # Instead of reimplementing the whole parsing logic in multiple places we + # can just ask this function to give us the prefix. + parse_naming_series(self.series, number_generator=fake_counter_backend) + + if prefix is None: + frappe.throw(_("Invalid Naming Series: {}").format(self.series)) + + return prefix + + def get_preview(self, doc=None) -> list[str]: + """Generate preview of naming series without using DB counters""" + generated_names = [] + for count in range(1, 4): + + def fake_counter(_prefix, digits): + # ignore B023: binding `count` is not necessary because + # function is evaluated immediately and it can not be done + # because of function signature requirement + return str(count).zfill(digits) # noqa: B023 + + generated_names.append(parse_naming_series(self.series, doc=doc, number_generator=fake_counter)) + return generated_names + + def update_counter(self, new_count: int) -> None: + """Warning: Incorrectly updating series can result in unusable transactions""" + Series = frappe.qb.DocType("Series") + prefix = self.get_prefix() + + # Initialize if not present in DB + if frappe.db.get_value("Series", prefix, "name", order_by="name") is None: + frappe.qb.into(Series).insert(prefix, 0).columns("name", "current").run() + + ( + frappe.qb.update(Series).set(Series.current, cint(new_count)).where(Series.name == prefix) + ).run() + + def get_current_value(self) -> int: + prefix = self.get_prefix() + return cint(frappe.db.get_value("Series", prefix, "current", order_by="name")) + + def set_new_name(doc): """ Sets the `name` property for the document based on various rules. @@ -36,7 +146,7 @@ def set_new_name(doc): doc.name = None if is_autoincremented(doc.doctype, meta): - doc.name = get_next_val(doc.doctype) + doc.name = frappe.db.get_next_sequence_val(doc.doctype) return if getattr(doc, "amended_from", None): @@ -71,12 +181,11 @@ def set_new_name(doc): doc.name = validate_name(doc.doctype, doc.name, meta.get_field("name_case")) -def is_autoincremented(doctype: str, meta: "Meta" = None): +def is_autoincremented(doctype: str, meta: Optional["Meta"] = None) -> bool: + """Checks if the doctype has autoincrement autoname set""" + if doctype in log_types: - if ( - frappe.local.autoincremented_status_map.get(frappe.local.site) is None - or frappe.local.autoincremented_status_map[frappe.local.site] == -1 - ): + if autoincremented_site_status_map.get(frappe.local.site) is None: if ( frappe.db.sql( f"""select data_type FROM information_schema.columns @@ -84,22 +193,19 @@ def is_autoincremented(doctype: str, meta: "Meta" = None): )[0][0] == "bigint" ): - frappe.local.autoincremented_status_map[frappe.local.site] = 1 + autoincremented_site_status_map[frappe.local.site] = 1 return True else: - frappe.local.autoincremented_status_map[frappe.local.site] = 0 + autoincremented_site_status_map[frappe.local.site] = 0 - elif frappe.local.autoincremented_status_map[frappe.local.site]: + elif autoincremented_site_status_map[frappe.local.site]: return True else: if not meta: meta = frappe.get_meta(doctype) - if getattr(meta, "issingle", False): - return False - - if meta.autoname == "autoincrement": + if not getattr(meta, "issingle", False) and meta.autoname == "autoincrement": return True return False @@ -175,32 +281,43 @@ def make_autoname(key="", doctype="", doc=""): if key == "hash": return frappe.generate_hash(doctype, 10) - if "#" not in key: - key = key + ".#####" - elif "." not in key: - error_message = _("Invalid naming series (. missing)") - if doctype: - error_message = _("Invalid naming series (. missing) for {0}").format(doctype) - - frappe.throw(error_message) - - parts = key.split(".") - n = parse_naming_series(parts, doctype, doc) - return n + series = NamingSeries(key) + return series.generate_next_name(doc) -def parse_naming_series(parts, doctype="", doc=""): - n = "" +def parse_naming_series( + parts: list[str] | str, + doctype=None, + doc: Optional["Document"] = None, + number_generator: Callable[[str, int], str] | None = None, +) -> str: + + """Parse the naming series and get next name. + + args: + parts: naming series parts (split by `.`) + doc: document to use for series that have parts using fieldnames + number_generator: Use different counter backend other than `tabSeries`. Primarily used for testing. + """ + + name = "" if isinstance(parts, str): parts = parts.split(".") + + if not number_generator: + number_generator = getseries + series_set = False today = now_datetime() for e in parts: + if not e: + continue + part = "" if e.startswith("#"): if not series_set: digits = len(e) - part = getseries(n, digits) + part = number_generator(name, digits) series_set = True elif e == "YY": part = today.strftime("%y") @@ -225,9 +342,11 @@ def parse_naming_series(parts, doctype="", doc=""): part = e if isinstance(part, str): - n += part + name += part + elif isinstance(part, NAMING_SERIES_PART_TYPES): + name += cstr(part).strip() - return n + return name def determine_consecutive_week_number(datetime): @@ -311,25 +430,27 @@ def revert_series_if_last(key, name, doc=None): frappe.db.sql("UPDATE `tabSeries` SET `current` = `current` - 1 WHERE `name`=%s", prefix) -def get_default_naming_series(doctype): +def get_default_naming_series(doctype: str) -> str | None: """get default value for `naming_series` property""" - naming_series = frappe.get_meta(doctype).get_field("naming_series").options or "" - if naming_series: - naming_series = naming_series.split("\n") - return naming_series[0] or naming_series[1] - else: - return None + naming_series_options = frappe.get_meta(doctype).get_naming_series_options() + + # Return first truthy options + # Empty strings are used to avoid populating forms by default + for option in naming_series_options: + if option: + return option -def validate_name(doctype: str, name: Union[int, str], case: Optional[str] = None): +def validate_name(doctype: str, name: int | str, case: str | None = None): + if not name: frappe.throw(_("No Name Specified for {0}").format(doctype)) if isinstance(name, int): if is_autoincremented(doctype): - # this will set the sequence val to be the provided name and set it to be used - # so that the sequence will start from the next val of the setted val(name) - set_next_val(doctype, name, is_val_used=True) + # this will set the sequence value to be the provided name/value and set it to be used + # so that the sequence will start from the next value + frappe.db.set_next_sequence_val(doctype, name, is_val_used=True) return name frappe.throw(_("Invalid name type (integer) for varchar name column"), frappe.NameError) @@ -348,8 +469,8 @@ def validate_name(doctype: str, name: Union[int, str], case: Optional[str] = Non frappe.throw(_("Name of {0} cannot be {1}").format(doctype, name), frappe.NameError) special_characters = "<>" - if re.findall("[{0}]+".format(special_characters), name): - message = ", ".join("'{0}'".format(c) for c in special_characters) + if re.findall(f"[{special_characters}]+", name): + message = ", ".join(f"'{c}'" for c in special_characters) frappe.throw( _("Name cannot contain special characters like {0}").format(message), frappe.NameError ) @@ -363,7 +484,7 @@ def append_number_if_name_exists(doctype, value, fieldname="name", separator="-" filters.update({fieldname: value}) exists = frappe.db.exists(doctype, filters) - regex = "^{value}{separator}\\d+$".format(value=re.escape(value), separator=separator) + regex = f"^{re.escape(value)}{separator}\\d+$" if exists: last = frappe.db.sql( @@ -381,7 +502,7 @@ def append_number_if_name_exists(doctype, value, fieldname="name", separator="-" else: count = "1" - value = "{0}{1}{2}".format(value, separator, count) + value = f"{value}{separator}{count}" return value @@ -435,6 +556,6 @@ def _format_autoname(autoname, doc): return parse_naming_series([trimmed_param], doc=doc) # Replace braced params with their parsed value - name = re.sub(r"(\{[\w | #]+\})", get_param_value_for_match, autoname_value) + name = BRACED_PARAMS_PATTERN.sub(get_param_value_for_match, autoname_value) return name diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index dee364ae8d..2a04ee7e11 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -1,15 +1,18 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE -from typing import TYPE_CHECKING, Dict, List, Optional +from types import NoneType +from typing import TYPE_CHECKING import frappe from frappe import _, bold +from frappe.model.document import Document from frappe.model.dynamic_links import get_dynamic_link_map from frappe.model.naming import validate_name from frappe.model.utils.user_settings import sync_user_settings, update_user_settings_data from frappe.query_builder import Field -from frappe.utils import cint +from frappe.utils.data import sbool from frappe.utils.password import rename_password +from frappe.utils.scheduler import is_scheduler_inactive if TYPE_CHECKING: from frappe.model.meta import Meta @@ -20,13 +23,22 @@ def update_document_title( *, doctype: str, docname: str, - title: Optional[str] = None, - name: Optional[str] = None, + title: str | None = None, + name: str | None = None, merge: bool = False, + enqueue: bool = False, **kwargs, ) -> str: """ - Update title from header in form view + Update the name or title of a document. Returns `name` if document was renamed, + `docname` if renaming operation was queued. + + :param doctype: DocType of the document + :param docname: Name of the document + :param title: New Title of the document + :param name: New Name of the document + :param merge: Merge the current Document with the existing one if exists + :param enqueue: Enqueue the rename operation, title is updated in current process """ # to maintain backwards API compatibility @@ -35,9 +47,13 @@ def update_document_title( # TODO: omit this after runtime type checking (ref: https://github.com/frappe/frappe/pull/14927) for obj in [docname, updated_title, updated_name]: - if not isinstance(obj, (str, type(None))): + if not isinstance(obj, (str, NoneType)): frappe.throw(f"{obj=} must be of type str or None") + # handle bad API usages + merge = sbool(merge) + enqueue = sbool(enqueue) + doc = frappe.get_doc(doctype, docname) doc.check_permission(permtype="write") @@ -49,11 +65,34 @@ def update_document_title( name_updated = updated_name and (updated_name != doc.name) if name_updated: - docname = rename_doc(doctype=doctype, old=docname, new=updated_name, merge=merge) + if enqueue and not is_scheduler_inactive(): + current_name = doc.name + + # before_name hook may have DocType specific validations or transformations + transformed_name = doc.run_method("before_rename", current_name, updated_name, merge) + if isinstance(transformed_name, dict): + transformed_name = transformed_name.get("new") + transformed_name = transformed_name or updated_name + + # run rename validations before queueing + # use savepoints to avoid partial renames / commits + validate_rename( + doctype=doctype, + old=current_name, + new=transformed_name, + meta=doc.meta, + merge=merge, + save_point=True, + ) + + doc.queue_action("rename", name=transformed_name, merge=merge) + else: + doc.rename(updated_name, merge=merge) if title_updated: try: - frappe.db.set_value(doctype, docname, title_field, updated_title) + setattr(doc, title_field, updated_title) + doc.save() frappe.msgprint(_("Saved"), alert=True, indicator="green") except Exception as e: if frappe.db.is_duplicate_entry(e): @@ -64,44 +103,64 @@ def update_document_title( ) raise - return docname + return doc.name def rename_doc( - doctype: str, - old: str, - new: str, + doctype: str | None = None, + old: str | None = None, + new: str = None, force: bool = False, merge: bool = False, ignore_permissions: bool = False, ignore_if_exists: bool = False, show_alert: bool = True, rebuild_search: bool = True, + doc: Document | None = None, + validate: bool = True, ) -> str: - """Rename a doc(dt, old) to doc(dt, new) and update all linked fields of type "Link".""" - if not frappe.db.exists(doctype, old): - frappe.errprint(_("Failed: {0} to {1} because {0} doesn't exist.").format(old, new)) - return + """Rename a doc(dt, old) to doc(dt, new) and update all linked fields of type "Link". - if ignore_if_exists and frappe.db.exists(doctype, new): - frappe.errprint(_("Failed: {0} to {1} because {1} already exists.").format(old, new)) - return + doc: Document object to be renamed. + new: New name for the record. If None, and doctype is specified, new name may be automatically generated via before_rename hooks. + doctype: DocType of the document. Not required if doc is passed. + old: Current name of the document. Not required if doc is passed. + force: Allow even if document is not allowed to be renamed. + merge: Merge with existing document of new name. + ignore_permissions: Ignore user permissions while renaming. + ignore_if_exists: Don't raise exception if document with new name already exists. This will quietely overwrite the existing document. + show_alert: Display alert if document is renamed successfully. + rebuild_search: Rebuild linked doctype search after renaming. + validate: Validate before renaming. If False, it is assumed that the caller has already validated. + """ + old_usage_style = doctype and old and new + new_usage_style = doc and new - if old == new: - frappe.errprint( - _("Ignored: {0} to {1} no changes made because old and new name are the same.").format(old, new) + if not (new_usage_style or old_usage_style): + raise TypeError( + "{doctype, old, new} or {doc, new} are required arguments for frappe.model.rename_doc" ) - return - force = cint(force) - merge = cint(merge) + old = old or doc.name + doctype = doctype or doc.doctype + force = sbool(force) + merge = sbool(merge) meta = frappe.get_meta(doctype) - # call before_rename - old_doc = frappe.get_doc(doctype, old) - out = old_doc.run_method("before_rename", old, new, merge) or {} - new = (out.get("new") or new) if isinstance(out, dict) else (out or new) - new = validate_rename(doctype, new, meta, merge, force, ignore_permissions) + if validate: + old_doc = doc or frappe.get_doc(doctype, old) + out = old_doc.run_method("before_rename", old, new, merge) or {} + new = (out.get("new") or new) if isinstance(out, dict) else (out or new) + new = validate_rename( + doctype=doctype, + old=old, + new=new, + meta=meta, + merge=merge, + force=force, + ignore_permissions=ignore_permissions, + ignore_if_exists=ignore_if_exists, + ) if not merge: rename_parent_and_child(doctype, old, new, meta) @@ -139,11 +198,12 @@ def rename_doc( rename_password(doctype, old, new) # update user_permissions - frappe.db.sql( - """UPDATE `tabDefaultValue` SET `defvalue`=%s WHERE `parenttype`='User Permission' - AND `defkey`=%s AND `defvalue`=%s""", - (new, doctype, old), - ) + DefaultValue = frappe.qb.DocType("DefaultValue") + frappe.qb.update(DefaultValue).set(DefaultValue.defvalue, new).where( + (DefaultValue.parenttype == "User Permission") + & (DefaultValue.defkey == doctype) + & (DefaultValue.defvalue == old) + ).run() if merge: new_doc.add_comment("Edit", _("merged {0} into {1}").format(frappe.bold(old), frappe.bold(new))) @@ -194,7 +254,7 @@ def update_assignments(old: str, new: str, doctype: str) -> None: frappe.db.set_value(doctype, new, "_assign", frappe.as_json(unique_assignments, indent=0)) -def update_user_settings(old: str, new: str, link_fields: List[Dict]) -> None: +def update_user_settings(old: str, new: str, link_fields: list[dict]) -> None: """ Update the user settings of all the linked doctypes while renaming. """ @@ -207,15 +267,13 @@ def update_user_settings(old: str, new: str, link_fields: List[Dict]) -> None: # find the user settings for the linked doctypes linked_doctypes = {d.parent for d in link_fields if not d.issingle} - user_settings_details = frappe.db.sql( - """SELECT `user`, `doctype`, `data` - FROM `__UserSettings` - WHERE `data` like %s - AND `doctype` IN ('{doctypes}')""".format( - doctypes="', '".join(linked_doctypes) - ), - (old), - as_dict=1, + UserSettings = frappe.qb.Table("__UserSettings") + + user_settings_details = ( + frappe.qb.from_(UserSettings) + .select("user", "doctype", "data") + .where(UserSettings.data.like(old) & UserSettings.doctype.isin(linked_doctypes)) + .run(as_dict=True) ) # create the dict using the doctype name as key and values as list of the user settings @@ -240,37 +298,33 @@ def update_customizations(old: str, new: str) -> None: def update_attachments(doctype: str, old: str, new: str) -> None: - try: - if old != "File Data" and doctype != "DocType": - frappe.db.sql( - """update `tabFile` set attached_to_name=%s - where attached_to_name=%s and attached_to_doctype=%s""", - (new, old, doctype), - ) - except frappe.db.ProgrammingError as e: - if not frappe.db.is_column_missing(e): - raise + if doctype != "DocType": + File = frappe.qb.DocType("File") + + frappe.qb.update(File).set(File.attached_to_name, new).where( + (File.attached_to_name == old) & (File.attached_to_doctype == doctype) + ).run() def rename_versions(doctype: str, old: str, new: str) -> None: - frappe.db.sql( - """UPDATE `tabVersion` SET `docname`=%s WHERE `ref_doctype`=%s AND `docname`=%s""", - (new, doctype, old), - ) + Version = frappe.qb.DocType("Version") + + frappe.qb.update(Version).set(Version.docname, new).where( + (Version.docname == old) & (Version.ref_doctype == doctype) + ).run() def rename_eps_records(doctype: str, old: str, new: str) -> None: - epl = frappe.qb.DocType("Energy Point Log") - ( - frappe.qb.update(epl) - .set(epl.reference_name, new) - .where((epl.reference_doctype == doctype) & (epl.reference_name == old)) + EPL = frappe.qb.DocType("Energy Point Log") + + frappe.qb.update(EPL).set(EPL.reference_name, new).where( + (EPL.reference_doctype == doctype) & (EPL.reference_name == old) ).run() def rename_parent_and_child(doctype: str, old: str, new: str, meta: "Meta") -> None: - # rename the doc - frappe.db.sql("UPDATE `tab{0}` SET `name`={1} WHERE `name`={1}".format(doctype, "%s"), (new, old)) + frappe.qb.update(doctype).set("name", new).where(Field("name") == old).run() + update_autoname_field(doctype, new, meta) update_child_docs(old, new, meta) @@ -280,20 +334,36 @@ def update_autoname_field(doctype: str, new: str, meta: "Meta") -> None: if meta.get("autoname"): field = meta.get("autoname").split(":") if field and field[0] == "field": - frappe.db.sql( - "UPDATE `tab{0}` SET `{1}`={2} WHERE `name`={2}".format(doctype, field[1], "%s"), (new, new) - ) + frappe.qb.update(doctype).set(field[1], new).where(Field("name") == new).run() def validate_rename( - doctype: str, new: str, meta: "Meta", merge: bool, force: bool, ignore_permissions: bool + doctype: str, + old: str, + new: str, + meta: "Meta", + merge: bool, + force: bool = False, + ignore_permissions: bool = False, + ignore_if_exists: bool = False, + save_point=False, ) -> str: # using for update so that it gets locked and someone else cannot edit it while this rename is going on! + if save_point: + _SAVE_POINT = f"validate_rename_{frappe.generate_hash(length=8)}" + frappe.db.savepoint(_SAVE_POINT) + exists = ( frappe.qb.from_(doctype).where(Field("name") == new).for_update().select("name").run(pluck=True) ) exists = exists[0] if exists else None + if not frappe.db.exists(doctype, old): + frappe.throw(_("Can't rename {0} to {1} because {0} doesn't exist.").format(old, new)) + + if old == new: + frappe.throw(_("No changes made because old and new name are the same.").format(old, new)) + if merge and not exists: frappe.throw(_("{0} {1} does not exist, select a new target to merge").format(doctype, new)) @@ -301,7 +371,7 @@ def validate_rename( # for fixing case, accents exists = None - if (not merge) and exists: + if not merge and exists and not ignore_if_exists: frappe.throw(_("Another {0} with name {1} exists, select another name").format(doctype, new)) if not ( @@ -315,6 +385,9 @@ def validate_rename( # validate naming like it's done in doc.py new = validate_name(doctype, new) + if save_point: + frappe.db.rollback(save_point=_SAVE_POINT) + return new @@ -337,12 +410,10 @@ def rename_doctype(doctype: str, old: str, new: str) -> None: def update_child_docs(old: str, new: str, meta: "Meta") -> None: # update "parent" for df in meta.get_table_fields(): - frappe.db.sql( - "update `tab%s` set parent=%s where parent=%s" % (df.options, "%s", "%s"), (new, old) - ) + frappe.qb.update(df.options).set("parent", new).where(Field("parent") == old).run() -def update_link_field_values(link_fields: List[Dict], old: str, new: str, doctype: str) -> None: +def update_link_field_values(link_fields: list[dict], old: str, new: str, doctype: str) -> None: for field in link_fields: if field["issingle"]: try: @@ -378,63 +449,52 @@ def update_link_field_values(link_fields: List[Dict], old: str, new: str, doctyp field["parent"] = new -def get_link_fields(doctype: str) -> List[Dict]: +def get_link_fields(doctype: str) -> list[dict]: # get link fields from tabDocField if not frappe.flags.link_fields: frappe.flags.link_fields = {} if doctype not in frappe.flags.link_fields: - link_fields = frappe.db.sql( - """\ - select parent, fieldname, - (select issingle from tabDocType dt - where dt.name = df.parent) as issingle - from tabDocField df - where - df.options=%s and df.fieldtype='Link'""", - (doctype,), - as_dict=1, + dt = frappe.qb.DocType("DocType") + df = frappe.qb.DocType("DocField") + cf = frappe.qb.DocType("Custom Field") + ps = frappe.qb.DocType("Property Setter") + + st_issingle = frappe.qb.from_(dt).select(dt.issingle).where(dt.name == df.parent).as_("issingle") + standard_fields = ( + frappe.qb.from_(df) + .select(df.parent, df.fieldname, st_issingle) + .where((df.options == doctype) & (df.fieldtype == "Link")) + .run(as_dict=True) ) - # get link fields from tabCustom Field - custom_link_fields = frappe.db.sql( - """\ - select dt as parent, fieldname, - (select issingle from tabDocType dt - where dt.name = df.dt) as issingle - from `tabCustom Field` df - where - df.options=%s and df.fieldtype='Link'""", - (doctype,), - as_dict=1, + cf_issingle = frappe.qb.from_(dt).select(dt.issingle).where(dt.name == cf.dt).as_("issingle") + custom_fields = ( + frappe.qb.from_(cf) + .select(cf.dt.as_("parent"), cf.fieldname, cf_issingle) + .where((cf.options == doctype) & (cf.fieldtype == "Link")) + .run(as_dict=True) ) - # add custom link fields list to link fields list - link_fields += custom_link_fields - - # remove fields whose options have been changed using property setter - property_setter_link_fields = frappe.db.sql( - """\ - select ps.doc_type as parent, ps.field_name as fieldname, - (select issingle from tabDocType dt - where dt.name = ps.doc_type) as issingle - from `tabProperty Setter` ps - where - ps.property_type='options' and - ps.field_name is not null and - ps.value=%s""", - (doctype,), - as_dict=1, + ps_issingle = ( + frappe.qb.from_(dt).select(dt.issingle).where(dt.name == ps.doc_type).as_("issingle") + ) + property_setter_fields = ( + frappe.qb.from_(ps) + .select(ps.doc_type.as_("parent"), ps.field_name.as_("fieldname"), ps_issingle) + .where((ps.property == "options") & (ps.value == doctype) & (ps.field_name.notnull())) + .run(as_dict=True) ) - link_fields += property_setter_link_fields - - frappe.flags.link_fields[doctype] = link_fields + frappe.flags.link_fields[doctype] = standard_fields + custom_fields + property_setter_fields return frappe.flags.link_fields[doctype] def update_options_for_fieldtype(fieldtype: str, old: str, new: str) -> None: + CustomField = frappe.qb.DocType("Custom Field") + PropertySetter = frappe.qb.DocType("Property Setter") + if frappe.conf.developer_mode: for name in frappe.get_all("DocField", filters={"options": old}, pluck="parent"): doctype = frappe.get_doc("DocType", name) @@ -446,132 +506,106 @@ def update_options_for_fieldtype(fieldtype: str, old: str, new: str) -> None: if save: doctype.save() else: - frappe.db.sql( - """update `tabDocField` set options=%s - where fieldtype=%s and options=%s""", - (new, fieldtype, old), - ) + DocField = frappe.qb.DocType("DocField") + frappe.qb.update(DocField).set(DocField.options, new).where( + (DocField.fieldtype == fieldtype) & (DocField.options == old) + ).run() - frappe.db.sql( - """update `tabCustom Field` set options=%s - where fieldtype=%s and options=%s""", - (new, fieldtype, old), - ) + frappe.qb.update(CustomField).set(CustomField.options, new).where( + (CustomField.fieldtype == fieldtype) & (CustomField.options == old) + ).run() - frappe.db.sql( - """update `tabProperty Setter` set value=%s - where property='options' and value=%s""", - (new, old), - ) + frappe.qb.update(PropertySetter).set(PropertySetter.value, new).where( + (PropertySetter.property == "options") & (PropertySetter.value == old) + ).run() -def get_select_fields(old: str, new: str) -> List[Dict]: +def get_select_fields(old: str, new: str) -> list[dict]: """ get select type fields where doctype's name is hardcoded as new line separated list """ + df = frappe.qb.DocType("DocField") + dt = frappe.qb.DocType("DocType") + cf = frappe.qb.DocType("Custom Field") + ps = frappe.qb.DocType("Property Setter") + # get link fields from tabDocField - select_fields = frappe.db.sql( - """ - select parent, fieldname, - (select issingle from tabDocType dt - where dt.name = df.parent) as issingle - from tabDocField df - where - df.parent != %s and df.fieldtype = 'Select' and - df.options like {0} """.format( - frappe.db.escape("%" + old + "%") - ), - (new,), - as_dict=1, + st_issingle = frappe.qb.from_(dt).select(dt.issingle).where(dt.name == df.parent).as_("issingle") + standard_fields = ( + frappe.qb.from_(df) + .select(df.parent, df.fieldname, st_issingle) + .where((df.parent != new) & (df.fieldtype == "Select") & (df.options.like(f"%{old}%"))) + .run(as_dict=True) ) # get link fields from tabCustom Field - custom_select_fields = frappe.db.sql( - """ - select dt as parent, fieldname, - (select issingle from tabDocType dt - where dt.name = df.dt) as issingle - from `tabCustom Field` df - where - df.dt != %s and df.fieldtype = 'Select' and - df.options like {0} """.format( - frappe.db.escape("%" + old + "%") - ), - (new,), - as_dict=1, + cf_issingle = frappe.qb.from_(dt).select(dt.issingle).where(dt.name == cf.dt).as_("issingle") + custom_select_fields = ( + frappe.qb.from_(cf) + .select(cf.dt.as_("parent"), cf.fieldname, cf_issingle) + .where((cf.dt != new) & (cf.fieldtype == "Select") & (cf.options.like(f"%{old}%"))) + .run(as_dict=True) ) - # add custom link fields list to link fields list - select_fields += custom_select_fields - # remove fields whose options have been changed using property setter - property_setter_select_fields = frappe.db.sql( - """ - select ps.doc_type as parent, ps.field_name as fieldname, - (select issingle from tabDocType dt - where dt.name = ps.doc_type) as issingle - from `tabProperty Setter` ps - where - ps.doc_type != %s and - ps.property_type='options' and - ps.field_name is not null and - ps.value like {0} """.format( - frappe.db.escape("%" + old + "%") - ), - (new,), - as_dict=1, + ps_issingle = ( + frappe.qb.from_(dt).select(dt.issingle).where(dt.name == ps.doc_type).as_("issingle") + ) + property_setter_select_fields = ( + frappe.qb.from_(ps) + .select(ps.doc_type.as_("parent"), ps.field_name.as_("fieldname"), ps_issingle) + .where( + (ps.doc_type != new) + & (ps.property == "options") + & (ps.field_name.notnull()) + & (ps.value.like(f"%{old}%")) + ) + .run(as_dict=True) ) - select_fields += property_setter_select_fields - - return select_fields + return standard_fields + custom_select_fields + property_setter_select_fields def update_select_field_values(old: str, new: str): - frappe.db.sql( - """ - update `tabDocField` set options=replace(options, %s, %s) - where - parent != %s and fieldtype = 'Select' and - (options like {0} or options like {1})""".format( - frappe.db.escape("%" + "\n" + old + "%"), frappe.db.escape("%" + old + "\n" + "%") - ), - (old, new, new), - ) + from frappe.query_builder.functions import Replace - frappe.db.sql( - """ - update `tabCustom Field` set options=replace(options, %s, %s) - where - dt != %s and fieldtype = 'Select' and - (options like {0} or options like {1})""".format( - frappe.db.escape("%" + "\n" + old + "%"), frappe.db.escape("%" + old + "\n" + "%") - ), - (old, new, new), - ) + DocField = frappe.qb.DocType("DocField") + CustomField = frappe.qb.DocType("Custom Field") + PropertySetter = frappe.qb.DocType("Property Setter") - frappe.db.sql( - """ - update `tabProperty Setter` set value=replace(value, %s, %s) - where - doc_type != %s and field_name is not null and - property='options' and - (value like {0} or value like {1})""".format( - frappe.db.escape("%" + "\n" + old + "%"), frappe.db.escape("%" + old + "\n" + "%") - ), - (old, new, new), - ) + frappe.qb.update(DocField).set(DocField.options, Replace(DocField.options, old, new)).where( + (DocField.fieldtype == "Select") + & (DocField.parent != new) + & (DocField.options.like(f"%\n{old}%") | DocField.options.like(f"%{old}\n%")) + ).run() + + frappe.qb.update(CustomField).set( + CustomField.options, Replace(CustomField.options, old, new) + ).where( + (CustomField.fieldtype == "Select") + & (CustomField.dt != new) + & (CustomField.options.like(f"%\n{old}%") | CustomField.options.like(f"%{old}\n%")) + ).run() + + frappe.qb.update(PropertySetter).set( + PropertySetter.value, Replace(PropertySetter.value, old, new) + ).where( + (PropertySetter.property == "options") + & (PropertySetter.field_name.notnull()) + & (PropertySetter.doc_type != new) + & (PropertySetter.value.like(f"%\n{old}%") | PropertySetter.value.like(f"%{old}\n%")) + ).run() def update_parenttype_values(old: str, new: str): - child_doctypes = frappe.db.get_all( + child_doctypes = frappe.get_all( "DocField", fields=["options", "fieldname"], filters={"parent": new, "fieldtype": ["in", frappe.model.table_fields]}, ) - custom_child_doctypes = frappe.db.get_all( + custom_child_doctypes = frappe.get_all( "Custom Field", fields=["options", "fieldname"], filters={"dt": new, "fieldtype": ["in", frappe.model.table_fields]}, @@ -586,40 +620,35 @@ def update_parenttype_values(old: str, new: str): pluck="value", ) - child_doctypes = list(d["options"] for d in child_doctypes) - child_doctypes += property_setter_child_doctypes + child_doctypes = set(list(d["options"] for d in child_doctypes) + property_setter_child_doctypes) for doctype in child_doctypes: - frappe.db.sql(f"update `tab{doctype}` set parenttype=%s where parenttype=%s", (new, old)) + table = frappe.qb.DocType(doctype) + frappe.qb.update(table).set(table.parenttype, new).where(table.parenttype == old).run() def rename_dynamic_links(doctype: str, old: str, new: str): + Singles = frappe.qb.DocType("Singles") for df in get_dynamic_link_map().get(doctype, []): # dynamic link in single, just one value to check if frappe.get_meta(df.parent).issingle: refdoc = frappe.db.get_singles_dict(df.parent) if refdoc.get(df.options) == doctype and refdoc.get(df.fieldname) == old: - - frappe.db.sql( - """update tabSingles set value=%s where - field=%s and value=%s and doctype=%s""", - (new, df.fieldname, old, df.parent), - ) + frappe.qb.update(Singles).set(Singles.value, new).where( + (Singles.field == df.fieldname) & (Singles.doctype == df.parent) & (Singles.value == old) + ).run() else: # because the table hasn't been renamed yet! parent = df.parent if df.parent != new else old - frappe.db.sql( - """update `tab{parent}` set {fieldname}=%s - where {options}=%s and {fieldname}=%s""".format( - parent=parent, fieldname=df.fieldname, options=df.options - ), - (new, doctype, old), - ) + + frappe.qb.update(parent).set(df.fieldname, new).where( + (Field(df.options) == doctype) & (Field(df.fieldname) == old) + ).run() def bulk_rename( - doctype: str, rows: Optional[List[List]] = None, via_console: bool = False -) -> Optional[List[str]]: + doctype: str, rows: list[list] | None = None, via_console: bool = False +) -> list[str] | None: """Bulk rename documents :param doctype: DocType to be renamed @@ -660,7 +689,7 @@ def bulk_rename( def update_linked_doctypes( - doctype: str, docname: str, linked_to: str, value: str, ignore_doctypes: Optional[List] = None + doctype: str, docname: str, linked_to: str, value: str, ignore_doctypes: list | None = None ) -> None: from frappe.model.utils.rename_doc import update_linked_doctypes @@ -676,8 +705,8 @@ def update_linked_doctypes( def get_fetch_fields( - doctype: str, linked_to: str, ignore_doctypes: Optional[List] = None -) -> List[Dict]: + doctype: str, linked_to: str, ignore_doctypes: list | None = None +) -> list[dict]: from frappe.model.utils.rename_doc import get_fetch_fields show_deprecation_warning("get_fetch_fields") diff --git a/frappe/model/sync.py b/frappe/model/sync.py index a56d1f267f..df3999054a 100644 --- a/frappe/model/sync.py +++ b/frappe/model/sync.py @@ -53,22 +53,6 @@ def sync_for(app_name, force=0, reset_permissions=False): os.path.join(FRAPPE_PATH, "website", "doctype", website_module, f"{website_module}.json") ) - for data_migration_module in [ - "data_migration_mapping_detail", - "data_migration_mapping", - "data_migration_plan_mapping", - "data_migration_plan", - ]: - files.append( - os.path.join( - FRAPPE_PATH, - "data_migration", - "doctype", - data_migration_module, - f"{data_migration_module}.json", - ) - ) - for desk_module in [ "number_card", "dashboard_chart", @@ -80,6 +64,7 @@ def sync_for(app_name, force=0, reset_permissions=False): "workspace_link", "workspace_chart", "workspace_shortcut", + "workspace_quick_list", "workspace", ]: files.append(os.path.join(FRAPPE_PATH, "desk", "doctype", desk_module, f"{desk_module}.json")) @@ -99,7 +84,7 @@ def sync_for(app_name, force=0, reset_permissions=False): frappe.db.commit() # show progress bar - update_progress_bar("Updating DocTypes for {0}".format(app_name), i, l) + update_progress_bar(f"Updating DocTypes for {app_name}", i, l) # print each progress bar on new line print() @@ -123,8 +108,6 @@ def get_doc_files(files, start_path): "web_template", "notification", "print_style", - "data_migration_mapping", - "data_migration_plan", "workspace", "onboarding_step", "module_onboarding", diff --git a/frappe/model/utils/__init__.py b/frappe/model/utils/__init__.py index a0dd0d89e8..2220b3904f 100644 --- a/frappe/model/utils/__init__.py +++ b/frappe/model/utils/__init__.py @@ -1,12 +1,12 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE -import io import re import frappe from frappe import _ from frappe.build import html_to_js_template from frappe.utils import cstr +from frappe.utils.caching import site_cache STANDARD_FIELD_CONVERSION_MAP = { "name": "Link", @@ -21,10 +21,7 @@ STANDARD_FIELD_CONVERSION_MAP = { "_assign": "Text", "docstatus": "Int", } - -""" -Model utilities, unclassified functions -""" +INCLUDE_DIRECTIVE_PATTERN = re.compile(r"""{% include\s['"](.*)['"]\s%}""") def set_default(doc, key): @@ -50,7 +47,7 @@ def set_field_property(filters, key, value): for d in docs: d.get("fields", filters)[0].set(key, value) d.save() - print("Updated {0}".format(d.name)) + print(f"Updated {d.name}") frappe.db.commit() @@ -67,18 +64,18 @@ def render_include(content): # try 5 levels of includes for i in range(5): if "{% include" in content: - paths = re.findall(r"""{% include\s['"](.*)['"]\s%}""", content) + paths = INCLUDE_DIRECTIVE_PATTERN.findall(content) if not paths: frappe.throw(_("Invalid include path"), InvalidIncludePath) for path in paths: app, app_path = path.split("/", 1) - with io.open(frappe.get_app_path(app, app_path), "r", encoding="utf-8") as f: + with open(frappe.get_app_path(app, app_path), encoding="utf-8") as f: include = f.read() if path.endswith(".html"): include = html_to_js_template(path, include) - content = re.sub(r"""{{% include\s['"]{0}['"]\s%}}""".format(path), include, content) + content = re.sub(rf"""{{% include\s['"]{path}['"]\s%}}""", include, content) else: break @@ -93,12 +90,44 @@ def get_fetch_values(doctype, fieldname, value): :param fieldname: Link fieldname selected :param value: Value selected """ - out = {} - meta = frappe.get_meta(doctype) - link_df = meta.get_field(fieldname) - for df in meta.get_fields_to_fetch(fieldname): - # example shipping_address.gistin - link_field, source_fieldname = df.fetch_from.split(".", 1) - out[df.fieldname] = frappe.db.get_value(link_df.options, value, source_fieldname) - return out + result = frappe._dict() + meta = frappe.get_meta(doctype) + + # fieldname in target doctype: fieldname in source doctype + fields_to_fetch = { + df.fieldname: df.fetch_from.split(".", 1)[1] for df in meta.get_fields_to_fetch(fieldname) + } + + # nothing to fetch + if not fields_to_fetch: + return result + + # initialise empty values for target fields + for target_fieldname in fields_to_fetch: + result[target_fieldname] = None + + # fetch only if Link field has a truthy value + if not value: + return result + + db_values = frappe.db.get_value( + meta.get_options(fieldname), # source doctype + value, + tuple(set(fields_to_fetch.values())), # unique source fieldnames + as_dict=True, + ) + + # if value doesn't exist in source doctype, get_value returns None + if not db_values: + return result + + for target_fieldname, source_fieldname in fields_to_fetch.items(): + result[target_fieldname] = db_values.get(source_fieldname) + + return result + + +@site_cache(maxsize=128) +def is_virtual_doctype(doctype): + return frappe.db.get_value("DocType", doctype, "is_virtual") diff --git a/frappe/model/utils/link_count.py b/frappe/model/utils/link_count.py index 25dfe58139..9a7694b9f8 100644 --- a/frappe/model/utils/link_count.py +++ b/frappe/model/utils/link_count.py @@ -42,7 +42,7 @@ def update_link_count(): if key[0] not in ignore_doctypes: try: frappe.db.sql( - "update `tab{0}` set idx = idx + {1} where name=%s".format(key[0], count), + f"update `tab{key[0]}` set idx = idx + {count} where name=%s", key[1], auto_commit=1, ) diff --git a/frappe/model/utils/rename_doc.py b/frappe/model/utils/rename_doc.py index 00e2d78d5f..ae6649f057 100644 --- a/frappe/model/utils/rename_doc.py +++ b/frappe/model/utils/rename_doc.py @@ -2,14 +2,13 @@ # License: MIT. See LICENSE from itertools import product -from typing import Dict, List, Optional import frappe from frappe.model.rename_doc import get_link_fields def update_linked_doctypes( - doctype: str, docname: str, linked_to: str, value: str, ignore_doctypes: Optional[List] = None + doctype: str, docname: str, linked_to: str, value: str, ignore_doctypes: list | None = None ): """ linked_doctype_info_list = list formed by get_fetch_fields() function @@ -31,8 +30,8 @@ def update_linked_doctypes( def get_fetch_fields( - doctype: str, linked_to: str, ignore_doctypes: Optional[List] = None -) -> List[Dict]: + doctype: str, linked_to: str, ignore_doctypes: list | None = None +) -> list[dict]: """ doctype = Master DocType in which the changes are being made linked_to = DocType name of the field thats being updated in Master diff --git a/frappe/model/utils/rename_field.py b/frappe/model/utils/rename_field.py index 56e69455ef..9e4fc5d84a 100644 --- a/frappe/model/utils/rename_field.py +++ b/frappe/model/utils/rename_field.py @@ -40,7 +40,7 @@ def rename_field(doctype, old_fieldname, new_fieldname): ) else: # copy field value - frappe.db.sql("""update `tab%s` set `%s`=`%s`""" % (doctype, new_fieldname, old_fieldname)) + frappe.db.sql(f"""update `tab{doctype}` set `{new_fieldname}`=`{old_fieldname}`""") update_reports(doctype, old_fieldname, new_fieldname) update_users_report_view_settings(doctype, old_fieldname, new_fieldname) diff --git a/frappe/model/utils/user_settings.py b/frappe/model/utils/user_settings.py index a6ae1a818e..c12c7e27ba 100644 --- a/frappe/model/utils/user_settings.py +++ b/frappe/model/utils/user_settings.py @@ -11,9 +11,7 @@ filter_dict = {"doctype": 0, "docfield": 1, "operator": 2, "value": 3} def get_user_settings(doctype, for_update=False): - user_settings = frappe.cache().hget( - "_user_settings", "{0}::{1}".format(doctype, frappe.session.user) - ) + user_settings = frappe.cache().hget("_user_settings", f"{doctype}::{frappe.session.user}") if user_settings is None: user_settings = frappe.db.sql( @@ -43,9 +41,7 @@ def update_user_settings(doctype, user_settings, for_update=False): current.update(user_settings) - frappe.cache().hset( - "_user_settings", "{0}::{1}".format(doctype, frappe.session.user), json.dumps(current) - ) + frappe.cache().hset("_user_settings", f"{doctype}::{frappe.session.user}", json.dumps(current)) def sync_user_settings(): @@ -103,6 +99,4 @@ def update_user_settings_data( ) # clear that user settings from the redis cache - frappe.cache().hset( - "_user_settings", "{0}::{1}".format(user_setting.doctype, user_setting.user), None - ) + frappe.cache().hset("_user_settings", f"{user_setting.doctype}::{user_setting.user}", None) diff --git a/frappe/model/virtual_doctype.py b/frappe/model/virtual_doctype.py new file mode 100644 index 0000000000..dc228f9577 --- /dev/null +++ b/frappe/model/virtual_doctype.py @@ -0,0 +1,52 @@ +from typing import Protocol + +import frappe + + +class VirtualDoctype(Protocol): + """This class documents requirements that must be met by a doctype controller to function as virtual doctype + + + Additional requirements: + - DocType controller has to inherit from `frappe.model.document.Document` class + + Note: + - "Backend" here means any storage service, it can be a database, flat file or network call to API. + """ + + # ============ class/static methods ============ + + @staticmethod + def get_list(args) -> list[frappe._dict]: + """Similar to reportview.get_list""" + ... + + @staticmethod + def get_count(args) -> int: + """Similar to reportview.get_count, return total count of documents on listview.""" + ... + + @staticmethod + def get_stats(args): + """Similar to reportview.get_stats, return sidebar stats.""" + ... + + # ============ instance methods ============ + + def db_insert(self, *args, **kwargs) -> None: + """Serialize the `Document` object and insert it in backend.""" + ... + + def load_from_db(self) -> None: + """Using self.name initialize current document from backend data. + + This is responsible for updatinng __dict__ of class with all the fields on doctype.""" + ... + + def db_update(self, *args, **kwargs) -> None: + """Serialize the `Document` object and update existing document in backend.""" + ... + + def delete(self, *args, **kwargs) -> None: + """Delete the current document from backend""" + ... diff --git a/frappe/model/workflow.py b/frappe/model/workflow.py index 0edffaf2fb..923fbc1b3b 100644 --- a/frappe/model/workflow.py +++ b/frappe/model/workflow.py @@ -246,16 +246,17 @@ def bulk_workflow_approval(docnames, doctype, action): except Exception as e: if not frappe.message_log: # Exception is raised manually and not from msgprint or throw - message = "{0}".format(e.__class__.__name__) + message = f"{e.__class__.__name__}" if e.args: - message += " : {0}".format(e.args[0]) + message += f" : {e.args[0]}" message_dict = {"docname": docname, "message": message} failed_transactions[docname].append(message_dict) frappe.db.rollback() frappe.log_error( - frappe.get_traceback(), - "Workflow {0} threw an error for {1} {2}".format(action, doctype, docname), + title=f"Workflow {action} threw an error for {doctype} {docname}", + reference_doctype="Workflow", + reference_name=action, ) finally: if not message_dict: @@ -285,19 +286,19 @@ def bulk_workflow_approval(docnames, doctype, action): def print_workflow_log(messages, title, doctype, indicator): if messages.keys(): - msg = "
{0}
".format(title) + msg = f"{title}
" for doc in messages.keys(): if len(messages[doc]): - html = "{0}
".format(frappe.utils.get_link_to_form(doctype, doc)) + html = f"{frappe.utils.get_link_to_form(doctype, doc)}
" for log in messages[doc]: if log.get("message"): - html += "Error: {e}" + raise ImportError(msg) from e return doctype_python_modules[key] def get_module_name( - doctype: str, module: str, prefix: str = "", suffix: str = "", app: Optional[str] = None + doctype: str, module: str, prefix: str = "", suffix: str = "", app: str | None = None ): app = scrub(app or get_module_app(module)) module = scrub(module) @@ -246,18 +253,21 @@ def get_module_name( def get_module_app(module: str) -> str: - return frappe.local.module_app[scrub(module)] + app = frappe.local.module_app.get(scrub(module)) + if app is None: + frappe.throw(_("Module {} not found").format(module), exc=frappe.DoesNotExistError) + return app def get_app_publisher(module: str) -> str: - app = frappe.local.module_app[scrub(module)] + app = get_module_app(module) if not app: frappe.throw(_("App not found for module: {0}").format(module)) return frappe.get_hooks(hook="app_publisher", app_name=app)[0] def make_boilerplate( - template: str, doc: Union["Document", "frappe._dict"], opts: Union[Dict, "frappe._dict"] = None + template: str, doc: Union["Document", "frappe._dict"], opts: Union[dict, "frappe._dict"] = None ): target_path = get_doc_path(doc.module, doc.doctype, doc.name) template_name = template.replace("controller", scrub(doc.name)) @@ -306,7 +316,7 @@ def make_boilerplate( """ ) - with open(target_file_path, "w") as target, open(template_file_path, "r") as source: + with open(target_file_path, "w") as target, open(template_file_path) as source: template = source.read() controller_file_content = cstr(template).format( app_publisher=app_publisher, diff --git a/frappe/monitor.py b/frappe/monitor.py index 74f9e06ef3..8d5391cb77 100644 --- a/frappe/monitor.py +++ b/frappe/monitor.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE @@ -31,6 +30,8 @@ def log_file(): class Monitor: + __slots__ = ("data",) + def __init__(self, transaction_type, method, kwargs): try: self.data = frappe._dict( diff --git a/frappe/oauth.py b/frappe/oauth.py index e7fa101bfd..68e21ac88b 100644 --- a/frappe/oauth.py +++ b/frappe/oauth.py @@ -323,10 +323,7 @@ class OAuthWebRequestValidator(RequestValidator): # Check whether frappe server URL is set id_token_header = {"typ": "jwt", "alg": "HS256"} - user = frappe.get_doc( - "User", - frappe.session.user, - ) + user = frappe.get_doc("User", request.user) if request.nonce: id_token["nonce"] = request.nonce diff --git a/frappe/parallel_test_runner.py b/frappe/parallel_test_runner.py index f5367e9dc6..39a00235cb 100644 --- a/frappe/parallel_test_runner.py +++ b/frappe/parallel_test_runner.py @@ -46,7 +46,7 @@ class ParallelTestRunner: if hasattr(test_module, "global_test_dependencies"): for doctype in test_module.global_test_dependencies: - make_test_records(doctype) + make_test_records(doctype, commit=True) elapsed = time.time() - start_time elapsed = click.style(f" ({elapsed:.03}s)", fg="red") @@ -76,17 +76,17 @@ class ParallelTestRunner: def create_test_dependency_records(self, module, path, filename): if hasattr(module, "test_dependencies"): for doctype in module.test_dependencies: - make_test_records(doctype) + make_test_records(doctype, commit=True) if os.path.basename(os.path.dirname(path)) == "doctype": # test_data_migration_connector.py > data_migration_connector.json test_record_filename = re.sub("^test_", "", filename).replace(".py", ".json") test_record_file_path = os.path.join(path, test_record_filename) if os.path.exists(test_record_file_path): - with open(test_record_file_path, "r") as f: + with open(test_record_file_path) as f: doc = json.loads(f.read()) doctype = doc["name"] - make_test_records(doctype) + make_test_records(doctype, commit=True) def get_module(self, path, filename): app_path = frappe.get_pymodule_path(self.app) @@ -117,6 +117,7 @@ class ParallelTestRunner: class ParallelTestResult(unittest.TextTestResult): def startTest(self, test): + self.tb_locals = True self._started_at = time.time() super(unittest.TextTestResult, self).startTest(test) test_class = unittest.util.strclass(test.__class__) diff --git a/frappe/patches.txt b/frappe/patches.txt index bc2bc22637..2f6ebd334a 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -66,7 +66,6 @@ execute:frappe.delete_doc_if_exists('Page', 'user-permissions') frappe.patches.v10_0.set_no_copy_to_workflow_state frappe.patches.v10_0.increase_single_table_column_length frappe.patches.v11_0.create_contact_for_user -frappe.patches.v11_0.sync_stripe_settings_before_migrate frappe.patches.v11_0.update_list_user_settings frappe.patches.v11_0.rename_workflow_action_to_workflow_action_master #13-06-2018 frappe.patches.v11_0.rename_email_alert_to_notification #13-06-2018 @@ -106,13 +105,12 @@ frappe.patches.v12_0.set_default_incoming_email_port frappe.patches.v12_0.update_global_search frappe.patches.v12_0.setup_tags frappe.patches.v12_0.update_auto_repeat_status_and_not_submittable -frappe.patches.v12_0.copy_to_parent_for_tags frappe.patches.v12_0.create_notification_settings_for_user frappe.patches.v11_0.make_all_prepared_report_attachments_private #2019-11-26 frappe.patches.v12_0.setup_email_linking frappe.patches.v12_0.change_existing_dashboard_chart_filters frappe.patches.v12_0.set_correct_assign_value_in_docs #2020-07-13 -execute:frappe.delete_doc("Test Runner") +execute:frappe.delete_doc('DocType', 'Test Runner') # 2022-05-19 execute:frappe.delete_doc_if_exists('DocType', 'Google Maps Settings') execute:frappe.db.set_default('desktop:home_page', 'workspace') execute:frappe.delete_doc_if_exists('DocType', 'GSuite Settings') @@ -122,7 +120,7 @@ execute:frappe.delete_doc_if_exists('DocType', 'GCalendar Settings') frappe.patches.v12_0.remove_example_email_thread_notify execute:from frappe.desk.page.setup_wizard.install_fixtures import update_genders;update_genders() frappe.patches.v12_0.set_correct_url_in_files -execute:frappe.reload_doc('core', 'doctype', 'doctype') +execute:frappe.reload_doc('core', 'doctype', 'doctype') #2022-06-21 execute:frappe.reload_doc('custom', 'doctype', 'property_setter') frappe.patches.v13_0.remove_invalid_options_for_data_fields frappe.patches.v13_0.website_theme_custom_scss @@ -184,12 +182,20 @@ frappe.patches.v13_0.queryreport_columns frappe.patches.v13_0.jinja_hook frappe.patches.v13_0.update_notification_channel_if_empty frappe.patches.v13_0.set_first_day_of_the_week +frappe.patches.v13_0.encrypt_2fa_secrets +frappe.patches.v13_0.reset_corrupt_defaults execute:frappe.reload_doc('custom', 'doctype', 'custom_field') frappe.patches.v14_0.update_workspace2 # 20.09.2021 frappe.patches.v14_0.save_ratings_in_fraction #23-12-2021 frappe.patches.v14_0.transform_todo_schema frappe.patches.v14_0.remove_post_and_post_comment frappe.patches.v14_0.reset_creation_datetime +frappe.patches.v14_0.remove_is_first_startup +frappe.patches.v14_0.clear_long_pending_stale_logs +frappe.patches.v14_0.log_settings_migration +frappe.patches.v14_0.setup_likes_from_feedback +frappe.patches.v14_0.update_webforms +frappe.patches.v14_0.delete_payment_gateways [post_model_sync] frappe.patches.v14_0.drop_data_import_legacy @@ -199,3 +205,8 @@ frappe.patches.v14_0.remove_db_aggregation frappe.patches.v14_0.update_color_names_in_kanban_board_column frappe.patches.v14_0.update_is_system_generated_flag frappe.patches.v14_0.update_auto_account_deletion_duration +frappe.patches.v14_0.update_integration_request +frappe.patches.v14_0.set_document_expiry_default +frappe.patches.v14_0.delete_data_migration_tool +frappe.patches.v14_0.set_suspend_email_queue_default +frappe.patches.v14_0.different_encryption_key diff --git a/frappe/patches/v10_0/increase_single_table_column_length.py b/frappe/patches/v10_0/increase_single_table_column_length.py index 21b5a790ab..0b8e94a3f7 100644 --- a/frappe/patches/v10_0/increase_single_table_column_length.py +++ b/frappe/patches/v10_0/increase_single_table_column_length.py @@ -6,4 +6,4 @@ import frappe def execute(): for col in ("field", "doctype"): - frappe.db.sql_ddl("alter table `tabSingles` modify column `{0}` varchar(255)".format(col)) + frappe.db.sql_ddl(f"alter table `tabSingles` modify column `{col}` varchar(255)") diff --git a/frappe/patches/v11_0/delete_duplicate_user_permissions.py b/frappe/patches/v11_0/delete_duplicate_user_permissions.py index b986c6f825..f2ca6d51fe 100644 --- a/frappe/patches/v11_0/delete_duplicate_user_permissions.py +++ b/frappe/patches/v11_0/delete_duplicate_user_permissions.py @@ -13,7 +13,7 @@ def execute(): for record in duplicateRecords: frappe.db.sql( """delete from `tabUser Permission` - where allow=%s and user=%s and for_value=%s limit {0}""".format( + where allow=%s and user=%s and for_value=%s limit {}""".format( record.count - 1 ), (record.allow, record.user, record.for_value), diff --git a/frappe/patches/v11_0/drop_column_apply_user_permissions.py b/frappe/patches/v11_0/drop_column_apply_user_permissions.py index bfc4aee72c..0a0091624e 100644 --- a/frappe/patches/v11_0/drop_column_apply_user_permissions.py +++ b/frappe/patches/v11_0/drop_column_apply_user_permissions.py @@ -8,7 +8,7 @@ def execute(): for doctype in to_remove: if frappe.db.table_exists(doctype): if column in frappe.db.get_table_columns(doctype): - frappe.db.sql("alter table `tab{0}` drop column {1}".format(doctype, column)) + frappe.db.sql(f"alter table `tab{doctype}` drop column {column}") frappe.reload_doc("core", "doctype", "docperm", force=True) frappe.reload_doc("core", "doctype", "custom_docperm", force=True) diff --git a/frappe/patches/v11_0/fix_order_by_in_reports_json.py b/frappe/patches/v11_0/fix_order_by_in_reports_json.py index 3dfec0954f..4e955a338b 100644 --- a/frappe/patches/v11_0/fix_order_by_in_reports_json.py +++ b/frappe/patches/v11_0/fix_order_by_in_reports_json.py @@ -28,8 +28,8 @@ def execute(): sort_by = parts[1].split(" ") - json_data["order_by"] = "`tab{0}`.`{1}`".format(doc.ref_doctype, sort_by[0]) - json_data["order_by"] += " {0}".format(sort_by[1]) if len(sort_by) > 1 else "" + json_data["order_by"] = f"`tab{doc.ref_doctype}`.`{sort_by[0]}`" + json_data["order_by"] += f" {sort_by[1]}" if len(sort_by) > 1 else "" doc.json = json.dumps(json_data) doc.save() diff --git a/frappe/patches/v11_0/set_dropbox_file_backup.py b/frappe/patches/v11_0/set_dropbox_file_backup.py index c9dec31414..396491e8b3 100644 --- a/frappe/patches/v11_0/set_dropbox_file_backup.py +++ b/frappe/patches/v11_0/set_dropbox_file_backup.py @@ -4,6 +4,6 @@ from frappe.utils import cint def execute(): frappe.reload_doctype("Dropbox Settings") - check_dropbox_enabled = cint(frappe.db.get_value("Dropbox Settings", None, "enabled")) + check_dropbox_enabled = cint(frappe.db.get_single_value("Dropbox Settings", "enabled")) if check_dropbox_enabled == 1: frappe.db.set_value("Dropbox Settings", None, "file_backup", 1) diff --git a/frappe/patches/v11_0/sync_stripe_settings_before_migrate.py b/frappe/patches/v11_0/sync_stripe_settings_before_migrate.py deleted file mode 100644 index 019ecef67c..0000000000 --- a/frappe/patches/v11_0/sync_stripe_settings_before_migrate.py +++ /dev/null @@ -1,25 +0,0 @@ -import frappe -from frappe.utils.password import get_decrypted_password - - -def execute(): - publishable_key = frappe.db.sql( - "select value from tabSingles where doctype='Stripe Settings' and field='publishable_key'" - ) - if publishable_key: - secret_key = get_decrypted_password( - "Stripe Settings", "Stripe Settings", fieldname="secret_key", raise_exception=False - ) - if secret_key: - frappe.reload_doc("integrations", "doctype", "stripe_settings") - frappe.db.commit() - - settings = frappe.new_doc("Stripe Settings") - settings.gateway_name = ( - frappe.db.get_value("Global Defaults", None, "default_company") or "Stripe Settings" - ) - settings.publishable_key = publishable_key - settings.secret_key = secret_key - settings.save(ignore_permissions=True) - - frappe.db.delete("Singles", {"doctype": "Stripe Settings"}) diff --git a/frappe/patches/v11_0/sync_user_permission_doctype_before_migrate.py b/frappe/patches/v11_0/sync_user_permission_doctype_before_migrate.py deleted file mode 100644 index 6b7a7695f6..0000000000 --- a/frappe/patches/v11_0/sync_user_permission_doctype_before_migrate.py +++ /dev/null @@ -1,7 +0,0 @@ -import frappe - - -def execute(): - frappe.flags.in_patch = True - frappe.reload_doc("core", "doctype", "user_permission") - frappe.db.commit() diff --git a/frappe/patches/v11_0/update_list_user_settings.py b/frappe/patches/v11_0/update_list_user_settings.py index 146b29346c..5209b9e384 100644 --- a/frappe/patches/v11_0/update_list_user_settings.py +++ b/frappe/patches/v11_0/update_list_user_settings.py @@ -13,7 +13,7 @@ def execute(): # get user_settings for each user settings = frappe.db.sql( "select * from `__UserSettings` \ - where user={0}".format( + where user={}".format( frappe.db.escape(user.user) ), as_dict=True, diff --git a/frappe/patches/v12_0/copy_to_parent_for_tags.py b/frappe/patches/v12_0/copy_to_parent_for_tags.py deleted file mode 100644 index ae3702a0d5..0000000000 --- a/frappe/patches/v12_0/copy_to_parent_for_tags.py +++ /dev/null @@ -1,7 +0,0 @@ -import frappe - - -def execute(): - - frappe.db.sql("UPDATE `tabTag Link` SET parenttype=document_type") - frappe.db.sql("UPDATE `tabTag Link` SET parent=document_name") diff --git a/frappe/patches/v12_0/delete_duplicate_indexes.py b/frappe/patches/v12_0/delete_duplicate_indexes.py index 6a6b0b3204..1cb94ca50c 100644 --- a/frappe/patches/v12_0/delete_duplicate_indexes.py +++ b/frappe/patches/v12_0/delete_duplicate_indexes.py @@ -1,5 +1,3 @@ -from pymysql import InternalError - import frappe # This patch deletes all the duplicate indexes created for same column @@ -43,13 +41,13 @@ def execute(): # build drop index query for (table_name, index_list) in final_deletion_map.items(): query_list = [] - alter_query = "ALTER TABLE `{}`".format(table_name) + alter_query = f"ALTER TABLE `{table_name}`" for index in index_list: - query_list.append("{} DROP INDEX `{}`".format(alter_query, index)) + query_list.append(f"{alter_query} DROP INDEX `{index}`") for query in query_list: try: frappe.db.sql(query) - except InternalError: + except frappe.db.InternalError: pass diff --git a/frappe/patches/v12_0/delete_gsuite_if_exists.py b/frappe/patches/v12_0/delete_gsuite_if_exists.py deleted file mode 100644 index 1fb3a8c2d0..0000000000 --- a/frappe/patches/v12_0/delete_gsuite_if_exists.py +++ /dev/null @@ -1,9 +0,0 @@ -import frappe - - -def execute(): - """ - Remove GSuite Template and GSuite Settings - """ - frappe.delete_doc_if_exists("DocType", "GSuite Settings") - frappe.delete_doc_if_exists("DocType", "GSuite Templates") diff --git a/frappe/patches/v12_0/fix_public_private_files.py b/frappe/patches/v12_0/fix_public_private_files.py index e1ad2f1862..382e3c2db1 100644 --- a/frappe/patches/v12_0/fix_public_private_files.py +++ b/frappe/patches/v12_0/fix_public_private_files.py @@ -30,7 +30,7 @@ def generate_file(file_name): file_doc.file_url = new_doc.file_url file_doc.save() - except IOError: + except OSError: pass except Exception as e: print(e) diff --git a/frappe/patches/v12_0/init_desk_settings.py b/frappe/patches/v12_0/init_desk_settings.py deleted file mode 100644 index 5ec9764e8f..0000000000 --- a/frappe/patches/v12_0/init_desk_settings.py +++ /dev/null @@ -1,11 +0,0 @@ -import json - -import frappe -from frappe.config import get_modules_from_all_apps_for_user -from frappe.desk.moduleview import get_onboard_items - - -def execute(): - """Reset the initial customizations for desk, with modules, indices and links.""" - frappe.reload_doc("core", "doctype", "user") - frappe.db.sql("""update tabUser set home_settings = ''""") diff --git a/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py b/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py index 2207edd958..4d2061c5ac 100644 --- a/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py +++ b/frappe/patches/v12_0/move_timeline_links_to_dynamic_links.py @@ -22,7 +22,7 @@ def execute(): if communication.timeline_doctype and communication.timeline_name: name += 1 values.append( - """({0}, "{1}", "timeline_links", "Communication", "{2}", "{3}", "{4}", "{5}", "{6}", "{7}")""".format( + """({}, "{}", "timeline_links", "Communication", "{}", "{}", "{}", "{}", "{}", "{}")""".format( counter, str(name), frappe.db.escape(communication.name), @@ -37,7 +37,7 @@ def execute(): if communication.link_doctype and communication.link_name: name += 1 values.append( - """({0}, "{1}", "timeline_links", "Communication", "{2}", "{3}", "{4}", "{5}", "{6}", "{7}")""".format( + """({}, "{}", "timeline_links", "Communication", "{}", "{}", "{}", "{}", "{}", "{}")""".format( counter, str(name), frappe.db.escape(communication.name), @@ -55,7 +55,7 @@ def execute(): INSERT INTO `tabCommunication Link` (`idx`, `name`, `parentfield`, `parenttype`, `parent`, `link_doctype`, `link_name`, `creation`, `modified`, `modified_by`) - VALUES {0} + VALUES {} """.format( ", ".join([d for d in values]) ) diff --git a/frappe/patches/v12_0/remove_gcalendar_gmaps.py b/frappe/patches/v12_0/remove_gcalendar_gmaps.py deleted file mode 100644 index 1177441130..0000000000 --- a/frappe/patches/v12_0/remove_gcalendar_gmaps.py +++ /dev/null @@ -1,11 +0,0 @@ -import frappe - - -def execute(): - """ - Remove GCalendar and GCalendar Settings - Remove Google Maps Settings as its been merged with Delivery Trips - """ - frappe.delete_doc_if_exists("DocType", "GCalendar Account") - frappe.delete_doc_if_exists("DocType", "GCalendar Settings") - frappe.delete_doc_if_exists("DocType", "Google Maps Settings") diff --git a/frappe/patches/v12_0/set_correct_url_in_files.py b/frappe/patches/v12_0/set_correct_url_in_files.py index dca42a3c04..fee0b5d6fc 100644 --- a/frappe/patches/v12_0/set_correct_url_in_files.py +++ b/frappe/patches/v12_0/set_correct_url_in_files.py @@ -30,12 +30,10 @@ def execute(): if file_is_private: public_file_url = os.path.join(public_file_path, file_name) if os.path.exists(public_file_url): - frappe.db.set_value( - "File", file.name, {"file_url": "/files/{0}".format(file_name), "is_private": 0} - ) + frappe.db.set_value("File", file.name, {"file_url": f"/files/{file_name}", "is_private": 0}) else: private_file_url = os.path.join(private_file_path, file_name) if os.path.exists(private_file_url): frappe.db.set_value( - "File", file.name, {"file_url": "/private/files/{0}".format(file_name), "is_private": 1} + "File", file.name, {"file_url": f"/private/files/{file_name}", "is_private": 1} ) diff --git a/frappe/patches/v12_0/setup_tags.py b/frappe/patches/v12_0/setup_tags.py index 46482a102b..6bff8d3dac 100644 --- a/frappe/patches/v12_0/setup_tags.py +++ b/frappe/patches/v12_0/setup_tags.py @@ -17,7 +17,7 @@ def execute(): continue for _user_tags in frappe.db.sql( - "select `name`, `_user_tags` from `tab{0}`".format(doctype.name), as_dict=True + f"select `name`, `_user_tags` from `tab{doctype.name}`", as_dict=True ): if not _user_tags.get("_user_tags"): continue diff --git a/frappe/patches/v12_0/webpage_migrate_description_to_meta_tag.py b/frappe/patches/v12_0/webpage_migrate_description_to_meta_tag.py deleted file mode 100644 index 32473481b8..0000000000 --- a/frappe/patches/v12_0/webpage_migrate_description_to_meta_tag.py +++ /dev/null @@ -1,12 +0,0 @@ -import frappe - - -def execute(): - web_pages = frappe.get_all("Web Page", ["name", "description"]) - - for web_page in web_pages: - if web_page.description and web_page.route: - doc = frappe.new_doc("Website Route Meta") - doc.name = web_page.route - doc.append("meta_tags", {"key": "description", "value": web_page.description}) - doc.save() diff --git a/frappe/patches/v12_0/website_meta_tag_parent.py b/frappe/patches/v12_0/website_meta_tag_parent.py deleted file mode 100644 index 8920189826..0000000000 --- a/frappe/patches/v12_0/website_meta_tag_parent.py +++ /dev/null @@ -1,12 +0,0 @@ -import frappe - - -def execute(): - # convert all /path to path - frappe.db.sql( - """ - UPDATE `tabWebsite Meta Tag` - SET parent = SUBSTR(parent, 2) - WHERE parent like '/%' - """ - ) diff --git a/frappe/patches/v13_0/cleanup_desk_cards.py b/frappe/patches/v13_0/cleanup_desk_cards.py deleted file mode 100644 index 988e98a647..0000000000 --- a/frappe/patches/v13_0/cleanup_desk_cards.py +++ /dev/null @@ -1,75 +0,0 @@ -from json import loads - -import frappe -from frappe.desk.doctype.workspace.workspace import get_link_type, get_report_type - - -def execute(): - frappe.reload_doc("desk", "doctype", "workspace") - - pages = frappe.db.sql("Select `name` from `tabDesk Page`") - # pages = frappe.get_all("Workspace", filters={"is_standard": 0}, pluck="name") - - for page in pages: - rebuild_links(page[0]) - - frappe.delete_doc("DocType", "Desk Card") - - -def rebuild_links(page): - # Empty links table - - try: - doc = frappe.get_doc("Workspace", page) - except frappe.DoesNotExistError: - db_doc = get_doc_from_db(page) - - doc = frappe.get_doc(db_doc) - doc.insert(ignore_permissions=True) - - doc.links = [] - - for card in get_all_cards(page): - if isinstance(card.links, str): - links = loads(card.links) - else: - links = card.links - - doc.append( - "links", - {"label": card.label, "type": "Card Break", "icon": card.icon, "hidden": card.hidden or False}, - ) - - for link in links: - if not frappe.db.exists(get_link_type(link.get("type")), link.get("name")): - continue - - doc.append( - "links", - { - "label": link.get("label") or link.get("name"), - "type": "Link", - "link_type": get_link_type(link.get("type")), - "link_to": link.get("name"), - "onboard": link.get("onboard"), - "dependencies": ", ".join(link.get("dependencies", [])), - "is_query_report": get_report_type(link.get("name")) - if link.get("type").lower() == "report" - else 0, - }, - ) - - try: - doc.save(ignore_permissions=True) - except frappe.LinkValidationError: - print(doc.as_dict()) - - -def get_doc_from_db(page): - result = frappe.db.sql("SELECT * FROM `tabDesk Page` WHERE name=%s", [page], as_dict=True) - if result: - return result[0].update({"doctype": "Workspace"}) - - -def get_all_cards(page): - return frappe.db.get_all("Desk Card", filters={"parent": page}, fields=["*"], order_by="idx") diff --git a/frappe/patches/v13_0/encrypt_2fa_secrets.py b/frappe/patches/v13_0/encrypt_2fa_secrets.py new file mode 100644 index 0000000000..1814ff50c5 --- /dev/null +++ b/frappe/patches/v13_0/encrypt_2fa_secrets.py @@ -0,0 +1,45 @@ +import frappe +import frappe.defaults +from frappe.cache_manager import clear_defaults_cache +from frappe.twofactor import PARENT_FOR_DEFAULTS +from frappe.utils.password import encrypt + +DOCTYPE = "DefaultValue" +OLD_PARENT = "__default" + + +def execute(): + table = frappe.qb.DocType(DOCTYPE) + + # set parent for `*_otplogin` + ( + frappe.qb.update(table) + .set(table.parent, PARENT_FOR_DEFAULTS) + .where(table.parent == OLD_PARENT) + .where(table.defkey.like("%_otplogin")) + ).run() + + # update records for `*_otpsecret` + secrets = { + key: value + for key, value in frappe.defaults.get_defaults_for(parent=OLD_PARENT).items() + if key.endswith("_otpsecret") + } + + if not secrets: + return + + defvalue_cases = frappe.qb.terms.Case() + + for key, value in secrets.items(): + defvalue_cases.when(table.defkey == key, encrypt(value)) + + ( + frappe.qb.update(table) + .set(table.parent, PARENT_FOR_DEFAULTS) + .set(table.defvalue, defvalue_cases) + .where(table.parent == OLD_PARENT) + .where(table.defkey.like("%_otpsecret")) + ).run() + + clear_defaults_cache() diff --git a/frappe/patches/v13_0/generate_theme_files_in_public_folder.py b/frappe/patches/v13_0/generate_theme_files_in_public_folder.py index 9b905a9bbb..29ddca1108 100644 --- a/frappe/patches/v13_0/generate_theme_files_in_public_folder.py +++ b/frappe/patches/v13_0/generate_theme_files_in_public_folder.py @@ -14,6 +14,6 @@ def execute(): try: doc.generate_bootstrap_theme() doc.save() - except: # noqa: E722 + except Exception: print("Ignoring....") print(frappe.get_traceback()) diff --git a/frappe/patches/v13_0/reset_corrupt_defaults.py b/frappe/patches/v13_0/reset_corrupt_defaults.py new file mode 100644 index 0000000000..10e81c7ff1 --- /dev/null +++ b/frappe/patches/v13_0/reset_corrupt_defaults.py @@ -0,0 +1,33 @@ +import frappe +from frappe.patches.v13_0.encrypt_2fa_secrets import DOCTYPE +from frappe.patches.v13_0.encrypt_2fa_secrets import PARENT_FOR_DEFAULTS as TWOFACTOR_PARENT +from frappe.utils import cint + + +def execute(): + """ + This patch is needed to fix parent incorrectly set as `__2fa` because of + https://github.com/frappe/frappe/commit/a822092211533ff17ff9b92dd86f6f868ed63e2e + """ + + if not frappe.db.get_value( + DOCTYPE, {"parent": TWOFACTOR_PARENT, "defkey": ("not like", "%_otp%")}, "defkey" + ): + return + + # system settings + system_settings = frappe.get_single("System Settings") + system_settings.set_defaults() + + # home page + frappe.db.set_default( + "desktop:home_page", "workspace" if cint(system_settings.setup_complete) else "setup-wizard" + ) + + # letter head + try: + letter_head = frappe.get_doc("Letter Head", {"is_default": 1}) + letter_head.set_as_default() + + except frappe.DoesNotExistError: + pass diff --git a/frappe/patches/v13_0/set_unique_for_page_view.py b/frappe/patches/v13_0/set_unique_for_page_view.py index 1a674f6697..45351afdde 100644 --- a/frappe/patches/v13_0/set_unique_for_page_view.py +++ b/frappe/patches/v13_0/set_unique_for_page_view.py @@ -4,6 +4,4 @@ import frappe def execute(): frappe.reload_doc("website", "doctype", "web_page_view", force=True) site_url = frappe.utils.get_site_url(frappe.local.site) - frappe.db.sql( - """UPDATE `tabWeb Page View` set is_unique=1 where referrer LIKE '%{0}%'""".format(site_url) - ) + frappe.db.sql(f"""UPDATE `tabWeb Page View` set is_unique=1 where referrer LIKE '%{site_url}%'""") diff --git a/frappe/patches/v14_0/clear_long_pending_stale_logs.py b/frappe/patches/v14_0/clear_long_pending_stale_logs.py new file mode 100644 index 0000000000..53127cb197 --- /dev/null +++ b/frappe/patches/v14_0/clear_long_pending_stale_logs.py @@ -0,0 +1,41 @@ +import frappe +from frappe.core.doctype.log_settings.log_settings import clear_log_table +from frappe.utils import add_to_date, today + + +def execute(): + """Due to large size of log tables on old sites some table cleanups never finished during daily log clean up. This patch discards such data by using "big delete" code. + + ref: https://github.com/frappe/frappe/issues/16971 + """ + + DOCTYPE_RETENTION_MAP = { + "Error Log": get_current_setting("clear_error_log_after") or 90, + "Activity Log": get_current_setting("clear_activity_log_after") or 90, + "Email Queue": get_current_setting("clear_email_queue_after") or 30, + # child table on email queue + "Email Queue Recipient": get_current_setting("clear_email_queue_after") or 30, + "Error Snapshot": get_current_setting("clear_error_log_after") or 90, + # newly added + "Scheduled Job Log": 90, + } + + for doctype, retention in DOCTYPE_RETENTION_MAP.items(): + if is_log_cleanup_stuck(doctype, retention): + print(f"Clearing old {doctype} records") + clear_log_table(doctype, retention) + + +def is_log_cleanup_stuck(doctype: str, retention: int) -> bool: + """Check if doctype has data significantly older than configured cleanup period""" + threshold = add_to_date(today(), days=retention * -2) + + return bool(frappe.db.exists(doctype, {"modified": ("<", threshold)})) + + +def get_current_setting(fieldname): + try: + return frappe.db.get_single_value("Log Settings", fieldname) + except Exception: + # Field might be gone if patch is reattempted + pass diff --git a/frappe/patches/v14_0/copy_mail_data.py b/frappe/patches/v14_0/copy_mail_data.py index 6b976ba6fb..c44b7c9e92 100644 --- a/frappe/patches/v14_0/copy_mail_data.py +++ b/frappe/patches/v14_0/copy_mail_data.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import frappe diff --git a/frappe/patches/v14_0/delete_data_migration_tool.py b/frappe/patches/v14_0/delete_data_migration_tool.py new file mode 100644 index 0000000000..d0416cb1e7 --- /dev/null +++ b/frappe/patches/v14_0/delete_data_migration_tool.py @@ -0,0 +1,12 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +import frappe + + +def execute(): + doctypes = frappe.db.get_all("DocType", {"module": "Data Migration", "custom": 0}, pluck="name") + for doctype in doctypes: + frappe.delete_doc("DocType", doctype, ignore_missing=True) + + frappe.delete_doc("Module Def", "Data Migration", ignore_missing=True, force=True) diff --git a/frappe/patches/v14_0/delete_payment_gateways.py b/frappe/patches/v14_0/delete_payment_gateways.py new file mode 100644 index 0000000000..c06f63a2d3 --- /dev/null +++ b/frappe/patches/v14_0/delete_payment_gateways.py @@ -0,0 +1,16 @@ +import frappe + + +def execute(): + if "payments" in frappe.get_installed_apps(): + return + + for doctype in ( + "Payment Gateway", + "Razorpay Settings", + "Braintree Settings", + "PayPal Settings", + "Paytm Settings", + "Stripe Settings", + ): + frappe.delete_doc_if_exists("DocType", doctype, force=True) diff --git a/frappe/patches/v14_0/different_encryption_key.py b/frappe/patches/v14_0/different_encryption_key.py new file mode 100644 index 0000000000..3b80e15a73 --- /dev/null +++ b/frappe/patches/v14_0/different_encryption_key.py @@ -0,0 +1,16 @@ +import pathlib + +import frappe +from frappe.installer import update_site_config +from frappe.utils.backups import BACKUP_ENCRYPTION_CONFIG_KEY, get_backup_path + + +def execute(): + if frappe.conf.get(BACKUP_ENCRYPTION_CONFIG_KEY): + return + + backup_path = pathlib.Path(get_backup_path()) + encrypted_backups_present = bool(list(backup_path.glob("*-enc*"))) + + if encrypted_backups_present: + update_site_config(BACKUP_ENCRYPTION_CONFIG_KEY, frappe.local.conf.encryption_key) diff --git a/frappe/patches/v14_0/log_settings_migration.py b/frappe/patches/v14_0/log_settings_migration.py new file mode 100644 index 0000000000..203405e69b --- /dev/null +++ b/frappe/patches/v14_0/log_settings_migration.py @@ -0,0 +1,29 @@ +import frappe + + +def execute(): + old_settings = { + "Error Log": get_current_setting("clear_error_log_after"), + "Activity Log": get_current_setting("clear_activity_log_after"), + "Email Queue": get_current_setting("clear_email_queue_after"), + } + + frappe.reload_doc("core", "doctype", "Logs To Clear") + frappe.reload_doc("core", "doctype", "Log Settings") + + log_settings = frappe.get_doc("Log Settings") + log_settings.add_default_logtypes() + + for doctype, retention in old_settings.items(): + if retention: + log_settings.register_doctype(doctype, retention) + + log_settings.save() + + +def get_current_setting(fieldname): + try: + return frappe.db.get_single_value("Log Settings", fieldname) + except Exception: + # Field might be gone if patch is reattempted + pass diff --git a/frappe/patches/v14_0/remove_db_aggregation.py b/frappe/patches/v14_0/remove_db_aggregation.py index 6dc34a784b..4b0a58c2d6 100644 --- a/frappe/patches/v14_0/remove_db_aggregation.py +++ b/frappe/patches/v14_0/remove_db_aggregation.py @@ -30,6 +30,6 @@ def execute(): name, script = server_script["name"], server_script["script"] for agg in ["avg", "max", "min", "sum"]: - script = re.sub(f"frappe.db.{agg}\(", f"frappe.qb.{agg}(", script) + script = re.sub(f"frappe.db.{agg}\\(", f"frappe.qb.{agg}(", script) frappe.db.update("Server Script", name, "script", script) diff --git a/frappe/patches/v14_0/remove_is_first_startup.py b/frappe/patches/v14_0/remove_is_first_startup.py new file mode 100644 index 0000000000..cae38ce2ab --- /dev/null +++ b/frappe/patches/v14_0/remove_is_first_startup.py @@ -0,0 +1,8 @@ +import frappe + + +def execute(): + singles = frappe.qb.Table("tabSingles") + frappe.qb.from_(singles).delete().where( + (singles.doctype == "System Settings") & (singles.field == "is_first_startup") + ).run() diff --git a/frappe/patches/v14_0/set_document_expiry_default.py b/frappe/patches/v14_0/set_document_expiry_default.py new file mode 100644 index 0000000000..59a9db6c4d --- /dev/null +++ b/frappe/patches/v14_0/set_document_expiry_default.py @@ -0,0 +1,9 @@ +import frappe + + +def execute(): + frappe.db.set_value( + "System Settings", + "System Settings", + {"document_share_key_expiry": 30, "allow_older_web_view_links": 1}, + ) diff --git a/frappe/patches/v14_0/set_suspend_email_queue_default.py b/frappe/patches/v14_0/set_suspend_email_queue_default.py new file mode 100644 index 0000000000..8cdb05a177 --- /dev/null +++ b/frappe/patches/v14_0/set_suspend_email_queue_default.py @@ -0,0 +1,13 @@ +import frappe +from frappe.cache_manager import clear_defaults_cache + + +def execute(): + frappe.db.set_default( + "suspend_email_queue", + frappe.db.get_default("hold_queue", "Administrator") or 0, + parent="__default", + ) + + frappe.db.delete("DefaultValue", {"defkey": "hold_queue"}) + clear_defaults_cache() diff --git a/frappe/patches/v14_0/setup_likes_from_feedback.py b/frappe/patches/v14_0/setup_likes_from_feedback.py new file mode 100644 index 0000000000..d88f69ce4b --- /dev/null +++ b/frappe/patches/v14_0/setup_likes_from_feedback.py @@ -0,0 +1,30 @@ +import frappe + + +def execute(): + frappe.reload_doctype("Comment") + + if frappe.db.count("Feedback") > 20000: + frappe.db.auto_commit_on_many_writes = True + + for feedback in frappe.get_all("Feedback", fields=["*"]): + if feedback.like: + new_comment = frappe.new_doc("Comment") + new_comment.comment_type = "Like" + new_comment.comment_email = feedback.owner + new_comment.content = "Liked by: " + feedback.owner + new_comment.reference_doctype = feedback.reference_doctype + new_comment.reference_name = feedback.reference_name + new_comment.creation = feedback.creation + new_comment.modified = feedback.modified + new_comment.owner = feedback.owner + new_comment.modified_by = feedback.modified_by + new_comment.ip_address = feedback.ip_address + new_comment.db_insert() + + if frappe.db.auto_commit_on_many_writes: + frappe.db.auto_commit_on_many_writes = False + + # clean up + frappe.db.delete("Feedback") + frappe.db.commit() diff --git a/frappe/patches/v14_0/update_color_names_in_kanban_board_column.py b/frappe/patches/v14_0/update_color_names_in_kanban_board_column.py index f8d6f236cd..b568151273 100644 --- a/frappe/patches/v14_0/update_color_names_in_kanban_board_column.py +++ b/frappe/patches/v14_0/update_color_names_in_kanban_board_column.py @@ -1,7 +1,6 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # MIT License. See license.txt -from __future__ import unicode_literals import frappe diff --git a/frappe/patches/v14_0/update_github_endpoints.py b/frappe/patches/v14_0/update_github_endpoints.py index 8f9a06a043..5ea638f0a6 100644 --- a/frappe/patches/v14_0/update_github_endpoints.py +++ b/frappe/patches/v14_0/update_github_endpoints.py @@ -1,10 +1,9 @@ import frappe import json + def execute(): if frappe.db.exists("Social Login Key", "github"): - frappe.db.set_value("Social Login Key", "github", "auth_url_data", - json.dumps({ - "scope": "user:email" - }) + frappe.db.set_value( + "Social Login Key", "github", "auth_url_data", json.dumps({"scope": "user:email"}) ) diff --git a/frappe/patches/v14_0/update_integration_request.py b/frappe/patches/v14_0/update_integration_request.py new file mode 100644 index 0000000000..d067411166 --- /dev/null +++ b/frappe/patches/v14_0/update_integration_request.py @@ -0,0 +1,21 @@ +import frappe + + +def execute(): + doctype = "Integration Request" + + if not frappe.db.has_column(doctype, "integration_type"): + return + + frappe.db.set_value( + doctype, + {"integration_type": "Remote", "integration_request_service": ("!=", "PayPal")}, + "is_remote_request", + 1, + ) + frappe.db.set_value( + doctype, + {"integration_type": "Subscription Notification"}, + "request_description", + "Subscription Notification", + ) diff --git a/frappe/patches/v14_0/update_webforms.py b/frappe/patches/v14_0/update_webforms.py new file mode 100644 index 0000000000..46918f216e --- /dev/null +++ b/frappe/patches/v14_0/update_webforms.py @@ -0,0 +1,14 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + + +import frappe + + +def execute(): + frappe.reload_doc("website", "doctype", "web_form_list_column") + frappe.reload_doctype("Web Form") + + for web_form in frappe.db.get_all("Web Form", fields=["*"]): + if web_form.allow_multiple and not web_form.show_list: + frappe.db.set_value("Web Form", web_form.name, "show_list", True) diff --git a/frappe/patches/v14_0/update_workspace2.py b/frappe/patches/v14_0/update_workspace2.py index c6586f46a1..a6c9db503f 100644 --- a/frappe/patches/v14_0/update_workspace2.py +++ b/frappe/patches/v14_0/update_workspace2.py @@ -7,6 +7,16 @@ from frappe import _ def execute(): frappe.reload_doc("desk", "doctype", "workspace", force=True) + child_tables = frappe.get_all( + "DocField", + pluck="options", + filters={"fieldtype": ["in", frappe.model.table_fields], "parent": "Workspace"}, + ) + + for child_table in child_tables: + if child_table != "Has Role": + frappe.reload_doc("desk", "doctype", child_table, force=True) + for seq, workspace in enumerate(frappe.get_all("Workspace", order_by="name asc")): doc = frappe.get_doc("Workspace", workspace.name) content = create_content(doc) diff --git a/frappe/permissions.py b/frappe/permissions.py index 8980d2e63e..9b781015b4 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -6,7 +6,7 @@ import frappe import frappe.share from frappe import _, msgprint from frappe.query_builder import DocType -from frappe.utils import cint +from frappe.utils import cint, cstr rights = ( "select", @@ -28,6 +28,14 @@ rights = ( def check_admin_or_system_manager(user=None): + from frappe.utils.commands import warn + + warn( + "The function check_admin_or_system_manager will be deprecated in version 15." + 'Please use frappe.only_for("System Manager") instead.', + category=PendingDeprecationWarning, + ) + if not user: user = frappe.session.user @@ -360,9 +368,7 @@ def has_controller_permissions(doc, ptype, user=None): def get_doctypes_with_read(): - return list( - {p.parent if type(p.parent) == str else p.parent.encode("UTF8") for p in get_valid_perms()} - ) + return list({cstr(p.parent) for p in get_valid_perms() if p.parent}) def get_valid_perms(doctype=None, user=None): @@ -420,7 +426,7 @@ def get_roles(user=None, with_standard=True): # filter standard if required if not with_standard: - roles = filter(lambda x: x not in ["All", "Guest", "Administrator"], roles) + roles = [r for r in roles if r not in ["All", "Guest", "Administrator"]] return roles @@ -491,6 +497,7 @@ def add_user_permission( for_value=name, is_default=is_default, applicable_for=applicable_for, + apply_to_all_doctypes=0 if applicable_for else 1, hide_descendants=hide_descendants, ) ).insert(ignore_permissions=ignore_permissions) @@ -515,7 +522,7 @@ def clear_user_permissions_for_doctype(doctype, user=None): def can_import(doctype, raise_exception=False): if not ("System Manager" in frappe.get_roles() or has_permission(doctype, "import")): if raise_exception: - raise frappe.PermissionError("You are not allowed to import: {doctype}".format(doctype=doctype)) + raise frappe.PermissionError(f"You are not allowed to import: {doctype}") else: return False return True @@ -605,19 +612,17 @@ def reset_perms(doctype): frappe.db.delete("Custom DocPerm", {"parent": doctype}) -def get_linked_doctypes(dt): - return list( - set( - [dt] - + [ - d.options - for d in frappe.get_meta(dt).get( - "fields", - {"fieldtype": "Link", "ignore_user_permissions": ("!=", 1), "options": ("!=", "[Select]")}, - ) - ] +def get_linked_doctypes(dt: str) -> list: + meta = frappe.get_meta(dt) + linked_doctypes = [dt] + [ + d.options + for d in meta.get( + "fields", + {"fieldtype": "Link", "ignore_user_permissions": ("!=", 1), "options": ("!=", "[Select]")}, ) - ) + ] + + return list(set(linked_doctypes)) def get_doc_name(doc): diff --git a/frappe/printing/doctype/letter_head/letter_head.js b/frappe/printing/doctype/letter_head/letter_head.js index ca4dad2d07..55d97cf37f 100644 --- a/frappe/printing/doctype/letter_head/letter_head.js +++ b/frappe/printing/doctype/letter_head/letter_head.js @@ -1,8 +1,8 @@ // Copyright (c) 2017, Frappe Technologies and contributors // For license information, please see license.txt -frappe.ui.form.on('Letter Head', { - refresh: function(frm) { +frappe.ui.form.on("Letter Head", { + refresh: function (frm) { frm.flag_public_attachments = true; - } + }, }); diff --git a/frappe/printing/doctype/letter_head/letter_head.json b/frappe/printing/doctype/letter_head/letter_head.json index f723a6b489..d49b65ab36 100644 --- a/frappe/printing/doctype/letter_head/letter_head.json +++ b/frappe/printing/doctype/letter_head/letter_head.json @@ -9,6 +9,7 @@ "field_order": [ "letter_head_name", "source", + "footer_source", "column_break_3", "disabled", "is_default", @@ -20,7 +21,12 @@ "header_section", "content", "footer_section", - "footer" + "footer", + "footer_image_section", + "footer_image", + "footer_image_height", + "footer_image_width", + "footer_align" ], "fields": [ { @@ -93,7 +99,7 @@ "oldfieldtype": "Text Editor" }, { - "collapsible": 1, + "depends_on": "eval:doc.footer_source==='HTML' && doc.letter_head_name", "fieldname": "footer_section", "fieldtype": "Section Break", "label": "Footer" @@ -121,13 +127,48 @@ "fieldname": "image_width", "fieldtype": "Float", "label": "Image Width" + }, + { + "depends_on": "eval:doc.footer_source==='Image' && doc.letter_head_name", + "fieldname": "footer_image_section", + "fieldtype": "Section Break", + "label": "Footer Image" + }, + { + "fieldname": "footer_image", + "fieldtype": "Attach Image", + "label": "Image" + }, + { + "fieldname": "footer_image_height", + "fieldtype": "Float", + "label": "Image Height" + }, + { + "fieldname": "footer_image_width", + "fieldtype": "Float", + "label": "Image Width" + }, + { + "fieldname": "footer_align", + "fieldtype": "Select", + "label": "Align", + "options": "Left\nRight\nCenter" + }, + { + "default": "HTML", + "depends_on": "letter_head_name", + "fieldname": "footer_source", + "fieldtype": "Select", + "label": "Footer Based On", + "options": "Image\nHTML" } ], "icon": "fa fa-font", "idx": 1, "links": [], "max_attachments": 3, - "modified": "2021-10-03 14:37:58.314696", + "modified": "2022-06-16 23:10:46.852116", "modified_by": "Administrator", "module": "Printing", "name": "Letter Head", @@ -152,5 +193,6 @@ ], "sort_field": "modified", "sort_order": "ASC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/frappe/printing/doctype/letter_head/letter_head.py b/frappe/printing/doctype/letter_head/letter_head.py index 98c2fc7c2b..c48fd1fe25 100644 --- a/frappe/printing/doctype/letter_head/letter_head.py +++ b/frappe/printing/doctype/letter_head/letter_head.py @@ -26,21 +26,56 @@ class LetterHead(Document): def set_image(self): if self.source == "Image": - if self.image and is_image(self.image): - self.image_width = flt(self.image_width) - self.image_height = flt(self.image_height) - dimension = "width" if self.image_width > self.image_height else "height" - dimension_value = self.get("image_" + dimension) - self.content = f""" -
${__("No Preview")}
`); + `${__( + "No Preview" + )}
` + ); } }); }, - onload: function(frm) { + onload: function (frm) { frm.script_manager.trigger("print_style"); - } + }, }); diff --git a/frappe/printing/doctype/print_settings/print_settings.py b/frappe/printing/doctype/print_settings/print_settings.py index f52d08e6ec..36eaac2e68 100644 --- a/frappe/printing/doctype/print_settings/print_settings.py +++ b/frappe/printing/doctype/print_settings/print_settings.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2018, Frappe Technologies and contributors # License: MIT. See LICENSE diff --git a/frappe/printing/doctype/print_settings/test_print_settings.py b/frappe/printing/doctype/print_settings/test_print_settings.py index ba22df4438..6a6437bf97 100644 --- a/frappe/printing/doctype/print_settings/test_print_settings.py +++ b/frappe/printing/doctype/print_settings/test_print_settings.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2018, Frappe Technologies and Contributors # License: MIT. See LICENSE import unittest diff --git a/frappe/printing/doctype/print_style/print_style.js b/frappe/printing/doctype/print_style/print_style.js index 44c4a528f4..3177e1aa09 100644 --- a/frappe/printing/doctype/print_style/print_style.js +++ b/frappe/printing/doctype/print_style/print_style.js @@ -1,10 +1,10 @@ // Copyright (c) 2017, Frappe Technologies and contributors // For license information, please see license.txt -frappe.ui.form.on('Print Style', { - refresh: function(frm) { - frm.add_custom_button(__('Print Settings'), () => { - frappe.set_route('Form', 'Print Settings'); - }) - } +frappe.ui.form.on("Print Style", { + refresh: function (frm) { + frm.add_custom_button(__("Print Settings"), () => { + frappe.set_route("Form", "Print Settings"); + }); + }, }); diff --git a/frappe/printing/doctype/print_style/print_style.json b/frappe/printing/doctype/print_style/print_style.json index 29e88a460a..1d3c9a6189 100644 --- a/frappe/printing/doctype/print_style/print_style.json +++ b/frappe/printing/doctype/print_style/print_style.json @@ -1,214 +1,75 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 1, - "autoname": "field:print_style_name", - "beta": 0, - "creation": "2017-08-17 01:25:56.910716", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "allow_rename": 1, + "autoname": "field:print_style_name", + "creation": "2017-08-17 01:25:56.910716", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "print_style_name", + "disabled", + "standard", + "css", + "preview" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "print_style_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Print Style Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "print_style_name", + "fieldtype": "Data", + "label": "Print Style Name", + "reqd": 1, + "unique": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "disabled", - "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": "Disabled", - "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 - }, + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Disabled" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "standard", - "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": "Standard", - "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 - }, + "default": "0", + "fieldname": "standard", + "fieldtype": "Check", + "label": "Standard" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "css", - "fieldtype": "Code", - "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": "CSS", - "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 - }, + "fieldname": "css", + "fieldtype": "Code", + "label": "CSS", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "preview", - "fieldtype": "Attach Image", - "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": "Preview", - "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 + "fieldname": "preview", + "fieldtype": "Attach Image", + "hidden": 1, + "label": "Preview" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_field": "preview", - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2017-08-17 02:18:08.132853", - "modified_by": "Administrator", - "module": "Printing", - "name": "Print Style", - "name_case": "", - "owner": "Administrator", + ], + "image_field": "preview", + "links": [], + "modified": "2022-08-03 12:20:51.295775", + "modified_by": "Administrator", + "module": "Printing", + "name": "Print Style", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 } \ No newline at end of file diff --git a/frappe/printing/doctype/print_style/print_style.py b/frappe/printing/doctype/print_style/print_style.py index 00de829deb..2b0fbfe929 100644 --- a/frappe/printing/doctype/print_style/print_style.py +++ b/frappe/printing/doctype/print_style/print_style.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2017, Frappe Technologies and contributors # License: MIT. See LICENSE diff --git a/frappe/printing/doctype/print_style/test_print_style.py b/frappe/printing/doctype/print_style/test_print_style.py index ad2b61cc87..f8ce54b9bb 100644 --- a/frappe/printing/doctype/print_style/test_print_style.py +++ b/frappe/printing/doctype/print_style/test_print_style.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2017, Frappe Technologies and Contributors # License: MIT. See LICENSE import unittest diff --git a/frappe/printing/page/print/print.js b/frappe/printing/page/print/print.js index 122aea9fa1..90e7328a30 100644 --- a/frappe/printing/page/print/print.js +++ b/frappe/printing/page/print/print.js @@ -1,14 +1,14 @@ -frappe.pages['print'].on_page_load = function(wrapper) { +frappe.pages["print"].on_page_load = function (wrapper) { frappe.ui.make_app_page({ parent: wrapper, }); let print_view = new frappe.ui.form.PrintView(wrapper); - $(wrapper).bind('show', () => { + $(wrapper).bind("show", () => { const route = frappe.get_route(); const doctype = route[1]; - const docname = route[2]; + const docname = route.slice(2).join("/"); if (!frappe.route_options || !frappe.route_options.frm) { frappe.model.with_doc(doctype, docname, () => { let frm = { doctype: doctype, docname: docname }; @@ -19,7 +19,9 @@ frappe.pages['print'].on_page_load = function(wrapper) { }); }); } else { - print_view.frm = frappe.route_options.frm; + print_view.frm = frappe.route_options.frm.doctype + ? frappe.route_options.frm + : frappe.route_options.frm.frm; frappe.route_options.frm = null; print_view.show(print_view.frm); } @@ -36,7 +38,7 @@ frappe.ui.form.PrintView = class { make() { this.print_wrapper = this.page.main.empty().html( `'+__(this.print_format.doc_type)
- +'
{{ doc.name }}\
+ this.print_heading_template =
+ '\
+ ' +
+ __(this.print_format.doc_type) +
+ '
{{ doc.name }}\
';
}
this.layout_data = [];
this.fields_dict = {};
this.custom_html_dict = {};
- var section = null, column = null, me = this, custom_html_count = 0;
+ var section = null,
+ column = null,
+ me = this,
+ custom_html_count = 0;
// create a new placeholder for column and set
// it as "column"
- var set_column = function() {
- if(!section) set_section();
+ var set_column = function () {
+ if (!section) set_section();
column = me.get_new_column();
section.columns.push(column);
section.no_of_columns += 1;
- }
+ };
- var set_section = function(label) {
+ var set_section = function (label) {
section = me.get_new_section();
- if(label) section.label = label;
+ if (label) section.label = label;
column = null;
me.layout_data.push(section);
- }
+ };
// break the layout into sections and columns
// so that it is easier to render in a template
- $.each(this.data, function(i, f) {
+ $.each(this.data, function (i, f) {
me.fields_dict[f.fieldname] = f;
- if(!f.name && f.fieldname) {
+ if (!f.name && f.fieldname) {
// from format_data (designed format)
// print_hide should always be false
- if(f.fieldname==="_custom_html") {
+ if (f.fieldname === "_custom_html") {
f.label = "Custom HTML";
f.fieldtype = "Custom HTML";
// set custom html id to map data properties later
custom_html_count++;
f.custom_html_id = custom_html_count;
- me.custom_html_dict[f.custom_html_id] = f
+ me.custom_html_dict[f.custom_html_id] = f;
} else {
- f = $.extend(frappe.meta.get_docfield(me.print_format.doc_type,
- f.fieldname) || {}, f);
+ f = $.extend(
+ frappe.meta.get_docfield(me.print_format.doc_type, f.fieldname) || {},
+ f
+ );
}
}
- if(f.fieldtype==="Section Break") {
+ if (f.fieldtype === "Section Break") {
set_section(f.label);
-
- } else if(f.fieldtype==="Column Break") {
+ } else if (f.fieldtype === "Column Break") {
set_column();
+ } else if (
+ !in_list(["Section Break", "Column Break", "Tab Break", "Fold"], f.fieldtype) &&
+ f.label
+ ) {
+ if (!column) set_column();
- } else if (!in_list(["Section Break", "Column Break", "Tab Break", "Fold"], f.fieldtype)
- && f.label) {
- if(!column) set_column();
-
- if(f.fieldtype==="Table") {
+ if (f.fieldtype === "Table") {
me.add_table_properties(f);
}
- if(!f.print_hide) {
+ if (!f.print_hide) {
column.fields.push(f);
section.has_fields = true;
}
@@ -281,33 +298,38 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder {
});
// strip out empty sections
- this.layout_data = $.map(this.layout_data, function(s) {
- return s.has_fields ? s : null
+ this.layout_data = $.map(this.layout_data, function (s) {
+ return s.has_fields ? s : null;
});
}
get_new_section() {
- return {columns: [], no_of_columns: 0, label:''};
+ return { columns: [], no_of_columns: 0, label: "" };
}
get_new_column() {
- return {fields: []}
+ return { fields: [] };
}
add_table_properties(f) {
// build table columns and widths in a dict
// visible_columns
var me = this;
- if(!f.visible_columns) {
+ if (!f.visible_columns) {
me.init_visible_columns(f);
}
}
init_visible_columns(f) {
- f.visible_columns = []
- $.each(frappe.get_meta(f.options).fields, function(i, _f) {
- if (!in_list(["Section Break", "Column Break", "Tab Break"], _f.fieldtype) &&
- !_f.print_hide && f.label) {
-
+ f.visible_columns = [];
+ $.each(frappe.get_meta(f.options).fields, function (i, _f) {
+ if (
+ !in_list(["Section Break", "Column Break", "Tab Break"], _f.fieldtype) &&
+ !_f.print_hide &&
+ f.label
+ ) {
// column names set as fieldname|width
- f.visible_columns.push({fieldname: _f.fieldname,
- print_width: (_f.width || ""), print_hide:0});
+ f.visible_columns.push({
+ fieldname: _f.fieldname,
+ print_width: _f.width || "",
+ print_hide: 0,
+ });
}
});
}
@@ -315,118 +337,125 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder {
var me = this;
// drag from fields library
- Sortable.create(this.page.sidebar.find(".print-format-builder-sidebar-fields").get(0),
- {
- group: {
- name:'field', put: true, pull:"clone"
- },
- sort: false,
- onAdd: function(evt) {
- // on drop, trash!
- $(evt.item).fadeOut();
- }
- });
+ Sortable.create(this.page.sidebar.find(".print-format-builder-sidebar-fields").get(0), {
+ group: {
+ name: "field",
+ put: true,
+ pull: "clone",
+ },
+ sort: false,
+ onAdd: function (evt) {
+ // on drop, trash!
+ $(evt.item).fadeOut();
+ },
+ });
// sort, drag and drop between columns
- this.page.main.find(".print-format-builder-column").each(function() {
+ this.page.main.find(".print-format-builder-column").each(function () {
me.setup_sortable_for_column(this);
});
// section sorting
- Sortable.create(this.page.main.find(".print-format-builder-layout").get(0),
- { handle: ".print-format-builder-section-head" }
- );
+ Sortable.create(this.page.main.find(".print-format-builder-layout").get(0), {
+ handle: ".print-format-builder-section-head",
+ });
}
setup_sortable_for_column(col) {
var me = this;
Sortable.create(col, {
group: {
- name: 'field',
+ name: "field",
put: true,
- pull: true
+ pull: true,
},
- onAdd: function(evt) {
+ onAdd: function (evt) {
// on drop, change the HTML
var $item = $(evt.item);
- if(!$item.hasClass("print-format-builder-field")) {
+ if (!$item.hasClass("print-format-builder-field")) {
var fieldname = $item.attr("data-fieldname");
- if(fieldname==="_custom_html") {
+ if (fieldname === "_custom_html") {
var field = me.get_custom_html_field();
} else {
- var field = frappe.meta.get_docfield(me.print_format.doc_type,
- fieldname);
+ var field = frappe.meta.get_docfield(me.print_format.doc_type, fieldname);
}
- var html = frappe.render_template("print_format_builder_field",
- {field: field, me:me});
+ var html = frappe.render_template("print_format_builder_field", {
+ field: field,
+ me: me,
+ });
$item.replaceWith(html);
}
- }
+ },
});
-
}
setup_field_filter() {
var me = this;
- this.page.sidebar.find(".filter-fields").on("keyup", function() {
+ this.page.sidebar.find(".filter-fields").on("keyup", function () {
var text = $(this).val();
- me.page.sidebar.find(".field-label").each(function() {
- var show = !text || $(this).text().toLowerCase().indexOf(text.toLowerCase())!==-1;
+ me.page.sidebar.find(".field-label").each(function () {
+ var show =
+ !text || $(this).text().toLowerCase().indexOf(text.toLowerCase()) !== -1;
$(this).parent().toggle(show);
- })
+ });
});
}
setup_section_settings() {
var me = this;
- this.page.main.on("click", ".section-settings", function() {
+ this.page.main.on("click", ".section-settings", function () {
var section = $(this).parent().parent();
var no_of_columns = section.find(".section-column").length;
- var label = section.attr('data-label');
+ var label = section.attr("data-label");
// new dialog
var d = new frappe.ui.Dialog({
title: "Edit Section",
fields: [
{
- label:__("No of Columns"),
- fieldname:"no_of_columns",
- fieldtype:"Select",
+ label: __("No of Columns"),
+ fieldname: "no_of_columns",
+ fieldtype: "Select",
options: ["1", "2", "3", "4"],
},
{
- label:__("Section Heading"),
- fieldname:"label",
- fieldtype:"Data",
- description: __('Will only be shown if section headings are enabled')
+ label: __("Section Heading"),
+ fieldname: "label",
+ fieldtype: "Data",
+ description: __("Will only be shown if section headings are enabled"),
},
{
label: __("Remove Section"),
fieldname: "remove_section",
fieldtype: "Button",
- click: function() {
+ click: function () {
d.hide();
- section.fadeOut(function() {section.remove()});
+ section.fadeOut(function () {
+ section.remove();
+ });
},
input_class: "btn-danger",
input_css: {
- "margin-top": "20px"
- }
- }
+ "margin-top": "20px",
+ },
+ },
],
});
d.set_input("no_of_columns", no_of_columns + "");
d.set_input("label", label || "");
- d.set_primary_action(__("Update"), function() {
+ d.set_primary_action(__("Update"), function () {
// resize number of columns
- me.update_columns_in_section(section, no_of_columns,
- cint(d.get_value("no_of_columns")));
+ me.update_columns_in_section(
+ section,
+ no_of_columns,
+ cint(d.get_value("no_of_columns"))
+ );
- section.attr('data-label', d.get_value('label') || '');
- section.find('.section-label').html(d.get_value('label') || '');
+ section.attr("data-label", d.get_value("label") || "");
+ section.find(".section-label").html(d.get_value("label") || "");
d.hide();
});
@@ -437,49 +466,52 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder {
});
}
setup_field_settings() {
- this.page.main.find(".field-settings").on("click", e => {
+ this.page.main.find(".field-settings").on("click", (e) => {
const field = $(e.currentTarget).parent();
// new dialog
var d = new frappe.ui.Dialog({
- title: "Set Properties",
+ title: __("Set Properties"),
fields: [
{
label: __("Label"),
fieldname: "label",
- fieldtype: "Data"
+ fieldtype: "Data",
},
{
label: __("Align Value"),
fieldname: "align",
fieldtype: "Select",
- options: [{'label': __('Left'), 'value': 'left'}, {'label': __('Right'), 'value': 'right'}]
+ options: [
+ { label: __("Left", null, "alignment"), value: "left" },
+ { label: __("Right", null, "alignment"), value: "right" },
+ ],
},
{
label: __("Remove Field"),
fieldtype: "Button",
- click: function() {
+ click: function () {
d.hide();
field.remove();
},
input_class: "btn-danger",
- }
+ },
],
});
- d.set_value('label', field.attr("data-label"));
+ d.set_value("label", field.attr("data-label"));
- d.set_primary_action(__("Update"), function() {
- field.attr('data-align', d.get_value('align'));
- field.attr('data-label', d.get_value('label'));
- field.find('.field-label').html(d.get_value('label'));
+ d.set_primary_action(__("Update"), function () {
+ field.attr("data-align", d.get_value("align"));
+ field.attr("data-label", d.get_value("label"));
+ field.find(".field-label").html(d.get_value("label"));
d.hide();
});
// set current value
- if (field.attr('data-align')) {
- d.set_value('align', field.attr('data-align'));
+ if (field.attr("data-align")) {
+ d.set_value("align", field.attr("data-align"));
} else {
- d.set_value('align', 'left');
+ d.set_value("align", "left");
}
d.show();
@@ -494,32 +526,33 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder {
// this is based on a dummy attribute custom_html_id, since all custom html
// fields have the same fieldname `_custom_html`
var me = this;
- this.page.main.find('[data-fieldtype="Custom HTML"]').each(function() {
- var fieldname = $(this).attr('data-fieldname');
- var content = $($(this).find('.html-content')[0]);
- var html = me.custom_html_dict[parseInt(content.attr('data-custom-html-id'))].options;
- content.data('content', html);
- })
+ this.page.main.find('[data-fieldtype="Custom HTML"]').each(function () {
+ var fieldname = $(this).attr("data-fieldname");
+ var content = $($(this).find(".html-content")[0]);
+ var html = me.custom_html_dict[parseInt(content.attr("data-custom-html-id"))].options;
+ content.data("content", html);
+ });
}
update_columns_in_section(section, no_of_columns, new_no_of_columns) {
var col_size = 12 / new_no_of_columns,
me = this,
- resize = function() {
- section.find(".section-column")
+ resize = function () {
+ section
+ .find(".section-column")
.removeClass()
.addClass("section-column")
- .addClass("col-md-" + col_size)
+ .addClass("col-md-" + col_size);
};
- if(new_no_of_columns < no_of_columns) {
+ if (new_no_of_columns < no_of_columns) {
// move contents of last n columns to previous column
- for(var i=no_of_columns; i > new_no_of_columns; i--) {
- var $col = $(section.find(".print-format-builder-column").get(i-1));
- var prev = section.find(".print-format-builder-column").get(i-2);
+ for (var i = no_of_columns; i > new_no_of_columns; i--) {
+ var $col = $(section.find(".print-format-builder-column").get(i - 1));
+ var prev = section.find(".print-format-builder-column").get(i - 2);
// append each field to prev
$col.parent().addClass("to-drop");
- $col.find(".print-format-builder-field").each(function() {
+ $col.find(".print-format-builder-field").each(function () {
$(this).appendTo(prev);
});
}
@@ -529,32 +562,33 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder {
// resize
resize();
-
- } else if(new_no_of_columns > no_of_columns) {
+ } else if (new_no_of_columns > no_of_columns) {
// add empty column and resize old columns
- for(var i=no_of_columns; i < new_no_of_columns; i++) {
- var col = $('\
- ')
- .appendTo(section);
- me.setup_sortable_for_column(col
- .find(".print-format-builder-column").get(0));
+ for (var i = no_of_columns; i < new_no_of_columns; i++) {
+ var col = $(
+ '\
+ '
+ ).appendTo(section);
+ me.setup_sortable_for_column(col.find(".print-format-builder-column").get(0));
}
// resize
resize();
}
-
}
setup_add_section() {
var me = this;
- this.page.main.find(".print-format-builder-add-section").on("click", function() {
+ this.page.main.find(".print-format-builder-add-section").on("click", function () {
// boostrap new section info
var section = me.get_new_section();
section.columns.push(me.get_new_column());
section.no_of_columns = 1;
- var $section = $(frappe.render_template("print_format_builder_section",
- {section: section, me: me}))
- .appendTo(me.page.main.find(".print-format-builder-layout"))
+ var $section = $(
+ frappe.render_template("print_format_builder_section", {
+ section: section,
+ me: me,
+ })
+ ).appendTo(me.page.main.find(".print-format-builder-layout"));
me.setup_sortable_for_column($section.find(".print-format-builder-column").get(0));
});
@@ -564,23 +598,25 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder {
var $heading = this.page.main.find(".print-format-builder-print-heading");
// set content property
- $heading.data('content', this.print_heading_template);
+ $heading.data("content", this.print_heading_template);
- this.page.main.find(".edit-heading").on("click", function() {
+ this.page.main.find(".edit-heading").on("click", function () {
var d = me.get_edit_html_dialog(__("Edit Heading"), __("Heading"), $heading);
- })
+ });
}
setup_column_selector() {
var me = this;
- this.page.main.on("click", ".select-columns", function() {
+ this.page.main.on("click", ".select-columns", function () {
var parent = $(this).parents(".print-format-builder-field:first"),
doctype = parent.attr("data-doctype"),
label = parent.attr("data-label"),
columns = parent.attr("data-columns").split(","),
- column_names = $.map(columns, function(v) { return v.split("|")[0]; }),
+ column_names = $.map(columns, function (v) {
+ return v.split("|")[0];
+ }),
widths = {};
- $.each(columns, function(i, v) {
+ $.each(columns, function (i, v) {
var parts = v.split("|");
widths[parts[0]] = parts[1] || "";
});
@@ -591,46 +627,51 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder {
var $body = $(d.body);
-
var doc_fields = frappe.get_meta(doctype).fields;
var docfields_by_name = {};
// docfields by fieldname
- $.each(doc_fields, function(j, f) {
- if(f) docfields_by_name[f.fieldname] = f;
- })
+ $.each(doc_fields, function (j, f) {
+ if (f) docfields_by_name[f.fieldname] = f;
+ });
// add field which are in column_names first to preserve order
var fields = [];
- $.each(column_names, function(i, v) {
- if(in_list(Object.keys(docfields_by_name), v)) {
+ $.each(column_names, function (i, v) {
+ if (in_list(Object.keys(docfields_by_name), v)) {
fields.push(docfields_by_name[v]);
}
- })
+ });
// add remaining fields
- $.each(doc_fields, function(j, f) {
- if (f && !in_list(column_names, f.fieldname)
- && !in_list(["Section Break", "Column Break", "Tab Break"], f.fieldtype) && f.label) {
+ $.each(doc_fields, function (j, f) {
+ if (
+ f &&
+ !in_list(column_names, f.fieldname) &&
+ !in_list(["Section Break", "Column Break", "Tab Break"], f.fieldtype) &&
+ f.label
+ ) {
fields.push(f);
}
- })
+ });
// render checkboxes
- $(frappe.render_template("print_format_builder_column_selector", {
- fields: fields,
- column_names: column_names,
- widths: widths
- })).appendTo(d.body);
+ $(
+ frappe.render_template("print_format_builder_column_selector", {
+ fields: fields,
+ column_names: column_names,
+ widths: widths,
+ })
+ ).appendTo(d.body);
Sortable.create($body.find(".column-selector-list").get(0));
- var get_width_input = function(fieldname) {
- return $body.find(".column-width[data-fieldname='"+ fieldname +"']")
- }
+ var get_width_input = function (fieldname) {
+ return $body.find(".column-width[data-fieldname='" + fieldname + "']");
+ };
// update data-columns property on update
- d.set_primary_action(__("Update"), function() {
+ d.set_primary_action(__("Update"), function () {
var visible_columns = [];
- $body.find("input:checked").each(function() {
+ $body.find("input:checked").each(function () {
var fieldname = $(this).attr("data-fieldname"),
width = get_width_input(fieldname).val() || "";
visible_columns.push(fieldname + "|" + width);
@@ -642,17 +683,17 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder {
let update_column_count_message = () => {
// show a warning if user selects more than 10 columns for a table
let columns_count = $body.find("input:checked").length;
- $body.find('.help-message').toggle(columns_count > 10);
- }
+ $body.find(".help-message").toggle(columns_count > 10);
+ };
update_column_count_message();
// enable / disable input based on selection
- $body.on("click", "input[type='checkbox']", function() {
+ $body.on("click", "input[type='checkbox']", function () {
var disabled = !$(this).prop("checked"),
input = get_width_input($(this).attr("data-fieldname"));
input.prop("disabled", disabled);
- if(disabled) input.val("");
+ if (disabled) input.val("");
update_column_count_message();
});
@@ -663,19 +704,24 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder {
});
}
get_visible_columns_string(f) {
- if(!f.visible_columns) {
+ if (!f.visible_columns) {
this.init_visible_columns(f);
}
- return $.map(f.visible_columns, function(v) { return v.fieldname + "|" + (v.print_width || "") }).join(",");
+ return $.map(f.visible_columns, function (v) {
+ return v.fieldname + "|" + (v.print_width || "");
+ }).join(",");
}
get_no_content() {
- return __("Edit to add content")
+ return __("Edit to add content");
}
setup_edit_custom_html() {
var me = this;
- this.page.main.on("click", ".edit-html", function() {
- me.get_edit_html_dialog(__("Edit Custom HTML"), __("Custom HTML"),
- $(this).parents(".print-format-builder-field:first").find(".html-content"));
+ this.page.main.on("click", ".edit-html", function () {
+ me.get_edit_html_dialog(
+ __("Edit Custom HTML"),
+ __("Custom HTML"),
+ $(this).parents(".print-format-builder-field:first").find(".html-content")
+ );
});
}
get_edit_html_dialog(title, label, $content) {
@@ -687,26 +733,31 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder {
fieldname: "content",
fieldtype: "Code",
label: label,
- options: "HTML"
+ options: "HTML",
},
{
fieldname: "help",
fieldtype: "HTML",
- options: '
' +
+ __(this.print_format.doc_type) +
+ '
{{ doc.name }}\
'
- + __("You can add dynamic properties from the document by using Jinja templating.")
- + __("For example: If you want to include the document ID, use {0}", ["{{ doc.name }}"])
- + '
" +
+ __(
+ "You can add dynamic properties from the document by using Jinja templating."
+ ) +
+ __("For example: If you want to include the document ID, use {0}", [
+ "{{ doc.name }}",
+ ]) +
+ "
+ ${__("SWATCHES")}
+ ${__("COLOR PICKER")}
${section_title}
- ${for_insert ? ``: ''} + ${ + for_insert + ? `` + : "" + }' + __(d.description) + ''; + html += '
' + __(d.description) + ""; } - return $('') - .data('item.autocomplete', d) - .prop('aria-selected', 'false') - .html('
' + html + '
') + return $("") + .data("item.autocomplete", d) + .prop("aria-selected", "false") + .html("" + html + "
") .get(0); }, sort: () => { return 0; - } + }, }; } @@ -97,42 +97,41 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui } setup_awesomplete() { - this.awesomplete = new Awesomplete( - this.input, - this.get_awesomplete_settings() - ); + this.awesomplete = new Awesomplete(this.input, this.get_awesomplete_settings()); - $(this.input_area) - .find('.awesomplete ul') - .css('min-width', '100%'); + $(this.input_area).find(".awesomplete ul").css("min-width", "100%"); this.init_option_cache(); - this.$input.on('input', frappe.utils.debounce((e) => { - const cached_options = this.$input.cache[this.doctype][this.df.fieldname][e.target.value]; - if (cached_options && cached_options.length) { - this.set_data(cached_options); - } else if (this.get_query || this.df.get_query) { - this.execute_query_if_exists(e.target.value); - } else { - this.awesomplete.list = this.get_data(); - } - }, 500)); + this.$input.on( + "input", + frappe.utils.debounce((e) => { + const cached_options = + this.$input.cache[this.doctype][this.df.fieldname][e.target.value]; + if (cached_options && cached_options.length) { + this.set_data(cached_options); + } else if (this.get_query || this.df.get_query) { + this.execute_query_if_exists(e.target.value); + } else { + this.awesomplete.list = this.get_data(); + } + }, 500) + ); - this.$input.on('focus', () => { + this.$input.on("focus", () => { if (!this.$input.val()) { - this.$input.val(''); - this.$input.trigger('input'); + this.$input.val(""); + this.$input.trigger("input"); } }); this.$input.on("blur", () => { - if(this.selected) { + if (this.selected) { this.selected = false; return; } var value = this.get_input_value(); - if(value!==this.last_value) { + if (value !== this.last_value) { this.parse_validate_and_set_in_model(value); } }); @@ -145,35 +144,35 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui this.autocomplete_open = false; }); - this.$input.on('awesomplete-selectcomplete', () => { - this.$input.trigger('change'); + this.$input.on("awesomplete-selectcomplete", () => { + this.$input.trigger("change"); }); } validate(value) { if (this.df.ignore_validation) { - return value || ''; + return value || ""; } - let valid_values = this.awesomplete._list.map(d => d.value); + let valid_values = this.awesomplete._list.map((d) => d.value); if (!valid_values.length) { return value; } if (valid_values.includes(value)) { return value; } else { - return ''; + return ""; } } parse_options(options) { - if (typeof options === 'string' && options[0] === '[') { + if (typeof options === "string" && options[0] === "[") { options = frappe.utils.parse_json(options); } - if (typeof options === 'string') { - options = options.split('\n'); + if (typeof options === "string") { + options = options.split("\n"); } - if (typeof options[0] === 'string') { - options = options.map(o => ({ label: o, value: o })); + if (typeof options[0] === "string") { + options = options.map((o) => ({ label: o, value: o })); } return options; } @@ -186,8 +185,8 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui return; } - let set_nulls = function(obj) { - $.each(obj, function(key, value) { + let set_nulls = function (obj) { + $.each(obj, function (key, value) { if (value !== undefined) { obj[key] = value; } @@ -195,7 +194,7 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui return obj; }; - let process_query_object = function(obj) { + let process_query_object = function (obj) { if (obj.query) { args.query = obj.query; } @@ -217,11 +216,7 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui args.query = get_query; } else { // get_query by function - var q = get_query( - (this.frm && this.frm.doc) || this.doc, - this.doctype, - this.docname - ); + var q = get_query((this.frm && this.frm.doc) || this.doc, this.doctype, this.docname); if (typeof q === "string") { // returns a string @@ -237,13 +232,13 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui method: args.query, args: args, callback: ({ message }) => { - if(!this.$input.is(":focus")) { + if (!this.$input.is(":focus")) { return; } this.$input.cache[this.doctype][this.df.fieldname][term] = message; this.set_data(message); - } - }) + }, + }); } } diff --git a/frappe/public/js/frappe/form/controls/barcode.js b/frappe/public/js/frappe/form/controls/barcode.js index 3a678b6a97..c130ecc039 100644 --- a/frappe/public/js/frappe/form/controls/barcode.js +++ b/frappe/public/js/frappe/form/controls/barcode.js @@ -1,36 +1,34 @@ -import JsBarcode from 'jsbarcode'; +import JsBarcode from "jsbarcode"; frappe.ui.form.ControlBarcode = class ControlBarcode extends frappe.ui.form.ControlData { make_wrapper() { // Create the elements for barcode area super.make_wrapper(); - this.default_svg = '