Browse Source

Merge branch 'develop' into web-form-list-button

version-14
Jannat Patel 3 years ago
committed by GitHub
parent
commit
def2b6fbd1
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 1040 additions and 142 deletions
  1. +59
    -0
      cypress/fixtures/child_table_doctype_1.js
  2. +6
    -0
      cypress/fixtures/doctype_with_child_table.js
  3. +2
    -0
      cypress/integration/dashboard_links.js
  4. +107
    -0
      cypress/integration/grid_search.js
  5. +3
    -0
      cypress/integration/list_paging.js
  6. +12
    -10
      esbuild/esbuild.js
  7. +3
    -1
      frappe/__init__.py
  8. +4
    -1
      frappe/commands/site.py
  9. +1
    -1
      frappe/core/doctype/data_export/exporter.py
  10. +14
    -0
      frappe/core/doctype/doctype/doctype.js
  11. +8
    -3
      frappe/core/doctype/doctype/doctype.json
  12. +13
    -0
      frappe/core/doctype/doctype/doctype.py
  13. +19
    -2
      frappe/core/doctype/doctype/test_doctype.py
  14. +4
    -1
      frappe/core/doctype/server_script/test_server_script.py
  15. +2
    -1
      frappe/core/doctype/user_permission/user_permission.js
  16. +4
    -5
      frappe/custom/doctype/customize_form/customize_form.js
  17. +2
    -1
      frappe/database/__init__.py
  18. +0
    -2
      frappe/database/database.py
  19. +23
    -1
      frappe/database/mariadb/schema.py
  20. +43
    -3
      frappe/database/postgres/database.py
  21. +17
    -1
      frappe/database/postgres/schema.py
  22. +8
    -0
      frappe/database/postgres/setup_db.py
  23. +80
    -0
      frappe/database/sequence.py
  24. +2
    -1
      frappe/desk/form/load.py
  25. +1
    -1
      frappe/desk/search.py
  26. +1
    -1
      frappe/email/doctype/newsletter/test_newsletter.py
  27. +7
    -2
      frappe/event_streaming/doctype/event_update_log/event_update_log.py
  28. +3
    -2
      frappe/frappeclient.py
  29. +1
    -1
      frappe/installer.py
  30. +1
    -1
      frappe/model/base_document.py
  31. +63
    -22
      frappe/model/db_query.py
  32. +1
    -1
      frappe/model/delete_doc.py
  33. +51
    -4
      frappe/model/naming.py
  34. +1
    -1
      frappe/patches.txt
  35. +10
    -6
      frappe/public/js/frappe/form/form.js
  36. +118
    -21
      frappe/public/js/frappe/form/grid.js
  37. +123
    -7
      frappe/public/js/frappe/form/grid_row.js
  38. +3
    -3
      frappe/public/js/frappe/form/toolbar.js
  39. +1
    -1
      frappe/public/js/frappe/list/list_view.js
  40. +5
    -5
      frappe/public/js/frappe/model/model.js
  41. +19
    -1
      frappe/public/js/frappe/utils/utils.js
  42. +24
    -1
      frappe/public/scss/common/grid.scss
  43. +2
    -1
      frappe/realtime.py
  44. +47
    -0
      frappe/tests/test_db.py
  45. +21
    -0
      frappe/tests/test_db_query.py
  46. +11
    -0
      frappe/tests/test_naming.py
  47. +50
    -6
      frappe/tests/ui_test_helpers.py
  48. +24
    -13
      frappe/utils/__init__.py
  49. +1
    -1
      frappe/utils/data.py
  50. +6
    -5
      frappe/utils/diff.py
  51. +3
    -1
      frappe/utils/global_search.py
  52. +6
    -0
      frappe/utils/install.py

+ 59
- 0
cypress/fixtures/child_table_doctype_1.js View File

@@ -0,0 +1,59 @@
export default {
name: "Child Table Doctype 1",
actions: [],
custom: 1,
autoname: "format: Test-{####}",
creation: "2022-02-09 20:15:21.242213",
doctype: "DocType",
editable_grid: 1,
engine: "InnoDB",
fields: [
{
fieldname: "data",
fieldtype: "Data",
in_list_view: 1,
label: "Data"
},
{
fieldname: "barcode",
fieldtype: "Barcode",
in_list_view: 1,
label: "Barcode"
},
{
fieldname: "check",
fieldtype: "Check",
in_list_view: 1,
label: "Check"
},
{
fieldname: "rating",
fieldtype: "Rating",
in_list_view: 1,
label: "Rating"
},
{
fieldname: "duration",
fieldtype: "Duration",
in_list_view: 1,
label: "Duration"
},
{
fieldname: "date",
fieldtype: "Date",
in_list_view: 1,
label: "Date"
}
],
links: [],
istable: 1,
modified: "2022-02-10 12:03:12.603763",
modified_by: "Administrator",
module: "Custom",
naming_rule: "By fieldname",
owner: "Administrator",
permissions: [],
sort_field: 'modified',
sort_order: 'ASC',
track_changes: 1
};

+ 6
- 0
cypress/fixtures/doctype_with_child_table.js View File

@@ -20,6 +20,12 @@ export default {
label: "Child Table",
options: "Child Table Doctype",
reqd: 1
},
{
fieldname: "child_table_1",
fieldtype: "Table",
label: "Child Table 1",
options: "Child Table Doctype 1"
}
],
links: [],


+ 2
- 0
cypress/integration/dashboard_links.js View File

@@ -1,5 +1,6 @@
import doctype_with_child_table from '../fixtures/doctype_with_child_table';
import child_table_doctype from '../fixtures/child_table_doctype';
import child_table_doctype_1 from '../fixtures/child_table_doctype_1';
import doctype_to_link from '../fixtures/doctype_to_link';
const doctype_to_link_name = doctype_to_link.name;
const child_table_doctype_name = child_table_doctype.name;
@@ -9,6 +10,7 @@ context('Dashboard links', () => {
cy.visit('/login');
cy.login();
cy.insert_doc('DocType', child_table_doctype, true);
cy.insert_doc('DocType', child_table_doctype_1, true);
cy.insert_doc('DocType', doctype_with_child_table, true);
cy.insert_doc('DocType', doctype_to_link, true);
return cy.window().its('frappe').then(frappe => {


+ 107
- 0
cypress/integration/grid_search.js View File

@@ -0,0 +1,107 @@
import doctype_with_child_table from '../fixtures/doctype_with_child_table';
import child_table_doctype from '../fixtures/child_table_doctype';
import child_table_doctype_1 from '../fixtures/child_table_doctype_1';
const doctype_with_child_table_name = doctype_with_child_table.name;

context('Grid Search', () => {
before(() => {
cy.visit('/login');
cy.login();
cy.visit('/app/website');
cy.insert_doc('DocType', child_table_doctype, true);
cy.insert_doc('DocType', child_table_doctype_1, true);
cy.insert_doc('DocType', doctype_with_child_table, true);
return cy.window().its('frappe').then(frappe => {
return frappe.xcall("frappe.tests.ui_test_helpers.insert_doctype_with_child_table_record", {
name: doctype_with_child_table_name
});
});
});

it('Test search row visibility', () => {
cy.window().its('frappe').then(frappe => {
frappe.model.user_settings.save('Doctype With Child Table', 'GridView', {
'Child Table Doctype 1': [
{'fieldname': 'data', 'columns': 2},
{'fieldname': 'barcode', 'columns': 1},
{'fieldname': 'check', 'columns': 1},
{'fieldname': 'rating', 'columns': 2},
{'fieldname': 'duration', 'columns': 2},
{'fieldname': 'date', 'columns': 2}
]
});
});

cy.visit(`/app/doctype-with-child-table/Test Grid Search`);

cy.get('.frappe-control[data-fieldname="child_table_1"]').as('table');
cy.get('@table').find('.grid-row-check:last').click();
cy.get('@table').find('.grid-footer').contains('Delete').click();
cy.get('.grid-heading-row .grid-row .search').should('not.exist');
});

it('test search field for different fieldtypes', () => {
cy.visit(`/app/doctype-with-child-table/Test Grid Search`);

cy.get('.frappe-control[data-fieldname="child_table_1"]').as('table');

// Index Column
cy.get('@table').find('.grid-heading-row .row-index.search input').type('3');
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 2);
cy.get('@table').find('.grid-heading-row .row-index.search input').clear();

// Data Column
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Data"]').type('Data');
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 1);
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Data"]').clear();

// Barcode Column
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Barcode"]').type('092');
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 4);
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Barcode"]').clear();

// Check Column
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Check"]').type('1');
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 9);
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Check"]').clear();

cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Check"]').type('0');
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 11);
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Check"]').clear();

// Rating Column
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Rating"]').type('3');
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 3);
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Rating"]').clear();

// Duration Column
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Duration"]').type('3d');
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 3);
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Duration"]').clear();

// Date Column
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Date"]').type('2022');
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 4);
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Date"]').clear();
});

it('test with multiple filter', () => {
cy.get('.frappe-control[data-fieldname="child_table_1"]').as('table');

// Data Column
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Data"]').type('a');
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 10);

// Barcode Column
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Barcode"]').type('0');
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 8);

// Duration Column
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Duration"]').type('d');
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 5);

// Date Column
cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Date"]').type('02-');
cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 2);
});
});

+ 3
- 0
cypress/integration/list_paging.js View File

@@ -31,5 +31,8 @@ context('List Paging', () => {
cy.get('.list-paging-area .btn-group .btn-paging[data-value="500"]').click();

cy.get('.list-paging-area .list-count').should('contain.text', '500 of');
cy.get('.list-paging-area .btn-more').click();

cy.get('.list-paging-area .list-count').should('contain.text', '1000 of');
});
});

