Merge branch 'develop' of github.com:frappe/frappe into refactor-file
This commit is contained in:
commit
47cf46cd49
153 changed files with 3124 additions and 1121 deletions
59
cypress/fixtures/child_table_doctype_1.js
Normal file
59
cypress/fixtures/child_table_doctype_1.js
Normal 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
|
||||
};
|
||||
|
|
@ -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 => {
|
||||
|
|
|
|||
107
cypress/integration/grid_search.js
Normal file
107
cypress/integration/grid_search.js
Normal 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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ context('Web Form', () => {
|
|||
cy.visit('/update-profile');
|
||||
cy.get_field('last_name', 'Data').type('_Test User', {force: true}).wait(200);
|
||||
cy.get('.web-form-actions .btn-primary').click();
|
||||
cy.wait(500);
|
||||
cy.get('.modal.show > .modal-dialog').should('be.visible');
|
||||
cy.wait(5000);
|
||||
cy.url().should('include', '/me');
|
||||
});
|
||||
|
||||
it('Navigate and Submit a MultiStep WebForm', () => {
|
||||
|
|
@ -16,14 +16,12 @@ context('Web Form', () => {
|
|||
cy.visit('/update-profile-duplicate');
|
||||
cy.get_field('last_name', 'Data').type('_Test User', {force: true}).wait(200);
|
||||
cy.get('.btn-next').should('be.visible');
|
||||
cy.get('.web-form-footer .btn-primary').should('not.be.visible');
|
||||
cy.get('.btn-next').click();
|
||||
cy.get('.btn-previous').should('be.visible');
|
||||
cy.get('.btn-next').should('not.be.visible');
|
||||
cy.get('.web-form-footer .btn-primary').should('be.visible');
|
||||
cy.get('.web-form-actions .btn-primary').click();
|
||||
cy.wait(500);
|
||||
cy.get('.modal.show > .modal-dialog').should('be.visible');
|
||||
cy.wait(5000);
|
||||
cy.url().should('include', '/me');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ from frappe.query_builder import (
|
|||
patch_query_execute,
|
||||
patch_query_aggregation,
|
||||
)
|
||||
from frappe.utils.data import cstr
|
||||
|
||||
__version__ = '14.0.0-dev'
|
||||
|
||||
|
|
@ -215,6 +216,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
|
||||
|
|
@ -858,8 +860,7 @@ def set_value(doctype, docname, fieldname, value=None):
|
|||
return frappe.client.set_value(doctype, docname, fieldname, value)
|
||||
|
||||
def get_cached_doc(*args, **kwargs):
|
||||
if args and len(args) > 1 and isinstance(args[1], str):
|
||||
key = get_document_cache_key(args[0], args[1])
|
||||
if key := can_cache_doc(args):
|
||||
# local cache
|
||||
doc = local.document_cache.get(key)
|
||||
if doc:
|
||||
|
|
@ -877,8 +878,24 @@ def get_cached_doc(*args, **kwargs):
|
|||
|
||||
return doc
|
||||
|
||||
def can_cache_doc(args):
|
||||
"""
|
||||
Determine if document should be cached based on get_doc params.
|
||||
Returns cache key if doc can be cached, None otherwise.
|
||||
"""
|
||||
|
||||
if not args:
|
||||
return
|
||||
|
||||
doctype = args[0]
|
||||
name = doctype if len(args) == 1 else args[1]
|
||||
|
||||
# Only cache if both doctype and name are strings
|
||||
if isinstance(doctype, str) and isinstance(name, str):
|
||||
return get_document_cache_key(doctype, name)
|
||||
|
||||
def get_document_cache_key(doctype, name):
|
||||
return '{0}::{1}'.format(doctype, name)
|
||||
return f'{doctype}::{name}'
|
||||
|
||||
def clear_document_cache(doctype, name):
|
||||
cache().hdel("last_modified", doctype)
|
||||
|
|
@ -919,8 +936,7 @@ def get_doc(*args, **kwargs):
|
|||
doc = frappe.model.document.get_doc(*args, **kwargs)
|
||||
|
||||
# set in cache
|
||||
if args and len(args) > 1:
|
||||
key = get_document_cache_key(args[0], args[1])
|
||||
if key := can_cache_doc(args):
|
||||
local.document_cache[key] = doc
|
||||
cache().hset('document_cache', key, doc.as_dict())
|
||||
|
||||
|
|
@ -970,8 +986,7 @@ def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reloa
|
|||
|
||||
def delete_doc_if_exists(doctype, name, force=0):
|
||||
"""Delete document if exists."""
|
||||
if db.exists(doctype, name):
|
||||
delete_doc(doctype, name, force=force)
|
||||
delete_doc(doctype, name, force=force, ignore_missing=True)
|
||||
|
||||
def reload_doctype(doctype, force=False, reset_permissions=False):
|
||||
"""Reload DocType from model (`[module]/[doctype]/[name]/[name].json`) files."""
|
||||
|
|
@ -1009,7 +1024,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`."""
|
||||
|
|
@ -1244,9 +1259,10 @@ def get_newargs(fn, kwargs):
|
|||
if hasattr(fn, 'fnargs'):
|
||||
fnargs = fn.fnargs
|
||||
else:
|
||||
fnargs = inspect.getfullargspec(fn).args
|
||||
fnargs.extend(inspect.getfullargspec(fn).kwonlyargs)
|
||||
varkw = inspect.getfullargspec(fn).varkw
|
||||
fullargspec = inspect.getfullargspec(fn)
|
||||
fnargs = fullargspec.args
|
||||
fnargs.extend(fullargspec.kwonlyargs)
|
||||
varkw = fullargspec.varkw
|
||||
|
||||
newargs = {}
|
||||
for a in kwargs:
|
||||
|
|
@ -1258,7 +1274,7 @@ def get_newargs(fn, kwargs):
|
|||
|
||||
return newargs
|
||||
|
||||
def make_property_setter(args, ignore_validate=False, validate_fields_for_doctype=True):
|
||||
def make_property_setter(args, ignore_validate=False, validate_fields_for_doctype=True, is_system_generated=True):
|
||||
"""Create a new **Property Setter** (for overriding DocType and DocField properties).
|
||||
|
||||
If doctype is not specified, it will create a property setter for all fields with the
|
||||
|
|
@ -1289,6 +1305,7 @@ def make_property_setter(args, ignore_validate=False, validate_fields_for_doctyp
|
|||
'property': args.property,
|
||||
'value': args.value,
|
||||
'property_type': args.property_type or "Data",
|
||||
'is_system_generated': is_system_generated,
|
||||
'__islocal': 1
|
||||
})
|
||||
ps.flags.ignore_validate = ignore_validate
|
||||
|
|
@ -1456,7 +1473,7 @@ def get_list(doctype, *args, **kwargs):
|
|||
:param fields: List of fields or `*`.
|
||||
:param filters: List of filters (see example).
|
||||
:param order_by: Order By e.g. `modified desc`.
|
||||
:param limit_page_start: Start results at record #. Default 0.
|
||||
:param limit_start: Start results at record #. Default 0.
|
||||
:param limit_page_length: No of records in the page. Default 20.
|
||||
|
||||
Example usage:
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
@ -725,7 +728,7 @@ def move(dest_dir, site):
|
|||
@click.command('set-password')
|
||||
@click.argument('user')
|
||||
@click.argument('password', required=False)
|
||||
@click.option('--logout-all-sessions', help='Logout from all sessions', is_flag=True, default=False)
|
||||
@click.option('--logout-all-sessions', help='Log out from all sessions', is_flag=True, default=False)
|
||||
@pass_context
|
||||
def set_password(context, user, password=None, logout_all_sessions=False):
|
||||
"Set password for a user on a site"
|
||||
|
|
@ -738,7 +741,7 @@ def set_password(context, user, password=None, logout_all_sessions=False):
|
|||
|
||||
@click.command('set-admin-password')
|
||||
@click.argument('admin-password', required=False)
|
||||
@click.option('--logout-all-sessions', help='Logout from all sessions', is_flag=True, default=False)
|
||||
@click.option('--logout-all-sessions', help='Log out from all sessions', is_flag=True, default=False)
|
||||
@pass_context
|
||||
def set_admin_password(context, admin_password=None, logout_all_sessions=False):
|
||||
"Set Administrator password for a site"
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2017, Frappe Technologies and contributors
|
||||
# Copyright (c) 2022, Frappe Technologies and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
from frappe import _
|
||||
from frappe.utils import get_fullname, now
|
||||
from frappe.model.document import Document
|
||||
from frappe.core.utils import set_timeline_doc
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.core.utils import set_timeline_doc
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder import DocType, Interval
|
||||
from frappe.query_builder.functions import Now
|
||||
from pypika.terms import PseudoColumn
|
||||
from frappe.utils import get_fullname, now
|
||||
|
||||
|
||||
class ActivityLog(Document):
|
||||
def before_insert(self):
|
||||
|
|
@ -49,5 +48,5 @@ def clear_activity_logs(days=None):
|
|||
days = 90
|
||||
doctype = DocType("Activity Log")
|
||||
frappe.db.delete(doctype, filters=(
|
||||
doctype.creation < PseudoColumn(f"({Now() - Interval(days=days)})")
|
||||
))
|
||||
doctype.creation < (Now() - Interval(days=days))
|
||||
))
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import unittest
|
|||
from urllib.parse import quote
|
||||
|
||||
import frappe
|
||||
from frappe.email.doctype.email_queue.email_queue import EmailQueue
|
||||
from frappe.core.doctype.communication.communication import get_emails
|
||||
from frappe.email.doctype.email_queue.email_queue import EmailQueue
|
||||
|
||||
test_records = frappe.get_test_records('Communication')
|
||||
|
||||
|
|
@ -202,7 +202,7 @@ class TestCommunication(unittest.TestCase):
|
|||
|
||||
self.assertIn(("Note", note.name), doc_links)
|
||||
|
||||
def parse_emails(self):
|
||||
def test_parse_emails(self):
|
||||
emails = get_emails(
|
||||
[
|
||||
'comm_recipient+DocType+DocName@example.com',
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -383,7 +383,7 @@ class TestFile(unittest.TestCase):
|
|||
}).insert(ignore_permissions=True)
|
||||
|
||||
test_file.make_thumbnail()
|
||||
self.assertEquals(test_file.thumbnail_url, '/files/image_small.jpg')
|
||||
self.assertEqual(test_file.thumbnail_url, '/files/image_small.jpg')
|
||||
|
||||
# test web image without extension
|
||||
test_file = frappe.get_doc({
|
||||
|
|
@ -400,7 +400,7 @@ class TestFile(unittest.TestCase):
|
|||
test_file.reload()
|
||||
test_file.file_url = "/files/image_small.jpg"
|
||||
test_file.make_thumbnail(suffix="xs", crop=True)
|
||||
self.assertEquals(test_file.thumbnail_url, '/files/image_small_xs.jpg')
|
||||
self.assertEqual(test_file.thumbnail_url, '/files/image_small_xs.jpg')
|
||||
|
||||
frappe.clear_messages()
|
||||
test_file.db_set('thumbnail_url', None)
|
||||
|
|
@ -408,7 +408,7 @@ class TestFile(unittest.TestCase):
|
|||
test_file.file_url = frappe.utils.get_url('unknown.jpg')
|
||||
test_file.make_thumbnail(suffix="xs")
|
||||
self.assertEqual(json.loads(frappe.message_log[0]).get("message"), f"File '{frappe.utils.get_url('unknown.jpg')}' not found")
|
||||
self.assertEquals(test_file.thumbnail_url, None)
|
||||
self.assertEqual(test_file.thumbnail_url, None)
|
||||
|
||||
def test_file_unzip(self):
|
||||
file_path = frappe.get_app_path('frappe', 'www/_test/assets/file.zip')
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ from frappe import _
|
|||
from frappe.model.document import Document
|
||||
from frappe.query_builder import DocType, Interval
|
||||
from frappe.query_builder.functions import Now
|
||||
from pypika.terms import PseudoColumn
|
||||
|
||||
|
||||
class LogSettings(Document):
|
||||
|
|
@ -19,7 +18,7 @@ class LogSettings(Document):
|
|||
def clear_error_logs(self):
|
||||
table = DocType("Error Log")
|
||||
frappe.db.delete(table, filters=(
|
||||
table.creation < PseudoColumn(f"({Now() - Interval(days=self.clear_error_log_after)})")
|
||||
table.creation < (Now() - Interval(days=self.clear_error_log_after))
|
||||
))
|
||||
|
||||
def clear_activity_logs(self):
|
||||
|
|
|
|||
|
|
@ -1,8 +1,100 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Frappe Technologies and Contributors
|
||||
# Copyright (c) 2022, Frappe Technologies and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestLogSettings(unittest.TestCase):
|
||||
pass
|
||||
from datetime import datetime
|
||||
|
||||
import frappe
|
||||
from frappe.utils import now_datetime, add_to_date
|
||||
from frappe.core.doctype.log_settings.log_settings import run_log_clean_up
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestLogSettings(FrappeTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
frappe.db.set_single_value(
|
||||
"Log Settings",
|
||||
{
|
||||
"clear_error_log_after": 1,
|
||||
"clear_activity_log_after": 1,
|
||||
"clear_email_queue_after": 1,
|
||||
},
|
||||
)
|
||||
|
||||
def setUp(self) -> None:
|
||||
if self._testMethodName == "test_delete_logs":
|
||||
self.datetime = frappe._dict()
|
||||
self.datetime.current = now_datetime()
|
||||
self.datetime.past = add_to_date(self.datetime.current, days=-4)
|
||||
setup_test_logs(self.datetime.past)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
if self._testMethodName == "test_delete_logs":
|
||||
del self.datetime
|
||||
|
||||
def test_delete_logs(self):
|
||||
# make sure test data is present
|
||||
activity_log_count = frappe.db.count(
|
||||
"Activity Log", {"creation": ("<=", self.datetime.past)}
|
||||
)
|
||||
error_log_count = frappe.db.count(
|
||||
"Error Log", {"creation": ("<=", self.datetime.past)}
|
||||
)
|
||||
email_queue_count = frappe.db.count(
|
||||
"Email Queue", {"creation": ("<=", self.datetime.past)}
|
||||
)
|
||||
|
||||
self.assertNotEqual(activity_log_count, 0)
|
||||
self.assertNotEqual(error_log_count, 0)
|
||||
self.assertNotEqual(email_queue_count, 0)
|
||||
|
||||
# run clean up job
|
||||
run_log_clean_up()
|
||||
|
||||
# test if logs are deleted
|
||||
activity_log_count = frappe.db.count(
|
||||
"Activity Log", {"creation": ("<", self.datetime.past)}
|
||||
)
|
||||
error_log_count = frappe.db.count(
|
||||
"Error Log", {"creation": ("<", self.datetime.past)}
|
||||
)
|
||||
email_queue_count = frappe.db.count(
|
||||
"Email Queue", {"creation": ("<", self.datetime.past)}
|
||||
)
|
||||
|
||||
self.assertEqual(activity_log_count, 0)
|
||||
self.assertEqual(error_log_count, 0)
|
||||
self.assertEqual(email_queue_count, 0)
|
||||
|
||||
|
||||
def setup_test_logs(past: datetime) -> None:
|
||||
activity_log = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Activity Log",
|
||||
"subject": "Test subject",
|
||||
"full_name": "test user2",
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
activity_log.db_set("creation", past)
|
||||
|
||||
error_log = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Error Log",
|
||||
"method": "test_method",
|
||||
"error": "traceback",
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
error_log.db_set("creation", past)
|
||||
|
||||
doc1 = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Email Queue",
|
||||
"sender": "test1@example.com",
|
||||
"message": "This is a test email1",
|
||||
"priority": 1,
|
||||
"expose_recipients": "test@receiver.com",
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
doc1.db_set("creation", past)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ from frappe.modules import make_boilerplate
|
|||
from frappe.core.doctype.page.page import delete_custom_role
|
||||
from frappe.core.doctype.custom_role.custom_role import get_custom_allowed_roles
|
||||
from frappe.desk.reportview import append_totals_row
|
||||
from frappe.utils.safe_exec import safe_exec
|
||||
from frappe.utils.safe_exec import safe_exec, check_safe_sql_query
|
||||
|
||||
|
||||
class Report(Document):
|
||||
|
|
@ -110,8 +110,7 @@ class Report(Document):
|
|||
if not self.query:
|
||||
frappe.throw(_("Must specify a Query to run"), title=_('Report Document Error'))
|
||||
|
||||
if not self.query.lower().startswith("select"):
|
||||
frappe.throw(_("Query must be a SELECT"), title=_('Report Document Error'))
|
||||
check_safe_sql_query(self.query)
|
||||
|
||||
result = [list(t) for t in frappe.db.sql(self.query, filters)]
|
||||
columns = self.get_columns() or [cstr(c[0]) for c in frappe.db.get_description()]
|
||||
|
|
|
|||
|
|
@ -1,17 +1,19 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import textwrap
|
||||
|
||||
import frappe, json, os
|
||||
import unittest
|
||||
from frappe.desk.query_report import run, save_report, add_total_row
|
||||
from frappe.desk.reportview import delete_report, save_report as _save_report
|
||||
from frappe.custom.doctype.customize_form.customize_form import reset_customization
|
||||
from frappe.core.doctype.user_permission.test_user_permission import create_user
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
test_records = frappe.get_test_records('Report')
|
||||
test_dependencies = ['User']
|
||||
|
||||
class TestReport(unittest.TestCase):
|
||||
class TestReport(FrappeTestCase):
|
||||
def test_report_builder(self):
|
||||
if frappe.db.exists('Report', 'User Activity Report'):
|
||||
frappe.delete_doc('Report', 'User Activity Report')
|
||||
|
|
@ -335,3 +337,29 @@ result = [
|
|||
self.assertEqual(result[-1][0], "Total")
|
||||
self.assertEqual(result[-1][1], 200)
|
||||
self.assertEqual(result[-1][2], 150.50)
|
||||
|
||||
def test_cte_in_query_report(self):
|
||||
cte_query = textwrap.dedent("""
|
||||
with enabled_users as (
|
||||
select name
|
||||
from `tabUser`
|
||||
where enabled = 1
|
||||
)
|
||||
select * from enabled_users;
|
||||
""")
|
||||
|
||||
report = frappe.get_doc({
|
||||
"doctype": "Report",
|
||||
"ref_doctype": "User",
|
||||
"report_name": "Enabled Users List",
|
||||
"report_type": "Query Report",
|
||||
"is_standard": "No",
|
||||
"query": cte_query,
|
||||
}).insert()
|
||||
|
||||
if frappe.db.db_type == "mariadb":
|
||||
col, rows = report.execute_query_report(filters={})
|
||||
self.assertEqual(col[0], "name")
|
||||
self.assertGreaterEqual(len(rows), 1)
|
||||
elif frappe.db.db_type == "postgres":
|
||||
self.assertRaises(frappe.PermissionError, report.execute_query_report, filters={})
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -253,8 +253,8 @@ class User(Document):
|
|||
self.email_new_password(new_password)
|
||||
|
||||
except frappe.OutgoingEmailError:
|
||||
print(frappe.get_traceback())
|
||||
pass # email server not set, don't send email
|
||||
# email server not set, don't send email
|
||||
frappe.log_error(frappe.get_traceback())
|
||||
|
||||
@Document.hook
|
||||
def validate_reset_password(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);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -1,133 +1,159 @@
|
|||
{
|
||||
"accept_payment": 0,
|
||||
"allow_comments": 0,
|
||||
"allow_delete": 0,
|
||||
"allow_edit": 1,
|
||||
"allow_incomplete": 0,
|
||||
"allow_multiple": 0,
|
||||
"allow_print": 0,
|
||||
"amount": 0.0,
|
||||
"amount_based_on_field": 0,
|
||||
"breadcrumbs": "[{\"title\": _(\"My Account\"), \"route\": \"me\"}]",
|
||||
"creation": "2016-09-19 05:16:59.242754",
|
||||
"doc_type": "User",
|
||||
"docstatus": 0,
|
||||
"doctype": "Web Form",
|
||||
"idx": 0,
|
||||
"introduction_text": "",
|
||||
"is_standard": 1,
|
||||
"login_required": 1,
|
||||
"max_attachment_size": 0,
|
||||
"modified": "2019-01-28 12:45:17.158069",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "edit-profile",
|
||||
"owner": "Administrator",
|
||||
"published": 1,
|
||||
"route": "update-profile",
|
||||
"show_in_grid": 0,
|
||||
"show_sidebar": 1,
|
||||
"sidebar_items": [],
|
||||
"success_message": "Profile updated successfully.",
|
||||
"success_url": "/me",
|
||||
"title": "Update Profile",
|
||||
"accept_payment": 0,
|
||||
"allow_comments": 0,
|
||||
"allow_delete": 0,
|
||||
"allow_edit": 1,
|
||||
"allow_incomplete": 0,
|
||||
"allow_multiple": 0,
|
||||
"allow_print": 0,
|
||||
"amount": 0.0,
|
||||
"amount_based_on_field": 0,
|
||||
"apply_document_permissions": 0,
|
||||
"breadcrumbs": "[{\"title\": _(\"My Account\"), \"route\": \"me\"}]",
|
||||
"creation": "2016-09-19 05:16:59.242754",
|
||||
"doc_type": "User",
|
||||
"docstatus": 0,
|
||||
"doctype": "Web Form",
|
||||
"idx": 0,
|
||||
"introduction_text": "",
|
||||
"is_multi_step_form": 0,
|
||||
"is_standard": 1,
|
||||
"login_required": 1,
|
||||
"max_attachment_size": 0,
|
||||
"modified": "2022-03-22 15:00:43.456738",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "edit-profile",
|
||||
"owner": "Administrator",
|
||||
"published": 1,
|
||||
"route": "update-profile",
|
||||
"route_to_success_link": 0,
|
||||
"show_attachments": 0,
|
||||
"show_in_grid": 0,
|
||||
"show_sidebar": 0,
|
||||
"sidebar_items": [],
|
||||
"success_message": "Profile updated successfully.",
|
||||
"success_url": "/me",
|
||||
"title": "Update Profile",
|
||||
"web_form_fields": [
|
||||
{
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"fieldname": "first_name",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"label": "First Name",
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"read_only": 0,
|
||||
"reqd": 1,
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"fieldname": "first_name",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"label": "First Name",
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"read_only": 0,
|
||||
"reqd": 1,
|
||||
"show_in_filter": 0
|
||||
},
|
||||
},
|
||||
{
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"fieldname": "middle_name",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"label": "Middle Name (Optional)",
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"read_only": 0,
|
||||
"reqd": 0,
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"fieldname": "middle_name",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"label": "Middle Name (Optional)",
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"read_only": 0,
|
||||
"reqd": 0,
|
||||
"show_in_filter": 0
|
||||
},
|
||||
},
|
||||
{
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"fieldname": "last_name",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"label": "Last Name",
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"read_only": 0,
|
||||
"reqd": 0,
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"fieldname": "last_name",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"label": "Last Name",
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"read_only": 0,
|
||||
"reqd": 0,
|
||||
"show_in_filter": 0
|
||||
},
|
||||
},
|
||||
{
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"description": "",
|
||||
"fieldname": "user_image",
|
||||
"fieldtype": "Attach Image",
|
||||
"hidden": 0,
|
||||
"label": "User Image",
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"read_only": 0,
|
||||
"reqd": 0,
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"fieldname": "",
|
||||
"fieldtype": "Column Break",
|
||||
"hidden": 0,
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"read_only": 0,
|
||||
"reqd": 0,
|
||||
"show_in_filter": 0
|
||||
},
|
||||
},
|
||||
{
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
"label": "More Information",
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"read_only": 0,
|
||||
"reqd": 0,
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"description": "",
|
||||
"fieldname": "user_image",
|
||||
"fieldtype": "Attach Image",
|
||||
"hidden": 0,
|
||||
"label": "Profile Picture",
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"read_only": 0,
|
||||
"reqd": 0,
|
||||
"show_in_filter": 0
|
||||
},
|
||||
},
|
||||
{
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"fieldname": "phone",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"label": "Phone",
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"read_only": 0,
|
||||
"reqd": 0,
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
"label": "More Information",
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"read_only": 0,
|
||||
"reqd": 0,
|
||||
"show_in_filter": 0
|
||||
},
|
||||
},
|
||||
{
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"fieldname": "mobile_no",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"label": "Mobile Number",
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"read_only": 0,
|
||||
"reqd": 0,
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"fieldname": "phone",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"label": "Phone",
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"read_only": 0,
|
||||
"reqd": 0,
|
||||
"show_in_filter": 0
|
||||
},
|
||||
},
|
||||
{
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"description": "",
|
||||
"fieldname": "language",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"label": "Language",
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"options": "Language",
|
||||
"read_only": 0,
|
||||
"reqd": 0,
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"fieldname": "mobile_no",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"label": "Mobile Number",
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"read_only": 0,
|
||||
"reqd": 0,
|
||||
"show_in_filter": 0
|
||||
},
|
||||
{
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"fieldname": "",
|
||||
"fieldtype": "Column Break",
|
||||
"hidden": 0,
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"read_only": 0,
|
||||
"reqd": 0,
|
||||
"show_in_filter": 0
|
||||
},
|
||||
{
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"description": "",
|
||||
"fieldname": "language",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"label": "Language",
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"options": "Language",
|
||||
"read_only": 0,
|
||||
"reqd": 0,
|
||||
"show_in_filter": 0
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
"document_type": "Setup",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"is_system_generated",
|
||||
"dt",
|
||||
"module",
|
||||
"label",
|
||||
|
|
@ -425,13 +426,20 @@
|
|||
"fieldtype": "Link",
|
||||
"label": "Module (for export)",
|
||||
"options": "Module Def"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_system_generated",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is System Generated",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-glass",
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2022-02-14 15:42:21.885999",
|
||||
"modified": "2022-02-28 22:22:54.893269",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Custom Field",
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ def create_custom_field_if_values_exist(doctype, df):
|
|||
frappe.db.count(dt=doctype, filters=IfNull(df.fieldname, "") != ""):
|
||||
create_custom_field(doctype, df)
|
||||
|
||||
def create_custom_field(doctype, df, ignore_validate=False):
|
||||
def create_custom_field(doctype, df, ignore_validate=False, is_system_generated=True):
|
||||
df = frappe._dict(df)
|
||||
if not df.fieldname and df.label:
|
||||
df.fieldname = frappe.scrub(df.label)
|
||||
|
|
@ -130,8 +130,7 @@ def create_custom_field(doctype, df, ignore_validate=False):
|
|||
"permlevel": 0,
|
||||
"fieldtype": 'Data',
|
||||
"hidden": 0,
|
||||
# Looks like we always use this programatically?
|
||||
# "is_standard": 1
|
||||
"is_system_generated": is_system_generated
|
||||
})
|
||||
custom_field.update(df)
|
||||
custom_field.flags.ignore_validate = ignore_validate
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -243,7 +242,8 @@ frappe.ui.form.on("Customize Form Field", {
|
|||
},
|
||||
fields_add: function(frm, cdt, cdn) {
|
||||
var f = frappe.model.get_doc(cdt, cdn);
|
||||
f.is_custom_field = 1;
|
||||
f.is_system_generated = false;
|
||||
f.is_custom_field = true;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -341,11 +341,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}));
|
||||
|
|
|
|||
|
|
@ -402,7 +402,7 @@ class CustomizeForm(Document):
|
|||
"property": prop,
|
||||
"value": value,
|
||||
"property_type": property_type
|
||||
})
|
||||
}, is_system_generated=False)
|
||||
|
||||
def get_existing_property_value(self, property_name, fieldname=None):
|
||||
# check if there is any need to make property setter!
|
||||
|
|
@ -487,12 +487,21 @@ def reset_customization(doctype):
|
|||
setters = frappe.get_all("Property Setter", filters={
|
||||
'doc_type': doctype,
|
||||
'field_name': ['!=', 'naming_series'],
|
||||
'property': ['!=', 'options']
|
||||
'property': ['!=', 'options'],
|
||||
'is_system_generated': False
|
||||
}, pluck='name')
|
||||
|
||||
for setter in setters:
|
||||
frappe.delete_doc("Property Setter", setter)
|
||||
|
||||
custom_fields = frappe.get_all("Custom Field", filters={
|
||||
'dt': doctype,
|
||||
'is_system_generated': False
|
||||
}, pluck='name')
|
||||
|
||||
for field in custom_fields:
|
||||
frappe.delete_doc("Custom Field", field)
|
||||
|
||||
frappe.clear_cache(doctype=doctype)
|
||||
|
||||
doctype_properties = {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
"document_type": "Setup",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"is_system_generated",
|
||||
"help",
|
||||
"sb0",
|
||||
"doctype_or_field",
|
||||
|
|
@ -103,13 +104,20 @@
|
|||
{
|
||||
"fieldname": "section_break_9",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_system_generated",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is System Generated",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-glass",
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-12-14 14:15:41.929071",
|
||||
"modified": "2022-02-28 22:24:12.377693",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Property Setter",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import re
|
|||
import string
|
||||
from contextlib import contextmanager
|
||||
from time import time
|
||||
from typing import Dict, List, Tuple, Union
|
||||
from typing import Dict, List, Optional, Tuple, Union
|
||||
|
||||
from pypika.terms import Criterion, NullValue, PseudoColumn
|
||||
|
||||
|
|
@ -119,6 +119,9 @@ class Database(object):
|
|||
if not run:
|
||||
return query
|
||||
|
||||
# remove \n \t from start and end of query
|
||||
query = re.sub(r'^\s*|\s*$', '', query)
|
||||
|
||||
if re.search(r'ifnull\(', query, flags=re.IGNORECASE):
|
||||
# replaces ifnull in query with coalesce
|
||||
query = re.sub(r'ifnull\(', 'coalesce(', query, flags=re.IGNORECASE)
|
||||
|
|
@ -142,8 +145,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)):
|
||||
|
|
@ -386,7 +387,7 @@ class Database(object):
|
|||
"""
|
||||
|
||||
ret = self.get_values(doctype, filters, fieldname, ignore, as_dict, debug,
|
||||
order_by, cache=cache, for_update=for_update, run=run, pluck=pluck, distinct=distinct)
|
||||
order_by, cache=cache, for_update=for_update, run=run, pluck=pluck, distinct=distinct, limit=1)
|
||||
|
||||
if not run:
|
||||
return ret
|
||||
|
|
@ -395,7 +396,7 @@ class Database(object):
|
|||
|
||||
def get_values(self, doctype, filters=None, fieldname="name", ignore=None, as_dict=False,
|
||||
debug=False, order_by="KEEP_DEFAULT_ORDERING", update=None, cache=False, for_update=False,
|
||||
run=True, pluck=False, distinct=False):
|
||||
run=True, pluck=False, distinct=False, limit=None):
|
||||
"""Returns multiple document properties.
|
||||
|
||||
:param doctype: DocType name.
|
||||
|
|
@ -425,14 +426,15 @@ class Database(object):
|
|||
|
||||
if isinstance(filters, list):
|
||||
out = self._get_value_for_many_names(
|
||||
doctype,
|
||||
filters,
|
||||
fieldname,
|
||||
order_by,
|
||||
doctype=doctype,
|
||||
names=filters,
|
||||
field=fieldname,
|
||||
order_by=order_by,
|
||||
debug=debug,
|
||||
run=run,
|
||||
pluck=pluck,
|
||||
distinct=distinct,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
else:
|
||||
|
|
@ -446,17 +448,18 @@ class Database(object):
|
|||
if order_by:
|
||||
order_by = "modified" if order_by == "KEEP_DEFAULT_ORDERING" else order_by
|
||||
out = self._get_values_from_table(
|
||||
fields,
|
||||
filters,
|
||||
doctype,
|
||||
as_dict,
|
||||
debug,
|
||||
order_by,
|
||||
update,
|
||||
fields=fields,
|
||||
filters=filters,
|
||||
doctype=doctype,
|
||||
as_dict=as_dict,
|
||||
debug=debug,
|
||||
order_by=order_by,
|
||||
update=update,
|
||||
for_update=for_update,
|
||||
run=run,
|
||||
pluck=pluck,
|
||||
distinct=distinct
|
||||
distinct=distinct,
|
||||
limit=limit,
|
||||
)
|
||||
except Exception as e:
|
||||
if ignore and (frappe.db.is_missing_column(e) or frappe.db.is_table_missing(e)):
|
||||
|
|
@ -558,7 +561,7 @@ class Database(object):
|
|||
def get_list(*args, **kwargs):
|
||||
return frappe.get_list(*args, **kwargs)
|
||||
|
||||
def set_single_value(self, doctype, fieldname, value, *args, **kwargs):
|
||||
def set_single_value(self, doctype: str, fieldname: Union[str, Dict], value: Optional[Union[str, int]] = None, *args, **kwargs):
|
||||
"""Set field value of Single DocType.
|
||||
|
||||
:param doctype: DocType of the single object
|
||||
|
|
@ -625,6 +628,7 @@ class Database(object):
|
|||
run=True,
|
||||
pluck=False,
|
||||
distinct=False,
|
||||
limit=None,
|
||||
):
|
||||
field_objects = []
|
||||
|
||||
|
|
@ -643,6 +647,7 @@ class Database(object):
|
|||
field_objects=field_objects,
|
||||
fields=fields,
|
||||
distinct=distinct,
|
||||
limit=limit,
|
||||
)
|
||||
if (
|
||||
fields == "*"
|
||||
|
|
@ -656,7 +661,7 @@ class Database(object):
|
|||
)
|
||||
return r
|
||||
|
||||
def _get_value_for_many_names(self, doctype, names, field, order_by, debug=False, run=True, pluck=False, distinct=False):
|
||||
def _get_value_for_many_names(self, doctype, names, field, order_by, debug=False, run=True, pluck=False, distinct=False, limit=None):
|
||||
names = list(filter(None, names))
|
||||
if names:
|
||||
return self.get_all(
|
||||
|
|
@ -669,6 +674,7 @@ class Database(object):
|
|||
as_list=1,
|
||||
run=run,
|
||||
distinct=distinct,
|
||||
limit_page_length=limit
|
||||
)
|
||||
else:
|
||||
return {}
|
||||
|
|
@ -884,27 +890,39 @@ class Database(object):
|
|||
return self.sql("select name from `tab{doctype}` limit 1".format(doctype=doctype))
|
||||
|
||||
def exists(self, dt, dn=None, cache=False):
|
||||
"""Returns true if document exists.
|
||||
"""Return the document name of a matching document, or None.
|
||||
|
||||
:param dt: DocType name.
|
||||
:param dn: Document name or filter dict."""
|
||||
if isinstance(dt, str):
|
||||
if dt!="DocType" and dt==dn:
|
||||
return True # single always exists (!)
|
||||
try:
|
||||
return self.get_value(dt, dn, "name", cache=cache)
|
||||
except Exception:
|
||||
return None
|
||||
Note: `cache` only works if `dt` and `dn` are of type `str`.
|
||||
|
||||
elif isinstance(dt, dict) and dt.get('doctype'):
|
||||
try:
|
||||
conditions = []
|
||||
for d in dt:
|
||||
if d == 'doctype': continue
|
||||
conditions.append([d, '=', dt[d]])
|
||||
return self.get_all(dt['doctype'], filters=conditions, as_list=1)
|
||||
except Exception:
|
||||
return None
|
||||
## Examples
|
||||
|
||||
Pass doctype and docname (only in this case we can cache the result)
|
||||
|
||||
```
|
||||
exists("User", "jane@example.org", cache=True)
|
||||
```
|
||||
|
||||
Pass a dict of filters including the `"doctype"` key:
|
||||
|
||||
```
|
||||
exists({"doctype": "User", "full_name": "Jane Doe"})
|
||||
```
|
||||
|
||||
Pass the doctype and a dict of filters:
|
||||
|
||||
```
|
||||
exists("User", {"full_name": "Jane Doe"})
|
||||
```
|
||||
"""
|
||||
if dt != "DocType" and dt == dn:
|
||||
# single always exists (!)
|
||||
return dn
|
||||
|
||||
if isinstance(dt, dict):
|
||||
dt = dt.copy() # don't modify the original dict
|
||||
dt, dn = dt.pop("doctype"), dt
|
||||
|
||||
return self.get_value(dt, dn, ignore=True, cache=cache)
|
||||
|
||||
def count(self, dt, filters=None, debug=False, cache=False):
|
||||
"""Returns `COUNT(*)` for given DocType and filters."""
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
80
frappe/database/sequence.py
Normal file
80
frappe/database/sequence.py
Normal 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})")
|
||||
|
|
@ -72,7 +72,8 @@ class ToDo(Document):
|
|||
assignments = frappe.get_all("ToDo", filters={
|
||||
"reference_type": self.reference_type,
|
||||
"reference_name": self.reference_name,
|
||||
"status": ("!=", "Cancelled")
|
||||
"status": ("!=", "Cancelled"),
|
||||
"allocated_to": ("is", "set")
|
||||
}, pluck="allocated_to")
|
||||
assignments.reverse()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -311,6 +312,7 @@ def get_assignments(dt, dn):
|
|||
'reference_type': dt,
|
||||
'reference_name': dn,
|
||||
'status': ('!=', 'Cancelled'),
|
||||
'allocated_to': ("is", "set")
|
||||
})
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
@ -355,7 +357,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")
|
||||
|
|
|
|||
|
|
@ -352,14 +352,10 @@ def export_query():
|
|||
)
|
||||
return
|
||||
|
||||
columns = get_columns_dict(data.columns)
|
||||
|
||||
from frappe.utils.xlsxutils import make_xlsx
|
||||
|
||||
data["result"] = handle_duration_fieldtype_values(
|
||||
data.get("result"), data.get("columns")
|
||||
)
|
||||
xlsx_data, column_widths = build_xlsx_data(columns, data, visible_idx, include_indentation)
|
||||
format_duration_fields(data)
|
||||
xlsx_data, column_widths = build_xlsx_data(data, visible_idx, include_indentation)
|
||||
xlsx_file = make_xlsx(xlsx_data, "Query Report", column_widths=column_widths)
|
||||
|
||||
frappe.response["filename"] = report_name + ".xlsx"
|
||||
|
|
@ -367,39 +363,18 @@ def export_query():
|
|||
frappe.response["type"] = "binary"
|
||||
|
||||
|
||||
def handle_duration_fieldtype_values(result, columns):
|
||||
for i, col in enumerate(columns):
|
||||
fieldtype = None
|
||||
if isinstance(col, str):
|
||||
col = col.split(":")
|
||||
if len(col) > 1:
|
||||
if col[1]:
|
||||
fieldtype = col[1]
|
||||
if "/" in fieldtype:
|
||||
fieldtype, options = fieldtype.split("/")
|
||||
else:
|
||||
fieldtype = "Data"
|
||||
else:
|
||||
fieldtype = col.get("fieldtype")
|
||||
def format_duration_fields(data: frappe._dict) -> None:
|
||||
for i, col in enumerate(data.columns):
|
||||
if col.get("fieldtype") != "Duration":
|
||||
continue
|
||||
|
||||
if fieldtype == "Duration":
|
||||
for entry in range(0, len(result)):
|
||||
row = result[entry]
|
||||
if isinstance(row, dict):
|
||||
val_in_seconds = row[col.fieldname]
|
||||
if val_in_seconds:
|
||||
duration_val = format_duration(val_in_seconds)
|
||||
row[col.fieldname] = duration_val
|
||||
else:
|
||||
val_in_seconds = row[i]
|
||||
if val_in_seconds:
|
||||
duration_val = format_duration(val_in_seconds)
|
||||
row[i] = duration_val
|
||||
|
||||
return result
|
||||
for row in data.result:
|
||||
index = col.fieldname if isinstance(row, dict) else i
|
||||
if row[index]:
|
||||
row[index] = format_duration(row[index])
|
||||
|
||||
|
||||
def build_xlsx_data(columns, data, visible_idx, include_indentation, ignore_visible_idx=False):
|
||||
def build_xlsx_data(data, visible_idx, include_indentation, ignore_visible_idx=False):
|
||||
result = [[]]
|
||||
column_widths = []
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ from frappe import _, is_whitelisted
|
|||
import re
|
||||
import wrapt
|
||||
|
||||
UNTRANSLATED_DOCTYPES = ["DocType", "Role"]
|
||||
|
||||
def sanitize_searchfield(searchfield):
|
||||
blacklisted_keywords = ['select', 'delete', 'drop', 'update', 'case', 'and', 'or', 'like']
|
||||
|
|
@ -114,6 +113,7 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0,
|
|||
or_filters = []
|
||||
|
||||
|
||||
translated_search_doctypes = frappe.get_hooks("translated_search_doctypes")
|
||||
# build from doctype
|
||||
if txt:
|
||||
search_fields = ["name"]
|
||||
|
|
@ -125,7 +125,7 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0,
|
|||
|
||||
for f in search_fields:
|
||||
fmeta = meta.get_field(f.strip())
|
||||
if (doctype not in UNTRANSLATED_DOCTYPES) and (f == "name" or (fmeta and fmeta.fieldtype in ["Data", "Text", "Small Text", "Long Text",
|
||||
if (doctype not in translated_search_doctypes) and (f == "name" or (fmeta and fmeta.fieldtype in ["Data", "Text", "Small Text", "Long Text",
|
||||
"Link", "Select", "Read Only", "Text Editor"])):
|
||||
or_filters.append([doctype, f.strip(), "like", "%{0}%".format(txt)])
|
||||
|
||||
|
|
@ -160,7 +160,7 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0,
|
|||
ptype = 'select' if frappe.only_has_select_perm(doctype) else 'read'
|
||||
ignore_permissions = True if doctype == "DocType" else (cint(ignore_user_permissions) and has_permission(doctype, ptype=ptype))
|
||||
|
||||
if doctype in UNTRANSLATED_DOCTYPES:
|
||||
if doctype in translated_search_doctypes:
|
||||
page_length = None
|
||||
|
||||
values = frappe.get_list(doctype,
|
||||
|
|
@ -175,7 +175,7 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0,
|
|||
as_list=not as_dict,
|
||||
strict=False)
|
||||
|
||||
if doctype in UNTRANSLATED_DOCTYPES:
|
||||
if doctype in translated_search_doctypes:
|
||||
# Filtering the values array so that query is included in very element
|
||||
values = (
|
||||
v for v in values
|
||||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -15,8 +15,6 @@ from frappe.utils.csvutils import to_csv
|
|||
from frappe.utils.xlsxutils import make_xlsx
|
||||
from frappe.desk.query_report import build_xlsx_data
|
||||
|
||||
max_reports_per_user = frappe.local.conf.max_reports_per_user or 3
|
||||
|
||||
|
||||
class AutoEmailReport(Document):
|
||||
def autoname(self):
|
||||
|
|
@ -46,6 +44,8 @@ class AutoEmailReport(Document):
|
|||
def validate_report_count(self):
|
||||
'''check that there are only 3 enabled reports per user'''
|
||||
count = frappe.db.sql('select count(*) from `tabAuto Email Report` where user=%s and enabled=1', self.user)[0][0]
|
||||
max_reports_per_user = frappe.local.conf.max_reports_per_user or 3
|
||||
|
||||
if count > max_reports_per_user + (-1 if self.flags.in_insert else 0):
|
||||
frappe.throw(_('Only {0} emailed reports are allowed per user').format(max_reports_per_user))
|
||||
|
||||
|
|
@ -104,7 +104,7 @@ class AutoEmailReport(Document):
|
|||
report_data['columns'] = columns
|
||||
report_data['result'] = data
|
||||
|
||||
xlsx_data, column_widths = build_xlsx_data(columns, report_data, [], 1, ignore_visible_idx=True)
|
||||
xlsx_data, column_widths = build_xlsx_data(report_data, [], 1, ignore_visible_idx=True)
|
||||
xlsx_file = make_xlsx(xlsx_data, "Auto Email Report", column_widths=column_widths)
|
||||
return xlsx_file.getvalue()
|
||||
|
||||
|
|
@ -113,7 +113,7 @@ class AutoEmailReport(Document):
|
|||
report_data['columns'] = columns
|
||||
report_data['result'] = data
|
||||
|
||||
xlsx_data, column_widths = build_xlsx_data(columns, report_data, [], 1, ignore_visible_idx=True)
|
||||
xlsx_data, column_widths = build_xlsx_data(report_data, [], 1, ignore_visible_idx=True)
|
||||
return to_csv(xlsx_data)
|
||||
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -111,7 +111,6 @@ class EmailQueue(Document):
|
|||
""" Send emails to recipients.
|
||||
"""
|
||||
if not self.can_send_now():
|
||||
frappe.db.rollback()
|
||||
return
|
||||
|
||||
with SendMailContext(self, is_background_task) as ctx:
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -240,7 +240,7 @@ class TestNotification(unittest.TestCase):
|
|||
self.assertTrue(email_queue)
|
||||
|
||||
# check if description is changed after alert since set_property_after_alert is set
|
||||
self.assertEquals(todo.description, 'Changed by Notification')
|
||||
self.assertEqual(todo.description, 'Changed by Notification')
|
||||
|
||||
recipients = [d.recipient for d in email_queue.recipients]
|
||||
self.assertTrue('test2@example.com' in recipients)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe
|
||||
from frappe import msgprint, _
|
||||
from frappe.utils.verified_command import get_signed_params, verify_request
|
||||
from frappe.utils import get_url, now_datetime, cint
|
||||
from frappe.query_builder import DocType, Interval
|
||||
from frappe.query_builder.functions import Now
|
||||
|
||||
def get_emails_sent_this_month(email_account=None):
|
||||
"""Get count of emails sent from a specific email account.
|
||||
|
|
@ -162,15 +164,16 @@ def get_queue():
|
|||
by priority desc, creation asc
|
||||
limit 500''', { 'now': now_datetime() }, as_dict=True)
|
||||
|
||||
def clear_outbox(days=None):
|
||||
def clear_outbox(days: int = None) -> None:
|
||||
"""Remove low priority older than 31 days in Outbox or configured in Log Settings.
|
||||
Note: Used separate query to avoid deadlock
|
||||
"""
|
||||
if not days:
|
||||
days=31
|
||||
days = days or 31
|
||||
email_queue = DocType("Email Queue")
|
||||
|
||||
email_queues = frappe.db.sql_list("""SELECT `name` FROM `tabEmail Queue`
|
||||
WHERE `priority`=0 AND `modified` < (NOW() - INTERVAL '{0}' DAY)""".format(days))
|
||||
email_queues = frappe.qb.from_(email_queue).select(email_queue.name).where(
|
||||
email_queue.modified < (Now() - Interval(days=days))
|
||||
).run(pluck=True)
|
||||
|
||||
if email_queues:
|
||||
frappe.db.delete("Email Queue", {"name": ("in", email_queues)})
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -110,3 +110,6 @@ class InvalidAuthorizationPrefix(CSRFTokenError): pass
|
|||
class InvalidAuthorizationToken(CSRFTokenError): pass
|
||||
class InvalidDatabaseFile(ValidationError): pass
|
||||
class ExecutableNotFound(FileNotFoundError): pass
|
||||
|
||||
class InvalidRemoteException(Exception):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
17
frappe/handler.py
Executable file → Normal file
17
frappe/handler.py
Executable file → Normal file
|
|
@ -251,11 +251,10 @@ def ping():
|
|||
|
||||
def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None):
|
||||
"""run a whitelisted controller method"""
|
||||
import inspect
|
||||
import json
|
||||
from inspect import getfullargspec
|
||||
|
||||
if not args:
|
||||
args = arg or ""
|
||||
if not args and arg:
|
||||
args = arg
|
||||
|
||||
if dt: # not called from a doctype (from a page)
|
||||
if not dn:
|
||||
|
|
@ -263,9 +262,7 @@ def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None):
|
|||
doc = frappe.get_doc(dt, dn)
|
||||
|
||||
else:
|
||||
if isinstance(docs, str):
|
||||
docs = json.loads(docs)
|
||||
|
||||
docs = frappe.parse_json(docs)
|
||||
doc = frappe.get_doc(docs)
|
||||
doc._original_modified = doc.modified
|
||||
doc.check_if_latest()
|
||||
|
|
@ -274,16 +271,16 @@ def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None):
|
|||
throw_permission_error()
|
||||
|
||||
try:
|
||||
args = json.loads(args)
|
||||
args = frappe.parse_json(args)
|
||||
except ValueError:
|
||||
args = args
|
||||
pass
|
||||
|
||||
method_obj = getattr(doc, method)
|
||||
fn = getattr(method_obj, '__func__', method_obj)
|
||||
is_whitelisted(fn)
|
||||
is_valid_http_method(fn)
|
||||
|
||||
fnargs = inspect.getfullargspec(method_obj).args
|
||||
fnargs = getfullargspec(method_obj).args
|
||||
|
||||
if not fnargs or (len(fnargs)==1 and fnargs[0]=="self"):
|
||||
response = doc.run_method(method)
|
||||
|
|
|
|||
|
|
@ -394,4 +394,6 @@ override_whitelisted_methods = {
|
|||
"frappe.core.doctype.file.file.create_new_folder": "frappe.core.api.file.create_new_folder",
|
||||
"frappe.core.doctype.file.file.move_file": "frappe.core.api.file.move_file",
|
||||
"frappe.core.doctype.file.file.zip_files": "frappe.core.api.file.zip_files",
|
||||
}
|
||||
}
|
||||
|
||||
translated_search_doctypes = ["DocType", "Role", "Country", "Gender", "Salutation"]
|
||||
|
|
|
|||
|
|
@ -5,10 +5,11 @@ import json
|
|||
import os
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
from typing import List, Dict
|
||||
from typing import List, Dict, Tuple
|
||||
|
||||
import frappe
|
||||
from frappe.defaults import _clear_cache
|
||||
from frappe.utils import is_git_url
|
||||
|
||||
|
||||
def _new_site(
|
||||
|
|
@ -34,7 +35,6 @@ def _new_site(
|
|||
from frappe.commands.scheduler import _is_scheduler_enabled
|
||||
from frappe.utils import get_site_path, scheduler, touch_file
|
||||
|
||||
|
||||
if not force and os.path.exists(site):
|
||||
print("Site {0} already exists".format(site))
|
||||
sys.exit(1)
|
||||
|
|
@ -124,6 +124,86 @@ def install_db(root_login=None, root_password=None, db_name=None, source_sql=Non
|
|||
frappe.flags.in_install_db = False
|
||||
|
||||
|
||||
def find_org(org_repo: str) -> Tuple[str, str]:
|
||||
""" find the org a repo is in
|
||||
|
||||
find_org()
|
||||
ref -> https://github.com/frappe/bench/blob/develop/bench/utils/__init__.py#L390
|
||||
|
||||
:param org_repo:
|
||||
:type org_repo: str
|
||||
|
||||
:raises InvalidRemoteException: if the org is not found
|
||||
|
||||
:return: organisation and repository
|
||||
:rtype: Tuple[str, str]
|
||||
"""
|
||||
from frappe.exceptions import InvalidRemoteException
|
||||
import requests
|
||||
|
||||
for org in ["frappe", "erpnext"]:
|
||||
res = requests.head(f"https://api.github.com/repos/{org}/{org_repo}")
|
||||
if res.ok:
|
||||
return org, org_repo
|
||||
|
||||
raise InvalidRemoteException
|
||||
|
||||
|
||||
def fetch_details_from_tag(_tag: str) -> Tuple[str, str, str]:
|
||||
""" parse org, repo, tag from string
|
||||
|
||||
fetch_details_from_tag()
|
||||
ref -> https://github.com/frappe/bench/blob/develop/bench/utils/__init__.py#L403
|
||||
|
||||
:param _tag: input string
|
||||
:type _tag: str
|
||||
|
||||
:return: organisation, repostitory, tag
|
||||
:rtype: Tuple[str, str, str]
|
||||
"""
|
||||
app_tag = _tag.split("@")
|
||||
org_repo = app_tag[0].split("/")
|
||||
|
||||
try:
|
||||
repo, tag = app_tag
|
||||
except ValueError:
|
||||
repo, tag = app_tag + [None]
|
||||
|
||||
try:
|
||||
org, repo = org_repo
|
||||
except Exception:
|
||||
org, repo = find_org(org_repo[0])
|
||||
|
||||
return org, repo, tag
|
||||
|
||||
|
||||
def parse_app_name(name: str) -> str:
|
||||
"""parse repo name from name
|
||||
|
||||
__setup_details_from_git()
|
||||
ref -> https://github.com/frappe/bench/blob/develop/bench/app.py#L114
|
||||
|
||||
|
||||
:param name: git tag
|
||||
:type name: str
|
||||
|
||||
:return: repository name
|
||||
:rtype: str
|
||||
"""
|
||||
name = name.rstrip("/")
|
||||
if os.path.exists(name):
|
||||
repo = os.path.split(name)[-1]
|
||||
elif is_git_url(name):
|
||||
if name.startswith("git@") or name.startswith("ssh://"):
|
||||
_repo = name.split(":")[1].rsplit("/", 1)[1]
|
||||
else:
|
||||
_repo = name.rsplit("/", 2)[2]
|
||||
repo = _repo.split(".")[0]
|
||||
else:
|
||||
_, repo, _ = fetch_details_from_tag(name)
|
||||
return repo
|
||||
|
||||
|
||||
def install_app(name, verbose=False, set_as_patched=True):
|
||||
from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
|
||||
from frappe.model.sync import sync_for
|
||||
|
|
@ -140,7 +220,8 @@ def install_app(name, verbose=False, set_as_patched=True):
|
|||
# install pre-requisites
|
||||
if app_hooks.required_apps:
|
||||
for app in app_hooks.required_apps:
|
||||
install_app(app, verbose=verbose)
|
||||
name = parse_app_name(name)
|
||||
install_app(name, verbose=verbose)
|
||||
|
||||
frappe.flags.in_install = name
|
||||
frappe.clear_cache()
|
||||
|
|
@ -611,7 +692,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)
|
||||
|
|
@ -963,10 +963,13 @@ class BaseDocument(object):
|
|||
from frappe.model.meta import get_default_df
|
||||
df = get_default_df(fieldname)
|
||||
|
||||
if not currency and df:
|
||||
currency = self.get(df.get("options"))
|
||||
if not frappe.db.exists('Currency', currency, cache=True):
|
||||
currency = None
|
||||
if (
|
||||
df.fieldtype == "Currency"
|
||||
and not currency
|
||||
and (currency_field := df.get("options"))
|
||||
and (currency_value := self.get(currency_field))
|
||||
):
|
||||
currency = frappe.db.get_value('Currency', currency_value, cache=True)
|
||||
|
||||
val = self.get(fieldname)
|
||||
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -272,7 +272,7 @@ def make_boilerplate(template, doc, opts=None):
|
|||
frappe.utils.cstr(source.read()).format(
|
||||
app_publisher=app_publisher,
|
||||
year=frappe.utils.nowdate()[:4],
|
||||
classname=doc.name.replace(" ", ""),
|
||||
classname=doc.name.replace(" ", "").replace("-", ""),
|
||||
base_class_import=base_class_import,
|
||||
base_class=base_class,
|
||||
doctype=doc.name, **opts,
|
||||
|
|
|
|||
|
|
@ -197,4 +197,5 @@ frappe.patches.v14_0.copy_mail_data #08.03.21
|
|||
frappe.patches.v14_0.update_github_endpoints #08-11-2021
|
||||
frappe.patches.v14_0.remove_db_aggregation
|
||||
frappe.patches.v14_0.update_color_names_in_kanban_board_column
|
||||
frappe.patches.v14_0.update_is_system_generated_flag
|
||||
frappe.patches.v14_0.update_auto_account_deletion_duration
|
||||
|
|
|
|||
17
frappe/patches/v14_0/update_is_system_generated_flag.py
Normal file
17
frappe/patches/v14_0/update_is_system_generated_flag.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import frappe
|
||||
|
||||
def execute():
|
||||
# assuming all customization generated by Admin is system generated customization
|
||||
custom_field = frappe.qb.DocType("Custom Field")
|
||||
(
|
||||
frappe.qb.update(custom_field)
|
||||
.set(custom_field.is_system_generated, True)
|
||||
.where(custom_field.owner == 'Administrator').run()
|
||||
)
|
||||
|
||||
property_setter = frappe.qb.DocType("Property Setter")
|
||||
(
|
||||
frappe.qb.update(property_setter)
|
||||
.set(property_setter.is_system_generated, True)
|
||||
.where(property_setter.owner == 'Administrator').run()
|
||||
)
|
||||
|
|
@ -594,4 +594,4 @@ def is_parent_valid(child_doctype, parent_doctype):
|
|||
from frappe.core.utils import find
|
||||
parent_meta = frappe.get_meta(parent_doctype)
|
||||
child_table_field_exists = find(parent_meta.get_table_fields(), lambda d: d.options == child_doctype)
|
||||
return not parent_meta.istable and child_table_field_exists
|
||||
return not parent_meta.istable and child_table_field_exists
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ ul.tree-children {
|
|||
}
|
||||
.tree-link .node-parent,
|
||||
.tree-link .node-leaf {
|
||||
margin-right: 5px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.tree-link.active i {
|
||||
color: #5e64ff;
|
||||
|
|
|
|||
|
|
@ -526,11 +526,13 @@ export default {
|
|||
error: true
|
||||
});
|
||||
capture.show();
|
||||
capture.submit(data_url => {
|
||||
let filename = `capture_${frappe.datetime.now_datetime().replaceAll(/[: -]/g, '_')}.png`;
|
||||
this.url_to_file(data_url, filename, 'image/png').then((file) =>
|
||||
this.add_files([file])
|
||||
);
|
||||
capture.submit(data_urls => {
|
||||
data_urls.forEach(data_url => {
|
||||
let filename = `capture_${frappe.datetime.now_datetime().replaceAll(/[: -]/g, '_')}.png`;
|
||||
this.url_to_file(data_url, filename, 'image/png').then((file) =>
|
||||
this.add_files([file])
|
||||
);
|
||||
});
|
||||
});
|
||||
},
|
||||
show_google_drive_picker() {
|
||||
|
|
|
|||
|
|
@ -37,8 +37,8 @@ frappe.ui.form.ControlAttach = class ControlAttach extends frappe.ui.form.Contro
|
|||
if(this.frm) {
|
||||
me.parse_validate_and_set_in_model(null);
|
||||
me.refresh();
|
||||
me.frm.attachments.remove_attachment_by_filename(me.value, function() {
|
||||
me.parse_validate_and_set_in_model(null);
|
||||
me.frm.attachments.remove_attachment_by_filename(me.value, async () => {
|
||||
await me.parse_validate_and_set_in_model(null);
|
||||
me.refresh();
|
||||
me.frm.doc.docstatus == 1 ? me.frm.save('Update') : me.frm.save();
|
||||
});
|
||||
|
|
@ -110,9 +110,9 @@ frappe.ui.form.ControlAttach = class ControlAttach extends frappe.ui.form.Contro
|
|||
return this.value || null;
|
||||
}
|
||||
|
||||
on_upload_complete(attachment) {
|
||||
async on_upload_complete(attachment) {
|
||||
if(this.frm) {
|
||||
this.parse_validate_and_set_in_model(attachment.file_url);
|
||||
await this.parse_validate_and_set_in_model(attachment.file_url);
|
||||
this.frm.attachments.update_attachment(attachment);
|
||||
this.frm.doc.docstatus == 1 ? this.frm.save('Update') : this.frm.save();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -454,7 +454,10 @@ class FormTimeline extends BaseTimeline {
|
|||
let edit_box = this.make_editable(edit_wrapper);
|
||||
let content_wrapper = comment_wrapper.find('.content');
|
||||
let more_actions_wrapper = comment_wrapper.find('.more-actions');
|
||||
if (frappe.model.can_delete("Comment")) {
|
||||
if (frappe.model.can_delete("Comment") && (
|
||||
frappe.session.user == doc.owner ||
|
||||
frappe.user.has_role("System Manager")
|
||||
)) {
|
||||
const delete_option = $(`
|
||||
<li>
|
||||
<a class="dropdown-item">
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
@ -1697,13 +1701,17 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
}
|
||||
|
||||
update_in_all_rows(table_fieldname, fieldname, value) {
|
||||
// update the child value in all tables where it is missing
|
||||
if(!value) return;
|
||||
var cl = this.doc[table_fieldname] || [];
|
||||
for(var i = 0; i < cl.length; i++){
|
||||
if(!cl[i][fieldname]) cl[i][fieldname] = value;
|
||||
}
|
||||
refresh_field("items");
|
||||
// Update the `value` of the field named `fieldname` in all rows of the
|
||||
// child table named `table_fieldname`.
|
||||
// Do not overwrite existing values.
|
||||
if (value === undefined) return;
|
||||
|
||||
frappe.model
|
||||
.get_children(this.doc, table_fieldname)
|
||||
.filter(child => !frappe.model.has_value(child.doctype, child.name, fieldname))
|
||||
.forEach(child =>
|
||||
frappe.model.set_value(child.doctype, child.name, fieldname, value)
|
||||
);
|
||||
}
|
||||
|
||||
get_sum(table_fieldname, fieldname) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -44,8 +44,17 @@ frappe.ui.form.Attachments = class Attachments {
|
|||
// add attachment objects
|
||||
var attachments = this.get_attachments();
|
||||
if(attachments.length) {
|
||||
attachments.forEach(function(attachment) {
|
||||
me.add_attachment(attachment)
|
||||
let exists = {};
|
||||
let unique_attachments = attachments.filter(attachment => {
|
||||
return Object.prototype.hasOwnProperty.call(
|
||||
exists,
|
||||
attachment.file_name
|
||||
)
|
||||
? false
|
||||
: (exists[attachment.file_name] = true);
|
||||
});
|
||||
unique_attachments.forEach(attachment => {
|
||||
me.add_attachment(attachment);
|
||||
});
|
||||
} else {
|
||||
this.attachments_label.removeClass("has-attachments");
|
||||
|
|
@ -75,7 +84,19 @@ frappe.ui.form.Attachments = class Attachments {
|
|||
remove_action = function(target_id) {
|
||||
frappe.confirm(__("Are you sure you want to delete the attachment?"),
|
||||
function() {
|
||||
me.remove_attachment(target_id);
|
||||
let target_attachment = me
|
||||
.get_attachments()
|
||||
.find(attachment => attachment.name === target_id);
|
||||
let to_be_removed = me
|
||||
.get_attachments()
|
||||
.filter(
|
||||
attachment =>
|
||||
attachment.file_name ===
|
||||
target_attachment.file_name
|
||||
);
|
||||
to_be_removed.forEach(attachment =>
|
||||
me.remove_attachment(attachment.name)
|
||||
);
|
||||
}
|
||||
);
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -375,7 +375,7 @@ export default class ListSettings {
|
|||
let me = this;
|
||||
|
||||
if (me.removed_fields) {
|
||||
me.removed_fields.concat(fields);
|
||||
me.removed_fields = me.removed_fields.concat(fields);
|
||||
} else {
|
||||
me.removed_fields = fields;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -138,6 +138,7 @@ frappe.render_tree = function(opts) {
|
|||
opts.base_url = frappe.urllib.get_base_url();
|
||||
opts.landscape = false;
|
||||
opts.print_css = frappe.boot.print_css;
|
||||
opts.print_format_css_path = frappe.assets.bundled_asset('print_format.bundle.css');
|
||||
var tree = frappe.render_template("print_tree", opts);
|
||||
var w = window.open();
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -28,6 +28,24 @@ frappe._.get_data_uri = element => {
|
|||
return data_uri;
|
||||
};
|
||||
|
||||
function get_file_input() {
|
||||
let input = document.createElement("input");
|
||||
input.setAttribute("type", "file");
|
||||
input.setAttribute("accept", "image/*");
|
||||
input.setAttribute("multiple", "");
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
function read(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(reader.result);
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Frappe's Capture object.
|
||||
*
|
||||
|
|
@ -45,6 +63,9 @@ frappe.ui.Capture = class {
|
|||
constructor(options = {}) {
|
||||
this.options = frappe.ui.Capture.OPTIONS;
|
||||
this.set_options(options);
|
||||
|
||||
this.facing_mode = "environment";
|
||||
this.images = [];
|
||||
}
|
||||
|
||||
set_options(options) {
|
||||
|
|
@ -53,69 +74,71 @@ frappe.ui.Capture = class {
|
|||
return this;
|
||||
}
|
||||
|
||||
render() {
|
||||
return navigator.mediaDevices.getUserMedia({ video: true }).then(stream => {
|
||||
this.stream = stream;
|
||||
show() {
|
||||
this.build_dialog();
|
||||
|
||||
this.dialog = new frappe.ui.Dialog({
|
||||
title: this.options.title,
|
||||
animate: this.options.animate,
|
||||
on_hide: () => this.stop_media_stream()
|
||||
});
|
||||
if (frappe.is_mobile()) {
|
||||
this.show_for_mobile();
|
||||
} else {
|
||||
this.show_for_desktop();
|
||||
}
|
||||
}
|
||||
|
||||
this.dialog.get_close_btn().on('click', () => {
|
||||
this.hide();
|
||||
});
|
||||
build_dialog() {
|
||||
let me = this;
|
||||
me.dialog = new frappe.ui.Dialog({
|
||||
title: this.options.title,
|
||||
animate: this.options.animate,
|
||||
fields: [
|
||||
{
|
||||
fieldtype: "HTML",
|
||||
fieldname: "capture"
|
||||
},
|
||||
{
|
||||
fieldtype: "HTML",
|
||||
fieldname: "total_count"
|
||||
}
|
||||
],
|
||||
on_hide: this.stop_media_stream()
|
||||
});
|
||||
|
||||
const set_take_photo_action = () => {
|
||||
this.dialog.set_primary_action(__('Take Photo'), () => {
|
||||
const data_url = frappe._.get_data_uri(video);
|
||||
$e.find('.fc-p').attr('src', data_url);
|
||||
me.$template = $(frappe.ui.Capture.TEMPLATE);
|
||||
|
||||
$e.find('.fc-s').hide();
|
||||
$e.find('.fc-p').show();
|
||||
let field = me.dialog.get_field("capture");
|
||||
$(field.wrapper).html(me.$template);
|
||||
|
||||
this.dialog.set_secondary_action_label(__('Retake'));
|
||||
this.dialog.get_secondary_btn().show();
|
||||
|
||||
this.dialog.set_primary_action(__('Submit'), () => {
|
||||
this.hide();
|
||||
if (this.callback) this.callback(data_url);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
set_take_photo_action();
|
||||
|
||||
this.dialog.set_secondary_action(() => {
|
||||
$e.find('.fc-p').hide();
|
||||
$e.find('.fc-s').show();
|
||||
|
||||
this.dialog.get_secondary_btn().hide();
|
||||
this.dialog.get_primary_btn().off('click');
|
||||
set_take_photo_action();
|
||||
});
|
||||
|
||||
this.dialog.get_secondary_btn().hide();
|
||||
|
||||
const $e = $(frappe.ui.Capture.TEMPLATE);
|
||||
|
||||
const video = $e.find('video')[0];
|
||||
video.srcObject = this.stream;
|
||||
video.play();
|
||||
const $container = $(this.dialog.body);
|
||||
|
||||
$container.html($e);
|
||||
me.dialog.get_close_btn().on('click', () => {
|
||||
me.hide();
|
||||
});
|
||||
}
|
||||
|
||||
show() {
|
||||
this.render()
|
||||
show_for_mobile() {
|
||||
let me = this;
|
||||
if (!me.input) {
|
||||
me.input = get_file_input();
|
||||
}
|
||||
|
||||
me.input.onchange = async () => {
|
||||
for (let file of me.input.files) {
|
||||
let f = await read(file);
|
||||
me.images.push(f);
|
||||
}
|
||||
|
||||
me.render_preview();
|
||||
me.dialog.show();
|
||||
};
|
||||
me.input.click();
|
||||
}
|
||||
|
||||
show_for_desktop() {
|
||||
let me = this;
|
||||
|
||||
this.render_stream()
|
||||
.then(() => {
|
||||
this.dialog.show();
|
||||
me.dialog.show();
|
||||
})
|
||||
.catch(err => {
|
||||
if (this.options.error) {
|
||||
if (me.options.error) {
|
||||
frappe.show_alert(frappe.ui.Capture.ERR_MESSAGE, 3);
|
||||
}
|
||||
|
||||
|
|
@ -123,6 +146,159 @@ frappe.ui.Capture = class {
|
|||
});
|
||||
}
|
||||
|
||||
render_stream() {
|
||||
let me = this;
|
||||
let constraints = {
|
||||
video: {
|
||||
facingMode: this.facing_mode
|
||||
}
|
||||
};
|
||||
|
||||
return navigator.mediaDevices.getUserMedia(constraints).then(stream => {
|
||||
me.stream = stream;
|
||||
me.dialog.custom_actions.empty();
|
||||
me.dialog.get_primary_btn().off('click');
|
||||
me.setup_take_photo_action();
|
||||
me.setup_preview_action();
|
||||
me.setup_toggle_camera();
|
||||
|
||||
me.$template.find('.fc-stream-container').show();
|
||||
me.$template.find('.fc-preview-container').hide();
|
||||
me.video = me.$template.find('video')[0];
|
||||
me.video.srcObject = me.stream;
|
||||
me.video.load();
|
||||
me.video.play();
|
||||
});
|
||||
}
|
||||
|
||||
render_preview() {
|
||||
this.stop_media_stream();
|
||||
this.$template.find('.fc-stream-container').hide();
|
||||
this.$template.find('.fc-preview-container').show();
|
||||
this.dialog.get_primary_btn().off('click');
|
||||
|
||||
let images = ``;
|
||||
|
||||
this.images.forEach((image, idx) => {
|
||||
images += `
|
||||
<div class="mt-1 p-1 rounded col-md-3 col-sm-4 col-xs-4" data-idx="${idx}">
|
||||
<span class="capture-remove-btn" data-idx="${idx}">
|
||||
${frappe.utils.icon("close", "lg")}
|
||||
</span>
|
||||
<img class="rounded" src="${image}" data-idx="${idx}">
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
this.$template.find('.fc-preview-container').empty();
|
||||
$(this.$template.find('.fc-preview-container')).html(
|
||||
`<div class="row">
|
||||
${images}
|
||||
</div>`
|
||||
);
|
||||
|
||||
this.setup_capture_action();
|
||||
this.setup_submit_action();
|
||||
this.setup_remove_action();
|
||||
this.update_count();
|
||||
this.dialog.custom_actions.empty();
|
||||
}
|
||||
|
||||
setup_take_photo_action() {
|
||||
let me = this;
|
||||
|
||||
this.dialog.set_primary_action(__('Take Photo'), () => {
|
||||
const data_url = frappe._.get_data_uri(me.video);
|
||||
|
||||
me.images.push(data_url);
|
||||
me.setup_preview_action();
|
||||
me.update_count();
|
||||
});
|
||||
}
|
||||
|
||||
setup_preview_action() {
|
||||
let me = this;
|
||||
|
||||
if (!this.images.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dialog.set_secondary_action_label(__("Preview"));
|
||||
this.dialog.set_secondary_action(() => {
|
||||
me.dialog.get_primary_btn().off('click');
|
||||
me.render_preview();
|
||||
});
|
||||
}
|
||||
|
||||
setup_remove_action() {
|
||||
let me = this;
|
||||
let elements = this.$template[0].getElementsByClassName("capture-remove-btn");
|
||||
|
||||
elements.forEach(el => {
|
||||
el.onclick = () => {
|
||||
let idx = parseInt(el.getAttribute("data-idx"));
|
||||
|
||||
me.images.splice(idx, 1);
|
||||
me.render_preview();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
update_count() {
|
||||
let field = this.dialog.get_field("total_count");
|
||||
let msg = `${__("Total Images")}: <b>${this.images.length}`;
|
||||
|
||||
if (this.images.length === 0) {
|
||||
msg = __("No Images");
|
||||
}
|
||||
|
||||
$(field.wrapper).html(`
|
||||
<div class="row mt-2">
|
||||
<div class="offset-4 col-4 d-flex justify-content-center">${msg}</b></div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
setup_toggle_camera() {
|
||||
let me = this;
|
||||
|
||||
this.dialog.add_custom_action(__("Switch Camera"), () => {
|
||||
me.facing_mode = me.facing_mode == "environment" ? "user" : "environment";
|
||||
|
||||
frappe.show_alert({
|
||||
message: __("Switching Camera")
|
||||
});
|
||||
|
||||
me.stop_media_stream();
|
||||
me.render_stream();
|
||||
}, "btn-switch");
|
||||
}
|
||||
|
||||
setup_capture_action() {
|
||||
let me = this;
|
||||
|
||||
this.dialog.set_secondary_action_label(__("Capture"));
|
||||
this.dialog.set_secondary_action(() => {
|
||||
if (frappe.is_mobile()) {
|
||||
me.show_for_mobile();
|
||||
} else {
|
||||
me.render_stream();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setup_submit_action() {
|
||||
let me = this;
|
||||
|
||||
this.dialog.set_primary_action(__('Submit'), () => {
|
||||
me.hide();
|
||||
|
||||
if (me.callback) {
|
||||
me.callback(me.images);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
hide() {
|
||||
if (this.dialog) this.dialog.hide();
|
||||
this.stop_media_stream();
|
||||
|
|
@ -148,11 +324,11 @@ frappe.ui.Capture.OPTIONS = {
|
|||
frappe.ui.Capture.ERR_MESSAGE = __('Unable to load camera.');
|
||||
frappe.ui.Capture.TEMPLATE = `
|
||||
<div class="frappe-capture">
|
||||
<div class="panel panel-default">
|
||||
<div class="embed-responsive embed-responsive-16by9">
|
||||
<img class="fc-p embed-responsive-item" style="object-fit: contain; display: none;"/>
|
||||
<video class="fc-s embed-responsive-item">${frappe.ui.Capture.ERR_MESSAGE}</video>
|
||||
</div>
|
||||
<div class="embed-responsive embed-responsive-16by9 fc-stream-container">
|
||||
<video class="fc-stream embed-responsive-item">${frappe.ui.Capture.ERR_MESSAGE}</video>
|
||||
</div>
|
||||
<div class="fc-preview-container px-2" style="display: none;">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -47,13 +47,17 @@ frappe.ui.Page = class Page {
|
|||
}
|
||||
|
||||
setup_scroll_handler() {
|
||||
window.addEventListener('scroll', () => {
|
||||
if (document.documentElement.scrollTop) {
|
||||
$('.page-head').toggleClass('drop-shadow', true);
|
||||
let last_scroll = 0;
|
||||
window.addEventListener('scroll', frappe.utils.throttle(() => {
|
||||
$('.page-head').toggleClass('drop-shadow', !!document.documentElement.scrollTop);
|
||||
let current_scroll = document.documentElement.scrollTop;
|
||||
if (current_scroll > 0 && last_scroll <= current_scroll) {
|
||||
$('.page-head').css("top", "-15px");
|
||||
} else {
|
||||
$('.page-head').removeClass('drop-shadow');
|
||||
$('.page-head').css("top", "var(--navbar-height)");
|
||||
}
|
||||
});
|
||||
last_scroll = current_scroll;
|
||||
}), 500);
|
||||
}
|
||||
|
||||
get_empty_state(title, message, primary_action) {
|
||||
|
|
|
|||
|
|
@ -231,7 +231,7 @@ Object.assign(frappe.utils, {
|
|||
if (tt && (tt.substr(0, 1)===">" || tt.substr(0, 4)===">")) {
|
||||
part.push(t);
|
||||
} else {
|
||||
out.concat(part);
|
||||
out = out.concat(part);
|
||||
out.push(t);
|
||||
part = [];
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ frappe.views.CalendarView = class CalendarView extends frappe.views.ListView {
|
|||
.then(() => {
|
||||
this.page_title = __('{0} Calendar', [this.page_title]);
|
||||
this.calendar_settings = frappe.views.calendar[this.doctype] || {};
|
||||
this.calendar_name = frappe.utils.to_title_case(frappe.get_route()[3] || '');
|
||||
this.calendar_name = frappe.get_route()[3];
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -72,12 +72,17 @@ frappe.views.CalendarView = class CalendarView extends frappe.views.ListView {
|
|||
const calendar_name = this.calendar_name;
|
||||
|
||||
return new Promise(resolve => {
|
||||
if (calendar_name === 'Default') {
|
||||
if (calendar_name === 'default') {
|
||||
Object.assign(options, frappe.views.calendar[this.doctype]);
|
||||
resolve(options);
|
||||
} else {
|
||||
frappe.model.with_doc('Calendar View', calendar_name, () => {
|
||||
const doc = frappe.get_doc('Calendar View', calendar_name);
|
||||
if (!doc) {
|
||||
frappe.show_alert(__("{0} is not a valid Calendar. Redirecting to default Calendar.", [calendar_name.bold()]));
|
||||
frappe.set_route("List", this.doctype, "Calendar", "default");
|
||||
return;
|
||||
}
|
||||
Object.assign(options, {
|
||||
field_map: {
|
||||
id: "name",
|
||||
|
|
|
|||
|
|
@ -1,91 +1,106 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
<title>{{ title }}</title>
|
||||
<link href="{{ base_url }}/assets/frappe/css/bootstrap.css" rel="stylesheet">
|
||||
<link type="text/css" rel="stylesheet"
|
||||
href="{{ base_url }}/assets/frappe/css/font-awesome.css">
|
||||
<link rel="stylesheet" type="text/css" href="{{ base_url }}/assets/frappe/css/tree.css">
|
||||
<style>
|
||||
{{ print_css }}
|
||||
</style>
|
||||
<style>
|
||||
.tree.opened::before,
|
||||
.tree-node.opened::before,
|
||||
.tree:last-child::after,
|
||||
.tree-node:last-child::after {
|
||||
z-index: 1;
|
||||
border-left: 1px solid #d1d8dd;
|
||||
background: none;
|
||||
}
|
||||
.tree a,
|
||||
.tree-link {
|
||||
text-decoration: none;
|
||||
cursor: default;
|
||||
}
|
||||
.tree.opened > .tree-children > .tree-node > .tree-link::before,
|
||||
.tree-node.opened > .tree-children > .tree-node > .tree-link::before {
|
||||
border-top: 1px solid #d1d8dd;
|
||||
z-index: 1;
|
||||
background: none;
|
||||
}
|
||||
i.fa.fa-fw.fa-folder {
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
}
|
||||
.tree:last-child::after, .tree-node:last-child::after {
|
||||
display: none;
|
||||
}
|
||||
.tree-node-toolbar {
|
||||
display: none;
|
||||
}
|
||||
i.octicon.octicon-primitive-dot.text-extra-muted {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: #d1d8dd;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
ul.tree-children {
|
||||
padding-left: 20px;
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
<title>{{ title }}</title>
|
||||
<link href="{{ base_url }}/assets/frappe/css/bootstrap.css" rel="stylesheet">
|
||||
<link type="text/css" rel="stylesheet"
|
||||
href="{{ base_url }}/assets/frappe/css/font-awesome.css">
|
||||
<link rel="stylesheet" type="text/css" href="{{ base_url }}/assets/frappe/css/tree.css">
|
||||
<link rel="stylesheet" type="text/css" href="{{ base_url }}{{ print_format_css_path }}">
|
||||
<style>
|
||||
{{ print_css }}
|
||||
</style>
|
||||
<style>
|
||||
.tree.opened::before,
|
||||
.tree-node.opened::before,
|
||||
.tree:last-child::after,
|
||||
.tree-node:last-child::after {
|
||||
z-index: 1;
|
||||
border-left: 1px solid #d1d8dd;
|
||||
background: none;
|
||||
}
|
||||
.tree a,
|
||||
.tree-link {
|
||||
text-decoration: none;
|
||||
cursor: default;
|
||||
}
|
||||
.tree.opened > .tree-children > .tree-node > .tree-link::before,
|
||||
.tree-node.opened > .tree-children > .tree-node > .tree-link::before {
|
||||
border-top: 1px solid #d1d8dd;
|
||||
z-index: 1;
|
||||
background: none;
|
||||
}
|
||||
i.fa.fa-fw.fa-folder {
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
}
|
||||
.tree:last-child::after, .tree-node:last-child::after {
|
||||
display: none;
|
||||
}
|
||||
.tree-node-toolbar {
|
||||
display: none;
|
||||
}
|
||||
i.octicon.octicon-primitive-dot.text-extra-muted {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: #d1d8dd;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="print-format-gutter">
|
||||
{% if print_settings.repeat_header_footer %}
|
||||
<div id="footer-html" class="visible-pdf">
|
||||
{% if print_settings.letter_head && print_settings.letter_head.footer %}
|
||||
<div class="letter-head-footer">
|
||||
{{ print_settings.letter_head.footer }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<p class="text-center small page-number visible-pdf">
|
||||
{{ __("Page {0} of {1}", [`<span class="page"></span>`, `<span class="topage"></span>`]) }}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="print-format {% if landscape %} landscape {% endif %}">
|
||||
{% if print_settings.letter_head %}
|
||||
<div {% if print_settings.repeat_header_footer %} id="header-html" class="hidden-pdf" {% endif %}>
|
||||
<div class="letter-head">{{ print_settings.letter_head.header }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="tree opened">
|
||||
{{ tree }}
|
||||
@media (max-width: 767px) {
|
||||
ul.tree-children {
|
||||
padding-left: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<svg id="frappe-symbols" aria-hidden="true" style="position: absolute; width: 0; height: 0; overflow: hidden;" class="d-block" xmlns="http://www.w3.org/2000/svg">
|
||||
<symbol viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg" id="icon-primitive-dot">
|
||||
<path d="M9.5 6a3.5 3.5 0 1 1-7 0 3.5 3.5 0 0 1 7 0z"></path>
|
||||
</symbol>
|
||||
|
||||
<symbol viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" id="icon-folder-open">
|
||||
<path d="M8.024 6.5H3a.5.5 0 0 0-.5.5v8a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V9.5A.5.5 0 0 0 17 9h-6.783a.5.5 0 0 1-.417-.224L8.441 6.724a.5.5 0 0 0-.417-.224z" stroke="var(--icon-stroke)" stroke-miterlimit="10" stroke-linecap="square"></path>
|
||||
<path d="M3.88 4.5v-1a.5.5 0 0 1 .5-.5h11.24a.5.5 0 0 1 .5.5V7" stroke="var(--icon-stroke)" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
</symbol>
|
||||
|
||||
<symbol viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" id="icon-folder-normal">
|
||||
<path d="M2.5 4v10a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V6.5a1 1 0 0 0-1-1h-6.283a.5.5 0 0 1-.417-.224L8.441 3.224A.5.5 0 0 0 8.024 3H3.5a1 1 0 0 0-1 1z" stroke="var(--icon-stroke)" stroke-miterlimit="10" stroke-linecap="square"></path>
|
||||
</symbol>
|
||||
</svg>
|
||||
<div class="print-format-gutter">
|
||||
{% if print_settings.repeat_header_footer %}
|
||||
<div id="footer-html" class="visible-pdf">
|
||||
{% if print_settings.letter_head && print_settings.letter_head.footer %}
|
||||
<div class="letter-head-footer">
|
||||
{{ print_settings.letter_head.footer }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<p class="text-center small page-number visible-pdf">
|
||||
{{ __("Page {0} of {1}", [`<span class="page"></span>`, `<span class="topage"></span>`]) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
{% endif %}
|
||||
|
||||
<div class="print-format {% if landscape %} landscape {% endif %}">
|
||||
{% if print_settings.letter_head %}
|
||||
<div {% if print_settings.repeat_header_footer %} id="header-html" class="hidden-pdf" {% endif %}>
|
||||
<div class="letter-head">{{ print_settings.letter_head.header }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="tree opened">
|
||||
{{ tree }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1026,7 +1026,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
|
|||
}
|
||||
if (!docfield || docfield.report_hide) return;
|
||||
|
||||
let title = __(docfield ? docfield.label : toTitle(fieldname));
|
||||
let title = __(docfield.label);
|
||||
if (doctype !== this.doctype) {
|
||||
title += ` (${__(doctype)})`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ export default class WebForm extends frappe.ui.FieldGroup {
|
|||
this.setup_listeners();
|
||||
if (this.introduction_text) this.set_form_description(this.introduction_text);
|
||||
if (this.allow_print && !this.is_new) this.setup_print_button();
|
||||
if (this.allow_delete && !this.is_new) this.setup_delete_button();
|
||||
if (this.is_new) this.setup_cancel_button();
|
||||
this.setup_primary_action();
|
||||
this.setup_previous_next_button();
|
||||
|
|
@ -79,9 +78,9 @@ export default class WebForm extends frappe.ui.FieldGroup {
|
|||
}
|
||||
|
||||
$('.web-form-footer').after(`
|
||||
<div id="form-step-footer" class="pull-right">
|
||||
<button class="btn btn-primary btn-previous btn-sm ml-2">${__("Previous")}</button>
|
||||
<button class="btn btn-primary btn-next btn-sm ml-2">${__("Next")}</button>
|
||||
<div id="form-step-footer" class="text-right">
|
||||
<button class="btn btn-default btn-previous btn-sm ml-2">${__("Previous")}</button>
|
||||
<button class="btn btn-default btn-next btn-sm ml-2">${__("Next")}</button>
|
||||
</div>
|
||||
`);
|
||||
|
||||
|
|
@ -141,6 +140,7 @@ export default class WebForm extends frappe.ui.FieldGroup {
|
|||
set_form_description(intro) {
|
||||
let intro_wrapper = document.getElementById('introduction');
|
||||
intro_wrapper.innerHTML = intro;
|
||||
intro_wrapper.classList.remove('hidden');
|
||||
}
|
||||
|
||||
add_button(name, type, action, wrapper_class=".web-form-actions") {
|
||||
|
|
@ -164,25 +164,18 @@ export default class WebForm extends frappe.ui.FieldGroup {
|
|||
this.save()
|
||||
);
|
||||
|
||||
this.add_button_to_footer(this.button_label || __("Save", null, "Button in web form"), "primary", () =>
|
||||
this.save()
|
||||
);
|
||||
if (!this.is_multi_step_form && $('.frappe-card').height() > 600) {
|
||||
// add button on footer if page is long
|
||||
this.add_button_to_footer(this.button_label || __("Save", null, "Button in web form"), "primary", () =>
|
||||
this.save()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
setup_cancel_button() {
|
||||
this.add_button_to_header(__("Cancel", null, "Button in web form"), "light", () => this.cancel());
|
||||
}
|
||||
|
||||
setup_delete_button() {
|
||||
frappe.has_permission(this.doc_type, "", "delete", () => {
|
||||
this.add_button_to_header(
|
||||
frappe.utils.icon('delete'),
|
||||
"danger",
|
||||
() => this.delete()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
setup_print_button() {
|
||||
this.add_button_to_header(
|
||||
frappe.utils.icon('print'),
|
||||
|
|
@ -359,17 +352,6 @@ export default class WebForm extends frappe.ui.FieldGroup {
|
|||
return true;
|
||||
}
|
||||
|
||||
delete() {
|
||||
frappe.call({
|
||||
type: "POST",
|
||||
method: "frappe.website.doctype.web_form.web_form.delete",
|
||||
args: {
|
||||
web_form_name: this.name,
|
||||
docname: this.doc.name
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
print() {
|
||||
window.open(`/printview?
|
||||
doctype=${this.doc_type}
|
||||
|
|
@ -386,21 +368,19 @@ export default class WebForm extends frappe.ui.FieldGroup {
|
|||
window.location.href = data;
|
||||
}
|
||||
|
||||
const success_dialog = new frappe.ui.Dialog({
|
||||
title: __("Saved Successfully"),
|
||||
secondary_action: () => {
|
||||
if (this.success_url) {
|
||||
window.location.href = this.success_url;
|
||||
} else if(this.login_required) {
|
||||
window.location.href =
|
||||
window.location.pathname + "?name=" + data.name;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
success_dialog.show();
|
||||
const success_message =
|
||||
this.success_message || __("Your information has been submitted");
|
||||
success_dialog.set_message(success_message);
|
||||
this.success_message || __("Submitted");
|
||||
|
||||
frappe.toast({message: success_message, indicator:'green'});
|
||||
|
||||
// redirect
|
||||
setTimeout(() => {
|
||||
if (this.success_url) {
|
||||
window.location.href = this.success_url;
|
||||
} else if(this.login_required) {
|
||||
window.location.href =
|
||||
window.location.pathname + "?name=" + data.name;
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export default class WebFormList {
|
|||
constructor(opts) {
|
||||
Object.assign(this, opts);
|
||||
frappe.web_form_list = this;
|
||||
this.wrapper = document.getElementById("datatable");
|
||||
this.wrapper = document.getElementById("list-table");
|
||||
this.make_actions();
|
||||
this.make_filters();
|
||||
$('.link-btn').remove();
|
||||
|
|
@ -16,7 +16,8 @@ export default class WebFormList {
|
|||
if (this.table) {
|
||||
Array.from(this.table.tBodies).forEach(tbody => tbody.remove());
|
||||
let check = document.getElementById('select-all');
|
||||
check.checked = false;
|
||||
if (check)
|
||||
check.checked = false;
|
||||
}
|
||||
this.rows = [];
|
||||
this.page_length = 20;
|
||||
|
|
@ -131,9 +132,39 @@ export default class WebFormList {
|
|||
this.make_table_head();
|
||||
}
|
||||
|
||||
this.append_rows(this.data);
|
||||
if (this.data.length) {
|
||||
this.append_rows(this.data);
|
||||
this.wrapper.appendChild(this.table);
|
||||
} else {
|
||||
let new_button = "";
|
||||
let empty_state = document.createElement("div");
|
||||
empty_state.classList.add("no-result", "text-muted", "flex", "justify-center", "align-center");
|
||||
|
||||
this.wrapper.appendChild(this.table);
|
||||
frappe.has_permission(this.doctype, "", "create", () => {
|
||||
new_button = `
|
||||
<a
|
||||
class="btn btn-primary btn-sm btn-new-doc hidden-xs"
|
||||
href="${window.location.pathname}?new=1">
|
||||
${__("Create a new {0}", [__(this.doctype)])}
|
||||
</a>
|
||||
`;
|
||||
|
||||
empty_state.innerHTML = `
|
||||
<div class="text-center">
|
||||
<div>
|
||||
<img
|
||||
src="/assets/frappe/images/ui-states/list-empty-state.svg"
|
||||
alt="Generic Empty State"
|
||||
class="null-state">
|
||||
</div>
|
||||
<p class="small mb-2">${__("No {0} found", [__(this.doctype)])}</p>
|
||||
${new_button}
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.wrapper.appendChild(empty_state);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
make_table_head() {
|
||||
|
|
@ -212,8 +243,7 @@ export default class WebFormList {
|
|||
"btn",
|
||||
"btn-secondary",
|
||||
"btn-sm",
|
||||
"ml-2",
|
||||
"text-white"
|
||||
"ml-2"
|
||||
);
|
||||
}
|
||||
else if (type == "danger") {
|
||||
|
|
@ -290,6 +320,7 @@ frappe.ui.WebFormListRow = class WebFormListRow {
|
|||
make_row() {
|
||||
// Add Checkboxes
|
||||
let cell = this.row.insertCell();
|
||||
cell.classList.add('list-col-checkbox');
|
||||
|
||||
this.checkbox = document.createElement("input");
|
||||
this.checkbox.type = "checkbox";
|
||||
|
|
@ -302,6 +333,7 @@ frappe.ui.WebFormListRow = class WebFormListRow {
|
|||
|
||||
// Add Serial Number
|
||||
let serialNo = this.row.insertCell();
|
||||
serialNo.classList.add('list-col-serial');
|
||||
serialNo.innerText = this.serial_number;
|
||||
|
||||
this.columns.forEach(field => {
|
||||
|
|
|
|||
|
|
@ -52,11 +52,11 @@ frappe.ready(function() {
|
|||
const data = setup_fields(r.message);
|
||||
let web_form_doc = data.web_form;
|
||||
|
||||
if (web_form_doc.name && web_form_doc.allow_edit === 0) {
|
||||
if (!window.location.href.includes("?new=1")) {
|
||||
window.location.replace(window.location.pathname + "?new=1");
|
||||
}
|
||||
}
|
||||
// if (web_form_doc.name && web_form_doc.allow_edit === 0) {
|
||||
// if (!window.location.href.includes("?new=1")) {
|
||||
// window.location.replace(window.location.pathname + "?new=1");
|
||||
// }
|
||||
// }
|
||||
let doc = r.message.doc || build_doc(r.message);
|
||||
web_form.prepare(web_form_doc, r.message.doc && web_form_doc.allow_edit === 1 ? r.message.doc : {});
|
||||
web_form.make();
|
||||
|
|
|
|||
|
|
@ -460,3 +460,10 @@ button.data-pill {
|
|||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.capture-remove-btn {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -111,8 +111,8 @@
|
|||
}
|
||||
|
||||
.avatar-large {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
|
||||
.standard-image {
|
||||
font-size: var(--text-2xl);
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@
|
|||
top: var(--navbar-height);
|
||||
background: var(--bg-color);
|
||||
margin-bottom: 5px;
|
||||
transition: 0.5s top;
|
||||
.page-head-content {
|
||||
height: var(--page-head-height);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -355,7 +355,7 @@ body[data-route^="Module"] .main-menu {
|
|||
display: none;
|
||||
}
|
||||
|
||||
input {
|
||||
input:not([data-fieldtype='Check']) {
|
||||
background: var(--control-bg-on-gray);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,13 @@
|
|||
$font-size-xs: 0.7rem;
|
||||
$font-size-sm: 0.85rem;
|
||||
$font-size-lg: 1.12rem;
|
||||
$font-size-xl: 1.25rem;
|
||||
$font-size-2xl: 1.5rem;
|
||||
$font-size-3xl: 2rem;
|
||||
$font-size-4xl: 2.5rem;
|
||||
$font-size-5xl: 3rem;
|
||||
$font-size-6xl: 4rem;
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
|
@ -14,45 +24,80 @@ img {
|
|||
height: auto;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4 {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: $font-size-3xl;
|
||||
font-weight: 800;
|
||||
line-height: 1.25;
|
||||
letter-spacing: -0.025em;
|
||||
margin-bottom: 1rem;
|
||||
margin-top: 3rem;
|
||||
margin-bottom: 0.75rem;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
font-size: $font-size-5xl;
|
||||
line-height: 2.5rem;
|
||||
font-size: $font-size-4xl;
|
||||
margin-top: 3.5rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
@include media-breakpoint-up(xl) {
|
||||
font-size: $font-size-6xl;
|
||||
line-height: 1;
|
||||
font-size: $font-size-5xl;
|
||||
margin-top: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: $font-size-xl;
|
||||
font-weight: 700;
|
||||
font-size: $font-size-2xl;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 0.75rem;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
font-size: $font-size-2xl;
|
||||
}
|
||||
@include media-breakpoint-up(md) {
|
||||
font-size: $font-size-3xl;
|
||||
margin-top: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
@include media-breakpoint-up(xl) {
|
||||
font-size: $font-size-4xl;
|
||||
margin-top: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: $font-size-base;
|
||||
font-weight: 600;
|
||||
font-size: $font-size-xl;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
font-size: $font-size-lg;
|
||||
font-size: $font-size-2xl;
|
||||
margin-top: 2.5rem;
|
||||
}
|
||||
@include media-breakpoint-up(md) {
|
||||
font-size: $font-size-xl;
|
||||
@include media-breakpoint-up(xl) {
|
||||
font-size: $font-size-3xl;
|
||||
margin-top: 3.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: $font-size-lg;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
font-size: $font-size-xl;
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
@include media-breakpoint-up(xl) {
|
||||
font-size: $font-size-2xl;
|
||||
margin-top: 1.75rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $body-color;
|
||||
}
|
||||
}
|
||||
|
||||
.btn.btn-lg {
|
||||
font-size: $font-size-lg;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,12 +57,12 @@
|
|||
|
||||
.blog-card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: top;
|
||||
margin-top: 0.5rem;
|
||||
|
||||
.avatar {
|
||||
margin-top: 0.4rem;
|
||||
margin-right: 0.5rem;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -119,106 +119,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-comment-button {
|
||||
margin-left: 35px;
|
||||
}
|
||||
|
||||
.timeline-dot {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 22px;
|
||||
background-color: var(--fg-color);
|
||||
border: 1px solid var(--dark-border-color);
|
||||
|
||||
&:before {
|
||||
content: ' ';
|
||||
background: var(--gray-600);
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
border-radius: 50%;
|
||||
height: 4px;
|
||||
width: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.blog-comments {
|
||||
.comment-form-wrapper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.add-comment-section {
|
||||
.login-required {
|
||||
padding: var(--padding-sm);
|
||||
border-radius: var(--border-radius-sm);
|
||||
box-shadow: var(--card-shadow);
|
||||
}
|
||||
|
||||
.new-comment {
|
||||
display: flex;
|
||||
padding: var(--padding-lg);
|
||||
box-shadow: var(--card-shadow);
|
||||
border-radius: var(--border-radius-md);
|
||||
|
||||
.new-comment-fields {
|
||||
flex: 1;
|
||||
|
||||
.form-label {
|
||||
font-weight: var(--text-bold);
|
||||
}
|
||||
|
||||
.comment-text-area textarea {
|
||||
resize: none;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.comment-by {
|
||||
padding-right: 0px !important;
|
||||
padding-bottom: 0px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#comment-list {
|
||||
position: relative;
|
||||
padding-left: var(--padding-xl);
|
||||
|
||||
&:before {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
top: var(--comment-timeline-top);
|
||||
bottom: var(--comment-timeline-bottom);
|
||||
border-left: 1px solid var(--dark-border-color);
|
||||
}
|
||||
|
||||
.comment-row {
|
||||
position: relative;
|
||||
|
||||
.comment-avatar {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: -17px;
|
||||
}
|
||||
|
||||
.comment-content {
|
||||
box-shadow: var(--card-shadow);
|
||||
border-radius: var(--border-radius-md);
|
||||
padding: var(--padding-md);
|
||||
margin-left: 35px;
|
||||
flex: 1;
|
||||
|
||||
.content p{
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
.web-footer {
|
||||
padding: 5rem 0;
|
||||
margin: 5rem 0;
|
||||
min-height: 140px;
|
||||
background-color: var(--fg-color);
|
||||
border-top: 1px solid $border-color;
|
||||
}
|
||||
|
||||
.footer-logo {
|
||||
|
|
@ -76,8 +78,6 @@
|
|||
}
|
||||
|
||||
.footer-info {
|
||||
margin-top: 1rem;
|
||||
border-top: 1px solid $border-color;
|
||||
color: $text-muted;
|
||||
font-size: $font-size-sm;
|
||||
}
|
||||
|
|
@ -98,4 +98,4 @@
|
|||
font-size: $font-size-sm;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
@import "../common/global";
|
||||
@import "../common/icons";
|
||||
@import "../common/alert";
|
||||
@import 'base';
|
||||
@import "../common/flex";
|
||||
@import "../common/buttons";
|
||||
@import "../common/modal";
|
||||
|
|
@ -14,6 +13,7 @@
|
|||
@import "../common/indicator";
|
||||
@import "../common/controls";
|
||||
@import "../common/awesomeplete";
|
||||
@import 'base';
|
||||
@import 'multilevel_dropdown';
|
||||
@import 'website_image';
|
||||
@import 'website_avatar';
|
||||
|
|
@ -311,3 +311,16 @@ h5.modal-title {
|
|||
.empty-list-icon {
|
||||
height: 70px;
|
||||
}
|
||||
|
||||
.null-state {
|
||||
height: 60px;
|
||||
width: auto;
|
||||
margin-bottom: var(--margin-md);
|
||||
img {
|
||||
fill: var(--fg-color);
|
||||
}
|
||||
}
|
||||
|
||||
.no-result {
|
||||
min-height: #{"calc(100vh - 284px)"};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +1,12 @@
|
|||
$font-sizes-desktop: (
|
||||
"sm": 0.75rem,
|
||||
"base": 1rem,
|
||||
"lg": 1.125rem,
|
||||
"xl": 1.41rem,
|
||||
"2xl": 1.6rem,
|
||||
"3xl": 2rem
|
||||
);
|
||||
|
||||
$font-sizes-mobile: (
|
||||
"sm": 0.75rem,
|
||||
"base": 1rem,
|
||||
"lg": 1.125rem,
|
||||
"xl": 1.25rem,
|
||||
"2xl": 1.5rem,
|
||||
"3xl": 1.75rem
|
||||
);
|
||||
.section-markdown > .from-markdown {
|
||||
max-width: 50rem;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.from-markdown {
|
||||
color: $gray-700;
|
||||
line-height: 1.7;
|
||||
letter-spacing: -0.011em;
|
||||
|
||||
> * + * {
|
||||
margin-top: 0.75rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
> :first-child {
|
||||
margin-top: 0;
|
||||
|
|
@ -47,6 +29,10 @@ $font-sizes-mobile: (
|
|||
list-style: decimal;
|
||||
}
|
||||
|
||||
p, li {
|
||||
font-size: $font-size-lg;
|
||||
}
|
||||
|
||||
li {
|
||||
padding-top: 1px;
|
||||
padding-bottom: 1px;
|
||||
|
|
@ -87,86 +73,6 @@ $font-sizes-mobile: (
|
|||
font-weight: 600;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: $gray-900;
|
||||
}
|
||||
|
||||
h2, h3, h4, h5, h6 {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: map-get($font-sizes-mobile, '3xl');
|
||||
line-height: 1.5;
|
||||
letter-spacing: -0.021em;
|
||||
font-weight: 700;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
font-size: map-get($font-sizes-desktop, '3xl');
|
||||
letter-spacing: -0.024em;
|
||||
}
|
||||
|
||||
// for byline
|
||||
& + p {
|
||||
margin-top: 1.5rem;
|
||||
font-size: map-get($font-sizes-mobile, 'xl');
|
||||
letter-spacing: -0.014em;
|
||||
line-height: 1.4;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
font-size: map-get($font-sizes-desktop, 'xl');
|
||||
letter-spacing: -0.0175em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: map-get($font-sizes-mobile, '2xl');
|
||||
line-height: 1.56;
|
||||
letter-spacing: -0.015em;
|
||||
margin-top: 4rem;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
font-size: map-get($font-sizes-desktop, '2xl');
|
||||
letter-spacing: -0.0195em;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: map-get($font-sizes-mobile, 'xl');
|
||||
line-height: 1.56;
|
||||
letter-spacing: -0.014em;
|
||||
margin-top: 2.25rem;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
font-size: map-get($font-sizes-desktop, 'xl');
|
||||
letter-spacing: -0.0175em;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: map-get($font-sizes-mobile, 'lg');
|
||||
line-height: 1.56;
|
||||
letter-spacing: -0.014em;
|
||||
margin-top: 2.5rem;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: map-get($font-sizes-mobile, 'base');
|
||||
line-height: 1.5;
|
||||
letter-spacing: -0.011em;
|
||||
font-weight: 600;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: map-get($font-sizes-mobile, 'sm');
|
||||
line-height: 1.35;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
tr > td,
|
||||
tr > th {
|
||||
font-size: $font-size-sm;
|
||||
|
|
|
|||
|
|
@ -27,15 +27,16 @@
|
|||
}
|
||||
}
|
||||
|
||||
.my-account-container {
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.account-info {
|
||||
background-color: var(--fg-color);
|
||||
box-shadow: var(--card-shadow);
|
||||
border-radius: var(--border-radius-md);
|
||||
padding: var(--padding-sm) 25px;
|
||||
max-width: 850px;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
padding: 0;
|
||||
|
|
@ -97,21 +98,3 @@
|
|||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
//styles for third party apps page
|
||||
//center wrt to outer most container and not immediate parent
|
||||
.empty-apps-state {
|
||||
position: relative;
|
||||
padding-top: 10rem;
|
||||
margin-left: -250px;
|
||||
text-align: center;
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
margin: auto;
|
||||
padding-top: 5rem;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,7 @@
|
|||
.hero-content {
|
||||
margin-top: 3rem;
|
||||
margin-bottom: 3rem;
|
||||
|
||||
.btn-primary {
|
||||
margin-top: 1rem;
|
||||
margin-right: 0.5rem;
|
||||
|
|
@ -15,16 +18,23 @@
|
|||
|
||||
.hero-title, .hero-subtitle {
|
||||
max-width: 42rem;
|
||||
margin-top: 0rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.lead {
|
||||
font-weight: normal;
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
@extend .lead;
|
||||
font-weight: 400;
|
||||
color: $gray-600;
|
||||
font-size: 1rem;
|
||||
font-size: $font-size-lg;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
font-size: 1.25rem;
|
||||
font-size: $font-size-xl;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -42,10 +52,10 @@
|
|||
.section-description {
|
||||
max-width: 56rem;
|
||||
margin-top: 0.5rem;
|
||||
font-size: $font-size-base;
|
||||
font-size: $font-size-lg;
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
font-size: $font-size-lg;
|
||||
@include media-breakpoint-up(media-breakpoint-up) {
|
||||
font-size: $font-size-xl;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -226,14 +236,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.section-markdown > .from-markdown {
|
||||
max-width: 42rem;
|
||||
}
|
||||
|
||||
.section-cta {
|
||||
padding: 3rem 2rem;
|
||||
text-align: center;
|
||||
background-color: $primary-light;
|
||||
background-color: $gray-200;
|
||||
border-radius: 0.75rem;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
|
|
@ -248,12 +254,7 @@
|
|||
.title {
|
||||
margin: 0 auto;
|
||||
max-width: 36rem;
|
||||
font-size: $font-size-2xl;
|
||||
font-weight: 800;
|
||||
line-height: 1.25;
|
||||
@include media-breakpoint-up(md) {
|
||||
font-size: $font-size-4xl;
|
||||
}
|
||||
}
|
||||
.subtitle {
|
||||
max-width: 36rem;
|
||||
|
|
@ -270,11 +271,15 @@
|
|||
margin-top: 0.5rem;
|
||||
font-size: $font-size-xs;
|
||||
}
|
||||
.action {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.section-small-cta {
|
||||
padding: 1.8rem;
|
||||
background-color: lighten($primary, 42%);
|
||||
background-color: var(--gray-200);
|
||||
border-radius: 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -294,26 +299,27 @@
|
|||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
max-width: 36rem;
|
||||
font-size: $font-size-xl;
|
||||
font-weight: 800;
|
||||
line-height: 1.25;
|
||||
@include media-breakpoint-up(md) {
|
||||
font-size: $font-size-2xl;
|
||||
}
|
||||
.section-title {
|
||||
line-height: 1;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
max-width: 36rem;
|
||||
font-size: $font-size-base;
|
||||
color: $gray-900;
|
||||
margin-bottom: 1.2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
font-size: $font-size-lg;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.action {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.section-cta-container {
|
||||
|
|
@ -379,6 +385,20 @@
|
|||
}
|
||||
}
|
||||
|
||||
.testimonial-author {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.avatar {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.split-section-content.align-top {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
|
@ -514,12 +534,12 @@
|
|||
|
||||
@include media-breakpoint-up(md) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 6rem;
|
||||
gap: 3rem 5rem;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
font-size: $font-size-xl;
|
||||
font-weight: bold;
|
||||
font-size: $font-size-lg;
|
||||
font-weight: 600;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
font-size: $font-size-2xl;
|
||||
|
|
@ -528,7 +548,7 @@
|
|||
|
||||
.feature-content {
|
||||
font-size: $font-size-base;
|
||||
margin-top: 1.75rem;
|
||||
margin-top: 1.25rem;
|
||||
|
||||
@include media-breakpoint-up(xl) {
|
||||
font-size: $font-size-lg;
|
||||
|
|
@ -630,9 +650,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.section-title + .section-features, .section-description + .section-features {
|
||||
&[data-columns="2"] {
|
||||
margin-top: 3.75rem;
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
&[data-columns="3"] {
|
||||
|
|
@ -651,6 +676,14 @@
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.feature-content {
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.feature-title, .feature-content {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
|
@ -666,3 +699,19 @@
|
|||
.section-with-embed .embed-container {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.section-video-wrapper {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.section-video {
|
||||
aspect-ratio: 16 / 9;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.video-thumbnail {
|
||||
aspect-ratio: 16 / 9;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ $font-size-lg: 1.125rem !default;
|
|||
$font-size-xl: 1.25rem !default;
|
||||
$font-size-2xl: 1.5rem !default;
|
||||
$font-size-3xl: 1.875rem !default;
|
||||
$font-size-4xl: 2.25rem !default;
|
||||
$font-size-4xl: 2.5rem !default;
|
||||
$font-size-5xl: 3rem !default;
|
||||
$font-size-6xl: 4rem !default;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,28 +1,47 @@
|
|||
@import "../common/form";
|
||||
|
||||
|
||||
[data-doctype="Web Form"] {
|
||||
.page-content-wrapper {
|
||||
.page_content {
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
|
||||
.frappe-card {
|
||||
padding: 1rem;
|
||||
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.web-form-head {
|
||||
margin: 0 -1rem;
|
||||
padding: 0 1rem 1rem 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
#introduction {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
#introduction p {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.web-form-actions button {
|
||||
margin-top: 0.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.frappe-card.list-card {
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.breadcrumb-container.container {
|
||||
@include media-breakpoint-up(sm) {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
|
||||
&.my-4 {
|
||||
background-color: var(--fg-color);
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
padding: 1.8rem;
|
||||
border-radius: var(--border-radius-md);
|
||||
box-shadow: var(--card-shadow);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -57,13 +76,21 @@
|
|||
}
|
||||
}
|
||||
|
||||
.web-form-wrapper~#datatable {
|
||||
.list-table {
|
||||
margin-left: -1rem;
|
||||
margin-right: -1rem;
|
||||
|
||||
.table {
|
||||
thead {
|
||||
th {
|
||||
border: 0;
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
color: var(--text-muted)
|
||||
color: var(--text-muted);
|
||||
|
||||
input[type="checkbox"] {
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -71,8 +98,22 @@
|
|||
color: var(--text-color);
|
||||
|
||||
td {
|
||||
border-top: 1px solid var(--border-color);
|
||||
font-size: 13px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
margin-left: 0.5rem;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.list-col-checkbox {
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
.list-col-serial {
|
||||
width: 1.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from pypika.functions import *
|
||||
from pypika.terms import Function
|
||||
from pypika.terms import Function, CustomFunction, ArithmeticExpression, Arithmetic
|
||||
from frappe.query_builder.utils import ImportMapper, db_type_is
|
||||
from frappe.query_builder.custom import GROUP_CONCAT, STRING_AGG, MATCH, TO_TSVECTOR
|
||||
from frappe.database.query import Query
|
||||
|
|
@ -25,6 +25,24 @@ Match = ImportMapper(
|
|||
}
|
||||
)
|
||||
|
||||
class _PostgresTimestamp(ArithmeticExpression):
|
||||
def __init__(self, datepart, timepart, alias=None):
|
||||
if isinstance(datepart, str):
|
||||
datepart = Cast(datepart, "date")
|
||||
if isinstance(timepart, str):
|
||||
timepart = Cast(timepart, "time")
|
||||
|
||||
super().__init__(operator=Arithmetic.add,
|
||||
left=datepart, right=timepart, alias=alias)
|
||||
|
||||
|
||||
CombineDatetime = ImportMapper(
|
||||
{
|
||||
db_type_is.MARIADB: CustomFunction("TIMESTAMP", ["date", "time"]),
|
||||
db_type_is.POSTGRES: _PostgresTimestamp,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _aggregate(function, dt, fieldname, filters, **kwargs):
|
||||
return (
|
||||
|
|
@ -46,4 +64,4 @@ def _avg(dt, fieldname, filters=None, **kwargs):
|
|||
return _aggregate(Avg, dt, fieldname, filters, **kwargs)
|
||||
|
||||
def _sum(dt, fieldname, filters=None, **kwargs):
|
||||
return _aggregate(Sum, dt, fieldname, filters, **kwargs)
|
||||
return _aggregate(Sum, dt, fieldname, filters, **kwargs)
|
||||
|
|
|
|||
|
|
@ -54,6 +54,9 @@ def patch_query_execute():
|
|||
This excludes the use of `frappe.db.sql` method while
|
||||
executing the query object
|
||||
"""
|
||||
from frappe.utils.safe_exec import check_safe_sql_query
|
||||
|
||||
|
||||
def execute_query(query, *args, **kwargs):
|
||||
query, params = prepare_query(query)
|
||||
return frappe.db.sql(query, params, *args, **kwargs) # nosemgrep
|
||||
|
|
@ -63,7 +66,7 @@ def patch_query_execute():
|
|||
|
||||
param_collector = NamedParameterWrapper()
|
||||
query = query.get_sql(param_wrapper=param_collector)
|
||||
if frappe.flags.in_safe_exec and not query.lower().strip().startswith("select"):
|
||||
if frappe.flags.in_safe_exec and not check_safe_sql_query(query, throw=False):
|
||||
callstack = inspect.stack()
|
||||
if len(callstack) >= 3 and ".py" in callstack[2].filename:
|
||||
# ignore any query builder methods called from python files
|
||||
|
|
@ -77,7 +80,7 @@ def patch_query_execute():
|
|||
#
|
||||
# if frame2 is server script it wont have a filename and hence
|
||||
# it shouldn't be allowed.
|
||||
# ps. stack() returns `"<unknown>"` as filename.
|
||||
# p.s. stack() returns `"<unknown>"` as filename if not a file.
|
||||
pass
|
||||
else:
|
||||
raise frappe.PermissionError('Only SELECT SQL allowed in scripting')
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
{% macro avatar(user_id=None, css_style=None, size="avatar-small") %}
|
||||
{% macro avatar(user_id=None, css_style=None, size="avatar-small", full_name=None, image=None) %}
|
||||
{% set user_info = frappe.utils.get_user_info_for_avatar(user_id) %}
|
||||
<span class="avatar {{ size }}" title="{{ user_info.name }}" style="{{ css_style or '' }}">
|
||||
{% if user_info.image %}
|
||||
<span class="avatar {{ size }}" title="{{ full_name or user_info.name }}" style="{{ css_style or '' }}">
|
||||
{% if image or user_info.image %}
|
||||
<img
|
||||
class="avatar-frame standard-image"
|
||||
src="{{ user_info.image }}"
|
||||
title="{{ user_info.name }}">
|
||||
src="{{ image or user_info.image }}"
|
||||
title="{{ full_name or user_info.name }}">
|
||||
</span>
|
||||
{% else %}
|
||||
<span
|
||||
class="avatar-frame standard-image"
|
||||
title="{{ user_info.name }}">
|
||||
{{ frappe.utils.get_abbr(user_info.name).upper() }}
|
||||
title="{{ full_name or user_info.name }}">
|
||||
{{ frappe.utils.get_abbr(full_name or user_info.name).upper() }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endmacro %}
|
||||
{% endmacro %}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue