Przeglądaj źródła

Merge branch 'develop' of github.com:frappe/frappe into multistep_webforms

version-14
hrwx 3 lat temu
rodzic
commit
cc5723b42f
65 zmienionych plików z 1147 dodań i 358 usunięć
  1. +3
    -1
      cypress/integration/control_rating.js
  2. +52
    -11
      cypress/integration/multi_select_dialog.js
  3. +15
    -6
      frappe/__init__.py
  4. +1
    -0
      frappe/client.py
  5. +1
    -2
      frappe/commands/site.py
  6. +6
    -4
      frappe/core/doctype/data_import/exporter.py
  7. +73
    -50
      frappe/core/doctype/data_import/importer.py
  8. +1
    -1
      frappe/core/doctype/doctype/doctype.py
  9. +3
    -2
      frappe/core/doctype/file/file.py
  10. +1
    -1
      frappe/core/doctype/report/boilerplate/controller.js
  11. +2
    -2
      frappe/core/doctype/report/boilerplate/controller.py
  12. +0
    -7
      frappe/core/doctype/server_script/server_script_utils.py
  13. +168
    -0
      frappe/custom/fixtures/temp_doctype.json
  14. +168
    -0
      frappe/custom/fixtures/temp_singles.json
  15. +23
    -10
      frappe/database/database.py
  16. +3
    -2
      frappe/database/mariadb/database.py
  17. +1
    -1
      frappe/database/mariadb/setup_db.py
  18. +5
    -2
      frappe/database/postgres/database.py
  19. +4
    -4
      frappe/desk/page/leaderboard/leaderboard.js
  20. +32
    -28
      frappe/desk/page/setup_wizard/setup_wizard.js
  21. +1
    -1
      frappe/desk/page/setup_wizard/setup_wizard.py
  22. +18
    -4
      frappe/handler.py
  23. +7
    -0
      frappe/installer.py
  24. +1
    -1
      frappe/integrations/doctype/webhook/__init__.py
  25. +10
    -5
      frappe/model/db_query.py
  26. +3
    -4
      frappe/model/document.py
  27. +15
    -7
      frappe/model/rename_doc.py
  28. +1
    -1
      frappe/patches.txt
  29. +1
    -1
      frappe/patches/v14_0/remove_db_aggregation.py
  30. +34
    -7
      frappe/patches/v14_0/save_ratings_in_fraction.py
  31. +12
    -1
      frappe/printing/doctype/print_format/print_format.py
  32. +4
    -3
      frappe/printing/page/print/print.js
  33. +2
    -2
      frappe/public/js/frappe/data_import/import_preview.js
  34. +3
    -0
      frappe/public/js/frappe/form/controls/datetime.js
  35. +4
    -4
      frappe/public/js/frappe/form/controls/link.js
  36. +44
    -29
      frappe/public/js/frappe/form/controls/rating.js
  37. +15
    -3
      frappe/public/js/frappe/form/form.js
  38. +10
    -6
      frappe/public/js/frappe/form/formatters.js
  39. +41
    -20
      frappe/public/js/frappe/form/multi_select_dialog.js
  40. +5
    -6
      frappe/public/js/frappe/list/list_view.js
  41. +4
    -0
      frappe/public/js/frappe/ui/page.js
  42. +0
    -1
      frappe/public/js/frappe/ui/tree.js
  43. +1
    -1
      frappe/public/js/frappe/utils/address_and_contact.js
  44. +2
    -4
      frappe/public/js/frappe/utils/user.js
  45. +2
    -1
      frappe/public/js/frappe/utils/utils.js
  46. +0
    -5
      frappe/public/js/frappe/views/container.js
  47. +2
    -4
      frappe/public/js/frappe/views/treeview.js
  48. +2
    -2
      frappe/public/scss/common/form.scss
  49. +9
    -5
      frappe/public/scss/common/grid.scss
  50. +22
    -12
      frappe/public/scss/desk/form.scss
  51. +19
    -16
      frappe/public/scss/desk/page.scss
  52. +11
    -10
      frappe/public/scss/print.bundle.scss
  53. +22
    -0
      frappe/templates/signup.html
  54. +23
    -1
      frappe/tests/test_db.py
  55. +81
    -0
      frappe/tests/test_fixture_import.py
  56. +24
    -1
      frappe/tests/test_safe_exec.py
  57. +3
    -1
      frappe/tests/test_translate.py
  58. +6
    -0
      frappe/utils/boilerplate.py
  59. +4
    -4
      frappe/utils/data.py
  60. +17
    -3
      frappe/utils/file_manager.py
  61. +60
    -7
      frappe/utils/safe_exec.py
  62. +2
    -2
      frappe/www/404.html
  63. +2
    -23
      frappe/www/login.html
  64. +12
    -0
      frappe/www/login.py
  65. +24
    -16
      frappe/www/update-password.html

+ 3
- 1
cypress/integration/control_rating.js Wyświetl plik

