@@ -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 | |||
}; |
@@ -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: [], | |||
@@ -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 => { | |||
@@ -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); | |||
}); | |||
}); |
@@ -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'); | |||
}); | |||
}); |
@@ -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(); | |||
}); | |||
@@ -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`.""" | |||
@@ -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') | |||
@@ -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)) | |||
@@ -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:')) { | |||
@@ -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 | |||
} |
@@ -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 | |||
@@ -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: | |||
@@ -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): | |||
@@ -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); | |||
} | |||
}, | |||
@@ -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})); |
@@ -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) | |||
@@ -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)): | |||
@@ -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}), | |||
@@ -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 |
@@ -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}), | |||
@@ -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}") |
@@ -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})") |
@@ -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") | |||
@@ -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 | |||
) | |||
@@ -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): | |||
@@ -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', | |||
@@ -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) | |||
@@ -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 | |||
@@ -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) | |||
@@ -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 ( | |||
@@ -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): | |||
@@ -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": | |||
@@ -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 | |||
@@ -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 { | |||
@@ -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; | |||
} | |||
}); | |||
} | |||
} | |||
@@ -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) { | |||
@@ -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() { | |||
@@ -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; | |||
@@ -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 | |||
@@ -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; | |||
} | |||
} | |||
}); |
@@ -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,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]) | |||
@@ -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) |
@@ -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', | |||
@@ -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({ | |||
@@ -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() |
@@ -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) | |||
@@ -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): | |||
@@ -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} | |||
) | |||
@@ -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( | |||
@@ -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 | |||
} | |||
] | |||