Merge branch 'develop' into esbuild

This commit is contained in:
Faris Ansari 2021-05-11 17:33:31 +05:30 committed by GitHub
commit d4b0be789f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 268 additions and 35 deletions

View file

@ -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 || []

View file

@ -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():

View file

@ -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:

View file

@ -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"):

View file

@ -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

View file

@ -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() {

View file

@ -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 = [];

View 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))

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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