@@ -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') | |||
@@ -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"); | |||
} | |||
}); | |||
}); | |||
}); |
@@ -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]) | |||
@@ -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, | |||
@@ -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) | |||
@@ -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') |
@@ -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 | |||
@@ -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() | |||
@@ -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,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 */ | |||
@@ -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 | |||
@@ -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: | |||
@@ -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" | |||
} | |||
] | |||
} |
@@ -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" | |||
} | |||
] | |||
} |
@@ -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 | |||
@@ -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 | |||
@@ -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( | |||
@@ -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" ( | |||
@@ -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], | |||
@@ -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; | |||
@@ -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"), | |||
@@ -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 | |||
@@ -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 | |||
@@ -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: | |||
@@ -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 | |||
@@ -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") | |||
@@ -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: | |||
@@ -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 |
@@ -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) |
@@ -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() |
@@ -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) | |||
@@ -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' } | |||
); | |||
} | |||
@@ -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 { | |||
@@ -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); | |||
} | |||
}; |
@@ -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, | |||
); | |||
} | |||
@@ -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'); | |||
} | |||
}); | |||
} | |||
@@ -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({ | |||
@@ -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>`; | |||
@@ -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' | |||
} | |||
}); | |||
@@ -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]; | |||
@@ -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'); | |||
@@ -299,7 +299,6 @@ frappe.ui.Tree = class { | |||
.appendTo($toolbar); | |||
$link.on('click', () => { | |||
obj.click(node); | |||
this.refresh(); | |||
}); | |||
}); | |||
@@ -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 | |||
}) | |||
@@ -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); | |||
}); | |||
}); |
@@ -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 | |||
@@ -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; | |||
@@ -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() { | |||
@@ -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); | |||
@@ -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; | |||
@@ -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); | |||
} | |||
@@ -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; | |||
} | |||
} | |||
} | |||
@@ -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; | |||
} | |||
} |
@@ -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> |
@@ -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) |
@@ -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]) |
@@ -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)""") |
@@ -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): | |||
@@ -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 | |||
@@ -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))), | |||
@@ -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) | |||
@@ -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 | |||
@@ -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 %} |
@@ -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,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"]) | |||
@@ -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){ | |||