@@ -83,12 +83,61 @@ class DocType(Document): | |||||
if not self.is_new(): | if not self.is_new(): | ||||
self.before_update = frappe.get_doc('DocType', self.name) | self.before_update = frappe.get_doc('DocType', self.name) | ||||
self.setup_fields_to_fetch() | self.setup_fields_to_fetch() | ||||
self.validate_field_name_conflicts() | |||||
check_email_append_to(self) | check_email_append_to(self) | ||||
if self.default_print_format and not self.custom: | if self.default_print_format and not self.custom: | ||||
frappe.throw(_('Standard DocType cannot have default print format, use Customize Form')) | frappe.throw(_('Standard DocType cannot have default print format, use Customize Form')) | ||||
if frappe.conf.get('developer_mode'): | |||||
self.owner = 'Administrator' | |||||
self.modified_by = 'Administrator' | |||||
def validate_field_name_conflicts(self): | |||||
"""Check if field names dont conflict with controller properties and methods""" | |||||
core_doctypes = [ | |||||
"Custom DocPerm", | |||||
"DocPerm", | |||||
"Custom Field", | |||||
"Customize Form Field", | |||||
"DocField", | |||||
] | |||||
if self.name in core_doctypes: | |||||
return | |||||
from frappe.model.base_document import get_controller | |||||
try: | |||||
controller = get_controller(self.name) | |||||
except ImportError: | |||||
controller = Document | |||||
available_objects = {x for x in dir(controller) if isinstance(x, str)} | |||||
property_set = { | |||||
x for x in available_objects if isinstance(getattr(controller, x, None), property) | |||||
} | |||||
method_set = { | |||||
x for x in available_objects if x not in property_set and callable(getattr(controller, x, None)) | |||||
} | |||||
for docfield in self.get("fields") or []: | |||||
conflict_type = None | |||||
field = docfield.fieldname | |||||
field_label = docfield.label or docfield.fieldname | |||||
if docfield.fieldname in method_set: | |||||
conflict_type = "controller method" | |||||
if docfield.fieldname in property_set: | |||||
conflict_type = "class property" | |||||
if conflict_type: | |||||
frappe.throw( | |||||
_("Fieldname '{0}' conflicting with a {1} of the name {2} in {3}") | |||||
.format(field_label, conflict_type, field, self.name) | |||||
) | |||||
def after_insert(self): | def after_insert(self): | ||||
# clear user cache so that on the next reload this doctype is included in boot | # clear user cache so that on the next reload this doctype is included in boot | ||||
clear_user_cache(frappe.session.user) | clear_user_cache(frappe.session.user) | ||||
@@ -1174,11 +1223,19 @@ def make_module_and_roles(doc, perm_fieldname="permissions"): | |||||
else: | else: | ||||
raise | raise | ||||
def check_if_fieldname_conflicts_with_methods(doctype, fieldname): | |||||
doc = frappe.get_doc({"doctype": doctype}) | |||||
method_list = [method for method in dir(doc) if isinstance(method, str) and callable(getattr(doc, method))] | |||||
def check_fieldname_conflicts(doctype, fieldname): | |||||
"""Checks if fieldname conflicts with methods or properties""" | |||||
if fieldname in method_list: | |||||
doc = frappe.get_doc({"doctype": doctype}) | |||||
available_objects = [x for x in dir(doc) if isinstance(x, str)] | |||||
property_list = [ | |||||
x for x in available_objects if isinstance(getattr(type(doc), x, None), property) | |||||
] | |||||
method_list = [ | |||||
x for x in available_objects if x not in property_list and callable(getattr(doc, x)) | |||||
] | |||||
if fieldname in method_list + property_list: | |||||
frappe.throw(_("Fieldname {0} conflicting with meta object").format(fieldname)) | frappe.throw(_("Fieldname {0} conflicting with meta object").format(fieldname)) | ||||
def clear_linked_doctype_cache(): | def clear_linked_doctype_cache(): | ||||
@@ -64,8 +64,8 @@ class CustomField(Document): | |||||
self.translatable = 0 | self.translatable = 0 | ||||
if not self.flags.ignore_validate: | if not self.flags.ignore_validate: | ||||
from frappe.core.doctype.doctype.doctype import check_if_fieldname_conflicts_with_methods | |||||
check_if_fieldname_conflicts_with_methods(self.dt, self.fieldname) | |||||
from frappe.core.doctype.doctype.doctype import check_fieldname_conflicts | |||||
check_fieldname_conflicts(self.dt, self.fieldname) | |||||
def on_update(self): | def on_update(self): | ||||
if not frappe.flags.in_setup_wizard: | if not frappe.flags.in_setup_wizard: | ||||
@@ -54,7 +54,8 @@ | |||||
"fieldname": "client_id", | "fieldname": "client_id", | ||||
"fieldtype": "Data", | "fieldtype": "Data", | ||||
"in_list_view": 1, | "in_list_view": 1, | ||||
"label": "Client Id" | |||||
"label": "Client Id", | |||||
"mandatory_depends_on": "eval:doc.redirect_uri" | |||||
}, | }, | ||||
{ | { | ||||
"fieldname": "redirect_uri", | "fieldname": "redirect_uri", | ||||
@@ -96,12 +97,14 @@ | |||||
{ | { | ||||
"fieldname": "authorization_uri", | "fieldname": "authorization_uri", | ||||
"fieldtype": "Data", | "fieldtype": "Data", | ||||
"label": "Authorization URI" | |||||
"label": "Authorization URI", | |||||
"mandatory_depends_on": "eval:doc.redirect_uri" | |||||
}, | }, | ||||
{ | { | ||||
"fieldname": "token_uri", | "fieldname": "token_uri", | ||||
"fieldtype": "Data", | "fieldtype": "Data", | ||||
"label": "Token URI" | |||||
"label": "Token URI", | |||||
"mandatory_depends_on": "eval:doc.redirect_uri" | |||||
}, | }, | ||||
{ | { | ||||
"fieldname": "revocation_uri", | "fieldname": "revocation_uri", | ||||
@@ -136,7 +139,7 @@ | |||||
"link_fieldname": "connected_app" | "link_fieldname": "connected_app" | ||||
} | } | ||||
], | ], | ||||
"modified": "2020-11-16 16:29:50.277405", | |||||
"modified": "2021-05-10 05:03:06.296863", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Integrations", | "module": "Integrations", | ||||
"name": "Connected App", | "name": "Connected App", | ||||
@@ -26,20 +26,27 @@ class ConnectedApp(Document): | |||||
self.redirect_uri = urljoin(base_url, callback_path) | self.redirect_uri = urljoin(base_url, callback_path) | ||||
def get_oauth2_session(self, user=None, init=False): | def get_oauth2_session(self, user=None, init=False): | ||||
"""Return an auto-refreshing OAuth2 session which is an extension of a requests.Session()""" | |||||
token = None | token = None | ||||
token_updater = None | token_updater = None | ||||
auto_refresh_kwargs = None | |||||
if not init: | if not init: | ||||
user = user or frappe.session.user | user = user or frappe.session.user | ||||
token_cache = self.get_user_token(user) | token_cache = self.get_user_token(user) | ||||
token = token_cache.get_json() | token = token_cache.get_json() | ||||
token_updater = token_cache.update_data | token_updater = token_cache.update_data | ||||
auto_refresh_kwargs = {'client_id': self.client_id} | |||||
client_secret = self.get_password('client_secret') | |||||
if client_secret: | |||||
auto_refresh_kwargs['client_secret'] = client_secret | |||||
return OAuth2Session( | return OAuth2Session( | ||||
client_id=self.client_id, | client_id=self.client_id, | ||||
token=token, | token=token, | ||||
token_updater=token_updater, | token_updater=token_updater, | ||||
auto_refresh_url=self.token_uri, | auto_refresh_url=self.token_uri, | ||||
auto_refresh_kwargs=auto_refresh_kwargs, | |||||
redirect_uri=self.redirect_uri, | redirect_uri=self.redirect_uri, | ||||
scope=self.get_scopes() | scope=self.get_scopes() | ||||
) | ) | ||||
@@ -34,8 +34,9 @@ def get_controller(doctype): | |||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
from frappe.utils.nestedset import NestedSet | from frappe.utils.nestedset import NestedSet | ||||
module_name, custom = frappe.db.get_value("DocType", doctype, ("module", "custom"), cache=True) \ | |||||
or ["Core", False] | |||||
module_name, custom = frappe.db.get_value( | |||||
"DocType", doctype, ("module", "custom"), cache=True | |||||
) or ["Core", False] | |||||
if custom: | if custom: | ||||
if frappe.db.field_exists("DocType", "is_tree"): | if frappe.db.field_exists("DocType", "is_tree"): | ||||
@@ -199,10 +199,39 @@ def getseries(key, digits): | |||||
def revert_series_if_last(key, name, doc=None): | def revert_series_if_last(key, name, doc=None): | ||||
if ".#" in key: | |||||
""" | |||||
Reverts the series for particular naming series: | |||||
* key is naming series - SINV-.YYYY-.#### | |||||
* name is actual name - SINV-2021-0001 | |||||
1. This function split the key into two parts prefix (SINV-YYYY) & hashes (####). | |||||
2. Use prefix to get the current index of that naming series from Series table | |||||
3. Then revert the current index. | |||||
*For custom naming series:* | |||||
1. hash can exist anywhere, if it exist in hashes then it take normal flow. | |||||
2. If hash doesn't exit in hashes, we get the hash from prefix, then update name and prefix accordingly. | |||||
*Example:* | |||||
1. key = SINV-.YYYY.- | |||||
* If key doesn't have hash it will add hash at the end | |||||
* prefix will be SINV-YYYY based on this will get current index from Series table. | |||||
2. key = SINV-.####.-2021 | |||||
* now prefix = SINV-#### and hashes = 2021 (hash doesn't exist) | |||||
* will search hash in key then accordingly get prefix = SINV- | |||||
3. key = ####.-2021 | |||||
* prefix = #### and hashes = 2021 (hash doesn't exist) | |||||
* will search hash in key then accordingly get prefix = "" | |||||
""" | |||||
if ".#" in key: | |||||
prefix, hashes = key.rsplit(".", 1) | prefix, hashes = key.rsplit(".", 1) | ||||
if "#" not in hashes: | if "#" not in hashes: | ||||
return | |||||
# get the hash part from the key | |||||
hash = re.search("#+", key) | |||||
if not hash: | |||||
return | |||||
name = name.replace(hashes, "") | |||||
prefix = prefix.replace(hash.group(), "") | |||||
else: | else: | ||||
prefix = key | prefix = key | ||||
@@ -474,14 +474,19 @@ frappe.Application = Class.extend({ | |||||
$('<link rel="icon" href="' + link + '" type="image/x-icon">').appendTo("head"); | $('<link rel="icon" href="' + link + '" type="image/x-icon">').appendTo("head"); | ||||
}, | }, | ||||
trigger_primary_action: function() { | trigger_primary_action: function() { | ||||
if(window.cur_dialog && cur_dialog.display) { | |||||
// trigger primary | |||||
cur_dialog.get_primary_btn().trigger("click"); | |||||
} else if(cur_frm && cur_frm.page.btn_primary.is(':visible')) { | |||||
cur_frm.page.btn_primary.trigger('click'); | |||||
} else if(frappe.container.page.save_action) { | |||||
frappe.container.page.save_action(); | |||||
} | |||||
// to trigger change event on active input before triggering primary action | |||||
$(document.activeElement).blur(); | |||||
// wait for possible JS validations triggered after blur (it might change primary button) | |||||
setTimeout(() => { | |||||
if (window.cur_dialog && cur_dialog.display) { | |||||
// trigger primary | |||||
cur_dialog.get_primary_btn().trigger("click"); | |||||
} else if (cur_frm && cur_frm.page.btn_primary.is(':visible')) { | |||||
cur_frm.page.btn_primary.trigger('click'); | |||||
} else if (frappe.container.page.save_action) { | |||||
frappe.container.page.save_action(); | |||||
} | |||||
}, 100); | |||||
}, | }, | ||||
set_rtl: function() { | set_rtl: function() { | ||||
@@ -7,7 +7,8 @@ export default class GridRow { | |||||
$.extend(this, opts); | $.extend(this, opts); | ||||
if (this.doc && this.parent_df.options) { | if (this.doc && this.parent_df.options) { | ||||
frappe.meta.make_docfield_copy_for(this.parent_df.options, this.doc.name, this.docfields); | frappe.meta.make_docfield_copy_for(this.parent_df.options, this.doc.name, this.docfields); | ||||
this.docfields = frappe.meta.get_docfields(this.parent_df.options, this.doc.name); | |||||
const docfields = frappe.meta.get_docfields(this.parent_df.options, this.doc.name); | |||||
this.docfields = docfields.length ? docfields : opts.docfields; | |||||
} | } | ||||
this.columns = {}; | this.columns = {}; | ||||
this.columns_list = []; | this.columns_list = []; | ||||
@@ -70,9 +70,9 @@ class TestNaming(unittest.TestCase): | |||||
name = 'TEST-{}-00001'.format(year) | name = 'TEST-{}-00001'.format(year) | ||||
frappe.db.sql("""INSERT INTO `tabSeries` (name, current) values (%s, 1)""", (series,)) | frappe.db.sql("""INSERT INTO `tabSeries` (name, current) values (%s, 1)""", (series,)) | ||||
revert_series_if_last(key, name) | revert_series_if_last(key, name) | ||||
count = frappe.db.sql("""SELECT current from `tabSeries` where name = %s""", series, as_dict=True)[0] | |||||
current_index = frappe.db.sql("""SELECT current from `tabSeries` where name = %s""", series, as_dict=True)[0] | |||||
self.assertEqual(count.get('current'), 0) | |||||
self.assertEqual(current_index.get('current'), 0) | |||||
frappe.db.sql("""delete from `tabSeries` where name = %s""", series) | frappe.db.sql("""delete from `tabSeries` where name = %s""", series) | ||||
series = 'TEST-{}-'.format(year) | series = 'TEST-{}-'.format(year) | ||||
@@ -80,9 +80,9 @@ class TestNaming(unittest.TestCase): | |||||
name = 'TEST-{}-00002'.format(year) | name = 'TEST-{}-00002'.format(year) | ||||
frappe.db.sql("""INSERT INTO `tabSeries` (name, current) values (%s, 2)""", (series,)) | frappe.db.sql("""INSERT INTO `tabSeries` (name, current) values (%s, 2)""", (series,)) | ||||
revert_series_if_last(key, name) | revert_series_if_last(key, name) | ||||
count = frappe.db.sql("""SELECT current from `tabSeries` where name = %s""", series, as_dict=True)[0] | |||||
current_index = frappe.db.sql("""SELECT current from `tabSeries` where name = %s""", series, as_dict=True)[0] | |||||
self.assertEqual(count.get('current'), 1) | |||||
self.assertEqual(current_index.get('current'), 1) | |||||
frappe.db.sql("""delete from `tabSeries` where name = %s""", series) | frappe.db.sql("""delete from `tabSeries` where name = %s""", series) | ||||
series = 'TEST-' | series = 'TEST-' | ||||
@@ -91,7 +91,29 @@ class TestNaming(unittest.TestCase): | |||||
frappe.db.sql("DELETE FROM `tabSeries` WHERE `name`=%s", series) | frappe.db.sql("DELETE FROM `tabSeries` WHERE `name`=%s", series) | ||||
frappe.db.sql("""INSERT INTO `tabSeries` (name, current) values (%s, 3)""", (series,)) | frappe.db.sql("""INSERT INTO `tabSeries` (name, current) values (%s, 3)""", (series,)) | ||||
revert_series_if_last(key, name) | revert_series_if_last(key, name) | ||||
count = frappe.db.sql("""SELECT current from `tabSeries` where name = %s""", series, as_dict=True)[0] | |||||
current_index = frappe.db.sql("""SELECT current from `tabSeries` where name = %s""", series, as_dict=True)[0] | |||||
self.assertEqual(count.get('current'), 2) | |||||
self.assertEqual(current_index.get('current'), 2) | |||||
frappe.db.sql("""delete from `tabSeries` where name = %s""", series) | |||||
series = 'TEST1-' | |||||
key = 'TEST1-.#####.-2021-22' | |||||
name = 'TEST1-00003-2021-22' | |||||
frappe.db.sql("DELETE FROM `tabSeries` WHERE `name`=%s", series) | |||||
frappe.db.sql("""INSERT INTO `tabSeries` (name, current) values (%s, 3)""", (series,)) | |||||
revert_series_if_last(key, name) | |||||
current_index = frappe.db.sql("""SELECT current from `tabSeries` where name = %s""", series, as_dict=True)[0] | |||||
self.assertEqual(current_index.get('current'), 2) | |||||
frappe.db.sql("""delete from `tabSeries` where name = %s""", series) | |||||
series = '' | |||||
key = '.#####.-2021-22' | |||||
name = '00003-2021-22' | |||||
frappe.db.sql("DELETE FROM `tabSeries` WHERE `name`=%s", series) | |||||
frappe.db.sql("""INSERT INTO `tabSeries` (name, current) values (%s, 3)""", (series,)) | |||||
revert_series_if_last(key, name) | |||||
current_index = frappe.db.sql("""SELECT current from `tabSeries` where name = %s""", series, as_dict=True)[0] | |||||
self.assertEqual(current_index.get('current'), 2) | |||||
frappe.db.sql("""delete from `tabSeries` where name = %s""", series) | frappe.db.sql("""delete from `tabSeries` where name = %s""", series) |
@@ -98,6 +98,7 @@ def get_dict(fortype, name=None): | |||||
translation_assets = cache.hget("translation_assets", frappe.local.lang, shared=True) or {} | translation_assets = cache.hget("translation_assets", frappe.local.lang, shared=True) or {} | ||||
if not asset_key in translation_assets: | if not asset_key in translation_assets: | ||||
messages = [] | |||||
if fortype=="doctype": | if fortype=="doctype": | ||||
messages = get_messages_from_doctype(name) | messages = get_messages_from_doctype(name) | ||||
elif fortype=="page": | elif fortype=="page": | ||||
@@ -109,14 +110,12 @@ def get_dict(fortype, name=None): | |||||
elif fortype=="jsfile": | elif fortype=="jsfile": | ||||
messages = get_messages_from_file(name) | messages = get_messages_from_file(name) | ||||
elif fortype=="boot": | elif fortype=="boot": | ||||
messages = [] | |||||
apps = frappe.get_all_apps(True) | apps = frappe.get_all_apps(True) | ||||
for app in apps: | for app in apps: | ||||
messages.extend(get_server_messages(app)) | messages.extend(get_server_messages(app)) | ||||
messages = deduplicate_messages(messages) | |||||
messages += frappe.db.sql("""select 'navbar', item_label from `tabNavbar Item` where item_label is not null""") | |||||
messages = get_messages_from_include_files() | |||||
messages += get_messages_from_navbar() | |||||
messages += get_messages_from_include_files() | |||||
messages += frappe.db.sql("select 'Print Format:', name from `tabPrint Format`") | messages += frappe.db.sql("select 'Print Format:', name from `tabPrint Format`") | ||||
messages += frappe.db.sql("select 'DocType:', name from tabDocType") | messages += frappe.db.sql("select 'DocType:', name from tabDocType") | ||||
messages += frappe.db.sql("select 'Role:', name from tabRole") | messages += frappe.db.sql("select 'Role:', name from tabRole") | ||||
@@ -124,6 +123,7 @@ def get_dict(fortype, name=None): | |||||
messages += frappe.db.sql("select '', format from `tabWorkspace Shortcut` where format is not null") | messages += frappe.db.sql("select '', format from `tabWorkspace Shortcut` where format is not null") | ||||
messages += frappe.db.sql("select '', title from `tabOnboarding Step`") | messages += frappe.db.sql("select '', title from `tabOnboarding Step`") | ||||
messages = deduplicate_messages(messages) | |||||
message_dict = make_dict_from_messages(messages, load_user_translation=False) | message_dict = make_dict_from_messages(messages, load_user_translation=False) | ||||
message_dict.update(get_dict_from_hooks(fortype, name)) | message_dict.update(get_dict_from_hooks(fortype, name)) | ||||
# remove untranslated | # remove untranslated | ||||
@@ -320,10 +320,22 @@ def get_messages_for_app(app, deduplicate=True): | |||||
# server_messages | # server_messages | ||||
messages.extend(get_server_messages(app)) | messages.extend(get_server_messages(app)) | ||||
# messages from navbar settings | |||||
messages.extend(get_messages_from_navbar()) | |||||
if deduplicate: | if deduplicate: | ||||
messages = deduplicate_messages(messages) | messages = deduplicate_messages(messages) | ||||
return messages | return messages | ||||
def get_messages_from_navbar(): | |||||
"""Return all labels from Navbar Items, as specified in Navbar Settings.""" | |||||
labels = frappe.get_all('Navbar Item', filters={'item_label': ('is', 'set')}, pluck='item_label') | |||||
return [('Navbar:', label, 'Label of a Navbar Item') for label in labels] | |||||
def get_messages_from_doctype(name): | def get_messages_from_doctype(name): | ||||
"""Extract all translatable messages for a doctype. Includes labels, Python code, | """Extract all translatable messages for a doctype. Includes labels, Python code, | ||||
Javascript code, html templates""" | Javascript code, html templates""" | ||||
@@ -490,8 +502,14 @@ def get_server_messages(app): | |||||
def get_messages_from_include_files(app_name=None): | def get_messages_from_include_files(app_name=None): | ||||
"""Returns messages from js files included at time of boot like desk.min.js for desk and web""" | """Returns messages from js files included at time of boot like desk.min.js for desk and web""" | ||||
messages = [] | messages = [] | ||||
for file in (frappe.get_hooks("app_include_js", app_name=app_name) or []) + (frappe.get_hooks("web_include_js", app_name=app_name) or []): | |||||
messages.extend(get_messages_from_file(os.path.join(frappe.local.sites_path, file))) | |||||
app_include_js = frappe.get_hooks("app_include_js", app_name=app_name) or [] | |||||
web_include_js = frappe.get_hooks("web_include_js", app_name=app_name) or [] | |||||
include_js = app_include_js + web_include_js | |||||
for js_path in include_js: | |||||
relative_path = os.path.join(frappe.local.sites_path, js_path.lstrip('/')) | |||||
messages_from_file = get_messages_from_file(relative_path) | |||||
messages.extend(messages_from_file) | |||||
return messages | return messages | ||||
@@ -9,6 +9,7 @@ from shutil import rmtree | |||||
import frappe | import frappe | ||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
from frappe.website.render import clear_cache | |||||
from frappe import _ | from frappe import _ | ||||
from frappe.modules.export_file import ( | from frappe.modules.export_file import ( | ||||
write_document_file, | write_document_file, | ||||
@@ -37,6 +38,19 @@ class WebTemplate(Document): | |||||
if was_standard and not self.standard: | if was_standard and not self.standard: | ||||
self.import_from_files() | self.import_from_files() | ||||
def on_update(self): | |||||
"""Clear cache for all Web Pages in which this template is used""" | |||||
routes = frappe.db.get_all( | |||||
"Web Page", | |||||
filters=[ | |||||
["Web Page Block", "web_template", "=", self.name], | |||||
["Web Page", "published", "=", 1], | |||||
], | |||||
pluck="route", | |||||
) | |||||
for route in routes: | |||||
clear_cache(route) | |||||
def on_trash(self): | def on_trash(self): | ||||
if frappe.conf.developer_mode and self.standard: | if frappe.conf.developer_mode and self.standard: | ||||
# delete template html and json files | # delete template html and json files | ||||