浏览代码

Merge branch 'develop' into log-settings

version-14
gavin 3 年前
committed by GitHub
父节点
当前提交
a7f1639e65
找不到此签名对应的密钥 GPG 密钥 ID: 4AEE18F83AFDEB23
共有 53 个文件被更改,包括 838 次插入188 次删除
  1. +59
    -0
      cypress/fixtures/child_table_doctype_1.js
  2. +6
    -0
      cypress/fixtures/doctype_with_child_table.js
  3. +2
    -0
      cypress/integration/dashboard_links.js
  4. +107
    -0
      cypress/integration/grid_search.js
  5. +3
    -0
      cypress/integration/list_paging.js
  6. +12
    -10
      esbuild/esbuild.js
  7. +7
    -6
      frappe/__init__.py
  8. +4
    -1
      frappe/commands/site.py
  9. +2
    -2
      frappe/core/doctype/communication/test_communication.py
  10. +3
    -3
      frappe/core/doctype/file/test_file.py
  11. +2
    -2
      frappe/core/doctype/user/user.py
  12. +2
    -1
      frappe/core/doctype/user_permission/user_permission.js
  13. +9
    -1
      frappe/custom/doctype/custom_field/custom_field.json
  14. +2
    -3
      frappe/custom/doctype/custom_field/custom_field.py
  15. +6
    -6
      frappe/custom/doctype/customize_form/customize_form.js
  16. +1
    -1
      frappe/custom/doctype/customize_form/customize_form.py
  17. +9
    -1
      frappe/custom/doctype/property_setter/property_setter.json
  18. +2
    -1
      frappe/database/__init__.py
  19. +54
    -34
      frappe/database/database.py
  20. +8
    -0
      frappe/database/postgres/setup_db.py
  21. +2
    -2
      frappe/email/doctype/auto_email_report/auto_email_report.py
  22. +1
    -1
      frappe/email/doctype/notification/test_notification.py
  23. +7
    -10
      frappe/handler.py
  24. +1
    -1
      frappe/model/base_document.py
  25. +2
    -1
      frappe/patches.txt
  26. +17
    -0
      frappe/patches/v14_0/update_is_system_generated_flag.py
  27. +1
    -1
      frappe/permissions.py
  28. +4
    -4
      frappe/public/js/frappe/form/controls/attach.js
  29. +4
    -1
      frappe/public/js/frappe/form/footer/form_timeline.js
  30. +9
    -5
      frappe/public/js/frappe/form/form.js
  31. +118
    -21
      frappe/public/js/frappe/form/grid.js
  32. +123
    -7
      frappe/public/js/frappe/form/grid_row.js
  33. +3
    -3
      frappe/public/js/frappe/form/toolbar.js
  34. +1
    -1
      frappe/public/js/frappe/list/list_settings.js
  35. +5
    -5
      frappe/public/js/frappe/model/model.js
  36. +9
    -5
      frappe/public/js/frappe/ui/page.js
  37. +20
    -2
      frappe/public/js/frappe/utils/utils.js
  38. +7
    -2
      frappe/public/js/frappe/views/calendar/calendar.js
  39. +1
    -1
      frappe/public/js/frappe/views/reports/report_view.js
  40. +36
    -6
      frappe/public/js/frappe/web_form/web_form_list.js
  41. +24
    -1
      frappe/public/scss/common/grid.scss
  42. +1
    -0
      frappe/public/scss/desk/page.scss
  43. +13
    -0
      frappe/public/scss/website/index.scss
  44. +3
    -3
      frappe/tests/test_base_document.py
  45. +38
    -3
      frappe/tests/test_db.py
  46. +3
    -3
      frappe/tests/test_document.py
  47. +2
    -2
      frappe/tests/test_search.py
  48. +50
    -6
      frappe/tests/ui_test_helpers.py
  49. +0
    -2
      frappe/translate.py
  50. +24
    -13
      frappe/utils/__init__.py
  51. +3
    -3
      frappe/utils/backups.py
  52. +6
    -0
      frappe/utils/install.py
  53. +0
    -1
      frappe/utils/nestedset.py

+ 59
- 0
cypress/fixtures/child_table_doctype_1.js 查看文件

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

+ 6
- 0
cypress/fixtures/doctype_with_child_table.js 查看文件

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


+ 2
- 0
cypress/integration/dashboard_links.js 查看文件

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


+ 107
- 0
cypress/integration/grid_search.js 查看文件

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 3
- 0
cypress/integration/list_paging.js 查看文件

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

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

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

+ 12
- 10
esbuild/esbuild.js 查看文件

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


+ 7
- 6
frappe/__init__.py 查看文件

@@ -978,8 +978,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."""
@@ -1252,9 +1251,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:
@@ -1266,7 +1266,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
@@ -1297,6 +1297,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


+ 4
- 1
frappe/commands/site.py 查看文件

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


+ 2
- 2
frappe/core/doctype/communication/test_communication.py 查看文件

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


+ 3
- 3
frappe/core/doctype/file/test_file.py 查看文件

@@ -382,7 +382,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({
@@ -399,7 +399,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)
@@ -407,7 +407,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')


+ 2
- 2
frappe/core/doctype/user/user.py 查看文件

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


+ 2
- 1
frappe/core/doctype/user_permission/user_permission.js 查看文件

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



+ 9
- 1
frappe/custom/doctype/custom_field/custom_field.json 查看文件

@@ -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",


+ 2
- 3
frappe/custom/doctype/custom_field/custom_field.py 查看文件

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


+ 6
- 6
frappe/custom/doctype/customize_form/customize_form.js 查看文件

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

+ 1
- 1
frappe/custom/doctype/customize_form/customize_form.py 查看文件

@@ -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!


+ 9
- 1
frappe/custom/doctype/property_setter/property_setter.json 查看文件

@@ -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",


+ 2
- 1
frappe/database/__init__.py 查看文件

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


+ 54
- 34
frappe/database/database.py 查看文件

@@ -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)
@@ -384,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
@@ -393,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.
@@ -423,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:
@@ -444,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)):
@@ -623,6 +628,7 @@ class Database(object):
run=True,
pluck=False,
distinct=False,
limit=None,
):
field_objects = []

@@ -641,6 +647,7 @@ class Database(object):
field_objects=field_objects,
fields=fields,
distinct=distinct,
limit=limit,
)
if (
fields == "*"
@@ -654,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(
@@ -667,6 +674,7 @@ class Database(object):
as_list=1,
run=run,
distinct=distinct,
limit_page_length=limit
)
else:
return {}
@@ -882,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."""


+ 8
- 0
frappe/database/postgres/setup_db.py 查看文件

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

+ 2
- 2
frappe/email/doctype/auto_email_report/auto_email_report.py 查看文件

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



+ 1
- 1
frappe/email/doctype/notification/test_notification.py 查看文件

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


+ 7
- 10
frappe/handler.py 查看文件

@@ -225,11 +225,10 @@ def ping():

def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None):
"""run a whitelisted controller method"""
import json
import inspect
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:
@@ -237,9 +236,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()
@@ -248,16 +245,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)


+ 1
- 1
frappe/model/base_document.py 查看文件

@@ -963,7 +963,7 @@ class BaseDocument(object):
from frappe.model.meta import get_default_df
df = get_default_df(fieldname)

if not currency and df:
if df.fieldtype == "Currency" and not currency:
currency = self.get(df.get("options"))
if not frappe.db.exists('Currency', currency, cache=True):
currency = None


+ 2
- 1
frappe/patches.txt 查看文件

@@ -146,7 +146,7 @@ frappe.patches.v13_0.update_duration_options
frappe.patches.v13_0.replace_old_data_import # 2020-06-24
frappe.patches.v13_0.create_custom_dashboards_cards_and_charts
frappe.patches.v13_0.rename_is_custom_field_in_dashboard_chart
frappe.patches.v13_0.add_standard_navbar_items # 2020-12-15
frappe.patches.v13_0.add_standard_navbar_items # 2022-03-15
frappe.patches.v13_0.generate_theme_files_in_public_folder
frappe.patches.v13_0.increase_password_length
frappe.patches.v12_0.fix_email_id_formatting
@@ -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
- 0
frappe/patches/v14_0/update_is_system_generated_flag.py 查看文件

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

+ 1
- 1
frappe/permissions.py 查看文件

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

+ 4
- 4
frappe/public/js/frappe/form/controls/attach.js 查看文件

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


+ 4
- 1
frappe/public/js/frappe/form/footer/form_timeline.js 查看文件

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


+ 9
- 5
frappe/public/js/frappe/form/form.js 查看文件

@@ -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 (cstr(doc.name) === me.docname) {
me.dirty();
if (!skip_dirty_trigger) {
me.dirty();
}

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

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

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

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


+ 118
- 21
frappe/public/js/frappe/form/grid.js 查看文件

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

return all_data;
}

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

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

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

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

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

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

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

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

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

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

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



+ 123
- 7
frappe/public/js/frappe/form/grid_row.js 查看文件

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

let me = this;
this.set_row_index();

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

