Merge branch 'develop' into feat-desk-refresh

This commit is contained in:
Maharshi Patel 2023-09-20 22:24:33 +05:30 committed by GitHub
commit 37f03069bf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 182 additions and 81 deletions

View file

@ -413,13 +413,7 @@ def sync_database(rollback: bool) -> bool:
def serve(
host=None,
port=8000,
profile=False,
no_reload=False,
no_threading=False,
site=None,
sites_path=".",
port=8000, profile=False, no_reload=False, no_threading=False, site=None, sites_path="."
):
global application, _site, _sites_path
_site = site
@ -444,7 +438,7 @@ def serve(
log.setLevel(logging.ERROR)
run_simple(
host,
"0.0.0.0",
int(port),
application,
exclude_patterns=["test_*"],

View file

@ -481,43 +481,35 @@ def install_app(context, apps, force=False):
@click.option("--format", "-f", type=click.Choice(["text", "json"]), default="text")
@pass_context
def list_apps(context, format):
"List apps in site"
"""
List apps in site.
"""
summary_dict = {}
def fix_whitespaces(text):
if site == context.sites[-1]:
text = text.rstrip()
if len(context.sites) == 1:
text = text.lstrip()
return text
def format_app(app):
name_len = max(len(app.app_name) for app in apps)
ver_len = max(len(app.app_version) for app in apps)
template = f"{{0:{name_len}}} {{1:{ver_len}}} {{2}}"
return template.format(app.app_name, app.app_version, app.git_branch)
for site in context.sites:
frappe.init(site=site)
frappe.connect()
site_title = click.style(f"{site}", fg="green") if len(context.sites) > 1 else ""
installed_apps_info = []
apps = frappe.get_single("Installed Applications").installed_applications
if apps:
name_len, ver_len = (max(len(x.get(y)) for x in apps) for y in ["app_name", "app_version"])
template = f"{{0:{name_len}}} {{1:{ver_len}}} {{2}}"
installed_applications = [
template.format(app.app_name, app.app_version, app.git_branch) for app in apps
]
applications_summary = "\n".join(installed_applications)
summary = f"{site_title}\n{applications_summary}\n"
summary_dict[site] = [app.app_name for app in apps]
installed_apps_info.extend(format_app(app) for app in apps)
else:
installed_applications = frappe.get_installed_apps()
applications_summary = "\n".join(installed_applications)
summary = f"{site_title}\n{applications_summary}\n"
summary_dict[site] = installed_applications
installed_apps_info.extend(frappe.get_installed_apps())
summary = fix_whitespaces(summary)
installed_apps_info_str = "\n".join(installed_apps_info)
summary = f"{site_title}\n{installed_apps_info_str}\n"
summary_dict[site] = [app.app_name for app in apps]
if format == "text" and applications_summary and summary:
if format == "text" and installed_apps_info and summary:
print(summary)
frappe.destroy()

View file

@ -922,7 +922,6 @@ def run_ui_tests(
@click.command("serve")
@click.option("--host", default="127.0.0.1")
@click.option("--port", default=8000)
@click.option("--profile", is_flag=True, default=False)
@click.option("--noreload", "no_reload", is_flag=True, default=False)
@ -931,7 +930,6 @@ def run_ui_tests(
@pass_context
def serve(
context,
host="127.0.0.1",
port=None,
profile=False,
no_reload=False,
@ -953,7 +951,6 @@ def serve(
no_threading = True
no_reload = True
frappe.app.serve(
host=host,
port=port,
profile=profile,
no_reload=no_reload,

View file

@ -1,7 +1,7 @@
frappe.ui.form.on("User", {
before_load: function (frm) {
var update_tz_select = function (user_language) {
frm.set_df_property("time_zone", "options", [""].concat(frappe.all_timezones));
let update_tz_options = function () {
frm.fields_dict.time_zone.set_data(frappe.all_timezones);
};
if (!frappe.all_timezones) {
@ -9,11 +9,11 @@ frappe.ui.form.on("User", {
method: "frappe.core.doctype.user.user.get_timezones",
callback: function (r) {
frappe.all_timezones = r.message.timezones;
update_tz_select();
update_tz_options();
},
});
} else {
update_tz_select();
update_tz_options();
}
},

View file

@ -191,7 +191,7 @@
},
{
"fieldname": "time_zone",
"fieldtype": "Select",
"fieldtype": "Autocomplete",
"label": "Time Zone"
},
{
@ -762,7 +762,7 @@
"link_fieldname": "user"
}
],
"modified": "2023-06-05 17:26:04.127555",
"modified": "2023-09-18 22:19:49.933972",
"modified_by": "Administrator",
"module": "Core",
"name": "User",

View file

@ -104,7 +104,7 @@ class User(Document):
simultaneous_sessions: DF.Int
social_logins: DF.Table[UserSocialLogin]
thread_notify: DF.Check
time_zone: DF.Literal
time_zone: DF.Autocomplete | None
unsubscribed: DF.Check
user_emails: DF.Table[UserEmail]
user_image: DF.AttachImage | None

View file

@ -879,7 +879,9 @@ class Database:
"""
from frappe.model.utils import is_single_doctype
if (dn is None or dt == dn) and is_single_doctype(dt):
if dn is None or dt == dn:
if not is_single_doctype(dt):
return
deprecation_warning(
"Calling db.set_value on single doctype is deprecated. This behaviour will be removed in future. Use db.set_single_value instead."
)

View file

@ -7,7 +7,8 @@
"engine": "InnoDB",
"field_order": [
"script",
"type"
"type",
"committed"
],
"fields": [
{
@ -23,11 +24,18 @@
"hidden": 1,
"label": "Type",
"read_only": 1
},
{
"default": "0",
"fieldname": "committed",
"fieldtype": "Check",
"label": "Committed",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-07-27 22:52:37.239039",
"modified": "2023-09-19 13:02:56.332137",
"modified_by": "Administrator",
"module": "Desk",
"name": "Console Log",

View file

@ -14,6 +14,7 @@ class ConsoleLog(Document):
if TYPE_CHECKING:
from frappe.types import DF
committed: DF.Check
script: DF.Code | None
type: DF.Data | None
# end: auto-generated types

View file

@ -280,6 +280,7 @@
"options": "DocType"
},
{
"description": "If set, only user with these roles can access this chart. If not set, DocType or Report permissions will be used.",
"fieldname": "roles",
"fieldtype": "Table",
"label": "Roles",
@ -287,7 +288,7 @@
}
],
"links": [],
"modified": "2023-08-28 20:20:54.186299",
"modified": "2023-09-18 13:41:05.263676",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard Chart",

View file

@ -79,7 +79,11 @@ def has_permission(doc, ptype, user):
if "System Manager" in roles:
return True
if doc.chart_type == "Report":
if doc.roles:
allowed = [d.role for d in doc.roles]
if has_common(roles, allowed):
return True
elif doc.chart_type == "Report":
if doc.report_name in get_allowed_report_names():
return True
else:
@ -87,11 +91,6 @@ def has_permission(doc, ptype, user):
if doc.document_type in allowed_doctypes:
return True
if doc.roles:
allowed = [d.role for d in doc.roles]
if has_common(roles, allowed):
return True
return False

View file

@ -108,8 +108,8 @@ let add_custom_button = (frm) => {
tour_name: frm.doc.name,
},
});
},
delete frappe.boot.user.onboarding_status[frm.doc.name]
delete frappe.boot.user.onboarding_status[frm.doc.name];
}
);
});
} else {

View file

@ -40,7 +40,9 @@ class SystemConsole(Document):
frappe.db.commit()
else:
frappe.db.rollback()
frappe.get_doc(dict(doctype="Console Log", script=self.console, type=self.type)).insert()
frappe.get_doc(
dict(doctype="Console Log", script=self.console, type=self.type, committed=self.commit)
).insert()
frappe.db.commit()

View file

@ -311,7 +311,7 @@ def get_internal_links(doc, link, link_doctype):
elif isinstance(link, list):
# get internal links in child documents
table_fieldname, link_fieldname = link
for row in doc.get(table_fieldname):
for row in doc.get(table_fieldname) or []:
value = row.get(link_fieldname)
if value and value not in names:
names.append(value)

View file

@ -108,6 +108,7 @@ def update_global_settings(args):
update_system_settings(args)
update_user_name(args)
set_timezone(args)
def run_post_setup_complete(args):
@ -248,6 +249,12 @@ def update_user_name(args):
add_all_roles_to(args.get("name"))
def set_timezone(args):
if args.get("timezone"):
for name in frappe.STANDARD_USERS:
frappe.db.set_value("User", name, "time_zone", args.get("timezone"))
def parse_args(args):
if not args:
args = frappe.local.form_dict

View file

@ -606,6 +606,20 @@ class TestInboundMail(FrappeTestCase):
reference_doc = inbound_mail.reference_document()
self.assertEqual(todo.name, reference_doc.name)
def test_reference_document_by_subject_match_with_accents(self):
subject = "Nouvelle tâche à faire 😃"
todo = self.new_todo(sender="test_sender@example.com", description=subject)
mail_content = (
self.get_test_mail(fname="incoming-subject-placeholder.raw")
.replace("{{ subject }}", f"RE: {subject}")
.encode("utf-8")
) # note: encode to bytes because that's what triggered the error
email_account = frappe.get_doc("Email Account", "_Test Email Account 1")
inbound_mail = InboundMail(mail_content, email_account, 12345, 1)
reference_doc = inbound_mail.reference_document()
self.assertEqual(todo.name, reference_doc.name)
def test_create_communication_from_mail(self):
# Create email queue record
mail_content = self.get_test_mail(fname="incoming-2.raw")

View file

@ -402,11 +402,19 @@ class Email:
"""Parse and decode `Subject` header."""
_subject = decode_header(self.mail.get("Subject", "No Subject"))
self.subject = _subject[0][0] or ""
if _subject[0][1]:
# Encoding is known by decode_header (might also be unknown-8bit)
self.subject = safe_decode(self.subject, _subject[0][1])
else:
# assume that the encoding is utf-8
self.subject = safe_decode(self.subject)[:140]
if isinstance(self.subject, bytes):
# Fall back to utf-8 if the charset is unknown or decoding fails
# Replace invalid characters with '<?>'
self.subject = self.subject.decode("utf-8", "replace")
# Convert non-string (e.g. None)
# Truncate to 140 chars (can be used as a document name)
self.subject = str(self.subject).strip()[:140]
if not self.subject:
self.subject = "No Subject"

View file

@ -35,6 +35,7 @@ app_include_css = [
"desk.bundle.css",
"report.bundle.css",
]
app_include_icons = ["frappe/public/icons/timeless/icons.svg"]
doctype_js = {
"Web Page": "public/js/frappe/utils/web_template.js",

View file

@ -10,14 +10,16 @@ from frappe import _
from frappe.utils import get_request_session
def make_request(method, url, auth=None, headers=None, data=None):
def make_request(method, url, auth=None, headers=None, data=None, json=None):
auth = auth or ""
data = data or {}
headers = headers or {}
try:
s = get_request_session()
frappe.flags.integration_request = s.request(method, url, data=data, auth=auth, headers=headers)
frappe.flags.integration_request = s.request(
method, url, data=data, auth=auth, headers=headers, json=json
)
frappe.flags.integration_request.raise_for_status()
if frappe.flags.integration_request.headers.get("content-type") == "text/plain; charset=utf-8":

View file

@ -0,0 +1,5 @@
<svg>
<symbol id="icon-TEST-ONLY">
<rect width="24" height="24" stroke="currentColor" />
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 109 B

View file

@ -10,7 +10,7 @@ frappe.ui.form.ControlIcon = class ControlIcon extends frappe.ui.form.ControlDat
get_all_icons() {
frappe.symbols = [];
$("#frappe-symbols > symbol[id]").each(function () {
$("#all-symbols > svg > symbol[id]").each(function () {
this.id.includes("icon-") && frappe.symbols.push(this.id.replace("icon-", ""));
});
}

View file

@ -176,6 +176,11 @@ frappe.ui.form.Form = class FrappeForm {
page: this.page,
description: __("Redo last action"),
});
frappe.ui.keys.add_shortcut({
shortcut: "ctrl+p",
action: () => this.print_doc(),
description: __("Print document"),
});
let grid_shortcut_keys = [
{

View file

@ -235,7 +235,11 @@ export default class BulkOperations {
}
edit(docnames, field_mappings, done) {
let field_options = Object.keys(field_mappings).sort();
let field_options = Object.keys(field_mappings).sort(function (a, b) {
return __(cstr(field_mappings[a].label)).localeCompare(
cstr(__(field_mappings[b].label))
);
});
const status_regex = /status/i;
const default_field = field_options.find((value) => status_regex.test(value));

View file

@ -10,22 +10,12 @@
<option value="" disabled selected>{{ __("Select Group By...") }}</option>
{% for (var parent_doctype in group_by_conditions) { %}
{% for (var val in group_by_conditions[parent_doctype]) { %}
{% if (parent_doctype !== doctype) { %}
<option
data-doctype="{{parent_doctype}}"
value="{{group_by_conditions[parent_doctype][val].fieldname}}"
>
{{ __(group_by_conditions[parent_doctype][val].label) }}
({{ __(parent_doctype) }})
{{ __(group_by_conditions[parent_doctype][val].label || group_by_conditions[parent_doctype][val].fieldname) }}
</option>
{% } else { %}
<option
data-doctype="{{parent_doctype}}"
value="{{group_by_conditions[parent_doctype][val].fieldname}}"
>
{{ __(group_by_conditions[parent_doctype][val].label) }}
</option>
{% } %}
{% } %}
{% } %}
</select>

View file

@ -369,7 +369,7 @@ frappe.ui.GroupBy = class {
const tag_field = { fieldname: "_user_tags", fieldtype: "Data", label: __("Tags") };
this.group_by_fields[this.doctype] = fields
.concat(tag_field)
.sort((a, b) => __(a.label).localeCompare(__(b.label)));
.sort((a, b) => __(cstr(a.label)).localeCompare(cstr(__(b.label))));
this.all_fields[this.doctype] = this.report_view.meta.fields;
const standard_fields_filter = (df) =>
@ -407,8 +407,9 @@ frappe.ui.GroupBy = class {
}
get_group_by_field_label() {
return this.group_by_fields[this.group_by_doctype].find(
let field = this.group_by_fields[this.group_by_doctype].find(
(field) => field.fieldname == this.group_by_field
).label;
);
return field.label || field.fieldname;
}
};

View file

@ -332,10 +332,39 @@ frappe.utils.sanitise_redirect = (url) => {
};
})();
/*
* Strips out url containing the text `javascript` with or without any HTML Entities in it
**/
const sanitise_javascript = (url) => {
// please do not ask how or why
const REGEX_SCRIPT =
/j[\s]*(&#x.{1,7})?a[\s]*(&#x.{1,7})?v[\s]*(&#x.{1,7})?a[\s]*(&#x.{1,7})?s[\s]*(&#x.{1,7})?c[\s]*(&#x.{1,7})?r[\s]*(&#x.{1,7})?i[\s]*(&#x.{1,7})?p[\s]*(&#x.{1,7})?t/gi;
/*
* Written below split into parts, but actual is in one line regardless of whitespaces
* /
* j
* \s*(&#x.{1,7})?
* a
* \s*(&#x.{1,7})?
* v
* \s*(&#x.{1,7})?
* a
* \s*(&#x.{1,7})?
* s
* \s*(&#x.{1,7})?
* c
* \s*(&#x.{1,7})?
* r
* \s*(&#x.{1,7})?
* i
* \s*(&#x.{1,7})?
* p
* \s*(&#x.{1,7})?
* t
* /gi
* */
const REGEX_ESC_UNIT = /\s*(&#x.{1,7})?/;
const REGEX_SCRIPT = new RegExp(
Array.from("javascript").join(REGEX_ESC_UNIT.source),
"gi"
);
return url.replace(REGEX_SCRIPT, "");
};

View file

@ -693,6 +693,12 @@ class TestDBSetValue(FrappeTestCase):
current_value = frappe.db.get_single_value("System Settings", "deny_multiple_sessions")
self.assertEqual(current_value, changed_value)
def test_none_no_set_value(self):
frappe.db.set_value("User", None, "middle_name", "test")
with self.assertQueryCount(0):
frappe.db.set_value("User", None, "middle_name", "test")
frappe.db.set_value("User", "User", "middle_name", "test")
def test_update_single_row_single_column(self):
frappe.db.set_value("ToDo", self.todo1.name, "description", "test_set_value change 1")
updated_value = frappe.db.get_value("ToDo", self.todo1.name, "description")

View file

@ -391,6 +391,27 @@ class TestWebsite(FrappeTestCase):
delattr(frappe.local, "request")
frappe.set_user("Guest")
def test_include_icons(self):
from frappe import get_hooks
TEST_ICONS_PATH = "frappe/public/icons/timeless/test.svg"
def patched_get_hooks(*args, **kwargs):
return_value = get_hooks(*args, **kwargs)
if isinstance(return_value, dict) and "app_include_icons" in return_value:
return_value.app_include_icons.append(TEST_ICONS_PATH)
return return_value
with patch.object(frappe, "get_hooks", patched_get_hooks):
frappe.set_user("Administrator")
set_request(method="GET", path="/app")
content = get_response_content("/app")
# icon is available in a symbol tag
self.assertIn('id="icon-TEST-ONLY"', content)
delattr(frappe.local, "request")
frappe.set_user("Guest")
def patched_get_hooks(hook, value):
def wrapper(*args, **kwargs):

View file

@ -365,6 +365,11 @@ app_license = "{app_license}"
# doctype_tree_js = {{"doctype" : "public/js/doctype_tree.js"}}
# doctype_calendar_js = {{"doctype" : "public/js/doctype_calendar.js"}}
# Svg Icons
# ------------------
# include app icons in desk
# app_include_icons = "{app_name}/public/icons.svg"
# Home Pages
# ----------

View file

@ -309,7 +309,7 @@ def get_eta(from_time, percent_complete):
def _get_system_timezone():
return frappe.db.get_system_setting("time_zone") or "Asia/Kolkata" # Default to India ?!
return frappe.get_system_settings("time_zone") or "Asia/Kolkata" # Default to India ?!
def get_system_timezone():

View file

@ -27,6 +27,11 @@
<body>
{% include "public/icons/timeless/icons.svg" %}
{% include "public/icons/espresso/icons.svg" %}
<div id="all-symbols" style="display:none!important;">
{%- for path in include_icons -%}
<!-- {{ path }} -->{%- include path ignore missing -%}
{%- endfor -%}
</div>
{% include "templates/includes/splash_screen.html" %}
<div class="main-section">
<header></header>

View file

@ -44,6 +44,7 @@ def get_context(context):
include_js = hooks.get("app_include_js", []) + frappe.conf.get("app_include_js", [])
include_css = hooks.get("app_include_css", []) + frappe.conf.get("app_include_css", [])
include_icons = hooks.get("app_include_icons", [])
context.update(
{
@ -51,6 +52,7 @@ def get_context(context):
"build_version": frappe.utils.get_build_version(),
"include_js": include_js,
"include_css": include_css,
"include_icons": include_icons,
"layout_direction": "rtl" if is_rtl() else "ltr",
"lang": frappe.local.lang,
"sounds": hooks["sounds"],