+ 12
- 10
esbuild/esbuild.js View File

@@ -286,7 +286,7 @@ function get_watch_config() {
notify_redis({ error });
} else {
let {
assets_json,
new_assets_json,
prev_assets_json
} = await write_assets_json(result.metafile);

@@ -294,7 +294,7 @@ function get_watch_config() {
if (prev_assets_json) {
changed_files = get_rebuilt_assets(
prev_assets_json,
assets_json
new_assets_json
);

let timestamp = new Date().toLocaleTimeString();
@@ -384,6 +384,7 @@ let prev_assets_json;
let curr_assets_json;

async function write_assets_json(metafile) {
let rtl = false;
prev_assets_json = curr_assets_json;
let out = {};
for (let output in metafile.outputs) {
@@ -392,13 +393,14 @@ async function write_assets_json(metafile) {
if (info.entryPoint) {
let key = path.basename(info.entryPoint);
if (key.endsWith('.css') && asset_path.includes('/css-rtl/')) {
rtl = true;
key = `rtl_${key}`;
}
out[key] = asset_path;
}
}

let assets_json_path = path.resolve(assets_path, "assets.json");
let assets_json_path = path.resolve(assets_path, `assets${rtl?'-rtl':''}.json`);
let assets_json;
try {
assets_json = await fs.promises.readFile(assets_json_path, "utf-8");
@@ -407,21 +409,21 @@ async function write_assets_json(metafile) {
}
assets_json = JSON.parse(assets_json);
// update with new values
assets_json = Object.assign({}, assets_json, out);
curr_assets_json = assets_json;
let new_assets_json = Object.assign({}, assets_json, out);
curr_assets_json = new_assets_json;

await fs.promises.writeFile(
assets_json_path,
JSON.stringify(assets_json, null, 4)
JSON.stringify(new_assets_json, null, 4)
);
await update_assets_json_in_cache(assets_json);
await update_assets_json_in_cache();
return {
assets_json,
new_assets_json,
prev_assets_json
};
}

function update_assets_json_in_cache(assets_json) {
function update_assets_json_in_cache() {
// update assets_json cache in redis, so that it can be read directly by python
return new Promise(resolve => {
let client = get_redis_subscriber("redis_cache");
@@ -429,7 +431,7 @@ function update_assets_json_in_cache(assets_json) {
client.on("error", _ => {
log_warn("Cannot connect to redis_cache to update assets_json");
});
client.set("assets_json", JSON.stringify(assets_json), err => {
client.del("assets_json", err => {
client.unref();
resolve();
});


+ 3
- 1
frappe/__init__.py View File

@@ -35,6 +35,7 @@ from frappe.query_builder import (
patch_query_execute,
patch_query_aggregation,
)
from frappe.utils.data import cstr

__version__ = '14.0.0-dev'

@@ -214,6 +215,7 @@ def init(site, sites_path=None, new_site=False):
local.cache = {}
local.document_cache = {}
local.meta_cache = {}
local.autoincremented_status_map = {site: -1}
local.form_dict = _dict()
local.session = _dict()
local.dev_server = _dev_server
@@ -1015,7 +1017,7 @@ def get_module(modulename):

def scrub(txt):
"""Returns sluggified string. e.g. `Sales Order` becomes `sales_order`."""
return txt.replace(' ', '_').replace('-', '_').lower()
return cstr(txt).replace(' ', '_').replace('-', '_').lower()

def unscrub(txt):
"""Returns titlified string. e.g. `sales_order` becomes `Sales Order`."""


+ 4
- 1
frappe/commands/site.py View File

@@ -677,7 +677,9 @@ def _drop_site(site, db_root_username=None, db_root_password=None, archived_site

try:
if not no_backup:
scheduled_backup(ignore_files=False, force=True)
click.secho(f"Taking backup of {site}", fg="green")
odb = scheduled_backup(ignore_files=False, force=True, verbose=True)
odb.print_summary()
except Exception as err:
if force:
pass
@@ -692,6 +694,7 @@ def _drop_site(site, db_root_username=None, db_root_password=None, archived_site
click.echo("\n".join(messages))
sys.exit(1)

click.secho("Dropping site database and user", fg="green")
drop_user_and_database(frappe.conf.db_name, db_root_username, db_root_password)

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


+ 1
- 1
frappe/core/doctype/data_export/exporter.py View File

@@ -324,7 +324,7 @@ class DataExporter:
d = doc.copy()
meta = frappe.get_meta(dt)
if self.all_doctypes:
d.name = '"'+ d.name+'"'
d.name = f'"{d.name}"'

if len(rows) < rowidx + 1:
rows.append([""] * (len(self.columns) + 1))


+ 14
- 0
frappe/core/doctype/doctype/doctype.js View File

@@ -61,6 +61,13 @@ frappe.ui.form.on('DocType', {
frm.events.set_naming_rule_description(frm);
},

istable: (frm) => {
if (frm.doc.istable && frm.is_new()) {
frm.set_value('autoname', 'autoincrement');
frm.set_value('allow_rename', 0);
}
},

naming_rule: function(frm) {
// set the "autoname" property based on naming_rule
if (frm.doc.naming_rule && !frm.__from_autoname) {
@@ -70,6 +77,10 @@ frappe.ui.form.on('DocType', {

if (frm.doc.naming_rule=='Set by user') {
frm.set_value('autoname', 'Prompt');
} else if (frm.doc.naming_rule === 'Autoincrement') {
frm.set_value('autoname', 'autoincrement');
// set allow rename to be false when using autoincrement
frm.set_value('allow_rename', 0);
} else if (frm.doc.naming_rule=='By fieldname') {
frm.set_value('autoname', 'field:');
} else if (frm.doc.naming_rule=='By "Naming Series" field') {
@@ -91,6 +102,7 @@ frappe.ui.form.on('DocType', {
set_naming_rule_description(frm) {
let naming_rule_description = {
'Set by user': '',
'Autoincrement': 'Uses Auto Increment feature of database.<br><b>WARNING: After using this option, any other naming option will not be accessible.</b>',
'By fieldname': 'Format: <code>field:[fieldname]</code>. Valid fieldname must exist',
'By "Naming Series" field': 'Format: <code>naming_series:[fieldname]</code>. Fieldname called <code>naming_series</code> must exist',
'Expression': 'Format: <code>format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####}</code> - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.',
@@ -111,6 +123,8 @@ frappe.ui.form.on('DocType', {
frm.__from_autoname = true;
if (frm.doc.autoname.toLowerCase() === 'prompt') {
frm.set_value('naming_rule', 'Set by user');
} else if (frm.doc.autoname.toLowerCase() === 'autoincrement') {
frm.set_value('naming_rule', 'Autoincrement');
} else if (frm.doc.autoname.startsWith('field:')) {
frm.set_value('naming_rule', 'By fieldname');
} else if (frm.doc.autoname.startsWith('naming_series:')) {


+ 8
- 3
frappe/core/doctype/doctype/doctype.json View File

@@ -208,7 +208,7 @@
"label": "Naming"
},
{
"description": "Naming Options:\n<ol><li><b>field:[fieldname]</b> - By Field</li><li><b>naming_series:</b> - By Naming Series (field called naming_series must be present</li><li><b>Prompt</b> - Prompt user for a name</li><li><b>[series]</b> - Series by prefix (separated by a dot); for example PRE.#####</li>\n<li><b>format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####}</b> - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.</li></ol>",
"description": "Naming Options:\n<ol><li><b>field:[fieldname]</b> - By Field</li><li><b>autoincrement</b> - Uses Databases' Auto Increment feature</li><li><b>naming_series:</b> - By Naming Series (field called naming_series must be present</li><li><b>Prompt</b> - Prompt user for a name</li><li><b>[series]</b> - Series by prefix (separated by a dot); for example PRE.#####</li>\n<li><b>format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####}</b> - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.</li></ol>",
"fieldname": "autoname",
"fieldtype": "Data",
"label": "Auto Name",
@@ -216,6 +216,7 @@
"oldfieldtype": "Data"
},
{
"depends_on": "eval:doc.naming_rule !== \"Autoincrement\"",
"fieldname": "name_case",
"fieldtype": "Select",
"label": "Name Case",
@@ -282,6 +283,7 @@
},
{
"default": "1",
"depends_on": "eval:doc.naming_rule !== \"Autoincrement\"",
"fieldname": "allow_rename",
"fieldtype": "Check",
"label": "Allow Rename",
@@ -565,7 +567,7 @@
"fieldtype": "Select",
"label": "Naming Rule",
"length": 40,
"options": "\nSet by user\nBy fieldname\nBy \"Naming Series\" field\nExpression\nExpression (old style)\nRandom\nBy script"
"options": "\nSet by user\nAutoincrement\nBy fieldname\nBy \"Naming Series\" field\nExpression\nExpression (old style)\nRandom\nBy script"
},
{
"fieldname": "migration_hash",
@@ -593,6 +595,7 @@
],
"icon": "fa fa-bolt",
"idx": 6,
"index_web_pages_for_search": 1,
"links": [
{
"group": "Views",
@@ -670,10 +673,11 @@
"link_fieldname": "reference_doctype"
}
],
"modified": "2022-01-07 16:07:06.196534",
"modified": "2022-02-15 21:47:16.467217",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType",
"naming_rule": "Set by user",
"owner": "Administrator",
"permissions": [
{
@@ -703,5 +707,6 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

+ 13
- 0
frappe/core/doctype/doctype/doctype.py View File

@@ -60,6 +60,7 @@ class DocType(Document):

self.check_developer_mode()

self.validate_autoname()
self.validate_name()

self.set_defaults_for_single_and_table()
@@ -714,6 +715,18 @@ class DocType(Document):
self.name)
return max_idx and max_idx[0][0] or 0

def validate_autoname(self):
if not self.is_new():
doc_before_save = self.get_doc_before_save()
if doc_before_save:
if (self.autoname == "autoincrement" and doc_before_save.autoname != "autoincrement") \
or (self.autoname != "autoincrement" and doc_before_save.autoname == "autoincrement"):
frappe.throw(_("Cannot change to/from Autoincrement naming rule"))

else:
if self.autoname == "autoincrement":
self.allow_rename = 0

def validate_name(self, name=None):
if not name:
name = self.name


+ 19
- 2
frappe/core/doctype/doctype/test_doctype.py View File

@@ -505,7 +505,23 @@ class TestDocType(unittest.TestCase):

dt.delete()

def new_doctype(name, unique=0, depends_on='', fields=None):
def test_autoincremented_doctype_transition(self):
frappe.delete_doc("testy_autoinc_dt")
dt = new_doctype("testy_autoinc_dt", autoincremented=True).insert(ignore_permissions=True)
dt.autoname = "hash"

try:
dt.save(ignore_permissions=True)
except frappe.ValidationError as e:
self.assertEqual(e.args[0], "Cannot change to/from Autoincrement naming rule")
else:
self.fail("Shouldnt be possible to transition autoincremented doctype to any other naming rule")
finally:
# cleanup
dt.delete(ignore_permissions=True)


def new_doctype(name, unique=0, depends_on='', fields=None, autoincremented=False):
doc = frappe.get_doc({
"doctype": "DocType",
"module": "Core",
@@ -521,7 +537,8 @@ def new_doctype(name, unique=0, depends_on='', fields=None):
"role": "System Manager",
"read": 1,
}],
"name": name
"name": name,
"autoname": "autoincrement" if autoincremented else ""
})

if fields:


+ 4
- 1
frappe/core/doctype/server_script/test_server_script.py View File

@@ -112,7 +112,10 @@ class TestServerScript(unittest.TestCase):
self.assertEqual(frappe.get_doc('Server Script', 'test_return_value').execute_method(), 'hello')

def test_permission_query(self):
self.assertTrue('where (1 = 1)' in frappe.db.get_list('ToDo', run=False))
if frappe.conf.db_type == "mariadb":
self.assertTrue('where (1 = 1)' in frappe.db.get_list('ToDo', run=False))
else:
self.assertTrue('where (1 = \'1\')' in frappe.db.get_list('ToDo', run=False))
self.assertTrue(isinstance(frappe.db.get_list('ToDo'), list))

def test_attribute_error(self):


+ 2
- 1
frappe/core/doctype/user_permission/user_permission.js View File

@@ -44,8 +44,9 @@ frappe.ui.form.on('User Permission', {

set_applicable_for_constraint: frm => {
frm.toggle_reqd('applicable_for', !frm.doc.apply_to_all_doctypes);

if (frm.doc.apply_to_all_doctypes && frm.doc.applicable_for) {
frm.set_value('applicable_for', null);
frm.set_value('applicable_for', null, null, true);
}
},



+ 4
- 5
frappe/custom/doctype/customize_form/customize_form.js View File

@@ -14,7 +14,6 @@ frappe.ui.form.on("Customize Form", {
},

onload: function(frm) {
frm.disable_save();
frm.set_query("doc_type", function() {
return {
translate_values: false,
@@ -110,7 +109,7 @@ frappe.ui.form.on("Customize Form", {
},

refresh: function(frm) {
frm.disable_save();
frm.disable_save(true);
frm.page.clear_icons();

if (frm.doc.doc_type) {
@@ -169,7 +168,7 @@ frappe.ui.form.on("Customize Form", {
doc_type = localStorage.getItem("customize_doctype");
}
if (doc_type) {
setTimeout(() => frm.set_value("doc_type", doc_type), 1000);
setTimeout(() => frm.set_value("doc_type", doc_type, false, true), 1000);
}
},

@@ -341,11 +340,11 @@ frappe.customize_form.confirm = function(msg, frm) {
}

frappe.customize_form.clear_locals_and_refresh = function(frm) {
delete frm.doc.__unsaved;
// clear doctype from locals
frappe.model.clear_doc("DocType", frm.doc.doc_type);
delete frappe.meta.docfield_copy[frm.doc.doc_type];

frm.refresh();
}
};

extend_cscript(cur_frm.cscript, new frappe.model.DocTypeController({frm: cur_frm}));

+ 2
- 1
frappe/database/__init__.py View File

@@ -18,7 +18,8 @@ def setup_database(force, source_sql=None, verbose=None, no_mariadb_socket=False
def drop_user_and_database(db_name, root_login=None, root_password=None):
import frappe
if frappe.conf.db_type == 'postgres':
pass
import frappe.database.postgres.setup_db
return frappe.database.postgres.setup_db.drop_user_and_database(db_name, root_login, root_password)
else:
import frappe.database.mariadb.setup_db
return frappe.database.mariadb.setup_db.drop_user_and_database(db_name, root_login, root_password)


+ 0
- 2
frappe/database/database.py View File

@@ -142,8 +142,6 @@ class Database(object):
self.log_query(query, values, debug, explain)

if values!=():
if isinstance(values, dict):
values = dict(values)

# MySQL-python==1.2.5 hack!
if not isinstance(values, (dict, tuple, list)):


+ 23
- 1
frappe/database/mariadb/schema.py View File

@@ -1,12 +1,16 @@
import frappe
from frappe import _
from frappe.database.schema import DBTable
from frappe.database.sequence import create_sequence
from frappe.model import log_types


class MariaDBTable(DBTable):
def create(self):
additional_definitions = ""
engine = self.meta.get("engine") or "InnoDB"
varchar_len = frappe.db.VARCHAR_LEN
name_column = f"name varchar({varchar_len}) primary key"

# columns
column_defs = self.get_column_definitions()
@@ -29,9 +33,27 @@ class MariaDBTable(DBTable):
)
) + ',\n'

# creating sequence(s)
if (not self.meta.issingle and self.meta.autoname == "autoincrement")\
or self.doctype in log_types:

# NOTE: using a very small cache - as during backup, if the sequence was used in anyform,
# it drops the cache and uses the next non cached value in setval func and
# puts that in the backup file, which will start the counter
# from that value when inserting any new record in the doctype.
# By default the cache is 1000 which will mess up the sequence when
# using the system after a restore.
# issue link: https://jira.mariadb.org/browse/MDEV-21786
create_sequence(self.doctype, check_not_exists=True, cache=50)

# NOTE: not used nextval func as default as the ability to restore
# database with sequences has bugs in mariadb and gives a scary error.
# issue link: https://jira.mariadb.org/browse/MDEV-21786
name_column = "name bigint primary key"

# create table
query = f"""create table `{self.table_name}` (
name varchar({varchar_len}) not null primary key,
{name_column},
creation datetime(6),
modified datetime(6),
modified_by varchar({varchar_len}),


+ 43
- 3
frappe/database/postgres/database.py View File

@@ -99,8 +99,13 @@ class PostgresDatabase(Database):
return db_size[0].get('database_size')

# pylint: disable=W0221
def sql(self, query, *args, **kwargs):
return super(PostgresDatabase, self).sql(modify_query(query), *args, **kwargs)
def sql(self, query, values=(), *args, **kwargs):
return super(PostgresDatabase, self).sql(
modify_query(query),
modify_values(values),
*args,
**kwargs
)

def get_tables(self, cached=True):
return [d[0] for d in self.sql("""select table_name
@@ -333,10 +338,45 @@ def modify_query(query):
if re.search('from tab', query, flags=re.IGNORECASE):
query = re.sub(r'from tab([\w-]*)', r'from "tab\1"', query, flags=re.IGNORECASE)

# only find int (with/without signs), ignore decimals (with/without signs), ignore hashes (which start with numbers),
# drop .0 from decimals and add quotes around them
#
# >>> query = "c='abcd' , a >= 45, b = -45.0, c = 40, d=4500.0, e=3500.53, f=40psdfsd, g=9092094312, h=12.00023"
# >>> re.sub(r"([=><]+)\s*(?!\d+[a-zA-Z])(?![+-]?\d+\.\d\d+)([+-]?\d+)(\.0)?", r"\1 '\2'", query)
# "c='abcd' , a >= '45', b = '-45', c = '40', d= '4500', e=3500.53, f=40psdfsd, g= '9092094312', h=12.00023

query = re.sub(r"([=><]+)\s*(?!\d+[a-zA-Z])(?![+-]?\d+\.\d\d+)([+-]?\d+)(\.0)?", r"\1 '\2'", query)
return query

def modify_values(values):
def stringify_value(value):
if isinstance(value, int):
value = str(value)
elif isinstance(value, float):
truncated_float = int(value)
if value == truncated_float:
value = str(truncated_float)

return value

if not values:
return values

if isinstance(values, dict):
for k, v in values.items():
values[k] = stringify_value(v)
elif isinstance(values, (tuple, list)):
new_values = []
for val in values:
new_values.append(stringify_value(val))
values = new_values
else:
values = stringify_value(values)

return values

def replace_locate_with_strpos(query):
# strpos is the locate equivalent in postgres
if re.search(r'locate\(', query, flags=re.IGNORECASE):
query = re.sub(r'locate\(([^,]+),([^)]+)\)', r'strpos(\2, \1)', query, flags=re.IGNORECASE)
query = re.sub(r'locate\(([^,]+),([^)]+)(\)?)\)', r'strpos(\2\3, \1)', query, flags=re.IGNORECASE)
return query

+ 17
- 1
frappe/database/postgres/schema.py View File

@@ -2,10 +2,14 @@ import frappe
from frappe import _
from frappe.utils import cint, flt
from frappe.database.schema import DBTable, get_definition
from frappe.database.sequence import create_sequence
from frappe.model import log_types


class PostgresTable(DBTable):
def create(self):
varchar_len = frappe.db.VARCHAR_LEN
name_column = f"name varchar({varchar_len}) primary key"

additional_definitions = ""
# columns
@@ -26,9 +30,21 @@ class PostgresTable(DBTable):
)
)

# creating sequence(s)
if (not self.meta.issingle and self.meta.autoname == "autoincrement")\
or self.doctype in log_types:

# The sequence cache is per connection.
# Since we're opening and closing connections for every transaction this results in skipping the cache
# to the next non-cached value hence not using cache in postgres.
# ref: https://stackoverflow.com/questions/21356375/postgres-9-0-4-sequence-skipping-numbers
create_sequence(self.doctype, check_not_exists=True)
name_column = "name bigint primary key"

# TODO: set docstatus length
# create table
frappe.db.sql(f"""create table `{self.table_name}` (
name varchar({varchar_len}) not null primary key,
{name_column},
creation timestamp(6),
modified timestamp(6),
modified_by varchar({varchar_len}),


+ 8
- 0
frappe/database/postgres/setup_db.py View File

@@ -95,3 +95,11 @@ def get_root_connection(root_login=None, root_password=None):
frappe.local.flags.root_connection = frappe.database.get_db(user=root_login, password=root_password)

return frappe.local.flags.root_connection


def drop_user_and_database(db_name, root_login, root_password):
root_conn = get_root_connection(frappe.flags.root_login or root_login, frappe.flags.root_password or root_password)
root_conn.commit()
root_conn.sql(f"SELECT pg_terminate_backend (pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = %s", (db_name, ))
root_conn.sql(f"DROP DATABASE IF EXISTS {db_name}")
root_conn.sql(f"DROP USER IF EXISTS {db_name}")

+ 80
- 0
frappe/database/sequence.py View File

@@ -0,0 +1,80 @@
from frappe import db, scrub


def create_sequence(
doctype_name: str,
*,
slug: str = "_id_seq",
check_not_exists: bool = False,
cycle: bool = False,
cache: int = 0,
start_value: int = 0,
increment_by: int = 0,
min_value: int = 0,
max_value: int = 0
) -> str:

query = "create sequence"
sequence_name = scrub(doctype_name + slug)

if check_not_exists:
query += " if not exists"

query += f" {sequence_name}"

if cache:
query += f" cache {cache}"
else:
# in postgres, the default is cache 1
if db.db_type == "mariadb":
query += " nocache"

if start_value:
# default is 1
query += f" start with {start_value}"

if increment_by:
# default is 1
query += f" increment by {increment_by}"

if min_value:
# default is 1
query += f" min value {min_value}"

if max_value:
query += f" max value {max_value}"

if not cycle:
if db.db_type == "mariadb":
query += " nocycle"
else:
query += " cycle"

db.sql(query)

return sequence_name


def get_next_val(doctype_name: str, slug: str = "_id_seq") -> int:
if db.db_type == "postgres":
return db.sql(f"select nextval(\'\"{scrub(doctype_name + slug)}\"\')")[0][0]
return db.sql(f"select nextval(`{scrub(doctype_name + slug)}`)")[0][0]


def set_next_val(
doctype_name: str,
next_val: int,
*,
slug: str = "_id_seq",
is_val_used :bool = False
) -> None:

if not is_val_used:
is_val_used = 0 if db.db_type == "mariadb" else "f"
else:
is_val_used = 1 if db.db_type == "mariadb" else "t"

if db.db_type == "postgres":
db.sql(f"SELECT SETVAL('\"{scrub(doctype_name + slug)}\"', {next_val}, '{is_val_used}')")
else:
db.sql(f"SELECT SETVAL(`{scrub(doctype_name + slug)}`, {next_val}, {is_val_used})")

+ 2
- 1
frappe/desk/form/load.py View File

@@ -10,6 +10,7 @@ import frappe.desk.form.meta
from frappe.model.utils.user_settings import get_user_settings
from frappe.permissions import get_doc_permissions
from frappe.desk.form.document_follow import is_document_followed
from frappe.utils.data import cstr
from frappe import _
from frappe import _dict
from urllib.parse import quote
@@ -355,7 +356,7 @@ def get_document_email(doctype, name):
return None

email = email.split("@")
return "{0}+{1}+{2}@{3}".format(email[0], quote(doctype), quote(name), email[1])
return "{0}+{1}+{2}@{3}".format(email[0], quote(doctype), quote(cstr(name)), email[1])

def get_automatic_email_link():
return frappe.db.get_value("Email Account", {"enable_incoming": 1, "enable_automatic_linking": 1}, "email_id")


+ 1
- 1
frappe/desk/search.py View File

@@ -257,7 +257,7 @@ def scrub_custom_query(query, key, txt):
def relevance_sorter(key, query, as_dict):
value = _(key.name if as_dict else key[0])
return (
value.lower().startswith(query.lower()) is not True,
cstr(value).lower().startswith(query.lower()) is not True,
value
)



+ 1
- 1
frappe/email/doctype/newsletter/test_newsletter.py View File

@@ -51,7 +51,7 @@ class TestNewsletterMixin:
"reference_name": newsletter,
})
frappe.delete_doc("Newsletter", newsletter)
frappe.db.delete("Newsletter Email Group", newsletter)
frappe.db.delete("Newsletter Email Group", {"parent": newsletter})
newsletters.remove(newsletter)

def setup_email_group(self):


+ 7
- 2
frappe/event_streaming/doctype/event_update_log/event_update_log.py View File

@@ -203,12 +203,17 @@ def get_unread_update_logs(consumer_name, dt, dn):
SELECT
update_log.name
FROM `tabEvent Update Log` update_log
JOIN `tabEvent Update Log Consumer` consumer ON consumer.parent = update_log.name
JOIN `tabEvent Update Log Consumer` consumer ON consumer.parent = %(log_name)s
WHERE
consumer.consumer = %(consumer)s
AND update_log.ref_doctype = %(dt)s
AND update_log.docname = %(dn)s
""", {'consumer': consumer_name, "dt": dt, "dn": dn}, as_dict=0)]
""", {
"consumer": consumer_name,
"dt": dt,
"dn": dn,
"log_name": "update_log.name" if frappe.conf.db_type == "mariadb" else "CAST(update_log.name AS VARCHAR)"
}, as_dict=0)]

logs = frappe.get_all(
'Event Update Log',


+ 3
- 2
frappe/frappeclient.py View File

@@ -7,6 +7,7 @@ import json
import requests

import frappe
from frappe.utils.data import cstr


class AuthError(Exception):
@@ -122,7 +123,7 @@ class FrappeClient(object):
'''Update a remote document

:param doc: dict or Document object to be updated remotely. `name` is mandatory for this'''
url = self.url + "/api/resource/" + doc.get("doctype") + "/" + doc.get("name")
url = self.url + "/api/resource/" + doc.get("doctype") + "/" + cstr(doc.get("name"))
res = self.session.put(url, data={"data":frappe.as_json(doc)}, verify=self.verify, headers=self.headers)
return frappe._dict(self.post_process(res))

@@ -207,7 +208,7 @@ class FrappeClient(object):
if fields:
params["fields"] = json.dumps(fields)

res = self.session.get(self.url + "/api/resource/" + doctype + "/" + name,
res = self.session.get(self.url + "/api/resource/" + doctype + "/" + cstr(name),
params=params, verify=self.verify, headers=self.headers)

return self.post_process(res)


+ 1
- 1
frappe/installer.py View File

@@ -611,7 +611,7 @@ def is_downgrade(sql_file_path, verbose=False):
downgrade = backup_version > current_version

if verbose and downgrade:
print("Your site will be downgraded from Frappe {0} to {1}".format(current_version, backup_version))
print(f"Your site will be downgraded from Frappe {backup_version} to {current_version}")

return downgrade



+ 1
- 1
frappe/model/base_document.py View File

@@ -475,7 +475,7 @@ class BaseDocument(object):
d = self.get_valid_dict(convert_dates_to_str=True, ignore_nulls = self.doctype in DOCTYPES_FOR_DOCTYPE)

# don't update name, as case might've been changed
name = d['name']
name = cstr(d['name'])
del d['name']

columns = list(d)


+ 63
- 22
frappe/model/db_query.py View File

@@ -164,7 +164,8 @@ class DatabaseQuery(object):

# left join parent, child tables
for child in self.tables[1:]:
args.tables += f" {self.join} {child} on ({child}.parent = {self.tables[0]}.name)"
parent_name = self.cast_name(f"{self.tables[0]}.name")
args.tables += f" {self.join} {child} on ({child}.parent = {parent_name})"

if self.grouped_or_conditions:
self.conditions.append(f"({' or '.join(self.grouped_or_conditions)})")
@@ -318,21 +319,60 @@ class DatabaseQuery(object):
]
# add tables from fields
if self.fields:
for field in self.fields:
if not ("tab" in field and "." in field) or any(x for x in sql_functions if x in field):
for i, field in enumerate(self.fields):
# add cast in locate/strpos
func_found = False
for func in sql_functions:
if func in field.lower():
self.fields[i] = self.cast_name(field, func)
func_found = True
break

if func_found or not ("tab" in field and "." in field):
continue

table_name = field.split('.')[0]

if table_name.lower().startswith('group_concat('):
table_name = table_name[13:]
if table_name.lower().startswith('ifnull('):
table_name = table_name[7:]
if not table_name[0]=='`':
table_name = f"`{table_name}`"
if table_name not in self.tables:
self.append_table(table_name)

def cast_name(self, column: str, sql_function: str = "",) -> str:
if frappe.db.db_type == "postgres":
if "name" in column.lower():
if "cast(" not in column.lower() or "::" not in column:
if not sql_function:
return f"cast({column} as varchar)"

elif sql_function == "locate(":
return re.sub(
r'locate\(([^,]+),([^)]+)\)',
r'locate(\1, cast(\2 as varchar))',
column,
flags=re.IGNORECASE
)

elif sql_function == "strpos(":
return re.sub(
r'strpos\(([^,]+),([^)]+)\)',
r'strpos(cast(\1 as varchar), \2)',
column,
flags=re.IGNORECASE
)

elif sql_function == "ifnull(":
return re.sub(
r"ifnull\(([^,]+)",
r"ifnull(cast(\1 as varchar)",
column,
flags=re.IGNORECASE
)

return column

def append_table(self, table_name):
self.tables.append(table_name)
doctype = table_name[4:-1]
@@ -423,6 +463,8 @@ class DatabaseQuery(object):
ifnull(`tabDocType`.`fieldname`, fallback) operator "value"
"""

# TODO: refactor

from frappe.boot import get_additional_filters_from_hooks
additional_filters_config = get_additional_filters_from_hooks()
f = get_filter(self.doctype, f, additional_filters_config)
@@ -432,15 +474,16 @@ class DatabaseQuery(object):
self.append_table(tname)

if 'ifnull(' in f.fieldname:
column_name = f.fieldname
column_name = self.cast_name(f.fieldname, "ifnull(")
else:
column_name = f"{tname}.{f.fieldname}"

can_be_null = True
column_name = self.cast_name(f"{tname}.{f.fieldname}")

if f.operator.lower() in additional_filters_config:
f.update(get_additional_filter_field(additional_filters_config, f, f.value))

meta = frappe.get_meta(f.doctype)
can_be_null = True

# prepare in condition
if f.operator.lower() in ('ancestors of', 'descendants of', 'not ancestors of', 'not descendants of'):
values = f.value or ''
@@ -449,12 +492,8 @@ class DatabaseQuery(object):
# if not isinstance(values, (list, tuple)):
# values = values.split(",")

ref_doctype = f.doctype

if frappe.get_meta(f.doctype).get_field(f.fieldname) is not None :
ref_doctype = frappe.get_meta(f.doctype).get_field(f.fieldname).options

result=[]
field = meta.get_field(f.fieldname)
ref_doctype = field.options if field else f.doctype

lft, rgt = '', ''
if f.value:
@@ -474,29 +513,30 @@ class DatabaseQuery(object):
}, order_by='`lft` DESC')

fallback = "''"
value = [frappe.db.escape((v.name or '').strip(), percent=False) for v in result]
value = [frappe.db.escape((cstr(v.name) or '').strip(), percent=False) for v in result]
if len(value):
value = f"({', '.join(value)})"
else:
value = "('')"

# changing operator to IN as the above code fetches all the parent / child values and convert into tuple
# which can be directly used with IN operator to query.
f.operator = 'not in' if f.operator.lower() in ('not ancestors of', 'not descendants of') else 'in'


elif f.operator.lower() in ('in', 'not in'):
values = f.value or ''
if isinstance(values, str):
values = values.split(",")

fallback = "''"
value = [frappe.db.escape((v or '').strip(), percent=False) for v in values]
value = [frappe.db.escape((cstr(v) or '').strip(), percent=False) for v in values]
if len(value):
value = f"({', '.join(value)})"
else:
value = "('')"

else:
df = frappe.get_meta(f.doctype).get("fields", {"fieldname": f.fieldname})
df = meta.get("fields", {"fieldname": f.fieldname})
df = df[0] if df else None

if df and df.fieldtype in ("Check", "Float", "Int", "Currency", "Percent"):
@@ -513,7 +553,8 @@ class DatabaseQuery(object):
fallback = "'0001-01-01 00:00:00'"

elif f.operator.lower() in ('between') and \
(f.fieldname in ('creation', 'modified') or (df and (df.fieldtype=="Date" or df.fieldtype=="Datetime"))):
(f.fieldname in ('creation', 'modified') or
(df and (df.fieldtype=="Date" or df.fieldtype=="Datetime"))):

value = get_between_date_filter(f.value, df)
fallback = "'0001-01-01 00:00:00'"
@@ -528,7 +569,7 @@ class DatabaseQuery(object):
fallback = "''"
can_be_null = True

if 'ifnull' not in column_name:
if 'ifnull' not in column_name.lower():
column_name = f'ifnull({column_name}, {fallback})'

elif df and df.fieldtype=="Date":
@@ -570,7 +611,7 @@ class DatabaseQuery(object):
value = f"{tname}.{quote}{f.value.name}{quote}"

# escape value
elif isinstance(value, str) and not f.operator.lower() == 'between':
elif isinstance(value, str) and f.operator.lower() != 'between':
value = f"{frappe.db.escape(value, percent=False)}"

if (


+ 1
- 1
frappe/model/delete_doc.py View File

@@ -158,7 +158,7 @@ def update_naming_series(doc):
and getattr(doc, "naming_series", None):
revert_series_if_last(doc.naming_series, doc.name, doc)

elif doc.meta.autoname.split(":")[0] not in ("Prompt", "field", "hash"):
elif doc.meta.autoname.split(":")[0] not in ("Prompt", "field", "hash", "autoincrement"):
revert_series_if_last(doc.meta.autoname, doc.name, doc)

def delete_from_table(doctype, name, ignore_doctypes, doc):


+ 51
- 4
frappe/model/naming.py View File

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

from typing import Optional
from typing import Optional, TYPE_CHECKING, Union
import frappe
from frappe import _
from frappe.database.sequence import get_next_val, set_next_val
from frappe.utils import now_datetime, cint, cstr
import re
from frappe.model import log_types
from frappe.query_builder import DocType

if TYPE_CHECKING:
from frappe.model.meta import Meta


def set_new_name(doc):
"""
@@ -24,11 +28,16 @@ def set_new_name(doc):

doc.run_method("before_naming")

autoname = frappe.get_meta(doc.doctype).autoname or ""
meta = frappe.get_meta(doc.doctype)
autoname = meta.autoname or ""

if autoname.lower() != "prompt" and not frappe.flags.in_import:
doc.name = None

if is_autoincremented(doc.doctype, meta):
doc.name = get_next_val(doc.doctype)
return

if getattr(doc, "amended_from", None):
_set_amended_name(doc)
return
@@ -64,9 +73,37 @@ def set_new_name(doc):
doc.name = validate_name(
doc.doctype,
doc.name,
frappe.get_meta(doc.doctype).get_field("name_case")
meta.get_field("name_case")
)

def is_autoincremented(doctype: str, meta: "Meta" = None):
if doctype in log_types:
if frappe.local.autoincremented_status_map.get(frappe.local.site) is None or \
frappe.local.autoincremented_status_map[frappe.local.site] == -1:
if frappe.db.sql(
f"""select data_type FROM information_schema.columns
where column_name = 'name' and table_name = 'tab{doctype}'"""
)[0][0] == "bigint":
frappe.local.autoincremented_status_map[frappe.local.site] = 1
return True
else:
frappe.local.autoincremented_status_map[frappe.local.site] = 0

elif frappe.local.autoincremented_status_map[frappe.local.site]:
return True

else:
if not meta:
meta = frappe.get_meta(doctype)

if getattr(meta, "issingle", False):
return False

if meta.autoname == "autoincrement":
return True

return False

def set_name_from_naming_options(autoname, doc):
"""
Get a name based on the autoname field option
@@ -284,9 +321,19 @@ def get_default_naming_series(doctype):
return None


def validate_name(doctype: str, name: str, case: Optional[str] = None):
def validate_name(doctype: str, name: Union[int, str], case: Optional[str] = None):
if not name:
frappe.throw(_("No Name Specified for {0}").format(doctype))

if isinstance(name, int):
if is_autoincremented(doctype):
# this will set the sequence val to be the provided name and set it to be used
# so that the sequence will start from the next val of the setted val(name)
set_next_val(doctype, name, is_val_used=True)
return name

frappe.throw(_("Invalid name type (integer) for varchar name column"), frappe.NameError)

if name.startswith("New "+doctype):
frappe.throw(_("There were some errors setting the name, please contact the administrator"), frappe.NameError)
if case == "Title Case":


+ 1
- 1
frappe/patches.txt View File

@@ -146,7 +146,7 @@ frappe.patches.v13_0.update_duration_options
frappe.patches.v13_0.replace_old_data_import # 2020-06-24
frappe.patches.v13_0.create_custom_dashboards_cards_and_charts
frappe.patches.v13_0.rename_is_custom_field_in_dashboard_chart
frappe.patches.v13_0.add_standard_navbar_items # 2020-12-15
frappe.patches.v13_0.add_standard_navbar_items # 2022-03-15
frappe.patches.v13_0.generate_theme_files_in_public_folder
frappe.patches.v13_0.increase_password_length
frappe.patches.v12_0.fix_email_id_formatting


+ 10
- 6
frappe/public/js/frappe/form/form.js View File

@@ -246,10 +246,12 @@ frappe.ui.form.Form = class FrappeForm {
var me = this;

// on main doc
frappe.model.on(me.doctype, "*", function(fieldname, value, doc) {
frappe.model.on(me.doctype, "*", function(fieldname, value, doc, skip_dirty_trigger=false) {
// set input
if(doc.name===me.docname) {
me.dirty();
if (cstr(doc.name) === me.docname) {
if (!skip_dirty_trigger) {
me.dirty();
}

let field = me.fields_dict[fieldname];
field && field.refresh(fieldname);
@@ -953,10 +955,12 @@ frappe.ui.form.Form = class FrappeForm {
this.toolbar.set_primary_action();
}

disable_save() {
disable_save(set_dirty=false) {
// IMPORTANT: this function should be called in refresh event
this.save_disabled = true;
this.toolbar.current_status = null;
// field changes should make form dirty
this.set_dirty = set_dirty;
this.page.clear_primary_action();
}

@@ -1447,7 +1451,7 @@ frappe.ui.form.Form = class FrappeForm {
return doc;
}

set_value(field, value, if_missing) {
set_value(field, value, if_missing, skip_dirty_trigger=false) {
var me = this;
var _set = function(f, v) {
var fieldobj = me.fields_dict[f];
@@ -1467,7 +1471,7 @@ frappe.ui.form.Form = class FrappeForm {
me.refresh_field(f);
return Promise.resolve();
} else {
return frappe.model.set_value(me.doctype, me.doc.name, f, v);
return frappe.model.set_value(me.doctype, me.doc.name, f, v, me.fieldtype, skip_dirty_trigger);
}
}
} else {


+ 118
- 21
frappe/public/js/frappe/form/grid.js View File

@@ -35,7 +35,7 @@ export default class Grid {
&& this.frm.meta.__form_grid_templates[this.df.fieldname]) {
this.template = this.frm.meta.__form_grid_templates[this.df.fieldname];
}
this.filter = {};
this.is_grid = true;
this.debounced_refresh = this.refresh.bind(this);
this.debounced_refresh = frappe.utils.debounce(this.debounced_refresh, 100);
@@ -274,6 +274,8 @@ export default class Grid {
}

make_head() {
if (this.prevent_build) return;

// labels
if (this.header_row) {
$(this.parent).find(".grid-heading-row .grid-row").remove();
@@ -286,12 +288,42 @@ export default class Grid {
grid: this,
configure_columns: true
});

this.header_search = new GridRow({
parent: $(this.parent).find(".grid-heading-row"),
parent_df: this.df,
docfields: this.docfields,
frm: this.frm,
grid: this,
show_search: true
});

Object.keys(this.filter).length !== 0 &&
this.update_search_columns();
}

update_search_columns() {
for (const field in this.filter) {
if (this.filter[field] && !this.header_search.search_columns[field]) {
delete this.filter[field];
this.data = this.get_data(Object.keys(this.filter).length !== 0);
break;
}

if (this.filter[field] && this.filter[field].value) {
let $input = this.header_search.row_index.find('input');
if (field && field !== 'row-index') {
$input = this.header_search.search_columns[field].find('input');
}
$input.val(this.filter[field].value);
}
}
}

refresh(force) {
refresh() {
if (this.frm && this.frm.setting_dependency) return;

this.data = this.get_data();
this.data = this.get_data(Object.keys(this.filter).length !== 0);

!this.wrapper && this.make();
let $rows = $(this.parent).find('.rows');
@@ -453,7 +485,7 @@ export default class Grid {
}

make_sortable($rows) {
new Sortable($rows.get(0), {
this.grid_sortable = new Sortable($rows.get(0), {
group: { name: this.df.fieldname },
handle: '.sortable-handle',
draggable: '.grid-row',
@@ -484,14 +516,78 @@ export default class Grid {
$(this.frm.wrapper).trigger("grid-make-sortable", [this.frm]);
}

get_data() {
var data = this.frm ?
this.frm.doc[this.df.fieldname] || []
: this.df.data || this.get_modal_data();
// data.sort(function(a, b) { return a.idx - b.idx});
get_data(filter_field) {
let data = [];
if (filter_field) {
data = this.get_filtered_data();
} else {
data = this.frm ?
this.frm.doc[this.df.fieldname] || []
: this.df.data || this.get_modal_data();
}
return data;
}

get_filtered_data() {
if (!this.frm) return;

let all_data = this.frm.doc[this.df.fieldname];

for (const field in this.filter) {
all_data = all_data.filter(data => {
let {df, value} = this.filter[field];
return this.get_data_based_on_fieldtype(df, data, value.toLowerCase());
});
}

return all_data;
}

get_data_based_on_fieldtype(df, data, value) {
let fieldname = df.fieldname;
let fieldtype = df.fieldtype;
let fieldvalue = data[fieldname];

if (fieldtype === "Check") {
value = frappe.utils.string_to_boolean(value);
return (Boolean(fieldvalue) === value) && data;
} else if (fieldtype === "Sr No" && data.idx.toString().includes(value)) {
return data;
} else if (fieldtype === "Duration" && fieldvalue) {
let formatted_duration = frappe.utils.get_formatted_duration(fieldvalue);

if (formatted_duration.includes(value)) {
return data;
}
} else if (fieldtype === "Barcode" && fieldvalue) {
let barcode = fieldvalue.startsWith('<svg') ?
$(fieldvalue).attr('data-barcode-value') : fieldvalue;

if (barcode.toLowerCase().includes(value)) {
return data;
}
} else if (["Datetime", "Date"].includes(fieldtype) && fieldvalue) {
let user_formatted_date = frappe.datetime.str_to_user(fieldvalue);

if (user_formatted_date.includes(value)) {
return data;
}
} else if (["Currency", "Float", "Int", "Percent", "Rating"].includes(fieldtype)) {
let num = fieldvalue || 0;

if (fieldtype === "Rating") {
let out_of_rating = parseInt(df.options) || 5;
num = num * out_of_rating;
}

if (num.toString().indexOf(value) > -1) {
return data;
}
} else if (fieldvalue && fieldvalue.toLowerCase().includes(value)) {
return data;
}
}

get_modal_data() {
return this.df.get_data ? this.df.get_data().filter(data => {
if (!this.deleted_docs || !in_list(this.deleted_docs, data.name)) {
@@ -775,18 +871,19 @@ export default class Grid {
}

setup_user_defined_columns() {
if (this.frm) {
let user_settings = frappe.get_user_settings(this.frm.doctype, 'GridView');
if (user_settings && user_settings[this.doctype] && user_settings[this.doctype].length) {
this.user_defined_columns = user_settings[this.doctype].map(row => {
let column = frappe.meta.get_docfield(this.doctype, row.fieldname);
if (column) {
column.in_list_view = 1;
column.columns = row.columns;
return column;
}
});
}
if (!this.frm) return;

let user_settings = frappe.get_user_settings(this.frm.doctype, 'GridView');
if (user_settings && user_settings[this.doctype] && user_settings[this.doctype].length) {
this.user_defined_columns = user_settings[this.doctype].map(row => {
let column = frappe.meta.get_docfield(this.doctype, row.fieldname);

if (column) {
column.in_list_view = 1;
column.columns = row.columns;
return column;
}
});
}
}



+ 123
- 7
frappe/public/js/frappe/form/grid_row.js View File

@@ -8,7 +8,7 @@ export default class GridRow {
this.set_docfields();
this.columns = {};
this.columns_list = [];
this.row_check_html = '<input type="checkbox" class="grid-row-check pull-left">';
this.row_check_html = '<input type="checkbox" class="grid-row-check">';
this.make();
}
make() {
@@ -204,23 +204,65 @@ export default class GridRow {
}));
}
render_row(refresh) {
var me = this;
if (this.show_search && !this.show_search_row()) return;

let me = this;
this.set_row_index();

// index (1, 2, 3 etc)
if(!this.row_index) {
if (!this.row_index && !this.show_search) {
// REDESIGN-TODO: Make translation contextual, this No is Number
var txt = (this.doc ? this.doc.idx : __("No."));
this.row_index = $(
`<div class="row-index sortable-handle col">

this.row_check = $(
`<div class="row-check sortable-handle col">
${this.row_check_html}
<span class="hidden-xs">${txt}</span></div>`)
</div>`)
.appendTo(this.row);

this.row_index = $(
`<div class="row-index sortable-handle col hidden-xs">
<span>${txt}</span>
</div>`)
.appendTo(this.row)
.on('click', function(e) {
if(!$(e.target).hasClass('grid-row-check')) {
me.toggle_view();
}
});
} else if (this.show_search) {
this.row_check = $(
`<div class="row-check col search"></div>`
).appendTo(this.row);

this.row_index = $(
`<div class="row-index col search hidden-xs">
<input type="text" class="form-control input-xs text-center" >
</div>`
).appendTo(this.row);

this.row_index.find('input').on('keyup', frappe.utils.debounce((e) => {
let df = {
fieldtype: "Sr No"
};

this.grid.filter['row-index'] = {
df: df,
value: e.target.value
};

if (e.target.value == "") {
delete this.grid.filter['row-index'];
}

this.grid.grid_sortable
.option('disabled', Object.keys(this.grid.filter).length !== 0);

this.grid.prevent_build = true;
me.grid.refresh();
this.grid.prevent_build = false;
}, 500));
frappe.utils.only_allow_num_decimal(this.row_index.find('input'));
} else {
this.row_index.find('span').html(txt);
}
@@ -546,6 +588,7 @@ export default class GridRow {

setup_columns() {
this.focus_set = false;
this.search_columns = {};

this.grid.setup_visible_columns();
this.grid.visible_columns.forEach((col, ci) => {
@@ -561,8 +604,10 @@ export default class GridRow {
txt = __(txt);
}
let column;
if (!this.columns[df.fieldname]) {
if (!this.columns[df.fieldname] && !this.show_search) {
column = this.make_column(df, colsize, txt, ci);
} else if (!this.columns[df.fieldname] && this.show_search) {
column = this.make_search_column(df, colsize);
} else {
column = this.columns[df.fieldname];
this.refresh_field(df.fieldname, txt);
@@ -580,6 +625,77 @@ export default class GridRow {
}
}
});

if (this.show_search) {
// last empty column
$(`<div class="col grid-static-col col-xs-1"></div>`)
.appendTo(this.row);
}
}

show_search_row() {
// show or remove search columns based on grid rows
this.show_search = this.frm && this.frm.doc &&
this.frm.doc[this.grid.df.fieldname] &&
this.frm.doc[this.grid.df.fieldname].length >= 20;
!this.show_search && this.wrapper.remove();
return this.show_search;
}

make_search_column(df, colsize) {
let title = "";
let input_class = "";
let is_disabled = "";

if (["Text", "Small Text"].includes(df.fieldtype)) {
input_class = "grid-overflow-no-ellipsis";
} else if (["Int", "Currency", "Float", "Percent"].includes(df.fieldtype)) {
input_class = "text-right";
} else if (df.fieldtype === "Check") {
title = __("1 = True & 0 = False");
input_class = "text-center";
} else if (df.fieldtype === 'Password') {
is_disabled = 'disabled';
title = __('Password cannot be filtered');
}

let $col = $('<div class="col grid-static-col col-xs-'+colsize+' search"></div>')
.appendTo(this.row);

let $search_input = $(`
<input
type="text"
class="form-control input-xs ${input_class}"
title="${title}"
data-fieldtype="${df.fieldtype}"
${is_disabled}
>
`).appendTo($col);

this.search_columns[df.fieldname] = $col;

$search_input.on('keyup', frappe.utils.debounce((e) => {
this.grid.filter[df.fieldname] = {
df: df,
value: e.target.value
};

if (e.target.value == '') {
delete this.grid.filter[df.fieldname];
}

this.grid.grid_sortable
.option('disabled', Object.keys(this.grid.filter).length !== 0);

this.grid.prevent_build = true;
this.grid.refresh();
this.grid.prevent_build = false;
}, 500));

["Currency", "Float", "Int", "Percent", "Rating"].includes(df.fieldtype) &&
frappe.utils.only_allow_num_decimal($search_input);

return $col;
}

make_column(df, colsize, txt, ci) {


+ 3
- 3
frappe/public/js/frappe/form/toolbar.js View File

@@ -534,14 +534,14 @@ frappe.ui.form.Toolbar = class Toolbar {
});
}
show_title_as_dirty() {
if(this.frm.save_disabled)
if (this.frm.save_disabled && !this.frm.set_dirty)
return;

if(this.frm.doc.__unsaved) {
if (this.frm.is_dirty()) {
this.page.set_indicator(__("Not Saved"), "orange");
}

$(this.frm.wrapper).attr("data-state", this.frm.doc.__unsaved ? "dirty" : "clean");
$(this.frm.wrapper).attr("data-state", this.frm.is_dirty() ? "dirty" : "clean");
}

show_jump_to_field_dialog() {


+ 1
- 1
frappe/public/js/frappe/list/list_view.js View File

@@ -915,7 +915,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
return this.settings.get_form_link(doc);
}

const docname = doc.name.match(/[%'"#\s]/)
const docname = cstr(doc.name).match(/[%'"#\s]/)
? encodeURIComponent(doc.name)
: doc.name;



+ 5
- 5
frappe/public/js/frappe/model/model.js View File

@@ -412,7 +412,7 @@ $.extend(frappe.model, {
}
},

set_value: function(doctype, docname, fieldname, value, fieldtype) {
set_value: function(doctype, docname, fieldname, value, fieldtype, skip_dirty_trigger=false) {
/* help: Set a value locally (if changed) and execute triggers */

var doc;
@@ -438,11 +438,11 @@ $.extend(frappe.model, {
}

doc[key] = value;
tasks.push(() => frappe.model.trigger(key, value, doc));
tasks.push(() => frappe.model.trigger(key, value, doc, skip_dirty_trigger));
} else {
// execute link triggers (want to reselect to execute triggers)
if(in_list(["Link", "Dynamic Link"], fieldtype) && doc) {
tasks.push(() => frappe.model.trigger(key, value, doc));
tasks.push(() => frappe.model.trigger(key, value, doc, skip_dirty_trigger));
}
}
});
@@ -467,7 +467,7 @@ $.extend(frappe.model, {
frappe.model.events[doctype][fieldname].push(fn);
},

trigger: function(fieldname, value, doc) {
trigger: function(fieldname, value, doc, skip_dirty_trigger=false) {
const tasks = [];

function enqueue_events(events) {
@@ -477,7 +477,7 @@ $.extend(frappe.model, {
if (!fn) continue;

tasks.push(() => {
const return_value = fn(fieldname, value, doc);
const return_value = fn(fieldname, value, doc, skip_dirty_trigger);

// if the trigger returns a promise, return it,
// or use the default promise frappe.after_ajax


+ 19
- 1
frappe/public/js/frappe/utils/utils.js View File

@@ -1102,7 +1102,7 @@ Object.assign(frappe.utils, {
seconds: round(seconds % 60)
};

if (duration_options.hide_days) {
if (duration_options && duration_options.hide_days) {
total_duration.hours = round(seconds / 3600);
total_duration.days = 0;
}
@@ -1462,5 +1462,23 @@ Object.assign(frappe.utils, {
console.log(error); // eslint-disable-line
return Promise.resolve(name);
}
},

only_allow_num_decimal(input) {
input.on('input', (e) => {
let self = $(e.target);
self.val(self.val().replace(/[^0-9.]/g, ''));
if ((e.which != 46 || self.val().indexOf('.') != -1) && (e.which < 48 || e.which > 57)) {
e.preventDefault();
}
});
},

string_to_boolean(string) {
switch (string.toLowerCase().trim()) {
case "t": case "true": case "y": case "yes": case "1": return true;
case "f": case "false": case "n": case "no": case "0": case null: return false;
default: return string;
}
}
});

+ 24
- 1
frappe/public/scss/common/grid.scss View File

@@ -89,6 +89,29 @@
height: 34px;
padding: 8px;
max-height: 200px;

&.search {
padding: 7px !important;

input {
height: -webkit-fill-available;
padding: 3px 7px;
}
}
}

.row-check {
height: 34px;
padding: 8px 3px !important;
text-align: center;

input {
margin-right: 0 !important;
}

&.search {
padding: 0 !important;
}
}

.grid-row-check {
@@ -124,7 +147,6 @@

.grid-row > .row {
.col:last-child {
margin-right: calc(-1 * var(--margin-sm));
border-right: none;
}

@@ -427,6 +449,7 @@
}

.page-number {
background-color: var(--fg-color);
padding: 0 3px;
}



+ 2
- 1
frappe/realtime.py View File

@@ -2,6 +2,7 @@
# License: MIT. See LICENSE

import frappe
from frappe.utils.data import cstr
import os
import redis

@@ -118,7 +119,7 @@ def get_user_info():
}

def get_doc_room(doctype, docname):
return ''.join([frappe.local.site, ':doc:', doctype, '/', docname])
return ''.join([frappe.local.site, ':doc:', doctype, '/', cstr(docname)])

def get_user_room(user):
return ''.join([frappe.local.site, ':user:', user])


+ 47
- 0
frappe/tests/test_db.py View File

@@ -562,3 +562,50 @@ class TestDDLCommandsPost(unittest.TestCase):
""",
)
self.assertEquals(len(indexs_in_table), 1)

@run_only_if(db_type_is.POSTGRES)
def test_modify_query(self):
from frappe.database.postgres.database import modify_query

query = "select * from `tabtree b` where lft > 13 and rgt <= 16 and name =1.0 and parent = 4134qrsdc and isgroup = 1.00045"
self.assertEqual(
"select * from \"tabtree b\" where lft > \'13\' and rgt <= '16' and name = '1' and parent = 4134qrsdc and isgroup = 1.00045",
modify_query(query)
)

query = "select locate(\".io\", \"frappe.io\"), locate(\"3\", cast(3 as varchar)), locate(\"3\", 3::varchar)"
self.assertEqual(
"select strpos( \"frappe.io\", \".io\"), strpos( cast(3 as varchar), \"3\"), strpos( 3::varchar, \"3\")",
modify_query(query)
)

@run_only_if(db_type_is.POSTGRES)
def test_modify_values(self):
from frappe.database.postgres.database import modify_values

self.assertEqual(
{"abcd": "23", "efgh": "23", "ijkl": 23.0345, "mnop": "wow"},
modify_values({"abcd": 23, "efgh": 23.0, "ijkl": 23.0345, "mnop": "wow"})
)
self.assertEqual(
["23", "23", 23.00004345, "wow"],
modify_values((23, 23.0, 23.00004345, "wow"))
)

def test_sequence_table_creation(self):
from frappe.core.doctype.doctype.test_doctype import new_doctype

dt = new_doctype("autoinc_dt_seq_test", autoincremented=True).insert(ignore_permissions=True)

if frappe.db.db_type == "postgres":
self.assertTrue(
frappe.db.sql("""select sequence_name FROM information_schema.sequences
where sequence_name ilike 'autoinc_dt_seq_test%'""")[0][0]
)
else:
self.assertTrue(
frappe.db.sql("""select data_type FROM information_schema.tables
where table_type = 'SEQUENCE' and table_name like 'autoinc_dt_seq_test%'""")[0][0]
)

dt.delete(ignore_permissions=True)

+ 21
- 0
frappe/tests/test_db_query.py View File

@@ -494,6 +494,27 @@ class TestReportview(unittest.TestCase):
response = execute_cmd("frappe.desk.reportview.get")
self.assertListEqual(response["keys"], ["field_label", "field_name", "_aggregate_column", 'columns'])

def test_cast_name(self):
from frappe.core.doctype.doctype.test_doctype import new_doctype

dt = new_doctype("autoinc_dt_test", autoincremented=True).insert(ignore_permissions=True)

query = DatabaseQuery("autoinc_dt_test").execute(
fields=["locate('1', `tabautoinc_dt_test`.`name`)", "`tabautoinc_dt_test`.`name`"],
filters={"name": 1},
run=False
)

if frappe.db.db_type == "postgres":
self.assertTrue("strpos( cast( \"tabautoinc_dt_test\".\"name\" as varchar), \'1\')" in query)
self.assertTrue("where cast(\"tabautoinc_dt_test\".name as varchar) = \'1\'" in query)
else:
self.assertTrue("locate(\'1\', `tabautoinc_dt_test`.`name`)" in query)
self.assertTrue("where `tabautoinc_dt_test`.name = 1" in query)

dt.delete(ignore_permissions=True)


def add_child_table_to_blog_post():
child_table = frappe.get_doc({
'doctype': 'DocType',


+ 11
- 0
frappe/tests/test_naming.py View File

@@ -245,6 +245,17 @@ class TestNaming(unittest.TestCase):
})
self.assertRaises(frappe.ValidationError, tag.insert)

def test_autoincremented_naming(self):
from frappe.core.doctype.doctype.test_doctype import new_doctype

doctype = "autoinc_doctype" + frappe.generate_hash(length=5)
dt = new_doctype(doctype, autoincremented=True).insert(ignore_permissions=True)

for i in range(1, 20):
self.assertEqual(frappe.new_doc(doctype).save(ignore_permissions=True).name, i)

dt.delete(ignore_permissions=True)


def make_invalid_todo():
frappe.get_doc({


+ 50
- 6
frappe/tests/ui_test_helpers.py View File

@@ -136,13 +136,14 @@ def create_contact_records():

@frappe.whitelist()
def create_multiple_todo_records():
values = []
if frappe.db.get_all('ToDo', {'description': 'Multiple ToDo 1'}):
return
for index in range(501):
frappe.get_doc({
'doctype': 'ToDo',
'description': 'Multiple ToDo {}'.format(index+1)
}).insert()
for index in range(1, 1002):
values.append(('100{}'.format(index), 'Multiple ToDo {}'.format(index)))
frappe.db.bulk_insert('ToDo', fields=['name', 'description'], values=set(values))

def insert_contact(first_name, phone_number):
doc = frappe.get_doc({
@@ -271,4 +272,47 @@ def update_child_table(name):
'options': 'Doctype to Link'
})

doc.save()
doc.save()


@frappe.whitelist()
def insert_doctype_with_child_table_record(name):
if frappe.db.get_all(name, {'title': 'Test Grid Search'}):
return

def insert_child(doc, data, barcode, check, rating, duration, date):
doc.append('child_table_1', {
'data': data,
'barcode': barcode,
'check': check,
'rating': rating,
'duration': duration,
'date': date,
})

doc = frappe.new_doc(name)
doc.title = 'Test Grid Search'
doc.append('child_table', {'title': 'Test Grid Search'})

insert_child(doc, 'Data', '09709KJKKH2432', 1, 0.5, 266851, "2022-02-21")
insert_child(doc, 'Test', '09209KJHKH2432', 1, 0.8, 547877, "2021-05-27")
insert_child(doc, 'New', '09709KJHYH1132', 0, 0.1, 3, "2019-03-02")
insert_child(doc, 'Old', '09701KJHKH8750', 0, 0, 127455, "2022-01-11")
insert_child(doc, 'Alpha', '09204KJHKH2432', 0, 0.6, 364, "2019-12-31")
insert_child(doc, 'Delta', '09709KSPIO2432', 1, 0.9, 1242000, "2020-04-21")
insert_child(doc, 'Update', '76989KJLVA2432', 0, 1, 183845, "2022-02-10")
insert_child(doc, 'Delete', '29189KLHVA1432', 0, 0, 365647, "2021-05-07")
insert_child(doc, 'Make', '09689KJHAA2431', 0, 0.3, 24, "2020-11-11")
insert_child(doc, 'Create', '09709KLKKH2432', 1, 0.3, 264851, "2021-02-21")
insert_child(doc, 'Group', '09209KJLKH2432', 1, 0.8, 537877, "2020-03-15")
insert_child(doc, 'Slide', '01909KJHYH1132', 0, 0.5, 9, "2018-03-02")
insert_child(doc, 'Drop', '09701KJHKH8750', 1, 0, 127255, "2018-01-01")
insert_child(doc, 'Beta', '09204QJHKN2432', 0, 0.6, 354, "2017-12-30")
insert_child(doc, 'Flag', '09709KXPIP2432', 1, 0, 1241000, "2021-04-21")
insert_child(doc, 'Upgrade', '75989ZJLVA2432', 0.8, 1, 183645, "2020-08-13")
insert_child(doc, 'Down', '28189KLHRA1432', 1, 0, 362647, "2020-06-17")
insert_child(doc, 'Note', '09689DJHAA2431', 0, 0.1, 29, "2021-09-11")
insert_child(doc, 'Click', '08189DJHAA2431', 1, 0.3, 209, "2020-07-04")
insert_child(doc, 'Drag', '08189DIHAA2981', 0, 0.7, 342628, "2022-05-04")

doc.insert()

+ 24
- 13
frappe/utils/__init__.py View File

@@ -796,22 +796,33 @@ def get_assets_json():

# using .get instead of .get_value to avoid pickle.loads
try:
assets_json = cache.get("assets_json")
except ConnectionError:
assets_json = None

# if value found, decode it
if assets_json is not None:
try:
assets_json = assets_json.decode('utf-8')
except (UnicodeDecodeError, AttributeError):
if not frappe.conf.developer_mode:
assets_json = cache.get("assets_json").decode('utf-8')
else:
assets_json = None
except (UnicodeDecodeError, AttributeError, ConnectionError):
assets_json = None

if not assets_json:
assets_json = frappe.read_file("assets/assets.json")
cache.set_value("assets_json", assets_json, shared=True)

frappe.local.assets_json = frappe.safe_decode(assets_json)
# get merged assets.json and assets-rtl.json
assets_dict = frappe.parse_json(
frappe.read_file("assets/assets.json")
)

assets_rtl = frappe.read_file("assets/assets-rtl.json")
if assets_rtl:
assets_dict.update(
frappe.parse_json(assets_rtl)
)
frappe.local.assets_json = frappe.as_json(assets_dict)
# save in cache
cache.set_value("assets_json", frappe.local.assets_json,
shared=True)

return assets_dict
else:
# from cache, decode and send
frappe.local.assets_json = frappe.safe_decode(assets_json)

return frappe.parse_json(frappe.local.assets_json)



+ 1
- 1
frappe/utils/data.py View File

@@ -1494,7 +1494,7 @@ def expand_relative_urls(html):
return html

def quoted(url):
return cstr(quote(encode(url), safe=b"~@#$&()*!+=:;,.?/'"))
return cstr(quote(encode(cstr(url)), safe=b"~@#$&()*!+=:;,.?/'"))

def quote_urls(html):
def _quote_url(match):


+ 6
- 5
frappe/utils/diff.py View File

@@ -1,14 +1,15 @@
import json
from difflib import unified_diff
from typing import List
from typing import List, Union

import frappe
from frappe.utils import pretty_date
from frappe.utils.data import cstr


@frappe.whitelist()
def get_version_diff(
from_version: str, to_version: str, fieldname: str = "script"
from_version: Union[int, str], to_version: Union[int, str], fieldname: str = "script"
) -> List[str]:

before, before_timestamp = _get_value_from_version(from_version, fieldname)
@@ -23,15 +24,15 @@ def get_version_diff(
diff = unified_diff(
before,
after,
fromfile=from_version,
tofile=to_version,
fromfile=cstr(from_version),
tofile=cstr(to_version),
fromfiledate=before_timestamp,
tofiledate=after_timestamp,
)
return list(diff)


def _get_value_from_version(version_name: str, fieldname: str):
def _get_value_from_version(version_name: Union[int, str], fieldname: str):
version = frappe.get_list(
"Version", fields=["data", "modified"], filters={"name": version_name}
)


+ 3
- 1
frappe/utils/global_search.py View File

@@ -9,6 +9,8 @@ import os
from frappe.utils import cint, strip_html_tags
from frappe.utils.html_utils import unescape_html
from frappe.model.base_document import get_controller
from frappe.utils.data import cstr


def setup_global_search_table():
"""
@@ -251,7 +253,7 @@ def update_global_search(doc):
if hasattr(doc, 'is_website_published') and doc.meta.allow_guest_to_view:
published = 1 if doc.is_website_published() else 0

title = (doc.get_title() or '')[:int(frappe.db.VARCHAR_LEN)]
title = (cstr(doc.get_title()) or '')[:int(frappe.db.VARCHAR_LEN)]
route = doc.get('route') if doc else ''

value = dict(


+ 6
- 0
frappe/utils/install.py View File

@@ -255,6 +255,12 @@ def add_standard_navbar_items():
'item_type': 'Action',
'action': 'frappe.ui.toolbar.show_shortcuts(event)',
'is_standard': 1
},
{
'item_label': 'Frappe Support',
'item_type': 'Route',
'route': 'https://frappe.io/support',
'is_standard': 1
}
]



Loading…
Cancel
Save