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

This commit is contained in:
Jannat Patel 2022-03-19 09:51:27 +05:30 committed by GitHub
commit def2b6fbd1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 1039 additions and 141 deletions

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
};

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: [],

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 => {

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);
});
});

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');
});
});

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();
});

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`."""

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

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

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:')) {

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
}

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

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:

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

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);
}
},

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}));

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)

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

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}),

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

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}),

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}")

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})")

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

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
)

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

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',

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)

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

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)

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 (

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

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

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

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 {

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();
}
refresh(force) {
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() {
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;
}
});
}
}

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

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

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;

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

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;
}
}
});

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;
}

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

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)

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',

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

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

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:
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 value found, decode it
if assets_json is not None:
try:
assets_json = assets_json.decode('utf-8')
except (UnicodeDecodeError, AttributeError):
assets_json = None
if not assets_json:
assets_json = frappe.read_file("assets/assets.json")
cache.set_value("assets_json", assets_json, shared=True)
# get merged assets.json and assets-rtl.json
assets_dict = frappe.parse_json(
frappe.read_file("assets/assets.json")
)
frappe.local.assets_json = frappe.safe_decode(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)

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

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

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(

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
}
]