return $col;
}

make_column(df, colsize, txt, ci) {


+ 3
- 3
frappe/public/js/frappe/form/toolbar.js 查看文件

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

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

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

show_jump_to_field_dialog() {


+ 1
- 1
frappe/public/js/frappe/list/list_settings.js 查看文件

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


+ 5
- 5
frappe/public/js/frappe/model/model.js 查看文件

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


+ 9
- 5
frappe/public/js/frappe/ui/page.js 查看文件

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


+ 20
- 2
frappe/public/js/frappe/utils/utils.js 查看文件

@@ -231,7 +231,7 @@ Object.assign(frappe.utils, {
if (tt && (tt.substr(0, 1)===">" || tt.substr(0, 4)==="&gt;")) {
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;
}
}
});

+ 7
- 2
frappe/public/js/frappe/views/calendar/calendar.js 查看文件

@@ -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
- 1
frappe/public/js/frappe/views/reports/report_view.js 查看文件

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


+ 36
- 6
frappe/public/js/frappe/web_form/web_form_list.js 查看文件

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

this.wrapper.appendChild(this.table);
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");

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


+ 24
- 1
frappe/public/scss/common/grid.scss 查看文件

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



+ 1
- 0
frappe/public/scss/desk/page.scss 查看文件

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


+ 13
- 0
frappe/public/scss/website/index.scss 查看文件

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

+ 3
- 3
frappe/tests/test_base_document.py 查看文件

@@ -7,12 +7,12 @@ class TestBaseDocument(unittest.TestCase):
def test_docstatus(self):
doc = BaseDocument({"docstatus": 0})
self.assertTrue(doc.docstatus.is_draft())
self.assertEquals(doc.docstatus, 0)
self.assertEqual(doc.docstatus, 0)

doc.docstatus = 1
self.assertTrue(doc.docstatus.is_submitted())
self.assertEquals(doc.docstatus, 1)
self.assertEqual(doc.docstatus, 1)

doc.docstatus = 2
self.assertTrue(doc.docstatus.is_cancelled())
self.assertEquals(doc.docstatus, 2)
self.assertEqual(doc.docstatus, 2)

+ 38
- 3
frappe/tests/test_db.py 查看文件

@@ -14,7 +14,7 @@ from frappe.database.database import Database
from frappe.query_builder import Field
from frappe.query_builder.functions import Concat_ws
from frappe.tests.test_query_builder import db_type_is, run_only_if
from frappe.utils import add_days, now, random_string
from frappe.utils import add_days, now, random_string, cint
from frappe.utils.testutils import clear_custom_fields


@@ -84,6 +84,27 @@ class TestDB(unittest.TestCase):
),
)

def test_get_value_limits(self):

# check both dict and list style filters
filters = [{"enabled": 1}, [["enabled", "=", 1]]]
for filter in filters:
self.assertEqual(1, len(frappe.db.get_values("User", filters=filter, limit=1)))
# count of last touched rows as per DB-API 2.0 https://peps.python.org/pep-0249/#rowcount
self.assertGreaterEqual(1, cint(frappe.db._cursor.rowcount))
self.assertEqual(2, len(frappe.db.get_values("User", filters=filter, limit=2)))
self.assertGreaterEqual(2, cint(frappe.db._cursor.rowcount))

# without limits length == count
self.assertEqual(len(frappe.db.get_values("User", filters=filter)),
frappe.db.count("User", filter))

frappe.db.get_value("User", filters=filter)
self.assertGreaterEqual(1, cint(frappe.db._cursor.rowcount))

frappe.db.exists("User", filter)
self.assertGreaterEqual(1, cint(frappe.db._cursor.rowcount))

def test_escape(self):
frappe.db.escape("香港濟生堂製藥有限公司 - IT".encode("utf-8"))

@@ -301,6 +322,20 @@ class TestDB(unittest.TestCase):
# recover transaction to continue other tests
raise Exception

def test_exists(self):
dt, dn = "User", "Administrator"
self.assertEqual(frappe.db.exists(dt, dn, cache=True), dn)
self.assertEqual(frappe.db.exists(dt, dn), dn)
self.assertEqual(frappe.db.exists(dt, {"name": ("=", dn)}), dn)

filters = {"doctype": dt, "name": ("like", "Admin%")}
self.assertEqual(frappe.db.exists(filters), dn)
self.assertEqual(
filters["doctype"], dt
) # make sure that doctype was not removed from filters

self.assertEqual(frappe.db.exists(dt, [["name", "=", dn]]), dn)