@@ -20,12 +20,13 @@ context('Control Rating', () => {

cy.get('div.rating')
.children('svg')
.find('.right-half')
.first()
.click()
.should('have.class', 'star-click');
cy.get('@dialog').then(dialog => {
var value = dialog.get_value('rate');
expect(value).to.equal(1);
expect(value).to.equal(1/7);
dialog.hide();
});
});
@@ -35,6 +36,7 @@ context('Control Rating', () => {

cy.get('div.rating')
.children('svg')
.find('.right-half')
.first()
.invoke('trigger', 'mouseenter')
.should('have.class', 'star-hover')


+ 52
- 11
cypress/integration/multi_select_dialog.js Wyświetl plik

@@ -2,32 +2,47 @@ context('MultiSelectDialog', () => {
before(() => {
cy.login();
cy.visit('/app');
const contact_template = {
"doctype": "Contact",
"first_name": "Test",
"status": "Passive",
"email_ids": [
{
"doctype": "Contact Email",
"email_id": "test@example.com",
"is_primary": 0
}
]
};
const promises = Array.from({length: 25})
.map(() => cy.insert_doc('Contact', contact_template, true));
Promise.all(promises);
});

function open_multi_select_dialog() {
cy.window().its('frappe').then(frappe => {
new frappe.ui.form.MultiSelectDialog({
doctype: "Assignment Rule",
doctype: "Contact",
target: {},
setters: {
document_type: null,
priority: null
status: null,
gender: null
},
add_filters_group: 1,
allow_child_item_selection: 1,
child_fieldname: "assignment_days",
child_columns: ["day"]
child_fieldname: "email_ids",
child_columns: ["email_id", "is_primary"]
});
});
}

it('multi select dialog api works', () => {
it('checks multi select dialog api works', () => {
open_multi_select_dialog();
cy.get_open_dialog().should('contain', 'Select Assignment Rules');
cy.get_open_dialog().should('contain', 'Select Contacts');
});

it('checks for filters', () => {
['search_term', 'document_type', 'priority'].forEach(fieldname => {
['search_term', 'status', 'gender'].forEach(fieldname => {
cy.get_open_dialog().get(`.frappe-control[data-fieldname="${fieldname}"]`).should('exist');
});

@@ -42,17 +57,43 @@ context('MultiSelectDialog', () => {

cy.get_open_dialog()
.get(`.frappe-control[data-fieldname="allow_child_item_selection"]`)
.find('input[data-fieldname="allow_child_item_selection"]')
.should('exist')
.click();
.click({force: true});

cy.get_open_dialog()
.get(`.frappe-control[data-fieldname="child_selection_area"]`)
.should('exist');

cy.get_open_dialog()
.get(`.dt-row-header`).should('contain', 'Assignment Rule');
.get(`.dt-row-header`).should('contain', 'Contact');

cy.get_open_dialog()
.get(`.dt-row-header`).should('contain', 'Day');
.get(`.dt-row-header`).should('contain', 'Email Id');

cy.get_open_dialog()
.get(`.dt-row-header`).should('contain', 'Is Primary');
});

it('tests more button', () => {
cy.get_open_dialog()
.get(`.frappe-control[data-fieldname="more_btn"]`)
.should('exist')
.as('more-btn');
cy.get_open_dialog().get('.list-item-container').should(($rows) => {
expect($rows).to.have.length(20);
});

cy.intercept('POST', 'api/method/frappe.client.get_list').as('get-more-records');
cy.get('@more-btn').find('button').click({force: true});
cy.wait('@get-more-records');

cy.get_open_dialog().get('.list-item-container').should(($rows) => {
if ($rows.length <= 20) {
throw new Error("More button doesn't work");
}
});

});
});

+ 15
- 6
frappe/__init__.py Wyświetl plik

@@ -740,17 +740,26 @@ def has_permission(doctype=None, ptype="read", doc=None, user=None, verbose=Fals
:param doc: [optional] Checks User permissions for given doc.
:param user: [optional] Check for given user. Default: current user.
:param parent_doctype: Required when checking permission for a child DocType (unless doc is specified)."""
import frappe.permissions

if not doctype and doc:
doctype = doc.doctype

import frappe.permissions
out = frappe.permissions.has_permission(doctype, ptype, doc=doc, verbose=verbose, user=user,
raise_exception=throw, parent_doctype=parent_doctype)

if throw and not out:
if doc:
frappe.throw(_("No permission for {0}").format(doc.doctype + " " + doc.name))
else:
frappe.throw(_("No permission for {0}").format(doctype))
# mimics frappe.throw
document_label = f"{doc.doctype} {doc.name}" if doc else doctype
msgprint(
_("No permission for {0}").format(document_label),
raise_exception=ValidationError,
title=None,
indicator='red',
is_minimizable=None,
wide=None,
as_list=False
)

return out

@@ -1203,7 +1212,7 @@ def read_file(path, raise_not_found=False):
def get_attr(method_string):
"""Get python method object from its name."""
app_name = method_string.split(".")[0]
if not local.flags.in_install and app_name not in get_installed_apps():
if not local.flags.in_uninstall and not local.flags.in_install and app_name not in get_installed_apps():
throw(_("App {0} is not installed").format(app_name), AppNotInstalledError)

modulename = '.'.join(method_string.split('.')[:-1])


+ 1
- 0
frappe/client.py Wyświetl plik

@@ -32,6 +32,7 @@ def get_list(doctype, fields=None, filters=None, order_by=None,

args = frappe._dict(
doctype=doctype,
parent_doctype=parent,
fields=fields,
filters=filters,
or_filters=or_filters,


+ 1
- 2
frappe/commands/site.py Wyświetl plik

@@ -698,8 +698,7 @@ def _drop_site(site, root_login='root', root_password=None, archived_sites_path=

archived_sites_path = archived_sites_path or os.path.join(frappe.get_app_path('frappe'), '..', '..', '..', 'archived', 'sites')

if not os.path.exists(archived_sites_path):
os.mkdir(archived_sites_path)
os.makedirs(archived_sites_path, exist_ok=True)

move(archived_sites_path, site)



+ 6
- 4
frappe/core/doctype/data_import/exporter.py Wyświetl plik

@@ -5,6 +5,7 @@
import typing

import frappe
from frappe import _
from frappe.model import (
display_fieldtypes,
no_value_fields,
@@ -215,9 +216,9 @@ class Exporter:
for df in self.fields:
is_parent = not df.is_child_table_field
if is_parent:
label = df.label
label = _(df.label)
else:
label = "{0} ({1})".format(df.label, df.child_table_df.label)
label = "{0} ({1})".format(_(df.label), _(df.child_table_df.label))

if label in header:
# this label is already in the header,
@@ -227,6 +228,7 @@ class Exporter:
label = "{0}".format(df.fieldname)
else:
label = "{0}.{1}".format(df.child_table_df.fieldname, df.fieldname)

header.append(label)

self.csv_array.append(header)
@@ -253,10 +255,10 @@ class Exporter:
self.build_xlsx_response()

def build_csv_response(self):
build_csv_response(self.get_csv_array_for_export(), self.doctype)
build_csv_response(self.get_csv_array_for_export(), _(self.doctype))

def build_xlsx_response(self):
build_xlsx_response(self.get_csv_array_for_export(), self.doctype)
build_xlsx_response(self.get_csv_array_for_export(), _(self.doctype))

def group_children_data_by_parent(self, children_data: typing.Dict[str, list]):
return groupby_metric(children_data, key='parent')

+ 73
- 50
frappe/core/doctype/data_import/importer.py Wyświetl plik

@@ -262,7 +262,7 @@ class Importer:
rows = [header_row]
rows += [row.data for row in self.import_file.data if row.row_number in row_indexes]

build_csv_response(rows, self.doctype)
build_csv_response(rows, _(self.doctype))

def print_import_log(self, import_log):
failed_records = [log for log in import_log if not log.success]
@@ -1009,18 +1009,14 @@ def build_fields_dict_for_column_matching(parent_doctype):
out = {}

# doctypes and fieldname if it is a child doctype
doctypes = [[parent_doctype, None]] + [
[df.options, df] for df in parent_meta.get_table_fields()
doctypes = [(parent_doctype, None)] + [
(df.options, df) for df in parent_meta.get_table_fields()
]

for doctype, table_df in doctypes:
translated_table_label = _(table_df.label) if table_df else None

# name field
name_by_label = (
"ID" if doctype == parent_doctype else "ID ({0})".format(table_df.label)
)
name_by_fieldname = (
"name" if doctype == parent_doctype else "{0}.name".format(table_df.fieldname)
)
name_df = frappe._dict(
{
"fieldtype": "Data",
@@ -1031,63 +1027,90 @@ def build_fields_dict_for_column_matching(parent_doctype):
}
)

if doctype != parent_doctype:
if doctype == parent_doctype:
name_headers = (
"name", # fieldname
"ID", # label
_("ID"), # translated label
)
else:
name_headers = (
"{0}.name".format(table_df.fieldname), # fieldname
"ID ({0})".format(table_df.label), # label
"{0} ({1})".format(_("ID"), translated_table_label), # translated label
)

name_df.is_child_table_field = True
name_df.child_table_df = table_df

out[name_by_label] = name_df
out[name_by_fieldname] = name_df
for header in name_headers:
out[header] = name_df

# other fields
fields = get_standard_fields(doctype) + frappe.get_meta(doctype).fields
for df in fields:
label = (df.label or "").strip()
fieldtype = df.fieldtype or "Data"
if fieldtype in no_value_fields:
continue

label = (df.label or "").strip()
translated_label = _(label)
parent = df.parent or parent_doctype
if fieldtype not in no_value_fields:
if parent_doctype == doctype:
# for parent doctypes keys will be
# Label
# label
# Label (label)
if not out.get(label):
# if Label is already set, don't set it again
# in case of duplicate column headers
out[label] = df
out[df.fieldname] = df
label_with_fieldname = "{0} ({1})".format(label, df.fieldname)
out[label_with_fieldname] = df

if parent_doctype == doctype:
# for parent doctypes keys will be
# Label, fieldname, Label (fieldname)

for header in (label, translated_label):
# if Label is already set, don't set it again
# in case of duplicate column headers
if header not in out:
out[header] = df

for header in (
df.fieldname,
f"{label} ({df.fieldname})",
f"{translated_label} ({df.fieldname})"
):
out[header] = df

else:
# for child doctypes keys will be
# Label (Table Field Label)
# table_field.fieldname

# create a new df object to avoid mutation problems
if isinstance(df, dict):
new_df = frappe._dict(df.copy())
else:
# in case there are multiple table fields with the same doctype
# for child doctypes keys will be
# Label (Table Field Label)
# table_field.fieldname
table_fields = parent_meta.get(
"fields", {"fieldtype": ["in", table_fieldtypes], "options": parent}
)
for table_field in table_fields:
by_label = "{0} ({1})".format(label, table_field.label)
by_fieldname = "{0}.{1}".format(table_field.fieldname, df.fieldname)
new_df = df.as_dict()

# create a new df object to avoid mutation problems
if isinstance(df, dict):
new_df = frappe._dict(df.copy())
else:
new_df = df.as_dict()
new_df.is_child_table_field = True
new_df.child_table_df = table_df

new_df.is_child_table_field = True
new_df.child_table_df = table_field
out[by_label] = new_df
out[by_fieldname] = new_df
for header in (
# fieldname
"{0}.{1}".format(table_df.fieldname, df.fieldname),
# label
"{0} ({1})".format(label, table_df.label),
# translated label
"{0} ({1})".format(translated_label, translated_table_label),
):
out[header] = new_df

# if autoname is based on field
# add an entry for "ID (Autoname Field)"
autoname_field = get_autoname_field(parent_doctype)
if autoname_field:
out["ID ({})".format(autoname_field.label)] = autoname_field
# ID field should also map to the autoname field
out["ID"] = autoname_field
out["name"] = autoname_field
for header in (
"ID ({})".format(autoname_field.label), # label
"{0} ({1})".format(_("ID"), _(autoname_field.label)), # translated label

# ID field should also map to the autoname field
"ID",
_("ID"),
"name",
):
out[header] = autoname_field

return out



+ 1
- 1
frappe/core/doctype/doctype/doctype.py Wyświetl plik

@@ -381,7 +381,7 @@ class DocType(Document):
document_cls_tag = f"class {despaced_name}(Document)"
document_import_tag = "from frappe.model.document import Document"
website_generator_cls_tag = f"class {despaced_name}(WebsiteGenerator)"
website_generator_import_tag = "from frappe.website.generators.website_generator import WebsiteGenerator"
website_generator_import_tag = "from frappe.website.website_generator import WebsiteGenerator"

with open(controller_path) as f:
code = f.read()


+ 3
- 2
frappe/core/doctype/file/file.py Wyświetl plik

@@ -29,6 +29,7 @@ from frappe import _, conf, safe_decode
from frappe.model.document import Document
from frappe.utils import call_hook_method, cint, cstr, encode, get_files_path, get_hook_method, random_string, strip
from frappe.utils.image import strip_exif_data, optimize_image
from frappe.utils.file_manager import safe_b64decode

class MaxFileSizeReachedError(frappe.ValidationError):
pass
@@ -436,7 +437,7 @@ class File(Document):

if b"," in self.content:
self.content = self.content.split(b",")[1]
self.content = base64.b64decode(self.content)
self.content = safe_b64decode(self.content)

if not self.is_private:
self.is_private = 0
@@ -852,7 +853,7 @@ def extract_images_from_html(doc, content, is_private=False):
content = content.encode("utf-8")
if b"," in content:
content = content.split(b",")[1]
content = base64.b64decode(content)
content = safe_b64decode(content)

content = optimize_image(content, mtype)



+ 1
- 1
frappe/core/doctype/report/boilerplate/controller.js Wyświetl plik

@@ -1,4 +1,4 @@
// Copyright (c) 2016, {app_publisher} and contributors
// Copyright (c) {year}, {app_publisher} and contributors
// For license information, please see license.txt
/* eslint-disable */



+ 2
- 2
frappe/core/doctype/report/boilerplate/controller.py Wyświetl plik

@@ -1,5 +1,5 @@
# Copyright (c) 2013, {app_publisher} and contributors
# License: MIT. See LICENSE
# Copyright (c) {year}, {app_publisher} and contributors
# For license information, please see license.txt

# import frappe



+ 0
- 7
frappe/core/doctype/server_script/server_script_utils.py Wyświetl plik

@@ -19,13 +19,6 @@ EVENT_MAP = {
'on_update_after_submit': 'After Save (Submitted Document)'
}

def run_server_script_api(method):
# called via handler, execute an API script
script_name = get_server_script_map().get('_api', {}).get(method)
if script_name:
frappe.get_doc('Server Script', script_name).execute_method()
return True

def run_server_script_for_doc_event(doc, event):
# run document event method
if not event in EVENT_MAP:


+ 168
- 0
frappe/custom/fixtures/temp_doctype.json Wyświetl plik

@@ -0,0 +1,168 @@
{
"docstatus": 0,
"doctype": "DocType",
"name": "new-doctype-2",
"__islocal": 1,
"__unsaved": 1,
"owner": "Administrator",
"is_submittable": 0,
"istable": 0,
"issingle": 0,
"is_tree": 0,
"editable_grid": 1,
"quick_entry": 1,
"track_changes": 1,
"track_seen": 0,
"track_views": 0,
"custom": 1,
"beta": 0,
"is_virtual": 0,
"naming_rule": "",
"name_case": "",
"allow_rename": 1,
"hide_toolbar": 0,
"allow_copy": 0,
"allow_import": 0,
"allow_events_in_timeline": 0,
"allow_auto_repeat": 0,
"sort_field": "modified",
"sort_order": "DESC",
"document_type": "",
"show_preview_popup": 0,
"show_name_in_global_search": 0,
"email_append_to": 0,
"read_only": 0,
"in_create": 0,
"has_web_view": 0,
"allow_guest_to_view": 0,
"index_web_pages_for_search": 1,
"engine": "InnoDB",
"permissions": [
{
"docstatus": 0,
"doctype": "DocPerm",
"name": "new-docperm-2",
"__islocal": 1,
"__unsaved": 1,
"owner": "Administrator",
"if_owner": 0,
"permlevel": 0,
"select": 0,
"read": 1,
"write": 1,
"create": 1,
"delete": 1,
"submit": 0,
"cancel": 0,
"amend": 0,
"report": 1,
"export": 1,
"import": 0,
"set_user_permissions": 0,
"share": 1,
"print": 1,
"email": 1,
"parent": "new-doctype-2",
"parentfield": "permissions",
"parenttype": "DocType",
"idx": 1,
"role": "System Manager"
}
],
"__newname": "temp_doctype",
"module": "Custom",
"fields": [
{
"docstatus": 0,
"doctype": "DocField",
"name": "new-docfield-1",
"__islocal": 1,
"__unsaved": 1,
"owner": "Administrator",
"fieldtype": "Data",
"precision": "",
"non_negative": 0,
"hide_days": 0,
"hide_seconds": 0,
"reqd": 1,
"search_index": 0,
"fetch_if_empty": 0,
"hidden": 0,
"bold": 0,
"allow_in_quick_entry": 0,
"translatable": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"report_hide": 0,
"collapsible": 0,
"hide_border": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"in_preview": 0,
"in_filter": 0,
"in_global_search": 0,
"read_only": 0,
"allow_on_submit": 0,
"ignore_user_permissions": 0,
"allow_bulk_edit": 0,
"permlevel": 0,
"ignore_xss_filter": 0,
"unique": 0,
"no_copy": 0,
"set_only_once": 0,
"remember_last_selected_value": 0,
"parent": "new-doctype-2",
"parentfield": "fields",
"parenttype": "DocType",
"idx": 1,
"__unedited": false,
"label": "member_name"
},
{
"docstatus": 0,
"doctype": "DocField",
"name": "new-docfield-2",
"__islocal": 1,
"__unsaved": 1,
"owner": "Administrator",
"fieldtype": "Data",
"precision": "",
"non_negative": 0,
"hide_days": 0,
"hide_seconds": 0,
"reqd": 0,
"search_index": 0,
"fetch_if_empty": 0,
"hidden": 0,
"bold": 0,
"allow_in_quick_entry": 0,
"translatable": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"report_hide": 0,
"collapsible": 0,
"hide_border": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"in_preview": 0,
"in_filter": 0,
"in_global_search": 0,
"read_only": 0,
"allow_on_submit": 0,
"ignore_user_permissions": 0,
"allow_bulk_edit": 0,
"permlevel": 0,
"ignore_xss_filter": 0,
"unique": 0,
"no_copy": 0,
"set_only_once": 0,
"remember_last_selected_value": 0,
"parent": "new-doctype-2",
"parentfield": "fields",
"parenttype": "DocType",
"idx": 2,
"__unedited": false,
"label": "email"
}
]
}

+ 168
- 0
frappe/custom/fixtures/temp_singles.json Wyświetl plik

@@ -0,0 +1,168 @@
{
"docstatus": 0,
"doctype": "DocType",
"name": "new-doctype-1",
"__islocal": 1,
"__unsaved": 1,
"owner": "Administrator",
"is_submittable": 0,
"istable": 0,
"issingle": 1,
"is_tree": 0,
"editable_grid": 1,
"quick_entry": 0,
"track_changes": 1,
"track_seen": 0,
"track_views": 0,
"custom": 1,
"beta": 0,
"is_virtual": 0,
"naming_rule": "",
"name_case": "",
"allow_rename": 1,
"hide_toolbar": 0,
"allow_copy": 0,
"allow_import": 0,
"allow_events_in_timeline": 0,
"allow_auto_repeat": 0,
"sort_field": "modified",
"sort_order": "DESC",
"document_type": "",
"show_preview_popup": 0,
"show_name_in_global_search": 0,
"email_append_to": 0,
"read_only": 0,
"in_create": 0,
"has_web_view": 0,
"allow_guest_to_view": 0,
"index_web_pages_for_search": 1,
"engine": "InnoDB",
"permissions": [
{
"docstatus": 0,
"doctype": "DocPerm",
"name": "new-docperm-1",
"__islocal": 1,
"__unsaved": 1,
"owner": "Administrator",
"if_owner": 0,
"permlevel": 0,
"select": 0,
"read": 1,
"write": 1,
"create": 1,
"delete": 1,
"submit": 0,
"cancel": 0,
"amend": 0,
"report": 1,
"export": 1,
"import": 0,
"set_user_permissions": 0,
"share": 1,
"print": 1,
"email": 1,
"parent": "new-doctype-1",
"parentfield": "permissions",
"parenttype": "DocType",
"idx": 1,
"role": "System Manager"
}
],
"__newname": "temp_singles",
"module": "Custom",
"fields": [
{
"docstatus": 0,
"doctype": "DocField",
"name": "new-docfield-1",
"__islocal": 1,
"__unsaved": 1,
"owner": "Administrator",
"fieldtype": "Data",
"precision": "",
"non_negative": 0,
"hide_days": 0,
"hide_seconds": 0,
"reqd": 0,
"search_index": 0,
"fetch_if_empty": 0,
"hidden": 0,
"bold": 0,
"allow_in_quick_entry": 0,
"translatable": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"report_hide": 0,
"collapsible": 0,
"hide_border": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"in_preview": 0,
"in_filter": 0,
"in_global_search": 0,
"read_only": 0,
"allow_on_submit": 0,
"ignore_user_permissions": 0,
"allow_bulk_edit": 0,
"permlevel": 0,
"ignore_xss_filter": 0,
"unique": 0,
"no_copy": 0,
"set_only_once": 0,
"remember_last_selected_value": 0,
"parent": "new-doctype-1",
"parentfield": "fields",
"parenttype": "DocType",
"idx": 1,
"__unedited": false,
"label": "member_name"
},
{
"docstatus": 0,
"doctype": "DocField",
"name": "new-docfield-2",
"__islocal": 1,
"__unsaved": 1,
"owner": "Administrator",
"fieldtype": "Data",
"precision": "",
"non_negative": 0,
"hide_days": 0,
"hide_seconds": 0,
"reqd": 0,
"search_index": 0,
"fetch_if_empty": 0,
"hidden": 0,
"bold": 0,
"allow_in_quick_entry": 0,
"translatable": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"report_hide": 0,
"collapsible": 0,
"hide_border": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"in_preview": 0,
"in_filter": 0,
"in_global_search": 0,
"read_only": 0,
"allow_on_submit": 0,
"ignore_user_permissions": 0,
"allow_bulk_edit": 0,
"permlevel": 0,
"ignore_xss_filter": 0,
"unique": 0,
"no_copy": 0,
"set_only_once": 0,
"remember_last_selected_value": 0,
"parent": "new-doctype-1",
"parentfield": "fields",
"parenttype": "DocType",
"idx": 2,
"__unedited": false,
"label": "email"
}
]
}

+ 23
- 10
frappe/database/database.py Wyświetl plik

@@ -801,14 +801,27 @@ class Database(object):

frappe.local.realtime_log = []

def rollback(self):
"""`ROLLBACK` current transaction."""
self.sql("rollback")
self.begin()
for obj in frappe.local.rollback_observers:
if hasattr(obj, "on_rollback"):
obj.on_rollback()
frappe.local.rollback_observers = []
def savepoint(self, save_point):
"""Savepoints work as a nested transaction.

Changes can be undone to a save point by doing frappe.db.rollback(save_point)

Note: rollback watchers can not work with save points.
so only changes to database are undone when rolling back to a savepoint.
Avoid using savepoints when writing to filesystem."""
self.sql(f"savepoint {save_point}")

def rollback(self, *, save_point=None):
"""`ROLLBACK` current transaction. Optionally rollback to a known save_point."""
if save_point:
self.sql(f"rollback to savepoint {save_point}")
else:
self.sql("rollback")
self.begin()
for obj in frappe.local.rollback_observers:
if hasattr(obj, "on_rollback"):
obj.on_rollback()
frappe.local.rollback_observers = []

def field_exists(self, dt, fn):
"""Return true of field exists."""
@@ -824,9 +837,9 @@ class Database(object):
def has_table(self, doctype):
return self.table_exists(doctype)

def get_tables(self):
def get_tables(self, cached=True):
tables = frappe.cache().get_value('db_tables')
if not tables:
if not tables or not cached:
table_rows = self.sql("""
SELECT table_name
FROM information_schema.tables


+ 3
- 2
frappe/database/mariadb/database.py Wyświetl plik

@@ -135,9 +135,10 @@ class MariaDBDatabase(Database):
table_name = get_table_name(doctype)
return self.sql(f"DESC `{table_name}`")

def change_column_type(self, doctype: str, column: str, type: str) -> Union[List, Tuple]:
def change_column_type(self, doctype: str, column: str, type: str, nullable: bool = False) -> Union[List, Tuple]:
table_name = get_table_name(doctype)
return self.sql(f"ALTER TABLE `{table_name}` MODIFY `{column}` {type} NOT NULL")
null_constraint = "NOT NULL" if not nullable else ""
return self.sql(f"ALTER TABLE `{table_name}` MODIFY `{column}` {type} {null_constraint}")

# exception types
@staticmethod


+ 1
- 1
frappe/database/mariadb/setup_db.py Wyświetl plik

@@ -92,7 +92,7 @@ def bootstrap_database(db_name, verbose, source_sql=None):
import_db_from_sql(source_sql, verbose)

frappe.connect(db_name=db_name)
if 'tabDefaultValue' not in frappe.db.get_tables():
if 'tabDefaultValue' not in frappe.db.get_tables(cached=False):
from click import secho

secho(


+ 5
- 2
frappe/database/postgres/database.py Wyświetl plik

@@ -183,9 +183,12 @@ class PostgresDatabase(Database):
table_name = get_table_name(doctype)
return self.sql(f"SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_NAME = '{table_name}'")

def change_column_type(self, doctype: str, column: str, type: str) -> Union[List, Tuple]:
def change_column_type(self, doctype: str, column: str, type: str, nullable: bool = False) -> Union[List, Tuple]:
table_name = get_table_name(doctype)
return self.sql(f'ALTER TABLE "{table_name}" ALTER COLUMN "{column}" TYPE {type}')
null_constraint = "SET NOT NULL" if not nullable else "DROP NOT NULL"
return self.sql(f"""ALTER TABLE "{table_name}"
ALTER COLUMN "{column}" TYPE {type},
ALTER COLUMN "{column}" {null_constraint}""")

def create_auth_table(self):
self.sql_ddl("""create table if not exists "__Auth" (


+ 4
- 4
frappe/desk/page/leaderboard/leaderboard.js Wyświetl plik

@@ -382,10 +382,10 @@ class Leaderboard {
let timespan = this.options.selected_timespan.toLowerCase();
let current_date = frappe.datetime.now_date();
let date_range_map = {
"this week": [frappe.datetime.week_start(), current_date],
"this month": [frappe.datetime.month_start(), current_date],
"this quarter": [frappe.datetime.quarter_start(), current_date],
"this year": [frappe.datetime.year_start(), current_date],
"this week": [frappe.datetime.week_start(), frappe.datetime.week_end()],
"this month": [frappe.datetime.month_start(), frappe.datetime.month_end()],
"this quarter": [frappe.datetime.quarter_start(), frappe.datetime.quarter_end()],
"this year": [frappe.datetime.year_start(), frappe.datetime.year_end()],
"last week": [frappe.datetime.add_days(current_date, -7), current_date],
"last month": [frappe.datetime.add_months(current_date, -1), current_date],
"last quarter": [frappe.datetime.add_months(current_date, -3), current_date],


+ 32
- 28
frappe/desk/page/setup_wizard/setup_wizard.js Wyświetl plik

@@ -392,17 +392,24 @@ frappe.setup.slides_settings = [
fields: [
{
fieldname: "country", label: __("Your Country"), reqd: 1,
fieldtype: "Select"
fieldtype: "Autocomplete",
placeholder: __('Select Country')
},
{ fieldtype: "Section Break" },
{
fieldname: "timezone", label: __("Time Zone"), reqd: 1,
fieldtype: "Select"
fieldname: "timezone",
label: __("Time Zone"),
placeholder: __('Select Time Zone'),
reqd: 1,
fieldtype: "Select",
},
{ fieldtype: "Column Break" },
{
fieldname: "currency", label: __("Currency"), reqd: 1,
fieldtype: "Select"
fieldname: "currency",
label: __("Currency"),
placeholder: __('Select Currency'),
reqd: 1,
fieldtype: "Select",
}
],

@@ -512,7 +519,7 @@ frappe.setup.utils = {
frappe.setup.data.email = r.message.email;
callback(slide);
}
})
});
},

setup_language_field: function (slide) {
@@ -529,16 +536,19 @@ frappe.setup.utils = {

var country_field = slide.get_field('country');

slide.get_input("country").empty()
.add_options([""].concat(Object.keys(data.country_info).sort()));

slide.get_input("currency").empty()
.add_options(frappe.utils.unique([""].concat(
$.map(data.country_info, opts => opts.currency)
)).sort());
country_field.set_data(Object.keys(data.country_info).sort());

slide.get_input("currency")
.empty()
.add_options(
frappe.utils.unique(
$.map(data.country_info, opts => opts.currency).sort()
)
);

slide.get_input("timezone").empty()
.add_options([""].concat(data.all_timezones));
.add_options(data.all_timezones);

// set values if present
if (frappe.wizard.values.country) {
@@ -547,13 +557,9 @@ frappe.setup.utils = {
country_field.set_input(data.default_country);
}

if (frappe.wizard.values.currency) {
slide.get_field("currency").set_input(frappe.wizard.values.currency);
}
slide.get_field("currency").set_input(frappe.wizard.values.currency);

if (frappe.wizard.values.timezone) {
slide.get_field("timezone").set_input(frappe.wizard.values.timezone);
}
slide.get_field("timezone").set_input(frappe.wizard.values.timezone);

},

@@ -589,17 +595,15 @@ frappe.setup.utils = {

$timezone.empty();

if (!country) return;
// add country specific timezones first
if (country) {
var timezone_list = data.country_info[country].timezones || [];
$timezone.add_options(timezone_list.sort());
slide.get_field("currency").set_input(data.country_info[country].currency);
slide.get_field("currency").$input.trigger("change");
}
const timezone_list = data.country_info[country].timezones || [];
$timezone.add_options(timezone_list.sort());
slide.get_field("currency").set_input(data.country_info[country].currency);
slide.get_field("currency").$input.trigger("change");

// add all timezones at the end, so that user has the option to change it to any timezone
$timezone.add_options([""].concat(data.all_timezones));

$timezone.add_options(data.all_timezones);
slide.get_field("timezone").set_input($timezone.val());

// temporarily set date format
@@ -617,7 +621,7 @@ frappe.setup.utils = {
if (number_format === "#.###") {
number_format = "#.###,##";
} else if (number_format === "#,###") {
number_format = "#,###.##"
number_format = "#,###.##";
}

frappe.boot.sysdefaults.number_format = number_format;


+ 1
- 1
frappe/desk/page/setup_wizard/setup_wizard.py Wyświetl plik

@@ -151,7 +151,7 @@ def update_system_settings(args):
system_settings = frappe.get_doc("System Settings", "System Settings")
system_settings.update({
"country": args.get("country"),
"language": get_language_code(args.get("language")),
"language": get_language_code(args.get("language")) or 'en',
"time_zone": args.get("timezone"),
"float_precision": 3,
'date_format': frappe.db.get_value("Country", args.get("country"), "date_format"),


+ 18
- 4
frappe/handler.py Wyświetl plik

@@ -12,7 +12,7 @@ from frappe.utils.response import build_response
from frappe.utils.csvutils import build_csv_response
from frappe.utils.image import optimize_image
from mimetypes import guess_type
from frappe.core.doctype.server_script.server_script_utils import run_server_script_api
from frappe.core.doctype.server_script.server_script_utils import get_server_script_map


ALLOWED_MIMETYPES = ('image/png', 'image/jpeg', 'application/pdf', 'application/msword',
@@ -49,8 +49,9 @@ def execute_cmd(cmd, from_async=False):
break

# via server script
if run_server_script_api(cmd):
return None
server_script = get_server_script_map().get('_api', {}).get(cmd)
if server_script:
return run_server_script(server_script)

try:
method = get_attr(cmd)
@@ -66,7 +67,20 @@ def execute_cmd(cmd, from_async=False):

return frappe.call(method, **frappe.form_dict)


def run_server_script(server_script):
response = frappe.get_doc('Server Script', server_script).execute_method()

# some server scripts return output using flags (empty dict by default),
# while others directly modify frappe.response
# return flags if not empty dict (this overwrites frappe.response.message)
if response != {}:
return response

def is_valid_http_method(method):
if frappe.flags.in_safe_exec:
return

http_method = frappe.local.request.method

if http_method not in frappe.allowed_http_methods_for_whitelisted_func[method]:
@@ -260,7 +274,7 @@ def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None):

# build output as csv
if cint(frappe.form_dict.get('as_csv')):
build_csv_response(response, doc.doctype.replace(' ', ''))
build_csv_response(response, _(doc.doctype).replace(' ', ''))
return

frappe.response['message'] = response


+ 7
- 0
frappe/installer.py Wyświetl plik

@@ -208,6 +208,7 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False)
import click

site = frappe.local.site
app_hooks = frappe.get_hooks(app_name=app_name)

# dont allow uninstall app if not installed unless forced
if not force:
@@ -233,6 +234,9 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False)

frappe.flags.in_uninstall = True

for before_uninstall in app_hooks.before_uninstall or []:
frappe.get_attr(before_uninstall)()

modules = frappe.get_all("Module Def", filters={"app_name": app_name}, pluck="name")

drop_doctypes = _delete_modules(modules, dry_run=dry_run)
@@ -243,6 +247,9 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False)
frappe.get_single('Installed Applications').update_versions()
frappe.db.commit()

for after_uninstall in app_hooks.after_uninstall or []:
frappe.get_attr(after_uninstall)()

click.secho(f"Uninstalled App {app_name} from Site {site}", fg="green")
frappe.flags.in_uninstall = False



+ 1
- 1
frappe/integrations/doctype/webhook/__init__.py Wyświetl plik

@@ -7,7 +7,7 @@ import frappe

def run_webhooks(doc, method):
'''Run webhooks for this method'''
if frappe.flags.in_import or frappe.flags.in_patch or frappe.flags.in_install:
if frappe.flags.in_import or frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_migrate:
return

if frappe.flags.webhooks_executed is None:


+ 10
- 5
frappe/model/db_query.py Wyświetl plik

@@ -36,10 +36,12 @@ class DatabaseQuery(object):
ignore_ifnull=False, save_user_settings=False, save_user_settings_fields=False,
update=None, add_total_row=None, user_settings=None, reference_doctype=None,
run=True, strict=True, pluck=None, ignore_ddl=False, parent_doctype=None) -> List:
if not ignore_permissions and \
not frappe.has_permission(self.doctype, "select", user=user, parent_doctype=parent_doctype) and \
not frappe.has_permission(self.doctype, "read", user=user, parent_doctype=parent_doctype):

if (
not ignore_permissions
and not frappe.has_permission(self.doctype, "select", user=user, parent_doctype=parent_doctype)
and not frappe.has_permission(self.doctype, "read", user=user, parent_doctype=parent_doctype)
):
frappe.flags.error_message = _('Insufficient Permission for {0}').format(frappe.bold(self.doctype))
raise frappe.PermissionError(self.doctype)

@@ -787,12 +789,15 @@ class DatabaseQuery(object):
def check_parent_permission(parent, child_doctype):
if parent:
# User may pass fake parent and get the information from the child table
if child_doctype and not frappe.db.exists('DocField',
{'parent': parent, 'options': child_doctype}):
if child_doctype and not (
frappe.db.exists('DocField', {'parent': parent, 'options': child_doctype})
or frappe.db.exists('Custom Field', {'dt': parent, 'options': child_doctype})
):
raise frappe.PermissionError

if frappe.permissions.has_permission(parent):
return

# Either parent not passed or the user doesn't have permission on parent doctype of child table!
raise frappe.PermissionError



+ 3
- 4
frappe/model/document.py Wyświetl plik

@@ -220,13 +220,13 @@ class Document(BaseDocument):

self.set("__islocal", True)

self.check_permission("create")
self._set_defaults()
self.set_user_and_timestamp()
self.set_docstatus()
self.check_if_latest()
self.run_method("before_insert")
self._validate_links()
self.check_permission("create")
self.run_method("before_insert")
self.set_new_name(set_name=set_name, set_child_names=set_child_names)
self.set_parent_in_children()
self.validate_higher_perm_levels()
@@ -301,8 +301,7 @@ class Document(BaseDocument):
self.flags.ignore_version = frappe.flags.in_test if ignore_version is None else ignore_version

if self.get("__islocal") or not self.get("name"):
self.insert()
return
return self.insert()

self.check_permission("write", "save")



+ 15
- 7
frappe/model/rename_doc.py Wyświetl plik

@@ -32,11 +32,18 @@ def update_document_title(doctype, docname, title_field=None, old_title=None, ne

return docname

def rename_doc(doctype, old, new, force=False, merge=False, ignore_permissions=False, ignore_if_exists=False, show_alert=True):
"""
Renames a doc(dt, old) to doc(dt, new) and
updates all linked fields of type "Link"
"""
def rename_doc(
doctype,
old,
new,
force=False,
merge=False,
ignore_permissions=False,
ignore_if_exists=False,
show_alert=True,
rebuild_search=True
):
"""Rename a doc(dt, old) to doc(dt, new) and update all linked fields of type "Link"."""
if not frappe.db.exists(doctype, old):
return

@@ -104,7 +111,8 @@ def rename_doc(doctype, old, new, force=False, merge=False, ignore_permissions=F
frappe.delete_doc(doctype, old)

frappe.clear_cache()
frappe.enqueue('frappe.utils.global_search.rebuild_for_doctype', doctype=doctype)
if rebuild_search:
frappe.enqueue('frappe.utils.global_search.rebuild_for_doctype', doctype=doctype)

if show_alert:
frappe.msgprint(_('Document renamed from {0} to {1}').format(bold(old), bold(new)), alert=True, indicator='green')
@@ -492,7 +500,7 @@ def bulk_rename(doctype, rows=None, via_console = False):
if len(row) > 1 and row[0] and row[1]:
merge = len(row) > 2 and (row[2] == "1" or row[2].lower() == "true")
try:
if rename_doc(doctype, row[0], row[1], merge=merge):
if rename_doc(doctype, row[0], row[1], merge=merge, rebuild_search=False):
msg = _("Successful: {0} to {1}").format(row[0], row[1])
frappe.db.commit()
else:


+ 1
- 1
frappe/patches.txt Wyświetl plik

@@ -188,5 +188,5 @@ frappe.patches.v14_0.copy_mail_data #08.03.21
frappe.patches.v14_0.update_workspace2 # 20.09.2021
frappe.patches.v14_0.update_github_endpoints #08-11-2021
frappe.patches.v14_0.remove_db_aggregation
frappe.patches.v14_0.save_ratings_in_fraction
frappe.patches.v14_0.save_ratings_in_fraction #23-12-2021
frappe.patches.v14_0.update_color_names_in_kanban_board_column

+ 1
- 1
frappe/patches/v14_0/remove_db_aggregation.py Wyświetl plik

@@ -27,6 +27,6 @@ def execute():
name, script = server_script["name"], server_script["script"]

for agg in ["avg", "max", "min", "sum"]:
script = re.sub(f"frappe.db.{agg}(", f"frappe.qb.{agg}(", script)
script = re.sub(f"frappe.db.{agg}\(", f"frappe.qb.{agg}(", script)

frappe.db.update("Server Script", name, "script", script)

+ 34
- 7
frappe/patches/v14_0/save_ratings_in_fraction.py Wyświetl plik

@@ -1,12 +1,39 @@
import frappe
from frappe.query_builder import DocType


def execute():
rating_fields = frappe.get_all("DocField", fields=["parent", "fieldname"], filters={"fieldtype": "Rating"})
RATING_FIELD_TYPE = "decimal(3,2)"
rating_fields = frappe.get_all(
"DocField", fields=["parent", "fieldname"], filters={"fieldtype": "Rating"}
)

custom_rating_fields = frappe.get_all(
"Custom Field", fields=["dt", "fieldname"], filters={"fieldtype": "Rating"}
)

for _field in rating_fields + custom_rating_fields:
doctype_name = _field.get("parent") or _field.get("dt")
doctype = DocType(doctype_name)
field = _field.fieldname

# TODO: Add postgres support (for the check)
if (
frappe.conf.db_type == "mariadb"
and frappe.db.get_column_type(doctype_name, field) == RATING_FIELD_TYPE
):
continue

# commit any changes so far for upcoming DDL
frappe.db.commit()

# alter column types for rating fieldtype
frappe.db.change_column_type(doctype_name, column=field, type=RATING_FIELD_TYPE, nullable=True)

custom_rating_fields = frappe.get_all("Custom Field", fields=["dt", "fieldname"], filters={"fieldtype": "Rating"})
# update data: int => decimal
frappe.qb.update(doctype).set(
doctype[field], doctype[field] / 5
).run()

for field in rating_fields + custom_rating_fields:
doctype_name = field.get("parent") or field.get("dt")
doctype = frappe.qb.DocType(doctype_name)
field = field.fieldname
(frappe.qb.update(doctype_name).set(doctype[field], doctype[field]/5)).run()
# commit to flush updated rows
frappe.db.commit()

+ 12
- 1
frappe/printing/doctype/print_format/print_format.py Wyświetl plik

@@ -71,8 +71,19 @@ class PrintFormat(Document):

self.export_doc()

def after_rename(self, old: str, new: str, *args, **kwargs):
if self.doc_type:
frappe.clear_cache(doctype=self.doc_type)

# update property setter default_print_format if set
frappe.db.set_value("Property Setter", {
"doctype_or_field": "DocType",
"doc_type": self.doc_type,
"property": "default_print_format",
"value": old,
}, "value", new)

def export_doc(self):
# export
from frappe.modules.utils import export_module_json
export_module_json(self, self.standard == 'Yes', self.module)



+ 4
- 3
frappe/printing/page/print/print.js Wyświetl plik

@@ -52,8 +52,8 @@ frappe.ui.form.PrintView = class {
':Print Settings',
'Print Settings'
);
this.setup_toolbar();
this.setup_menu();
this.setup_toolbar();
this.setup_sidebar();
this.setup_keyboard_shortcuts();
}
@@ -81,8 +81,9 @@ frappe.ui.form.PrintView = class {
);

this.page.add_button(
frappe.utils.icon('refresh'),
() => this.refresh_print_format()
__('Refresh'),
() => this.refresh_print_format(),
{ icon: 'refresh' }
);
}



+ 2
- 2
frappe/public/js/frappe/data_import/import_preview.js Wyświetl plik

@@ -343,11 +343,11 @@ function get_fields_as_options(doctype, column_map) {
return [].concat(
...keys.map(key => {
return column_map[key].map(df => {
let label = df.label;
let label = __(df.label);
let value = df.fieldname;
if (doctype !== key) {
let table_field = frappe.meta.get_docfield(doctype, key);
label = `${df.label} (${table_field.label})`;
label = `${__(df.label)} (${__(table_field.label)})`;
value = `${table_field.fieldname}.${df.fieldname}`;
}
return {


+ 3
- 0
frappe/public/js/frappe/form/controls/datetime.js Wyświetl plik

@@ -81,6 +81,9 @@ frappe.ui.form.ControlDatetime = class ControlDatetime extends frappe.ui.form.Co

get_model_value() {
let value = super.get_model_value();
if (!value && !this.doc) {
value = this.last_value;
}
return frappe.datetime.get_datetime_as_string(value);
}
};

+ 4
- 4
frappe/public/js/frappe/form/controls/link.js Wyświetl plik

@@ -456,8 +456,7 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
this.docname, value);
}
validate_link_and_fetch(df, options, docname, value) {
if (!value) return;

let field_value = "";
const fetch_map = this.fetch_map;
const columns_to_fetch = Object.values(fetch_map);

@@ -471,15 +470,16 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
docname: value,
fields: columns_to_fetch,
}).then((response) => {
if (!response || !response.name) return null;
if (!docname || !columns_to_fetch.length) return response.name;

for (const [target_field, source_field] of Object.entries(fetch_map)) {
if (value) field_value = response[source_field];
frappe.model.set_value(
df.parent,
docname,
target_field,
response[source_field],
field_value,
df.fieldtype,
);
}


+ 44
- 29
frappe/public/js/frappe/form/controls/rating.js Wyświetl plik

@@ -4,8 +4,9 @@ frappe.ui.form.ControlRating = class ControlRating extends frappe.ui.form.Contro
let stars = '';
let number_of_stars = this.df.options || 5;
Array.from({length: cint(number_of_stars)}, (_, i) => i + 1).forEach(i => {
stars += `<svg class="icon icon-md" data-rating=${i}>
<use href="#icon-star"></use>
stars += `<svg class="icon icon-md" data-rating=${i} viewBox="0 0 24 24" fill="none">
<path class="right-half" d="M11.9987 3.00011C12.177 3.00011 12.3554 3.09303 12.4471 3.27888L14.8213 8.09112C14.8941 8.23872 15.0349 8.34102 15.1978 8.3647L20.5069 9.13641C20.917 9.19602 21.0807 9.69992 20.7841 9.9892L16.9421 13.7354C16.8243 13.8503 16.7706 14.0157 16.7984 14.1779L17.7053 19.4674C17.7753 19.8759 17.3466 20.1874 16.9798 19.9945L12.2314 17.4973C12.1586 17.459 12.0786 17.4398 11.9987 17.4398V3.00011Z" fill="var(--star-fill)" stroke="var(--star-fill)"/>
<path class="left-half" d="M11.9987 3.00011C11.8207 3.00011 11.6428 3.09261 11.5509 3.27762L9.15562 8.09836C9.08253 8.24546 8.94185 8.34728 8.77927 8.37075L3.42887 9.14298C3.01771 9.20233 2.85405 9.70811 3.1525 9.99707L7.01978 13.7414C7.13858 13.8564 7.19283 14.0228 7.16469 14.1857L6.25116 19.4762C6.18071 19.8842 6.6083 20.1961 6.97531 20.0045L11.7672 17.5022C11.8397 17.4643 11.9192 17.4454 11.9987 17.4454V3.00011Z" fill="var(--star-fill)" stroke="var(--star-fill)"/>
</svg>`;
});

@@ -17,45 +18,56 @@ frappe.ui.form.ControlRating = class ControlRating extends frappe.ui.form.Contro

$(this.input_area).html(star_template);

$(this.input_area).find('svg').hover((ev) => {
const el = $(ev.currentTarget);
let star_value = el.data('rating');
el.parent().children('svg').each( function(e) {
if (e < star_value) {
$(this).addClass('star-hover');
} else {
$(this).removeClass('star-hover');
}
});
}, (ev) => {
let me = this;
$(this.input_area).find('svg').on('mousemove', function(ev) {
me.update_rating(ev);
}).on('mouseout', function(ev) {
const el = $(ev.currentTarget);
el.parent().children('svg').each( function() {
$(this).removeClass('star-hover');
$(this).find('.left-half, .right-half').removeClass('star-hover');
});
});

$(this.input_area).find('svg').click((ev) => {
const el = $(ev.currentTarget);
let star_value = el.data('rating');
el.parent().children('svg').each( function(e) {
if (e < star_value) {
$(this).addClass('star-click');
} else {
$(this).removeClass('star-click');
}
});
let out_of_ratings = this.df.options || 5;
this.update_rating(ev, true);
});
}

update_rating(ev, click) {
const el = $(ev.currentTarget);
let star_value = el.data('rating');
let left_half = false;
let cls = 'star-click';
if (!click) cls = 'star-hover';

if ((ev.pageX - el.offset().left) < el.width() / 2) {
left_half = true;
star_value--;
}
el.parent().children('svg').each( function(e) {
if (e < star_value) {
$(this).find('.left-half, .right-half').addClass(cls);
} else if (e == star_value && left_half) {
$(this).find('.left-half').addClass(cls);
$(this).find('.right-half').removeClass(cls);
if (click) star_value += 0.5;
} else {
$(this).find('.left-half, .right-half').removeClass(cls);
}
});
if (click) {
let out_of_ratings = this.df.options || 5;
star_value = star_value/out_of_ratings;

this.validate_and_set_in_model(star_value, ev);
if (this.doctype && this.docname) {
this.set_input(star_value);
}
});
}
}

get_value() {
let out_of_ratings = this.df.options || 5;
return cint(this.value*out_of_ratings, null);
return this.value;
}
set_formatted_input(value) {
let out_of_ratings = this.df.options || 5;
@@ -63,9 +75,12 @@ frappe.ui.form.ControlRating = class ControlRating extends frappe.ui.form.Contro
let el = $(this.input_area).find('svg');
el.children('svg').prevObject.each( function(e) {
if (e < value) {
$(this).addClass('star-click');
$(this).find('.left-half, .right-half').addClass('star-click');

let is_half = e == Math.floor(value) && value % 1 == 0.5;
is_half && $(this).find('.right-half').removeClass('star-click');
} else {
$(this).removeClass('star-click');
$(this).find('.left-half, .right-half').removeClass('star-click');
}
});
}


+ 15
- 3
frappe/public/js/frappe/form/form.js Wyświetl plik

@@ -190,7 +190,7 @@ frappe.ui.form.Form = class FrappeForm {

setup_std_layout() {
this.form_wrapper = $('<div></div>').appendTo(this.layout_main);
this.body = $('<div></div>').appendTo(this.form_wrapper);
this.body = $('<div class="std-form-layout"></div>').appendTo(this.form_wrapper);

// only tray
this.meta.section_style='Simple'; // always simple!
@@ -211,12 +211,24 @@ frappe.ui.form.Form = class FrappeForm {
this.fields = this.layout.fields_list;

let dashboard_parent = $('<div class="form-dashboard">');
let dashboard_added = false;

if (this.layout.tabs.length) {
this.layout.tabs[0].wrapper.prepend(dashboard_parent);
this.layout.tabs.every(tab => {
if (tab.df.options === 'Dashboard') {
tab.wrapper.prepend(dashboard_parent);
dashboard_added = true;
return false;
}
return true;
});
if (!dashboard_added) {
this.layout.tabs[0].wrapper.prepend(dashboard_parent);
}
} else {
dashboard_parent.insertAfter(this.layout.wrapper.find('.form-message'));
this.layout.wrapper.find('.form-page').prepend(dashboard_parent);
}

this.dashboard = new frappe.ui.form.Dashboard(dashboard_parent, this);

this.tour = new frappe.ui.form.FormTour({


+ 10
- 6
frappe/public/js/frappe/form/formatters.js Wyświetl plik

@@ -63,12 +63,16 @@ frappe.form.formatters = {
);
return frappe.form.formatters._right(flt(value, precision) + "%", options);
},
Rating: function(value) {
const rating_html = `${[1, 2, 3, 4, 5].map(i =>
`<svg class="icon icon-md ${i <= (value || 0) ? "star-click": "" }" data-idx="${i}">
<use href="#icon-star"></use>
</svg>`
).join('')}`;
Rating: function(value, docfield) {
let rating_html = '';
let number_of_stars = docfield.options || 5;
value = value * number_of_stars;
Array.from({length: cint(number_of_stars)}, (_, i) => i + 1).forEach(i => {
rating_html += `<svg class="icon icon-md" data-rating=${i} viewBox="0 0 24 24" fill="none">
<path class="right-half ${i <= (value || 0) ? "star-click": "" }" d="M11.9987 3.00011C12.177 3.00011 12.3554 3.09303 12.4471 3.27888L14.8213 8.09112C14.8941 8.23872 15.0349 8.34102 15.1978 8.3647L20.5069 9.13641C20.917 9.19602 21.0807 9.69992 20.7841 9.9892L16.9421 13.7354C16.8243 13.8503 16.7706 14.0157 16.7984 14.1779L17.7053 19.4674C17.7753 19.8759 17.3466 20.1874 16.9798 19.9945L12.2314 17.4973C12.1586 17.459 12.0786 17.4398 11.9987 17.4398V3.00011Z" fill="var(--star-fill)" stroke="var(--star-fill)"/>
<path class="left-half ${i <= (value || 0) || (i - 0.5) == value ? "star-click": "" }" d="M11.9987 3.00011C11.8207 3.00011 11.6428 3.09261 11.5509 3.27762L9.15562 8.09836C9.08253 8.24546 8.94185 8.34728 8.77927 8.37075L3.42887 9.14298C3.01771 9.20233 2.85405 9.70811 3.1525 9.99707L7.01978 13.7414C7.13858 13.8564 7.19283 14.0228 7.16469 14.1857L6.25116 19.4762C6.18071 19.8842 6.6083 20.1961 6.97531 20.0045L11.7672 17.5022C11.8397 17.4643 11.9192 17.4454 11.9987 17.4454V3.00011Z" fill="var(--star-fill)" stroke="var(--star-fill)"/>
</svg>`;
});
return `<div class="rating">
${rating_html}
</div>`;


+ 41
- 20
frappe/public/js/frappe/form/multi_select_dialog.js Wyświetl plik

@@ -12,7 +12,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {

init() {
this.page_length = 20;
this.start = 0;
this.child_page_length = 20;
this.fields = this.get_fields();

this.make();
@@ -29,7 +29,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {

get_result_fields() {
const show_next_page = () => {
this.start += 20;
this.page_length += 20;
this.get_results();
};
return [
@@ -58,7 +58,15 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
get_child_selection_fields() {
const fields = [];
if (this.allow_child_item_selection && this.child_fieldname) {
const show_more_child_results = () => {
this.child_page_length += 20;
this.show_child_results();
};
fields.push({ fieldtype: "HTML", fieldname: "child_selection_area" });
fields.push({
fieldtype: "Button", fieldname: "more_child_btn", hidden: 1,
label: __("More"), click: show_more_child_results.bind(this)
});
}
return fields;
}
@@ -124,23 +132,27 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {

setup_results() {
this.$parent = $(this.dialog.body);
this.$wrapper = this.dialog.fields_dict.results_area.$wrapper.append(`<div class="results mt-3"
this.$wrapper = this.dialog.fields_dict.results_area.$wrapper.append(`<div class="results my-3"
style="border: 1px solid #d1d8dd; border-radius: 3px; height: 300px; overflow: auto;"></div>`);

this.$results = this.$wrapper.find('.results');
this.$results.append(this.make_list_row());
}

show_child_results() {
this.get_child_result().then(r => {
this.child_results = r.message || [];
this.render_child_datatable();

this.$wrapper.addClass('hidden');
this.$child_wrapper.removeClass('hidden');
this.dialog.fields_dict.more_btn.$wrapper.hide();
});
}

toggle_child_selection() {
if (this.dialog.fields_dict['allow_child_item_selection'].get_value()) {
this.get_child_result().then(r => {
this.child_results = r.message || [];
this.render_child_datatable();

this.$wrapper.addClass('hidden');
this.$child_wrapper.removeClass('hidden');
this.dialog.fields_dict.more_btn.$wrapper.hide();
});
this.show_child_results();
} else {
this.child_results = [];
this.get_results();
@@ -157,6 +169,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
this.child_datatable.rowmanager.checkMap = [];
this.child_datatable.refresh(this.get_child_datatable_rows());
this.$child_wrapper.find('.dt-scrollable').css('height', '300px');
this.$child_wrapper.find('.dt-scrollable').css('overflow-y', 'scroll');
}, 500);
}
}
@@ -167,14 +180,21 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
}

get_child_datatable_rows() {
return this.child_results.map(d => Object.values(d).slice(1)); // slice name field
if (this.child_results.length > this.child_page_length) {
this.dialog.fields_dict.more_child_btn.toggle(true);
} else {
this.dialog.fields_dict.more_child_btn.toggle(false);
}
return this.child_results
.slice(0, this.child_page_length)
.map(d => Object.values(d).slice(1)); // slice name field
}

setup_child_datatable() {
const header_columns = this.get_child_datatable_columns();
const rows = this.get_child_datatable_rows();
this.$child_wrapper = this.dialog.fields_dict.child_selection_area.$wrapper;
this.$child_wrapper.addClass('mt-3');
this.$child_wrapper.addClass('my-3');

this.child_datatable = new frappe.DataTable(this.$child_wrapper.get(0), {
columns: header_columns,
@@ -412,7 +432,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
this.empty_list();
}
more_btn.hide();
$(".modal-dialog .list-item--head").css("z-index", 0);
$(".modal-dialog .list-item--head").css("z-index", 1);

if (results.length === 0) return;
if (more) more_btn.show();
@@ -425,7 +445,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
me.$results.append(me.make_list_row(result));
});

this.$results.find(".list-item--head").css("z-index", 0);
this.$results.find(".list-item--head").css("z-index", 1);

if (frappe.flags.auto_scroll) {
this.$results.animate({ scrollTop: me.$results.prop('scrollHeight') }, 500);
@@ -486,8 +506,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
txt: this.dialog.fields_dict["search_term"].get_value(),
filters: filters,
filter_fields: filter_fields,
start: this.start,
page_length: this.page_length + 1,
page_length: this.page_length + 5,
query: this.get_query ? this.get_query().query : '',
as_dict: 1
};
@@ -501,9 +520,6 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
args: args,
});
const more = res.values.length && res.values.length > this.page_length ? 1 : 0;
if (more) {
res.values.pop();
}

return [res, more];
}
@@ -512,6 +528,10 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
const args = this.get_args_for_search();
const [res, more] = await this.perform_search(args);

if (more) {
res.values = res.values.splice(0, this.page_length);
}

this.results = [];
if (res.values.length) {
res.values.forEach(result => {
@@ -565,6 +585,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
filters: filters,
fields: ['name', 'parent', ...this.child_columns],
parent: this.doctype,
limit_page_length: this.child_page_length + 5,
order_by: 'parent'
}
});


+ 5
- 6
frappe/public/js/frappe/list/list_view.js Wyświetl plik

@@ -760,6 +760,11 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
: value;
}

if (df.fieldtype === "Rating") {
let out_of_ratings = df.options || 5;
_value = _value * out_of_ratings;
}

if (df.fieldtype === "Image") {
html = df.options ? `<img src="${doc[df.options]}"
style="max-height: 30px; max-width: 100%;">`
@@ -1967,12 +1972,6 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
if (!doctype) return;
frappe.provide("frappe.views.trees");

// refresh tree view
if (frappe.views.trees[doctype]) {
frappe.views.trees[doctype].tree.refresh();
return;
}

// refresh list view
const page_name = frappe.get_route_str();
const list_view = frappe.views.list_view[page_name];


+ 4
- 0
frappe/public/js/frappe/ui/page.js Wyświetl plik

@@ -714,6 +714,10 @@ frappe.ui.Page = class Page {
${opts.icon ? frappe.utils.icon(opts.icon): ''}
${label}
</button>`);
// Add actions as menu item in Mobile View (similar to "add_custom_button" in forms.js)
let menu_item = this.add_menu_item(label, click, false);
menu_item.parent().addClass("hidden-xl");

button.appendTo(this.custom_actions);
button.on('click', click);
this.custom_actions.removeClass('hide');


+ 0
- 1
frappe/public/js/frappe/ui/tree.js Wyświetl plik

@@ -299,7 +299,6 @@ frappe.ui.Tree = class {
.appendTo($toolbar);
$link.on('click', () => {
obj.click(node);
this.refresh();
});
});



+ 1
- 1
frappe/public/js/frappe/utils/address_and_contact.js Wyświetl plik

@@ -29,7 +29,7 @@ $.extend(frappe.contacts, {
}
},
get_last_doc: function(frm) {
const reverse_routes = frappe.route_history.reverse();
const reverse_routes = frappe.route_history.slice().reverse();
const last_route = reverse_routes.find(route => {
return route[0] === 'Form' && route[1] !== frm.doctype
})


+ 2
- 4
frappe/public/js/frappe/utils/user.js Wyświetl plik

@@ -11,9 +11,7 @@ frappe.user_info = function(uid) {
}

if(!(frappe.boot.user_info && frappe.boot.user_info[uid])) {
var user_info = {
fullname: frappe.utils.to_title_case(uid.split("@")[0]) || "Unknown"
};
var user_info = {fullname: uid || "Unknown"};
} else {
var user_info = frappe.boot.user_info[uid];
}
@@ -157,4 +155,4 @@ $(document).bind('mousemove', function() {
if(frappe.session_alive_timeout)
clearTimeout(frappe.session_alive_timeout);
frappe.session_alive_timeout = setTimeout('frappe.session_alive=false;', 30000);
});
});

+ 2
- 1
frappe/public/js/frappe/utils/utils.js Wyświetl plik

@@ -28,7 +28,8 @@ Object.defineProperty(Object.prototype, "setDefault", {
value: function(key, default_value) {
if (!(key in this)) this[key] = default_value;
return this[key];
}
},
writable: true
});

// Pluralize


+ 0
- 5
frappe/public/js/frappe/views/container.js Wyświetl plik

@@ -40,11 +40,6 @@ frappe.views.Container = class Container {
}
change_to(label) {
cur_page = this;
if(this.page && this.page.label === label) {
$(this.page).trigger('show');
}

var me = this;
if(label.tagName) {
// if sent the div, get the table
var page = label;


+ 2
- 4
frappe/public/js/frappe/views/treeview.js Wyświetl plik

@@ -242,6 +242,7 @@ frappe.views.TreeView = class TreeView {
frappe.model.rename_doc(me.doctype, node.label, function(new_name) {
node.$tree_link.find('a').text(new_name);
node.label = new_name;
me.tree.refresh();
});
},
btnClass: "hidden-xs"
@@ -317,10 +318,7 @@ frappe.views.TreeView = class TreeView {
args: args,
callback: function(r) {
if(!r.exc) {
if(node.expanded) {
me.tree.toggle_node(node);
}
me.tree.load_children(node, true);
me.tree.load_children(node);
}
},
always: function() {


+ 2
- 2
frappe/public/scss/common/form.scss Wyświetl plik

@@ -1,4 +1,5 @@
.form-control {
height: inherit;
border: none;
font-size: var(--text-md);
position: relative;
@@ -13,10 +14,9 @@
font-weight: normal;
font-size: var(--text-sm);
}
min-height: var(--input-height);
border-radius: $border-radius;
font-weight: 400;
padding: 8px 12px;
padding: 6px 12px;
cursor: default;
color: var(--disabled-text-color);
background-color: var(--disabled-control-bg);


+ 9
- 5
frappe/public/scss/common/grid.scss Wyświetl plik

@@ -79,10 +79,9 @@

.grid-static-col,
.row-index {
height: 39px;
padding: var(--padding-sm) var(--padding-md);
height: 34px;
padding: 8px;
max-height: 200px;
// border-right: 1px solid var(--border-color);
}

.grid-row-check {
@@ -108,6 +107,7 @@
.grid-row > .row {
.col:last-child {
margin-right: calc(-1 * var(--margin-sm));
border-right: none;
}

.col {
@@ -149,7 +149,7 @@
}

textarea {
height: 40px !important;
height: 37px !important;
}

.form-control {
@@ -157,7 +157,7 @@
border: 0px;
padding-top: 8px;
padding-bottom: 9px;
height: 40px;
height: 34px;
}

.link-btn {
@@ -196,6 +196,10 @@
}
}

.grid-static-col[data-fieldtype="Check"] .static-area {
padding-top: 2px;
}

.grid-static-col[data-fieldtype="Rating"] .field-area {
margin-top: 1rem;
margin-left: 1rem;


+ 22
- 12
frappe/public/scss/desk/form.scss Wyświetl plik

@@ -1,6 +1,12 @@
@import "../common/form.scss";
@import '~cropperjs/dist/cropper.min';

.std-form-layout > .form-layout > .form-page {
border-radius: var(--border-radius-md);
box-shadow: var(--card-shadow);
background-color: var(--card-bg);
}

.form-section, .form-dashboard-section {
margin: 0px;

@@ -12,6 +18,7 @@

.section-head {
@extend .head-title;
font-size: var(--text-base);
width: 100%;
padding: var(--padding-sm) var(--padding-md);
margin: 0;
@@ -47,8 +54,12 @@

.form-section.card-section,
.form-dashboard-section {
margin-bottom: var(--margin-lg);
@extend .frappe-card;
border-bottom: 1px solid var(--gray-200);
padding: var(--padding-xs);
}

.row.form-section.card-section.visible-section:last-child {
border-bottom: none;
}

.form-dashboard-section {
@@ -57,9 +68,8 @@
}
.section-body {
display: block;
padding-left: var(--padding-md);
padding-right: var(--padding-md);
padding-bottom: var(--padding-md);
padding: var(--padding-md);
padding-top: 0;
}
}

@@ -85,7 +95,8 @@

.comment-box {
@include card();
padding: 25px var(--padding-xl);
margin-top: var(--margin-lg);
padding: var(--padding-lg);
.comment-input-header {
@extend .head-title;
margin-bottom: var(--margin-sm);
@@ -304,19 +315,18 @@
}

.form-tabs-list {
margin-bottom: var(--margin-lg);
padding-left: var(--padding-xs);
border-bottom: 1px solid var(--gray-200);

.form-tabs {
.nav-item {
.nav-link {
padding-bottom: var(--padding-md);
color: var(--gray-700);
padding-left: 0;
padding-right: 0;
margin-right: var(--margin-xl);
padding: var(--padding-md) 0;
margin: 0 var(--margin-md);

&.active {
font-weight: 500;
font-weight: 600;
border-bottom: 1px solid var(--primary);
color: var(--text-color);
}


+ 19
- 16
frappe/public/scss/desk/page.scss Wyświetl plik

@@ -156,26 +156,29 @@
.result, .no-result, .freeze {
min-height: #{"calc(100vh - 284px)"};
}
}

.msg-box {
margin-bottom: 4em;
font-size: var(--text-sm);
.msg-box {
margin-bottom: 4em;
font-size: var(--text-sm);

// To compensate for perceived centering
.null-state {
height: 85px;
width: auto;
margin-bottom: var(--margin-md);
img {
fill: var(--fg-color);
}
// To compensate for perceived centering
.null-state {
height: 85px;
width: auto;
margin-bottom: var(--margin-md);
img {
fill: var(--fg-color);
}
}
p {
font-size: var(--text-md);
}

.meta-description {
width: 45%;
margin-right: auto;
margin-left: auto;
}
.meta-description {
width: 45%;
margin-right: auto;
margin-left: auto;
}
}
}


+ 11
- 10
frappe/public/scss/print.bundle.scss Wyświetl plik

@@ -1,13 +1,14 @@
@import "frappe/public/css/bootstrap.css";
@import './common/quill';
@import "./common/quill";
@import "./desk/css_variables";


// .print-format {
// .ql-snow .ql-editor {
// height: auto;
// min-height: 0;
// // max-height: 0;
// }
// }

// !! PDF Barcode hack !!
// Workaround for rendering barcodes prior to https://github.com/frappe/frappe/pull/15307
@media print {
svg[data-barcode-value] > rect {
fill: white !important;
}
svg[data-barcode-value] > g {
fill: black !important;
}
}

+ 22
- 0
frappe/templates/signup.html Wyświetl plik

@@ -0,0 +1,22 @@
<form class="form-signin form-signup hide" role="form">
<div class="page-card-body">
<div class="form-group">
<label class="form-label sr-only" for="signup_fullname">{{ _("Full Name") }}</label>
<input type="text" id="signup_fullname" class="form-control" placeholder="{{ _('Jane Doe') }}"
required autofocus>
</div>
<div class="form-group">
<label class="form-label sr-only" for="signup_email">{{ _("Email") }}</label>
<input type="email" id="signup_email" class="form-control"
placeholder="{{ _('jane@example.com') }}" required>
</div>
</div>
<div class="page-card-actions">
<button class="btn btn-sm btn-primary btn-block btn-signup"
type="submit">{{ _("Sign up") }}</button>

<p class="text-center sign-up-message">
<a href="#login" class="blue">{{ _("Have an account? Login") }}</a>
</p>
</div>
</form>

+ 23
- 1
frappe/tests/test_db.py Wyświetl plik

@@ -246,6 +246,28 @@ class TestDB(unittest.TestCase):
clear_custom_fields(test_doctype)


def test_savepoints(self):
frappe.db.rollback()
save_point = "todonope"

created_docs = []
failed_docs = []

for _ in range(5):
frappe.db.savepoint(save_point)
doc_gone = frappe.get_doc(doctype="ToDo", description="nope").save()
failed_docs.append(doc_gone.name)
frappe.db.rollback(save_point=save_point)
doc_kept = frappe.get_doc(doctype="ToDo", description="nope").save()
created_docs.append(doc_kept.name)
frappe.db.commit()

for d in failed_docs:
self.assertFalse(frappe.db.exists("ToDo", d))
for d in created_docs:
self.assertTrue(frappe.db.exists("ToDo", d))


@run_only_if(db_type_is.MARIADB)
class TestDDLCommandsMaria(unittest.TestCase):
test_table_name = "TestNotes"
@@ -368,4 +390,4 @@ class TestDDLCommandsPost(unittest.TestCase):
AND indexname = '{index_name}' ;
""",
)
self.assertEquals(len(indexs_in_table), 1)
self.assertEquals(len(indexs_in_table), 1)

+ 81
- 0
frappe/tests/test_fixture_import.py Wyświetl plik

@@ -0,0 +1,81 @@
import os
import unittest
from typing import List

import frappe
from frappe.core.doctype.data_import.data_import import export_json, import_doc
from frappe.desk.form.save import savedocs
from frappe.model.delete_doc import delete_doc


class TestFixtureImport(unittest.TestCase):
def create_new_doctype(self, DocType: str) -> None:
file = frappe.get_app_path("frappe", "custom", "fixtures", f"{DocType}.json")

file = open(file, "r")
doc = file.read()
file.close()

savedocs(doc, "Save")

def insert_dummy_data_and_export(self, DocType: str, dummy_name_list: List[str]) -> str:
for name in dummy_name_list:
doc = frappe.get_doc({"doctype": DocType, "member_name": name})
doc.insert()

path_to_exported_fixtures = os.path.join(os.getcwd(), f"{DocType}_data.json")

export_json(DocType, path_to_exported_fixtures)

return path_to_exported_fixtures

def test_fixtures_import(self):
self.assertFalse(frappe.db.exists("DocType", "temp_doctype"))

self.create_new_doctype("temp_doctype")

dummy_name_list = ["jhon", "jane"]
path_to_exported_fixtures = self.insert_dummy_data_and_export("temp_doctype", dummy_name_list)
frappe.db.truncate("temp_doctype")

import_doc(path_to_exported_fixtures)

delete_doc("DocType", "temp_doctype", delete_permanently=True)
os.remove(path_to_exported_fixtures)

self.assertEqual(frappe.db.count("temp_doctype"), len(dummy_name_list))

data = frappe.get_all("temp_doctype", "member_name")
frappe.db.truncate("temp_doctype")

imported_data = set()
for item in data:
imported_data.add(item["member_name"])

self.assertEqual(set(dummy_name_list), imported_data)

def test_singles_fixtures_import(self):
self.assertFalse(frappe.db.exists("DocType", "temp_singles"))

self.create_new_doctype("temp_singles")

dummy_name_list = ["Phoebe"]
path_to_exported_fixtures = self.insert_dummy_data_and_export("temp_singles", dummy_name_list)

singles_doctype = frappe.qb.DocType("Singles")
truncate_query = (
frappe.qb.from_(singles_doctype)
.delete()
.where(singles_doctype.doctype == "temp_singles")
)
truncate_query.run()

import_doc(path_to_exported_fixtures)

delete_doc("DocType", "temp_singles", delete_permanently=True)
os.remove(path_to_exported_fixtures)

data = frappe.db.get_single_value("temp_singles", "member_name")
truncate_query.run()

self.assertEqual(data, dummy_name_list[0])

+ 24
- 1
frappe/tests/test_safe_exec.py Wyświetl plik

@@ -31,4 +31,27 @@ class TestSafeExec(unittest.TestCase):
self.assertEqual(frappe.db.sql("SELECT Max(name) FROM tabUser"), _locals["out"])

def test_safe_query_builder(self):
self.assertRaises(frappe.PermissionError, safe_exec, '''frappe.qb.from_("User").delete().run()''')
self.assertRaises(frappe.PermissionError, safe_exec, '''frappe.qb.from_("User").delete().run()''')

def test_call(self):
# call non whitelisted method
self.assertRaises(
frappe.PermissionError,
safe_exec,
"""frappe.call("frappe.get_user")"""
)

# call whitelisted method
safe_exec("""frappe.call("ping")""")


def test_enqueue(self):
# enqueue non whitelisted method
self.assertRaises(
frappe.PermissionError,
safe_exec,
"""frappe.enqueue("frappe.get_user", now=True)"""
)

# enqueue whitelisted method
safe_exec("""frappe.enqueue("ping", now=True)""")

+ 3
- 1
frappe/tests/test_translate.py Wyświetl plik

@@ -14,7 +14,9 @@ from frappe.utils import set_request
dirname = os.path.dirname(__file__)
translation_string_file = os.path.join(dirname, 'translation_test_file.txt')
first_lang, second_lang, third_lang, fourth_lang, fifth_lang = choices(
frappe.get_all("Language", pluck="name"), k=5
# skip "en*" since it is a default language
frappe.get_all("Language", pluck="name", filters=[["name", "not like", "en%"]]),
k=5
)

class TestTranslate(unittest.TestCase):


+ 6
- 0
frappe/utils/boilerplate.py Wyświetl plik

@@ -203,6 +203,12 @@ app_license = "{app_license}"
# before_install = "{app_name}.install.before_install"
# after_install = "{app_name}.install.after_install"

# Uninstallation
# ------------

# before_uninstall = "{app_name}.uninstall.before_uninstall"
# after_uninstall = "{app_name}.uninstall.after_uninstall"

# Desk Notifications
# ------------------
# See frappe.core.notifications.get_notification_config


+ 4
- 4
frappe/utils/data.py Wyświetl plik

@@ -507,10 +507,10 @@ def get_timespan_date_range(timespan):
"yesterday": lambda: (add_to_date(today, days=-1),) * 2,
"today": lambda: (today, today),
"tomorrow": lambda: (add_to_date(today, days=1),) * 2,
"this week": lambda: (get_first_day_of_week(today), today),
"this month": lambda: (get_first_day(today), today),
"this quarter": lambda: (get_quarter_start(today), today),
"this year": lambda: (get_year_start(today), today),
"this week": lambda: (get_first_day_of_week(today), get_last_day_of_week(today)),
"this month": lambda: (get_first_day(today), get_last_day(today)),
"this quarter": lambda: (get_quarter_start(today), get_quarter_ending(today)),
"this year": lambda: (get_year_start(today), get_year_ending(today)),
"next week": lambda: (get_first_day_of_week(add_to_date(today, days=7)), get_last_day_of_week(add_to_date(today, days=7))),
"next month": lambda: (get_first_day(add_to_date(today, months=1)), get_last_day(add_to_date(today, months=1))),
"next quarter": lambda: (get_quarter_start(add_to_date(today, months=3)), get_quarter_ending(add_to_date(today, months=3))),


+ 17
- 3
frappe/utils/file_manager.py Wyświetl plik

@@ -1,4 +1,4 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE

import frappe
@@ -17,6 +17,20 @@ class MaxFileSizeReachedError(frappe.ValidationError):
pass


def safe_b64decode(binary: bytes) -> bytes:
"""Adds padding if doesn't already exist before decoding.

This attempts to avoid the `binascii.Error: Incorrect padding` error raised
when the number of trailing = is simply not enough :crie:. Although, it may
be an indication of corrupted data.

Refs:
* https://en.wikipedia.org/wiki/Base64
* https://stackoverflow.com/questions/2941995/python-ignore-incorrect-padding-error-when-base64-decoding
"""
return base64.b64decode(binary + b"===")


def get_file_url(file_data_name):
data = frappe.db.get_value("File", file_data_name, ["file_name", "file_url"], as_dict=True)
return data.file_url or data.file_name
@@ -112,7 +126,7 @@ def get_uploaded_content():
if 'filedata' in frappe.form_dict:
if "," in frappe.form_dict.filedata:
frappe.form_dict.filedata = frappe.form_dict.filedata.rsplit(",", 1)[1]
frappe.uploaded_content = base64.b64decode(frappe.form_dict.filedata)
frappe.uploaded_content = safe_b64decode(frappe.form_dict.filedata)
frappe.uploaded_filename = frappe.form_dict.filename
return frappe.uploaded_filename, frappe.uploaded_content
else:
@@ -126,7 +140,7 @@ def save_file(fname, content, dt, dn, folder=None, decode=False, is_private=0, d

if b"," in content:
content = content.split(b",")[1]
content = base64.b64decode(content)
content = safe_b64decode(content)

file_size = check_max_file_size(content)
content_hash = get_content_hash(content)


+ 60
- 7
frappe/utils/safe_exec.py Wyświetl plik

@@ -14,11 +14,12 @@ import frappe.integrations.utils
import frappe.utils
import frappe.utils.data
from frappe import _
from frappe.handler import execute_cmd
from frappe.frappeclient import FrappeClient
from frappe.modules import scrub
from frappe.website.utils import get_next_link, get_shade, get_toc
from frappe.www.printview import get_visible_columns
from frappe.utils.background_jobs import enqueue, get_jobs

class ServerScriptNotEnabled(frappe.PermissionError):
pass
@@ -74,7 +75,9 @@ def get_safe_globals():

add_data_utils(datautils)

if "_" in getattr(frappe.local, 'form_dict', {}):
form_dict = getattr(frappe.local, 'form_dict', frappe._dict())

if "_" in form_dict:
del frappe.local.form_dict["_"]

user = getattr(frappe.local, "session", None) and frappe.local.session.user or "Guest"
@@ -89,14 +92,16 @@ def get_safe_globals():
dict=dict,
log=frappe.log,
_dict=frappe._dict,
args=form_dict,
frappe=NamespaceDict(
call=call_whitelisted_function,
flags=frappe._dict(),
format=frappe.format_value,
format_value=frappe.format_value,
date_format=date_format,
time_format=time_format,
format_date=frappe.utils.data.global_date_format,
form_dict=getattr(frappe.local, 'form_dict', {}),
form_dict=form_dict,
bold=frappe.bold,
copy_doc=frappe.copy_doc,
errprint=frappe.errprint,
@@ -132,6 +137,7 @@ def get_safe_globals():
make_post_request=frappe.integrations.utils.make_post_request,
socketio_port=frappe.conf.socketio_port,
get_hooks=get_hooks,
enqueue=safe_enqueue,
sanitize_html=frappe.utils.sanitize_html,
log_error=frappe.log_error
),
@@ -147,7 +153,8 @@ def get_safe_globals():
guess_mimetype=mimetypes.guess_type,
html2text=html2text,
dev_server=1 if frappe._dev_server else 0,
run_script=run_script
run_script=run_script,
is_job_queued=is_job_queued,
)

add_module_properties(frappe.exceptions, out.frappe, lambda obj: inspect.isclass(obj) and issubclass(obj, Exception))
@@ -190,6 +197,55 @@ def get_safe_globals():

return out

def is_job_queued(job_name, queue="default"):
'''
:param job_name: used to identify a queued job, usually dotted path to function
:param queue: should be either long, default or short
'''

site = frappe.local.site
queued_jobs = get_jobs(site=site, queue=queue, key='job_name').get(site)
return queued_jobs and job_name in queued_jobs

def safe_enqueue(function, **kwargs):
'''
Enqueue function to be executed using a background worker
Accepts frappe.enqueue params like job_name, queue, timeout, etc.
in addition to params to be passed to function

:param function: whitelised function or API Method set in Server Script
'''

return enqueue(
'frappe.utils.safe_exec.call_whitelisted_function',
function=function,
**kwargs
)

def call_whitelisted_function(function, **kwargs):
'''Executes a whitelisted function or Server Script of type API'''

return call_with_form_dict(lambda: execute_cmd(function), kwargs)

def run_script(script, **kwargs):
'''run another server script'''

return call_with_form_dict(
lambda: frappe.get_doc('Server Script', script).execute_method(),
kwargs
)

def call_with_form_dict(function, kwargs):
# temporarily update form_dict, to use inside below call
form_dict = getattr(frappe.local, 'form_dict', frappe._dict())
if kwargs:
frappe.local.form_dict = form_dict.copy().update(kwargs)

try:
return function()
finally:
frappe.local.form_dict = form_dict

def get_python_builtins():
return {
'abs': abs,
@@ -221,9 +277,6 @@ def read_sql(query, *args, **kwargs):
raise frappe.PermissionError('Only SELECT SQL allowed in scripting')
return frappe.db.sql(query, *args, **kwargs)

def run_script(script):
'''run another server script'''
return frappe.get_doc('Server Script', script).execute_method()

def _getitem(obj, key):
# guard function for RestrictedPython


+ 2
- 2
frappe/www/404.html Wyświetl plik

@@ -15,10 +15,10 @@
{{ _("There's nothing here") }}
</h2>
<div class="text-muted error-text">
{{ _("The page you are looking for have gone missing.") }}
{{ _("The page you are looking for has gone missing.") }}
</div>
<div class="mt-6 back-to-home"><a href='/' class='btn btn-primary'>{{ _("Back to Home") }}</a></div>
</div>
</div>

{% endblock %}
{% endblock %}

+ 2
- 23
frappe/www/login.html Wyświetl plik

@@ -81,7 +81,7 @@
<div class="page-card-body">
<form class="form-signin form-login" role="form">
{{ email_login_body() }}
</form>
</form>
<div class="social-logins text-center">
<p class="text-muted login-divider">{{ _("or") }}</p>
<div class="social-login-buttons">
@@ -131,28 +131,7 @@
<div class="login-content page-card">
{{ logo_section() }}
{%- if not disable_signup -%}
<form class="form-signin form-signup hide" role="form">
<div class="page-card-body">
<div class="form-group">
<label class="form-label sr-only" for="signup_fullname">Full Name</label>
<input type="text" id="signup_fullname" class="form-control" placeholder="{{ _('Jane Doe') }}"
required autofocus>
</div>
<div class="form-group">
<label class="form-label sr-only" for="signup_email">Email</label>
<input type="email" id="signup_email" class="form-control"
placeholder="{{ _('jane@example.com') }}" required>
</div>
</div>
<div class="page-card-actions">
<button class="btn btn-sm btn-primary btn-block btn-signup"
type="submit">{{ _("Sign up") }}</button>

<p class="text-center sign-up-message">
<a href="#login" class="blue">{{ _("Have an account? Login") }}</a>
</p>
</div>
</form>
{{ signup_form_template }}
{%- else -%}
<div class='page-card-head mb-2'>
<span class='indicator gray'>{{_("Signup Disabled")}}</span>


+ 12
- 0
frappe/www/login.py Wyświetl plik

@@ -12,6 +12,7 @@ from frappe.utils.password import get_decrypted_password
from frappe.utils.html_utils import get_icon_html
from frappe.integrations.oauth2_logins import decoder_compat
from frappe.website.utils import get_home_page
from frappe.utils.jinja import guess_is_path

no_cache = True

@@ -39,6 +40,17 @@ def get_context(context):
frappe.get_hooks("app_logo_url")[-1])
context["app_name"] = (frappe.db.get_single_value('Website Settings', 'app_name') or
frappe.get_system_settings("app_name") or _("Frappe"))

signup_form_template = frappe.get_hooks("signup_form_template")
if signup_form_template and len(signup_form_template) and signup_form_template[0]:
path = signup_form_template[0]
if not guess_is_path(path):
path = frappe.get_attr(signup_form_template[0])()
else:
path = "frappe/templates/signup.html"
if path:
context["signup_form_template"] = frappe.get_template(path).render()

providers = [i.name for i in frappe.get_all("Social Login Key", filters={"enable_social_login":1}, order_by="name")]
for provider in providers:
client_id, base_url = frappe.get_value("Social Login Key", provider, ["client_id", "base_url"])


+ 24
- 16
frappe/www/update-password.html Wyświetl plik

@@ -12,21 +12,21 @@
<form id="reset-password">
<div class="form-group">
<input id="old_password" type="password"
class="form-control" placeholder="{{ _("Old Password") }}">
class="form-control" placeholder="{{ _('Old Password') }}">
</div>
<div class="form-group">
<input id="new_password" type="password"
class="form-control" placeholder="{{ _("New Password") }}">
class="form-control" placeholder="{{ _('New Password') }}">
<span class="password-strength-indicator indicator"></span>
</div>
<div class="form-group">
<input id="confirm_password" type="password"
class="form-control" placeholder="{{ _("Confirm Password") }}">
class="form-control" placeholder="{{ _('Confirm Password') }}">
<p class="password-mismatch-message text-muted small hidden mt-2"></p>
</div>
<p class='password-strength-message text-muted small hidden'></p>
<button type="submit" id="update"
<button type="submit" id="update"
class="btn btn-primary btn-block btn-update">{{_("Confirm")}}</button>
</form>
{%- if not disable_signup -%}
@@ -36,7 +36,6 @@
</div>
{%- endif -%}
</div>
</section>
<style>
</style>
@@ -49,7 +48,7 @@ frappe.ready(function() {
}

if(frappe.utils.get_url_arg("password_expired")) {
$(".password-box").html(__('The password of your account has expired.'));
$(".password-box").html("{{ _('The password of your account has expired.') }}");
}

$("#reset-password").on("submit", function() {
@@ -69,15 +68,23 @@ frappe.ready(function() {
}
const confirm_password = $('#confirm_password').val()
if (!args.old_password && !args.key) {
frappe.msgprint(__("Old Password Required."));
frappe.msgprint({
title: "{{ _('Missing Value') }}",
message: "{{ _('Please enter your old password.') }}",
clear: true
});
}
if (!args.new_password) {
frappe.msgprint(__("New Password Required."));
frappe.msgprint({
title: "{{ _('Missing Value') }}",
message: "{{ _('Please enter your new password.') }}",
clear: true
});
}
if (args.new_password !== confirm_password) {
$('.password-mismatch-message').text(__("Passwords do not match"))
$('.password-mismatch-message').text("{{ _('Passwords do not match') }}")
.removeClass('hidden text-muted').addClass('text-danger');
return false
return false;
}

frappe.call({
@@ -87,10 +94,10 @@ frappe.ready(function() {
args: args,
statusCode: {
401: function() {
$(".page-card-head .reset-password-heading").text(__("Invalid Password"));
$(".page-card-head .reset-password-heading").text("{{ _('Invalid Password') }}");
},
410: function({ responseJSON }) {
const title = __("Invalid Link");
const title = "{{ _('Invalid Link') }}";
const message = responseJSON.message;
$(".page-card-head .reset-password-heading").text(title);
frappe.msgprint({ title: title, message: message, clear: true });
@@ -100,10 +107,11 @@ frappe.ready(function() {
strength_indicator.addClass("hidden");
strength_message.addClass("hidden");
$(".page-card-head .reset-password-heading")
.html(__("Status Updated"));
.html("{{ _('Status Updated') }}");
if(r.message) {
frappe.msgprint({
message: __("Password Updated"),
title: "{{ _('Password set') }}",
message: "{{ _('Your new password has been set successfully.') }}",
// password is updated successfully
// clear any server message
clear: true
@@ -176,7 +184,7 @@ frappe.ready(function() {
var message = [];
feedback.help_msg = "";
if(!feedback.password_policy_validation_passed){
feedback.help_msg = "<br>" + "{{ _("Hint: Include symbols, numbers and capital letters in the password") }}";
feedback.help_msg = "<br>" + "{{ _('Hint: Include symbols, numbers and capital letters in the password') }}";
}
if (feedback) {
if(!feedback.password_policy_validation_passed){


Ładowanie…
Anuluj
Zapisz