Merge branch 'develop' into esbuild
This commit is contained in:
commit
d4b0be789f
13 changed files with 268 additions and 35 deletions
|
|
@ -103,7 +103,7 @@ frappe.ui.form.on('Auto Repeat', {
|
|||
frappe.auto_repeat.render_schedule = function(frm) {
|
||||
if (!frm.is_dirty() && frm.doc.status !== 'Disabled') {
|
||||
frm.call("get_auto_repeat_schedule").then(r => {
|
||||
frm.dashboard.wrapper.empty();
|
||||
frm.dashboard.reset();
|
||||
frm.dashboard.add_section(
|
||||
frappe.render_template("auto_repeat_schedule", {
|
||||
schedule_details: r.message || []
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -473,15 +473,20 @@ frappe.Application = class Application {
|
|||
$('<link rel="shortcut icon" href="' + link + '" type="image/x-icon">').appendTo("head");
|
||||
$('<link rel="icon" href="' + link + '" type="image/x-icon">').appendTo("head");
|
||||
}
|
||||
trigger_primary_action() {
|
||||
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();
|
||||
}
|
||||
trigger_primary_action: function() {
|
||||
// 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() {
|
||||
|
|
|
|||
|
|
@ -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 = [];
|
||||
|
|
|
|||
88
frappe/tests/test_boilerplate.py
Normal file
88
frappe/tests/test_boilerplate.py
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import ast
|
||||
import glob
|
||||
import os
|
||||
import shutil
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import frappe
|
||||
from frappe.utils.boilerplate import make_boilerplate
|
||||
|
||||
|
||||
class TestBoilerPlate(unittest.TestCase):
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
|
||||
bench_path = frappe.utils.get_bench_path()
|
||||
test_app_dir = os.path.join(bench_path, "apps", "test_app")
|
||||
if os.path.exists(test_app_dir):
|
||||
shutil.rmtree(test_app_dir)
|
||||
|
||||
def test_create_app(self):
|
||||
title = "Test App"
|
||||
description = "Test app for unit testing"
|
||||
publisher = "Test Publisher"
|
||||
email = "example@example.org"
|
||||
icon = "" # empty -> default
|
||||
color = ""
|
||||
app_license = "MIT"
|
||||
|
||||
user_input = [
|
||||
title,
|
||||
description,
|
||||
publisher,
|
||||
email,
|
||||
icon,
|
||||
color,
|
||||
app_license,
|
||||
]
|
||||
|
||||
bench_path = frappe.utils.get_bench_path()
|
||||
apps_dir = os.path.join(bench_path, "apps")
|
||||
app_name = "test_app"
|
||||
|
||||
with patch("builtins.input", side_effect=user_input):
|
||||
make_boilerplate(apps_dir, app_name)
|
||||
|
||||
root_paths = [
|
||||
app_name,
|
||||
"requirements.txt",
|
||||
"README.md",
|
||||
"setup.py",
|
||||
"license.txt",
|
||||
".git",
|
||||
]
|
||||
paths_inside_app = [
|
||||
"__init__.py",
|
||||
"hooks.py",
|
||||
"patches.txt",
|
||||
"templates",
|
||||
"www",
|
||||
"config",
|
||||
"modules.txt",
|
||||
"public",
|
||||
app_name,
|
||||
]
|
||||
|
||||
new_app_dir = os.path.join(bench_path, apps_dir, app_name)
|
||||
|
||||
all_paths = list()
|
||||
|
||||
for path in root_paths:
|
||||
all_paths.append(os.path.join(new_app_dir, path))
|
||||
|
||||
for path in paths_inside_app:
|
||||
all_paths.append(os.path.join(new_app_dir, app_name, path))
|
||||
|
||||
for path in all_paths:
|
||||
self.assertTrue(os.path.exists(path), msg=f"{path} should exist in new app")
|
||||
|
||||
# check if python files are parsable
|
||||
python_files = glob.glob(new_app_dir + "**/*.py", recursive=True)
|
||||
|
||||
for python_file in python_files:
|
||||
with open(python_file) as p:
|
||||
try:
|
||||
ast.parse(p.read())
|
||||
except Exception as e:
|
||||
self.fail(f"Can't parse python file in new app: {python_file}\n" + str(e))
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@
|
|||
|
||||
from __future__ import unicode_literals, print_function
|
||||
|
||||
from six.moves import input
|
||||
|
||||
import frappe, os, re, git
|
||||
from frappe.utils import touch_file, cstr
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue