@@ -83,12 +83,61 @@ class DocType(Document): | |||
if not self.is_new(): | |||
self.before_update = frappe.get_doc('DocType', self.name) | |||
self.setup_fields_to_fetch() | |||
self.validate_field_name_conflicts() | |||
check_email_append_to(self) | |||
if self.default_print_format and not self.custom: | |||
frappe.throw(_('Standard DocType cannot have default print format, use Customize Form')) | |||
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): | |||
# clear user cache so that on the next reload this doctype is included in boot | |||
clear_user_cache(frappe.session.user) | |||
@@ -1174,11 +1223,19 @@ def make_module_and_roles(doc, perm_fieldname="permissions"): | |||
else: | |||
raise | |||
def check_if_fieldname_conflicts_with_methods(doctype, fieldname): | |||
doc = frappe.get_doc({"doctype": doctype}) | |||
method_list = [method for method in dir(doc) if isinstance(method, str) and callable(getattr(doc, method))] | |||
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)) | |||
def clear_linked_doctype_cache(): | |||
@@ -64,8 +64,8 @@ class CustomField(Document): | |||
self.translatable = 0 | |||
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): | |||
if not frappe.flags.in_setup_wizard: | |||
@@ -54,7 +54,8 @@ | |||
"fieldname": "client_id", | |||
"fieldtype": "Data", | |||
"in_list_view": 1, | |||
"label": "Client Id" | |||
"label": "Client Id", | |||
"mandatory_depends_on": "eval:doc.redirect_uri" | |||
}, | |||
{ | |||
"fieldname": "redirect_uri", | |||
@@ -96,12 +97,14 @@ | |||
{ | |||
"fieldname": "authorization_uri", | |||
"fieldtype": "Data", | |||
"label": "Authorization URI" | |||
"label": "Authorization URI", | |||
"mandatory_depends_on": "eval:doc.redirect_uri" | |||
}, | |||
{ | |||
"fieldname": "token_uri", | |||
"fieldtype": "Data", | |||
"label": "Token URI" | |||
"label": "Token URI", | |||
"mandatory_depends_on": "eval:doc.redirect_uri" | |||
}, | |||
{ | |||
"fieldname": "revocation_uri", | |||
@@ -136,7 +139,7 @@ | |||
"link_fieldname": "connected_app" | |||
} | |||
], | |||
"modified": "2020-11-16 16:29:50.277405", | |||
"modified": "2021-05-10 05:03:06.296863", | |||
"modified_by": "Administrator", | |||
"module": "Integrations", | |||
"name": "Connected App", | |||
@@ -26,20 +26,27 @@ class ConnectedApp(Document): | |||
self.redirect_uri = urljoin(base_url, callback_path) | |||
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_updater = None | |||
auto_refresh_kwargs = None | |||
if not init: | |||
user = user or frappe.session.user | |||
token_cache = self.get_user_token(user) | |||
token = token_cache.get_json() | |||
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( | |||
client_id=self.client_id, | |||
token=token, | |||
token_updater=token_updater, | |||
auto_refresh_url=self.token_uri, | |||
auto_refresh_kwargs=auto_refresh_kwargs, | |||
redirect_uri=self.redirect_uri, | |||
scope=self.get_scopes() | |||
) | |||
@@ -34,8 +34,9 @@ def get_controller(doctype): | |||
from frappe.model.document import Document | |||
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 frappe.db.field_exists("DocType", "is_tree"): | |||
@@ -199,10 +199,39 @@ def getseries(key, digits): | |||
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) | |||
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: | |||
prefix = key | |||
@@ -474,14 +474,19 @@ frappe.Application = Class.extend({ | |||
$('<link rel="icon" href="' + link + '" type="image/x-icon">').appendTo("head"); | |||
}, | |||
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() { | |||
@@ -7,7 +7,8 @@ export default class GridRow { | |||
$.extend(this, opts); | |||
if (this.doc && this.parent_df.options) { | |||
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_list = []; | |||
@@ -70,9 +70,9 @@ class TestNaming(unittest.TestCase): | |||
name = 'TEST-{}-00001'.format(year) | |||
frappe.db.sql("""INSERT INTO `tabSeries` (name, current) values (%s, 1)""", (series,)) | |||
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) | |||
series = 'TEST-{}-'.format(year) | |||
@@ -80,9 +80,9 @@ class TestNaming(unittest.TestCase): | |||
name = 'TEST-{}-00002'.format(year) | |||
frappe.db.sql("""INSERT INTO `tabSeries` (name, current) values (%s, 2)""", (series,)) | |||
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) | |||
series = 'TEST-' | |||
@@ -91,7 +91,29 @@ class TestNaming(unittest.TestCase): | |||
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) | |||
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) |
@@ -98,6 +98,7 @@ def get_dict(fortype, name=None): | |||
translation_assets = cache.hget("translation_assets", frappe.local.lang, shared=True) or {} | |||
if not asset_key in translation_assets: | |||
messages = [] | |||
if fortype=="doctype": | |||
messages = get_messages_from_doctype(name) | |||
elif fortype=="page": | |||
@@ -109,14 +110,12 @@ def get_dict(fortype, name=None): | |||
elif fortype=="jsfile": | |||
messages = get_messages_from_file(name) | |||
elif fortype=="boot": | |||
messages = [] | |||
apps = frappe.get_all_apps(True) | |||
for app in apps: | |||
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 'DocType:', name from tabDocType") | |||
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 '', title from `tabOnboarding Step`") | |||
messages = deduplicate_messages(messages) | |||
message_dict = make_dict_from_messages(messages, load_user_translation=False) | |||
message_dict.update(get_dict_from_hooks(fortype, name)) | |||
# remove untranslated | |||
@@ -320,10 +320,22 @@ def get_messages_for_app(app, deduplicate=True): | |||
# server_messages | |||
messages.extend(get_server_messages(app)) | |||
# messages from navbar settings | |||
messages.extend(get_messages_from_navbar()) | |||
if deduplicate: | |||
messages = deduplicate_messages(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): | |||
"""Extract all translatable messages for a doctype. Includes labels, Python code, | |||
Javascript code, html templates""" | |||
@@ -490,8 +502,14 @@ def get_server_messages(app): | |||
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""" | |||
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 | |||
@@ -9,6 +9,7 @@ from shutil import rmtree | |||
import frappe | |||
from frappe.model.document import Document | |||
from frappe.website.render import clear_cache | |||
from frappe import _ | |||
from frappe.modules.export_file import ( | |||
write_document_file, | |||
@@ -37,6 +38,19 @@ class WebTemplate(Document): | |||
if was_standard and not self.standard: | |||
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): | |||
if frappe.conf.developer_mode and self.standard: | |||
# delete template html and json files | |||