@run_only_if(db_type_is.MARIADB)
class TestDDLCommandsMaria(unittest.TestCase):
@@ -357,7 +392,7 @@ class TestDDLCommandsMaria(unittest.TestCase):
WHERE Key_name = '{index_name}';
"""
)
self.assertEquals(len(indexs_in_table), 2)
self.assertEqual(len(indexs_in_table), 2)


class TestDBSetValue(unittest.TestCase):
@@ -561,7 +596,7 @@ class TestDDLCommandsPost(unittest.TestCase):
AND indexname = '{index_name}' ;
""",
)
self.assertEquals(len(indexs_in_table), 1)
self.assertEqual(len(indexs_in_table), 1)

@run_only_if(db_type_is.POSTGRES)
def test_modify_query(self):


+ 3
- 3
frappe/tests/test_document.py 查看文件

@@ -260,15 +260,15 @@ class TestDocument(unittest.TestCase):
'doctype': 'Test Formatted',
'currency': 100000
})
self.assertEquals(d.get_formatted('currency', currency='INR', format="#,###.##"), '₹ 100,000.00')
self.assertEqual(d.get_formatted('currency', currency='INR', format="#,###.##"), '₹ 100,000.00')

def test_limit_for_get(self):
doc = frappe.get_doc("DocType", "DocType")
# assuming DocType has more than 3 Data fields
self.assertEquals(len(doc.get("fields", limit=3)), 3)
self.assertEqual(len(doc.get("fields", limit=3)), 3)

# limit with filters
self.assertEquals(len(doc.get("fields", filters={"fieldtype": "Data"}, limit=3)), 3)
self.assertEqual(len(doc.get("fields", filters={"fieldtype": "Data"}, limit=3)), 3)

def test_virtual_fields(self):
"""Virtual fields are accessible via API and Form views, whenever .as_dict is invoked


+ 2
- 2
frappe/tests/test_search.py 查看文件

@@ -70,10 +70,10 @@ class TestSearch(unittest.TestCase):
result = frappe.response['results']

# Check whether the result is sorted or not
self.assertEquals(self.parent_doctype_name, result[0]['value'])
self.assertEqual(self.parent_doctype_name, result[0]['value'])

# Check whether searching for parent also list out children
self.assertEquals(len(result), len(self.child_doctypes_names) + 1)
self.assertEqual(len(result), len(self.child_doctypes_names) + 1)

#Search for the word "pay", part of the word "pays" (country) in french.
def test_link_search_in_foreign_language(self):


+ 50
- 6
frappe/tests/ui_test_helpers.py 查看文件

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

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

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

doc.save()
doc.save()


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

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

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

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

doc.insert()

+ 0
- 2
frappe/translate.py 查看文件

@@ -650,8 +650,6 @@ def extract_messages_from_code(code):
if isinstance(e, InvalidIncludePath):
frappe.clear_last_message()

pass

messages = []
pattern = r"_\(([\"']{,3})(?P<message>((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P<py_context>((?!\5).)*)\5)*(\s*,\s*(.)*?\s*(,\s*([\"'])(?P<js_context>((?!\11).)*)\11)*)*\)"



+ 24
- 13
frappe/utils/__init__.py 查看文件

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

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

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

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

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

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

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

return frappe.parse_json(frappe.local.assets_json)



+ 3
- 3
frappe/utils/backups.py 查看文件

@@ -15,7 +15,7 @@ import click

# imports - module imports
import frappe
from frappe import _, conf
from frappe import conf
from frappe.utils import get_file_size, get_url, now, now_datetime, cint
from frappe.utils.password import get_encryption_key

@@ -505,7 +505,7 @@ download only after 24 hours.""" % {
datetime_str.strftime("%d/%m/%Y %H:%M:%S") + """ - Backup ready to be downloaded"""
)

frappe.sendmail(recipients=recipient_list, msg=msg, subject=subject)
frappe.sendmail(recipients=recipient_list, message=msg, subject=subject)
return recipient_list


@@ -779,7 +779,7 @@ if __name__ == "__main__":
db_type=db_type,
db_port=db_port,
)
odb.send_email("abc.sql.gz")
odb.send_email()

if cmd == "delete_temp_backups":
delete_temp_backups()

+ 6
- 0
frappe/utils/install.py 查看文件

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



+ 0
- 1
frappe/utils/nestedset.py 查看文件

@@ -227,7 +227,6 @@ class NestedSet(Document):
update_nsm(self)
except frappe.DoesNotExistError:
if self.flags.on_rollback:
pass
frappe.message_log.pop()
else:
raise


正在加载...
取消
保存