Pārlūkot izejas kodu

Merge branch 'develop' into frm_call

version-14
Ankush Menat pirms 3 gadiem
committed by GitHub
vecāks
revīzija
87fb4d4459
Šim parakstam datu bāzē netika atrasta zināma atslēga GPG atslēgas ID: 4AEE18F83AFDEB23
90 mainītis faili ar 2522 papildinājumiem un 1305 dzēšanām
  1. +4
    -2
      .github/helper/install.sh
  2. +10
    -4
      .github/helper/roulette.py
  3. +16
    -0
      .github/workflows/ui-tests.yml
  4. +4
    -0
      .mergify.yml
  5. +30
    -0
      cypress/fixtures/child_table_doctype.js
  6. +45
    -0
      cypress/fixtures/doctype_to_link.js
  7. +46
    -0
      cypress/fixtures/doctype_with_child_table.js
  8. +45
    -0
      cypress/integration/control_link.js
  9. +24
    -0
      cypress/integration/dashboard_links.js
  10. +21
    -0
      cypress/integration/depends_on.js
  11. +92
    -0
      cypress/integration/grid.js
  12. +46
    -13
      cypress/integration/report_view.js
  13. +4
    -2
      frappe/api.py
  14. +10
    -0
      frappe/boot.py
  15. +14
    -133
      frappe/build.py
  16. +4
    -8
      frappe/commands/site.py
  17. +8
    -3
      frappe/commands/utils.py
  18. +20
    -11
      frappe/core/doctype/communication/communication.py
  19. +14
    -0
      frappe/core/doctype/communication/test_communication.py
  20. +8
    -1
      frappe/core/doctype/docfield/docfield.json
  21. +8
    -1
      frappe/core/doctype/doctype/doctype.json
  22. +25
    -20
      frappe/core/doctype/doctype/doctype.py
  23. +1
    -1
      frappe/core/doctype/file/file.py
  24. +109
    -1
      frappe/core/doctype/report/test_report.py
  25. +2
    -1
      frappe/core/page/permission_manager/permission_manager.js
  26. +1
    -0
      frappe/coverage.py
  27. +466
    -456
      frappe/custom/doctype/custom_field/custom_field.json
  28. +2
    -2
      frappe/custom/doctype/custom_field/custom_field.py
  29. +8
    -1
      frappe/custom/doctype/customize_form/customize_form.json
  30. +9
    -3
      frappe/custom/doctype/customize_form/customize_form.py
  31. +8
    -1
      frappe/custom/doctype/customize_form_field/customize_form_field.json
  32. +11
    -44
      frappe/custom/doctype/property_setter/property_setter.py
  33. +2
    -0
      frappe/database/database.py
  34. +1
    -0
      frappe/database/mariadb/framework_mariadb.sql
  35. +1
    -0
      frappe/database/postgres/framework_postgres.sql
  36. +4
    -1
      frappe/database/schema.py
  37. +16
    -0
      frappe/desk/doctype/dashboard/dashboard_list.js
  38. +56
    -1
      frappe/desk/form/load.py
  39. +17
    -7
      frappe/desk/query_report.py
  40. +59
    -15
      frappe/desk/reportview.py
  41. +50
    -6
      frappe/desk/search.py
  42. +1
    -2
      frappe/installer.py
  43. +130
    -59
      frappe/migrate.py
  44. +84
    -37
      frappe/model/base_document.py
  45. +1
    -5
      frappe/model/document.py
  46. +28
    -9
      frappe/model/meta.py
  47. +1
    -0
      frappe/patches.txt
  48. +7
    -0
      frappe/public/icons/timeless/symbol-defs.svg
  49. +4
    -1
      frappe/public/js/frappe/form/controls/base_control.js
  50. +71
    -10
      frappe/public/js/frappe/form/controls/link.js
  51. +7
    -1
      frappe/public/js/frappe/form/controls/multiselect_pills.js
  52. +5
    -2
      frappe/public/js/frappe/form/controls/table_multiselect.js
  53. +21
    -10
      frappe/public/js/frappe/form/form.js
  54. +8
    -5
      frappe/public/js/frappe/form/formatters.js
  55. +6
    -7
      frappe/public/js/frappe/form/grid.js
  56. +22
    -5
      frappe/public/js/frappe/form/grid_row.js
  57. +11
    -7
      frappe/public/js/frappe/form/layout.js
  58. +12
    -13
      frappe/public/js/frappe/form/multi_select_dialog.js
  59. +1
    -1
      frappe/public/js/frappe/form/quick_entry.js
  60. +30
    -21
      frappe/public/js/frappe/form/save.js
  61. +12
    -3
      frappe/public/js/frappe/form/script_manager.js
  62. +1
    -1
      frappe/public/js/frappe/form/tab.js
  63. +2
    -2
      frappe/public/js/frappe/list/base_list.js
  64. +3
    -2
      frappe/public/js/frappe/list/list_view.js
  65. +8
    -0
      frappe/public/js/frappe/request.js
  66. +11
    -1
      frappe/public/js/frappe/ui/filters/filter.js
  67. +8
    -5
      frappe/public/js/frappe/utils/common.js
  68. +37
    -0
      frappe/public/js/frappe/utils/utils.js
  69. +3
    -2
      frappe/public/js/frappe/views/reports/query_report.js
  70. +55
    -9
      frappe/public/js/frappe/views/reports/report_view.js
  71. +1
    -1
      frappe/public/js/frappe/views/treeview.js
  72. +2
    -0
      frappe/public/scss/common/css_variables.scss
  73. +25
    -1
      frappe/public/scss/desk/list.scss
  74. +1
    -1
      frappe/public/scss/desk/sidebar.scss
  75. +1
    -1
      frappe/templates/print_formats/standard.html
  76. +3
    -3
      frappe/templates/print_formats/standard_macros.html
  77. +166
    -78
      frappe/tests/test_api.py
  78. +133
    -25
      frappe/tests/test_commands.py
  79. +10
    -0
      frappe/tests/test_db.py
  80. +68
    -6
      frappe/tests/test_document.py
  81. +2
    -2
      frappe/tests/test_frappe_client.py
  82. +93
    -0
      frappe/tests/test_utils.py
  83. +20
    -0
      frappe/tests/ui_test_helpers.py
  84. +10
    -4
      frappe/translations/de.csv
  85. +2
    -1
      frappe/utils/__init__.py
  86. +2
    -1
      frappe/utils/backups.py
  87. +21
    -1
      frappe/utils/formatters.py
  88. +0
    -212
      frappe/utils/minify.py
  89. +1
    -1
      frappe/website/doctype/web_form/web_form.js
  90. +50
    -9
      frappe/www/printview.py

+ 4
- 2
.github/helper/install.sh Parādīt failu

@@ -50,7 +50,9 @@ if [ "$TYPE" == "server" ]; then sed -i 's/^socketio:/# socketio:/g' Procfile; f
if [ "$TYPE" == "server" ]; then sed -i 's/^redis_socketio:/# redis_socketio:/g' Procfile; fi if [ "$TYPE" == "server" ]; then sed -i 's/^redis_socketio:/# redis_socketio:/g' Procfile; fi


if [ "$TYPE" == "ui" ]; then bench setup requirements --node; fi if [ "$TYPE" == "ui" ]; then bench setup requirements --node; fi
if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi
bench setup requirements --dev

if [ "$TYPE" == "ui" ]; then sed -i 's/^web: bench serve/web: bench serve --with-coverage/g' Procfile; fi


# install node-sass which is required for website theme test # install node-sass which is required for website theme test
cd ./apps/frappe || exit cd ./apps/frappe || exit
@@ -60,4 +62,4 @@ cd ../..
bench start & bench start &
bench --site test_site reinstall --yes bench --site test_site reinstall --yes
if [ "$TYPE" == "server" ]; then bench --site test_site_producer reinstall --yes; fi if [ "$TYPE" == "server" ]; then bench --site test_site_producer reinstall --yes; fi
CI=Yes bench build --app frappe
if [ "$TYPE" == "server" ]; then CI=Yes bench build --app frappe; fi

+ 10
- 4
.github/helper/roulette.py Parādīt failu

@@ -41,6 +41,7 @@ if __name__ == "__main__":
# this is a push build, run all builds # this is a push build, run all builds
if not pr_number: if not pr_number:
os.system('echo "::set-output name=build::strawberry"') os.system('echo "::set-output name=build::strawberry"')
os.system('echo "::set-output name=build-server::strawberry"')
sys.exit(0) sys.exit(0)


files_list = files_list or get_files_list(pr_number=pr_number, repo=repo) files_list = files_list or get_files_list(pr_number=pr_number, repo=repo)
@@ -52,7 +53,8 @@ if __name__ == "__main__":
ci_files_changed = any(f for f in files_list if is_ci(f)) ci_files_changed = any(f for f in files_list if is_ci(f))
only_docs_changed = len(list(filter(is_docs, files_list))) == len(files_list) only_docs_changed = len(list(filter(is_docs, files_list))) == len(files_list)
only_frontend_code_changed = len(list(filter(is_frontend_code, files_list))) == len(files_list) only_frontend_code_changed = len(list(filter(is_frontend_code, files_list))) == len(files_list)
only_py_changed = len(list(filter(is_py, files_list))) == len(files_list)
updated_py_file_count = len(list(filter(is_py, files_list)))
only_py_changed = updated_py_file_count == len(files_list)


if ci_files_changed: if ci_files_changed:
print("CI related files were updated, running all build processes.") print("CI related files were updated, running all build processes.")
@@ -65,8 +67,12 @@ if __name__ == "__main__":
print("Only Frontend code was updated; Stopping Python build process.") print("Only Frontend code was updated; Stopping Python build process.")
sys.exit(0) sys.exit(0)


elif only_py_changed and build_type == "ui":
print("Only Python code was updated, stopping Cypress build process.")
sys.exit(0)
elif build_type == "ui":
if only_py_changed:
print("Only Python code was updated, stopping Cypress build process.")
sys.exit(0)
elif updated_py_file_count > 0:
# both frontend and backend code were updated
os.system('echo "::set-output name=build-server::strawberry"')


os.system('echo "::set-output name=build::strawberry"') os.system('echo "::set-output name=build::strawberry"')

+ 16
- 0
.github/workflows/ui-tests.yml Parādīt failu

@@ -141,6 +141,12 @@ jobs:
env: env:
CYPRESS_RECORD_KEY: 4a48f41c-11b3-425b-aa88-c58048fa69eb CYPRESS_RECORD_KEY: 4a48f41c-11b3-425b-aa88-c58048fa69eb


- name: Stop server
if: ${{ steps.check-build.outputs.build-server == 'strawberry' }}
run: |
ps -ef | grep "frappe serve" | awk '{print $2}' | xargs kill -s SIGINT 2> /dev/null || true
sleep 5

- name: Check If Coverage Report Exists - name: Check If Coverage Report Exists
id: check_coverage id: check_coverage
uses: andstor/file-existence-action@v1 uses: andstor/file-existence-action@v1
@@ -156,3 +162,13 @@ jobs:
directory: /home/runner/frappe-bench/apps/frappe/.cypress-coverage/ directory: /home/runner/frappe-bench/apps/frappe/.cypress-coverage/
verbose: true verbose: true
flags: ui-tests flags: ui-tests

- name: Upload Server Coverage Data
if: ${{ steps.check-build.outputs.build-server == 'strawberry' }}
uses: codecov/codecov-action@v2
with:
name: MariaDB
fail_ci_if_error: true
files: /home/runner/frappe-bench/sites/coverage.xml
verbose: true
flags: server

+ 4
- 0
.mergify.yml Parādīt failu

@@ -48,3 +48,7 @@ pull_request_rules:
actions: actions:
merge: merge:
method: squash method: squash
commit_message_template: |
{{ title }} (#{{ number }})

{{ body }}

+ 30
- 0
cypress/fixtures/child_table_doctype.js Parādīt failu

@@ -0,0 +1,30 @@
export default {
name: "Child Table Doctype",
actions: [],
custom: 1,
autoname: "field:title",
creation: "2022-02-09 20:15:21.242213",
doctype: "DocType",
editable_grid: 1,
engine: "InnoDB",
fields: [
{
fieldname: "title",
fieldtype: "Data",
in_list_view: 1,
label: "Title",
unique: 1
}
],
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
};

+ 45
- 0
cypress/fixtures/doctype_to_link.js Parādīt failu

@@ -0,0 +1,45 @@
export default {
name: "Doctype to Link",
actions: [],
custom: 1,
naming_rule: "By fieldname",
autoname: "field:title",
creation: "2022-02-09 20:15:21.242213",
doctype: "DocType",
editable_grid: 1,
engine: "InnoDB",
fields: [
{
"fieldname": "title",
"fieldtype": "Data",
"label": "Title",
"unique": 1
}
],
links: [
{
"group": "Child Doctype",
"link_doctype": "Doctype With Child Table",
"link_fieldname": "title"
}
],
modified: "2022-02-10 12:03:12.603763",
modified_by: "Administrator",
module: "Custom",
owner: "Administrator",
permissions: [
{
create: 1,
delete: 1,
email: 1,
print: 1,
read: 1,
role: 'System Manager',
share: 1,
write: 1
}
],
sort_field: 'modified',
sort_order: 'ASC',
track_changes: 1
};

+ 46
- 0
cypress/fixtures/doctype_with_child_table.js Parādīt failu

@@ -0,0 +1,46 @@
export default {
name: "Doctype With Child Table",
actions: [],
custom: 1,
autoname: "field:title",
creation: "2022-02-09 20:15:21.242213",
doctype: "DocType",
editable_grid: 1,
engine: "InnoDB",
fields: [
{
fieldname: "title",
fieldtype: "Data",
label: "Title",
unique: 1
},
{
fieldname: "child_table",
fieldtype: "Table",
label: "Child Table",
options: "Child Table Doctype",
reqd: 1
}
],
links: [],
modified: "2022-02-10 12:03:12.603763",
modified_by: "Administrator",
module: "Custom",
naming_rule: "By fieldname",
owner: "Administrator",
permissions: [
{
create: 1,
delete: 1,
email: 1,
print: 1,
read: 1,
role: 'System Manager',
share: 1,
write: 1
}
],
sort_field: 'modified',
sort_order: 'ASC',
track_changes: 1
};

+ 45
- 0
cypress/integration/control_link.js Parādīt failu

@@ -95,6 +95,51 @@ context('Control Link', () => {
}); });
}); });


it('show title field in link', () => {
get_dialog_with_link().as('dialog');

cy.insert_doc("Property Setter", {
"doctype": "Property Setter",
"doc_type": "ToDo",
"property": "show_title_field_in_link",
"property_type": "Check",
"doctype_or_field": "DocType",
"value": "1"
}, true);

cy.window().its('frappe').then(frappe => {
if (!frappe.boot) {
frappe.boot = {
link_title_doctypes: ['ToDo']
};
} else {
frappe.boot.link_title_doctypes = ['ToDo'];
}
});

cy.intercept('POST', '/api/method/frappe.desk.search.search_link').as('search_link');

cy.get('.frappe-control[data-fieldname=link] input').focus().as('input');
cy.wait('@search_link');
cy.get('@input').type('todo for link');
cy.wait('@search_link');
cy.get('.frappe-control[data-fieldname=link] ul').should('be.visible');
cy.get('.frappe-control[data-fieldname=link] input').type('{enter}', { delay: 100 });
cy.get('.frappe-control[data-fieldname=link] input').blur();
cy.get('@dialog').then(dialog => {
cy.get('@todos').then(todos => {
let field = dialog.get_field('link');
let value = field.get_value();
let label = field.get_label_value();

expect(value).to.eq(todos[0]);
expect(label).to.eq('this is a test todo for link');

cy.remove_doc("Property Setter", "ToDo-main-show_title_field_in_link");
});
});
});

it('should update dependant fields (via fetch_from)', () => { it('should update dependant fields (via fetch_from)', () => {
cy.get('@todos').then(todos => { cy.get('@todos').then(todos => {
cy.visit(`/app/todo/${todos[0]}`); cy.visit(`/app/todo/${todos[0]}`);


+ 24
- 0
cypress/integration/dashboard_links.js Parādīt failu

@@ -1,7 +1,21 @@
import doctype_with_child_table from '../fixtures/doctype_with_child_table';
import child_table_doctype from '../fixtures/child_table_doctype';
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;

context('Dashboard links', () => { context('Dashboard links', () => {
before(() => { before(() => {
cy.visit('/login'); cy.visit('/login');
cy.login(); cy.login();
cy.insert_doc('DocType', child_table_doctype, 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 => {
return frappe.xcall("frappe.tests.ui_test_helpers.update_child_table", {
name: child_table_doctype_name
});
});
}); });


it('Adding a new contact, checking for the counter on the dashboard and deleting the created contact', () => { it('Adding a new contact, checking for the counter on the dashboard and deleting the created contact', () => {
@@ -62,4 +76,14 @@ context('Dashboard links', () => {
cy.findByText('Website Analytics'); cy.findByText('Website Analytics');
}); });
}); });

it('check if child table is populated with linked field on creation from dashboard link', () => {
cy.new_form(doctype_to_link_name);
cy.fill_field("title", "Test Linking");
cy.findByRole("button", {name: "Save"}).click();

cy.get('.document-link .btn-new').click();
cy.get('.frappe-control[data-fieldname="child_table"] .rows .data-row .col[data-fieldname="doctype_to_link"]')
.should('contain.text', 'Test Linking');
});
}); });

+ 21
- 0
cypress/integration/depends_on.js Parādīt failu

@@ -55,10 +55,31 @@ context('Depends On', () => {
'read_only_depends_on': "eval:doc.test_field=='Some Other Value'", 'read_only_depends_on': "eval:doc.test_field=='Some Other Value'",
'options': "Child Test Depends On" 'options': "Child Test Depends On"
}, },
{
"label": "Dependent Tab",
"fieldname": "dependent_tab",
"fieldtype": "Tab Break",
"depends_on": "eval:doc.test_field=='Show Tab'"
},
{
"fieldname": "tab_section",
"fieldtype": "Section Break",
},
{
"label": "Field in Tab",
"fieldname": "field_in_tab",
"fieldtype": "Data",
}
] ]
}); });
}); });
}); });
it('should show the tab on other setting field value', () => {
cy.new_form('Test Depends On');
cy.fill_field('test_field', 'Show Tab');
cy.get('body').click();
cy.findByRole("tab", {name: "Dependent Tab"}).should('be.visible');
});
it('should set the field as mandatory depending on other fields value', () => { it('should set the field as mandatory depending on other fields value', () => {
cy.new_form('Test Depends On'); cy.new_form('Test Depends On');
cy.fill_field('test_field', 'Some Value'); cy.fill_field('test_field', 'Some Value');


+ 92
- 0
cypress/integration/grid.js Parādīt failu

@@ -0,0 +1,92 @@
context('Grid', () => {
beforeEach(() => {
cy.login();
cy.visit('/app/website');
});
before(() => {
cy.login();
cy.visit('/app/website');
return cy.window().its('frappe').then(frappe => {
return frappe.call("frappe.tests.ui_test_helpers.create_contact_phone_nos_records");
});
});
it('update docfield property using update_docfield_property', () => {
cy.visit('/app/contact/Test Contact');
cy.window().its("cur_frm").then(frm => {
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
let field = frm.get_field("phone_nos");
field.grid.update_docfield_property("is_primary_phone", "hidden", true);

cy.get('@table').find('[data-idx="1"] .edit-grid-row').click();
cy.get('.grid-row-open').as('table-form');
cy.get('@table-form').find('.frappe-control[data-fieldname="is_primary_phone"]').should("be.hidden");
cy.get('@table-form').find('.grid-footer-toolbar').click();

cy.get('@table').find('[data-idx="2"] .edit-grid-row').click();
cy.get('.grid-row-open').as('table-form');
cy.get('@table-form').find('.frappe-control[data-fieldname="is_primary_phone"]').should("be.hidden");
cy.get('@table-form').find('.grid-footer-toolbar').click();
});
});
it('update docfield property using toggle_display', () => {
cy.visit('/app/contact/Test Contact');
cy.window().its("cur_frm").then(frm => {
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
let field = frm.get_field("phone_nos");
field.grid.toggle_display("is_primary_mobile_no", false);

cy.get('@table').find('[data-idx="1"] .edit-grid-row').click();
cy.get('.grid-row-open').as('table-form');
cy.get('@table-form').find('.frappe-control[data-fieldname="is_primary_mobile_no"]').should("be.hidden");
cy.get('@table-form').find('.grid-footer-toolbar').click();

cy.get('@table').find('[data-idx="2"] .edit-grid-row').click();
cy.get('.grid-row-open').as('table-form');
cy.get('@table-form').find('.frappe-control[data-fieldname="is_primary_mobile_no"]').should("be.hidden");
cy.get('@table-form').find('.grid-footer-toolbar').click();
});
});
it('update docfield property using toggle_enable', () => {
cy.visit('/app/contact/Test Contact');
cy.window().its("cur_frm").then(frm => {
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
let field = frm.get_field("phone_nos");
field.grid.toggle_enable("phone", false);


cy.get('@table').find('[data-idx="1"] .edit-grid-row').click();
cy.get('.grid-row-open').as('table-form');
cy.get('@table-form').find('.frappe-control[data-fieldname="phone"] .control-value').should('have.class', 'like-disabled-input');
cy.get('@table-form').find('.grid-footer-toolbar').click();

cy.get('@table').find('[data-idx="2"] .edit-grid-row').click();
cy.get('.grid-row-open').as('table-form');
cy.get('@table-form').find('.frappe-control[data-fieldname="phone"] .control-value').should('have.class', 'like-disabled-input');
cy.get('@table-form').find('.grid-footer-toolbar').click();
});
});
it('update docfield property using toggle_reqd', () => {
cy.visit('/app/contact/Test Contact');
cy.window().its("cur_frm").then(frm => {
cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table');
let field = frm.get_field("phone_nos");
field.grid.toggle_reqd("phone", false);

cy.get('@table').find('[data-idx="1"] .edit-grid-row').click();
cy.get('.grid-row-open').as('table-form');
cy.get_field("phone").as('phone-field');
cy.get('@phone-field').focus().clear().wait(500).blur();
cy.get('@phone-field').should("not.have.class", "has-error");
cy.get('@table-form').find('.grid-footer-toolbar').click();

cy.get('@table').find('[data-idx="2"] .edit-grid-row').click();
cy.get('.grid-row-open').as('table-form');
cy.get_field("phone").as('phone-field');
cy.get('@phone-field').focus().clear().wait(500).blur();
cy.get('@phone-field').should("not.have.class", "has-error");
cy.get('@table-form').find('.grid-footer-toolbar').click();

});
});
});


+ 46
- 13
cypress/integration/report_view.js Parādīt failu

@@ -11,30 +11,63 @@ context('Report View', () => {
'title': 'Doc 1', 'title': 'Doc 1',
'description': 'Random Text', 'description': 'Random Text',
'enabled': 0, 'enabled': 0,
// submit document
'docstatus': 1
}, true).as('doc');
'docstatus': 1 // submit document
}, true);
return cy.window().its('frappe').then(frappe => {
return frappe.call("frappe.tests.ui_test_helpers.create_multiple_contact_records");
});
}); });

it('Field with enabled allow_on_submit should be editable.', () => { it('Field with enabled allow_on_submit should be editable.', () => {
cy.intercept('POST', 'api/method/frappe.client.set_value').as('value-update'); cy.intercept('POST', 'api/method/frappe.client.set_value').as('value-update');
cy.visit(`/app/List/${doctype_name}/Report`); cy.visit(`/app/List/${doctype_name}/Report`);

// check status column added from docstatus // check status column added from docstatus
cy.get('.dt-row-0 > .dt-cell--col-3').should('contain', 'Submitted'); cy.get('.dt-row-0 > .dt-cell--col-3').should('contain', 'Submitted');
let cell = cy.get('.dt-row-0 > .dt-cell--col-4'); let cell = cy.get('.dt-row-0 > .dt-cell--col-4');

// select the cell // select the cell
cell.dblclick(); cell.dblclick();
cell.get('.dt-cell__edit--col-4').findByRole('checkbox').check({ force: true }); cell.get('.dt-cell__edit--col-4').findByRole('checkbox').check({ force: true });

cy.wait('@value-update'); cy.wait('@value-update');
cy.get('@doc').then(doc => {
cy.call('frappe.client.get_value', {
doctype: doc.doctype,
filters: {
name: doc.name,
},
fieldname: 'enabled'
}).then(r => {
expect(r.message.enabled).to.equals(1);
});

cy.call('frappe.client.get_value', {
doctype: doctype_name,
filters: {
title: 'Doc 1',
},
fieldname: 'enabled'
}).then(r => {
expect(r.message.enabled).to.equals(1);
}); });
}); });

it('test load more with count selection buttons', () => {
cy.visit('/app/contact/view/report');

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

cy.get('.list-paging-area .btn-group .btn-paging[data-value="100"]').click();

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

// check if refresh works after load more
cy.get('.page-head .standard-actions [data-original-title="Refresh"]').click();
cy.get('.list-paging-area .list-count').should('contain.text', '300 of');

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

+ 4
- 2
frappe/api.py Parādīt failu

@@ -159,7 +159,10 @@ def get_request_form_data():
else: else:
data = frappe.local.form_dict.data data = frappe.local.form_dict.data


return frappe.parse_json(data)
try:
return frappe.parse_json(data)
except ValueError:
return frappe.local.form_dict




def validate_auth(): def validate_auth():
@@ -208,7 +211,6 @@ def validate_oauth(authorization_header):
pass pass





def validate_auth_via_api_keys(authorization_header): def validate_auth_via_api_keys(authorization_header):
""" """
Authenticate request using API keys and set session user Authenticate request using API keys and set session user


+ 10
- 0
frappe/boot.py Parādīt failu

@@ -89,6 +89,7 @@ def get_bootinfo():
bootinfo.additional_filters_config = get_additional_filters_from_hooks() bootinfo.additional_filters_config = get_additional_filters_from_hooks()
bootinfo.desk_settings = get_desk_settings() bootinfo.desk_settings = get_desk_settings()
bootinfo.app_logo_url = get_app_logo() bootinfo.app_logo_url = get_app_logo()
bootinfo.link_title_doctypes = get_link_title_doctypes()


return bootinfo return bootinfo


@@ -324,6 +325,15 @@ def get_desk_settings():
def get_notification_settings(): def get_notification_settings():
return frappe.get_cached_doc('Notification Settings', frappe.session.user) return frappe.get_cached_doc('Notification Settings', frappe.session.user)


def get_link_title_doctypes():
dts = frappe.get_all("DocType", {"show_title_field_in_link": 1})
custom_dts = frappe.get_all(
"Property Setter",
{"property": "show_title_field_in_link", "value": "1"},
["doc_type as name"],
)
return [d.name for d in dts + custom_dts if d]

def set_time_zone(bootinfo): def set_time_zone(bootinfo):
bootinfo.time_zone = { bootinfo.time_zone = {
"system": get_time_zone(), "system": get_time_zone(),


+ 14
- 133
frappe/build.py Parādīt failu

@@ -1,25 +1,21 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import os import os
import re
import json
import shutil import shutil
import re
import subprocess import subprocess
from distutils.spawn import find_executable
from subprocess import getoutput from subprocess import getoutput
from io import StringIO
from tempfile import mkdtemp, mktemp from tempfile import mkdtemp, mktemp
from distutils.spawn import find_executable

import frappe
from frappe.utils.minify import JavascriptMinify
from urllib.parse import urlparse


import click import click
import psutil import psutil
from urllib.parse import urlparse
from semantic_version import Version
from requests import head from requests import head
from requests.exceptions import HTTPError from requests.exceptions import HTTPError
from semantic_version import Version


import frappe


timestamps = {} timestamps = {}
app_paths = None app_paths = None
@@ -32,6 +28,7 @@ class AssetsNotDownloadedError(Exception):
class AssetsDontExistError(HTTPError): class AssetsDontExistError(HTTPError):
pass pass



def download_file(url, prefix): def download_file(url, prefix):
from requests import get from requests import get


@@ -277,12 +274,14 @@ def check_node_executable():
click.echo(f"{warn} Please install yarn using below command and try again.\nnpm install -g yarn") click.echo(f"{warn} Please install yarn using below command and try again.\nnpm install -g yarn")
click.echo() click.echo()



def get_node_env(): def get_node_env():
node_env = { node_env = {
"NODE_OPTIONS": f"--max_old_space_size={get_safe_max_old_space_size()}" "NODE_OPTIONS": f"--max_old_space_size={get_safe_max_old_space_size()}"
} }
return node_env return node_env



def get_safe_max_old_space_size(): def get_safe_max_old_space_size():
safe_max_old_space_size = 0 safe_max_old_space_size = 0
try: try:
@@ -296,6 +295,7 @@ def get_safe_max_old_space_size():


return safe_max_old_space_size return safe_max_old_space_size



def generate_assets_map(): def generate_assets_map():
symlinks = {} symlinks = {}


@@ -344,7 +344,6 @@ def clear_broken_symlinks():
os.remove(path) os.remove(path)





def unstrip(message: str) -> str: def unstrip(message: str) -> str:
"""Pads input string on the right side until the last available column in the terminal """Pads input string on the right side until the last available column in the terminal
""" """
@@ -397,94 +396,6 @@ def link_assets_dir(source, target, hard_link=False):
symlink(source, target, overwrite=True) symlink(source, target, overwrite=True)




def build(no_compress=False, verbose=False):
for target, sources in get_build_maps().items():
pack(os.path.join(assets_path, target), sources, no_compress, verbose)


def get_build_maps():
"""get all build.jsons with absolute paths"""
# framework js and css files

build_maps = {}
for app_path in app_paths:
path = os.path.join(app_path, "public", "build.json")
if os.path.exists(path):
with open(path) as f:
try:
for target, sources in (json.loads(f.read() or "{}")).items():
# update app path
source_paths = []
for source in sources:
if isinstance(source, list):
s = frappe.get_pymodule_path(source[0], *source[1].split("/"))
else:
s = os.path.join(app_path, source)
source_paths.append(s)

build_maps[target] = source_paths
except ValueError as e:
print(path)
print("JSON syntax error {0}".format(str(e)))
return build_maps


def pack(target, sources, no_compress, verbose):
outtype, outtxt = target.split(".")[-1], ""
jsm = JavascriptMinify()

for f in sources:
suffix = None
if ":" in f:
f, suffix = f.split(":")
if not os.path.exists(f) or os.path.isdir(f):
print("did not find " + f)
continue
timestamps[f] = os.path.getmtime(f)
try:
with open(f, "r") as sourcefile:
data = str(sourcefile.read(), "utf-8", errors="ignore")

extn = f.rsplit(".", 1)[1]

if (
outtype == "js"
and extn == "js"
and (not no_compress)
and suffix != "concat"
and (".min." not in f)
):
tmpin, tmpout = StringIO(data.encode("utf-8")), StringIO()
jsm.minify(tmpin, tmpout)
minified = tmpout.getvalue()
if minified:
outtxt += str(minified or "", "utf-8").strip("\n") + ";"

if verbose:
print("{0}: {1}k".format(f, int(len(minified) / 1024)))
elif outtype == "js" and extn == "html":
# add to frappe.templates
outtxt += html_to_js_template(f, data)
else:
outtxt += "\n/*\n *\t%s\n */" % f
outtxt += "\n" + data + "\n"

except Exception:
print("--Error in:" + f + "--")
print(frappe.get_traceback())

with open(target, "w") as f:
f.write(outtxt.encode("utf-8"))

print("Wrote %s - %sk" % (target, str(int(os.path.getsize(target) / 1024))))


def html_to_js_template(path, content):
"""returns HTML template content as Javascript code, adding it to `frappe.templates`"""
return """frappe.templates["{key}"] = '{content}';\n""".format(
key=path.rsplit("/", 1)[-1][:-5], content=scrub_html_template(content))


def scrub_html_template(content): def scrub_html_template(content):
"""Returns HTML content with removed whitespace and comments""" """Returns HTML content with removed whitespace and comments"""
# remove whitespace to a single space # remove whitespace to a single space
@@ -496,37 +407,7 @@ def scrub_html_template(content):
return content.replace("'", "\'") return content.replace("'", "\'")




def files_dirty():
for target, sources in get_build_maps().items():
for f in sources:
if ":" in f:
f, suffix = f.split(":")
if not os.path.exists(f) or os.path.isdir(f):
continue
if os.path.getmtime(f) != timestamps.get(f):
print(f + " dirty")
return True
else:
return False


def compile_less():
if not find_executable("lessc"):
return

for path in app_paths:
less_path = os.path.join(path, "public", "less")
if os.path.exists(less_path):
for fname in os.listdir(less_path):
if fname.endswith(".less") and fname != "variables.less":
fpath = os.path.join(less_path, fname)
mtime = os.path.getmtime(fpath)
if fpath in timestamps and mtime == timestamps[fpath]:
continue

timestamps[fpath] = mtime

print("compiling {0}".format(fpath))

css_path = os.path.join(path, "public", "css", fname.rsplit(".", 1)[0] + ".css")
os.system("lessc {0} > {1}".format(fpath, css_path))
def html_to_js_template(path, content):
"""returns HTML template content as Javascript code, adding it to `frappe.templates`"""
return """frappe.templates["{key}"] = '{content}';\n""".format(
key=path.rsplit("/", 1)[-1][:-5], content=scrub_html_template(content))

+ 4
- 8
frappe/commands/site.py Parādīt failu

@@ -447,21 +447,17 @@ def disable_user(context, email):
@pass_context @pass_context
def migrate(context, skip_failing=False, skip_search_index=False): def migrate(context, skip_failing=False, skip_search_index=False):
"Run patches, sync schema and rebuild files/translations" "Run patches, sync schema and rebuild files/translations"
from frappe.migrate import migrate
from frappe.migrate import SiteMigration


for site in context.sites: for site in context.sites:
click.secho(f"Migrating {site}", fg="green") click.secho(f"Migrating {site}", fg="green")
frappe.init(site=site)
frappe.connect()
try: try:
migrate(
context.verbose,
SiteMigration(
skip_failing=skip_failing, skip_failing=skip_failing,
skip_search_index=skip_search_index
)
skip_search_index=skip_search_index,
).run(site=site)
finally: finally:
print() print()
frappe.destroy()
if not context.sites: if not context.sites:
raise SiteNotSpecifiedError raise SiteNotSpecifiedError




+ 8
- 3
frappe/commands/utils.py Parādīt failu

@@ -742,8 +742,9 @@ def run_ui_tests(context, app, headless=False, parallel=True, with_coverage=Fals
@click.option('--profile', is_flag=True, default=False) @click.option('--profile', is_flag=True, default=False)
@click.option('--noreload', "no_reload", is_flag=True, default=False) @click.option('--noreload', "no_reload", is_flag=True, default=False)
@click.option('--nothreading', "no_threading", is_flag=True, default=False) @click.option('--nothreading', "no_threading", is_flag=True, default=False)
@click.option('--with-coverage', is_flag=True, default=False)
@pass_context @pass_context
def serve(context, port=None, profile=False, no_reload=False, no_threading=False, sites_path='.', site=None):
def serve(context, port=None, profile=False, no_reload=False, no_threading=False, sites_path='.', site=None, with_coverage=False):
"Start development web server" "Start development web server"
import frappe.app import frappe.app


@@ -751,8 +752,12 @@ def serve(context, port=None, profile=False, no_reload=False, no_threading=False
site = None site = None
else: else:
site = context.sites[0] site = context.sites[0]

frappe.app.serve(port=port, profile=profile, no_reload=no_reload, no_threading=no_threading, site=site, sites_path='.')
with CodeCoverage(with_coverage, 'frappe'):
if with_coverage:
# unable to track coverage with threading enabled
no_threading = True
no_reload = True
frappe.app.serve(port=port, profile=profile, no_reload=no_reload, no_threading=no_threading, site=site, sites_path='.')




@click.command('request') @click.command('request')


+ 20
- 11
frappe/core/doctype/communication/communication.py Parādīt failu

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


from collections import Counter from collections import Counter
from typing import List
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
@@ -367,15 +368,8 @@ def get_permission_query_conditions_for_communication(user):
return """`tabCommunication`.email_account in ({email_accounts})"""\ return """`tabCommunication`.email_account in ({email_accounts})"""\
.format(email_accounts=','.join(email_accounts)) .format(email_accounts=','.join(email_accounts))


def get_contacts(email_strings, auto_create_contact=False):
email_addrs = []

for email_string in email_strings:
if email_string:
result = getaddresses([email_string])
for email in result:
email_addrs.append(email[1])

def get_contacts(email_strings: List[str], auto_create_contact=False) -> List[str]:
email_addrs = get_emails(email_strings)
contacts = [] contacts = []
for email in email_addrs: for email in email_addrs:
email = get_email_without_link(email) email = get_email_without_link(email)
@@ -404,6 +398,17 @@ def get_contacts(email_strings, auto_create_contact=False):


return contacts return contacts


def get_emails(email_strings: List[str]) -> List[str]:
email_addrs = []

for email_string in email_strings:
if email_string:
result = getaddresses([email_string])
for email in result:
email_addrs.append(email[1])

return email_addrs

def add_contact_links_to_communication(communication, contact_name): def add_contact_links_to_communication(communication, contact_name):
contact_links = frappe.get_all("Dynamic Link", filters={ contact_links = frappe.get_all("Dynamic Link", filters={
"parenttype": "Contact", "parenttype": "Contact",
@@ -449,8 +454,12 @@ def get_email_without_link(email):
if not frappe.get_all("Email Account", filters={"enable_automatic_linking": 1}): if not frappe.get_all("Email Account", filters={"enable_automatic_linking": 1}):
return email return email


email_id = email.split("@")[0].split("+")[0]
email_host = email.split("@")[1]
try:
_email = email.split("@")
email_id = _email[0].split("+")[0]
email_host = _email[1]
except IndexError:
return email


return "{0}@{1}".format(email_id, email_host) return "{0}@{1}".format(email_id, email_host)




+ 14
- 0
frappe/core/doctype/communication/test_communication.py Parādīt failu

@@ -5,6 +5,7 @@ from urllib.parse import quote


import frappe import frappe
from frappe.email.doctype.email_queue.email_queue import EmailQueue from frappe.email.doctype.email_queue.email_queue import EmailQueue
from frappe.core.doctype.communication.communication import get_emails


test_records = frappe.get_test_records('Communication') test_records = frappe.get_test_records('Communication')


@@ -201,6 +202,19 @@ class TestCommunication(unittest.TestCase):


self.assertIn(("Note", note.name), doc_links) self.assertIn(("Note", note.name), doc_links)


def parse_emails(self):
emails = get_emails(
[
'comm_recipient+DocType+DocName@example.com',
'"First, LastName" <first.lastname@email.com>',
'test@user.com'
]
)

self.assertEqual(emails[0], "comm_recipient+DocType+DocName@example.com")
self.assertEqual(emails[1], "first.lastname@email.com")
self.assertEqual(emails[2], "test@user.com")

class TestCommunicationEmailMixin(unittest.TestCase): class TestCommunicationEmailMixin(unittest.TestCase):
def new_communication(self, recipients=None, cc=None, bcc=None): def new_communication(self, recipients=None, cc=None, bcc=None):
recipients = ', '.join(recipients or []) recipients = ', '.join(recipients or [])


+ 8
- 1
frappe/core/doctype/docfield/docfield.json Parādīt failu

@@ -17,6 +17,7 @@
"hide_days", "hide_days",
"hide_seconds", "hide_seconds",
"reqd", "reqd",
"is_virtual",
"search_index", "search_index",
"column_break_18", "column_break_18",
"options", "options",
@@ -534,13 +535,19 @@
"fieldname": "show_dashboard", "fieldname": "show_dashboard",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Show Dashboard" "label": "Show Dashboard"
},
{
"default": "0",
"fieldname": "is_virtual",
"fieldtype": "Check",
"label": "Virtual"
} }
], ],
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-01-03 11:56:19.812863",
"modified": "2022-01-27 21:22:20.529072",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Core", "module": "Core",
"name": "DocField", "name": "DocField",


+ 8
- 1
frappe/core/doctype/doctype/doctype.json Parādīt failu

@@ -46,6 +46,7 @@
"allow_auto_repeat", "allow_auto_repeat",
"view_settings", "view_settings",
"title_field", "title_field",
"show_title_field_in_link",
"search_fields", "search_fields",
"default_print_format", "default_print_format",
"sort_field", "sort_field",
@@ -582,6 +583,12 @@
"fieldname": "document_states_section", "fieldname": "document_states_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Document States" "label": "Document States"
},
{
"default": "0",
"fieldname": "show_title_field_in_link",
"fieldtype": "Check",
"label": "Show Title in Link Fields"
} }
], ],
"icon": "fa fa-bolt", "icon": "fa fa-bolt",
@@ -663,7 +670,7 @@
"link_fieldname": "reference_doctype" "link_fieldname": "reference_doctype"
} }
], ],
"modified": "2021-12-09 14:53:10.717788",
"modified": "2022-01-07 16:07:06.196534",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Core", "module": "Core",
"name": "DocType", "name": "DocType",


+ 25
- 20
frappe/core/doctype/doctype/doctype.py Parādīt failu

@@ -781,28 +781,30 @@ def validate_series(dt, autoname=None, name=None):


def validate_links_table_fieldnames(meta): def validate_links_table_fieldnames(meta):
"""Validate fieldnames in Links table""" """Validate fieldnames in Links table"""
if frappe.flags.in_patch: return
if frappe.flags.in_fixtures: return
if not meta.links: return
if not meta.links or frappe.flags.in_patch or frappe.flags.in_fixtures:
return


for index, link in enumerate(meta.links):
fieldnames = tuple(field.fieldname for field in meta.fields)
for index, link in enumerate(meta.links, 1):
link_meta = frappe.get_meta(link.link_doctype) link_meta = frappe.get_meta(link.link_doctype)
if not link_meta.get_field(link.link_fieldname): if not link_meta.get_field(link.link_fieldname):
message = _("Document Links Row #{0}: Could not find field {1} in {2} DocType").format(index+1, frappe.bold(link.link_fieldname), frappe.bold(link.link_doctype))
message = _("Document Links Row #{0}: Could not find field {1} in {2} DocType").format(index, frappe.bold(link.link_fieldname), frappe.bold(link.link_doctype))
frappe.throw(message, InvalidFieldNameError, _("Invalid Fieldname")) frappe.throw(message, InvalidFieldNameError, _("Invalid Fieldname"))


if link.is_child_table and not meta.get_field(link.table_fieldname):
message = _("Document Links Row #{0}: Could not find field {1} in {2} DocType").format(index+1, frappe.bold(link.table_fieldname), frappe.bold(meta.name))
frappe.throw(message, frappe.ValidationError, _("Invalid Table Fieldname"))
if not link.is_child_table:
continue


if link.is_child_table:
if not link.parent_doctype:
message = _("Document Links Row #{0}: Parent DocType is mandatory for internal links").format(index+1)
frappe.throw(message, frappe.ValidationError, _("Parent Missing"))
if not link.parent_doctype:
message = _("Document Links Row #{0}: Parent DocType is mandatory for internal links").format(index)
frappe.throw(message, frappe.ValidationError, _("Parent Missing"))


if not link.table_fieldname:
message = _("Document Links Row #{0}: Table Fieldname is mandatory for internal links").format(index+1)
frappe.throw(message, frappe.ValidationError, _("Table Fieldname Missing"))
if not link.table_fieldname:
message = _("Document Links Row #{0}: Table Fieldname is mandatory for internal links").format(index)
frappe.throw(message, frappe.ValidationError, _("Table Fieldname Missing"))

if link.table_fieldname not in fieldnames:
message = _("Document Links Row #{0}: Could not find field {1} in {2} DocType").format(index, frappe.bold(link.table_fieldname), frappe.bold(meta.name))
frappe.throw(message, frappe.ValidationError, _("Invalid Table Fieldname"))


def validate_fields_for_doctype(doctype): def validate_fields_for_doctype(doctype):
meta = frappe.get_meta(doctype, cached=False) meta = frappe.get_meta(doctype, cached=False)
@@ -1076,6 +1078,9 @@ def validate_fields(meta):
field.fetch_from = field.fetch_from.strip('\n').strip() field.fetch_from = field.fetch_from.strip('\n').strip()


def validate_data_field_type(docfield): def validate_data_field_type(docfield):
if docfield.get("is_virtual"):
return

if docfield.fieldtype == "Data" and not (docfield.oldfieldtype and docfield.oldfieldtype != "Data"): if docfield.fieldtype == "Data" and not (docfield.oldfieldtype and docfield.oldfieldtype != "Data"):
if docfield.options and (docfield.options not in data_field_options): if docfield.options and (docfield.options not in data_field_options):
df_str = frappe.bold(_(docfield.label)) df_str = frappe.bold(_(docfield.label))
@@ -1321,10 +1326,9 @@ def make_module_and_roles(doc, perm_fieldname="permissions"):
else: else:
raise raise


def check_fieldname_conflicts(doctype, fieldname):
def check_fieldname_conflicts(docfield):
"""Checks if fieldname conflicts with methods or properties""" """Checks if fieldname conflicts with methods or properties"""

doc = frappe.get_doc({"doctype": doctype})
doc = frappe.get_doc({"doctype": docfield.dt})
available_objects = [x for x in dir(doc) if isinstance(x, str)] available_objects = [x for x in dir(doc) if isinstance(x, str)]
property_list = [ property_list = [
x for x in available_objects if isinstance(getattr(type(doc), x, None), property) x for x in available_objects if isinstance(getattr(type(doc), x, None), property)
@@ -1332,9 +1336,10 @@ def check_fieldname_conflicts(doctype, fieldname):
method_list = [ method_list = [
x for x in available_objects if x not in property_list and callable(getattr(doc, x)) x for x in available_objects if x not in property_list and callable(getattr(doc, x))
] ]
msg = _("Fieldname {0} conflicting with meta object").format(docfield.fieldname)


if fieldname in method_list + property_list:
frappe.throw(_("Fieldname {0} conflicting with meta object").format(fieldname))
if docfield.fieldname in method_list + property_list:
frappe.msgprint(msg, raise_exception=not docfield.is_virtual)


def clear_linked_doctype_cache(): def clear_linked_doctype_cache():
frappe.cache().delete_value('linked_doctypes_without_ignore_user_permissions_enabled') frappe.cache().delete_value('linked_doctypes_without_ignore_user_permissions_enabled')


+ 1
- 1
frappe/core/doctype/file/file.py Parādīt failu

@@ -745,7 +745,7 @@ def delete_file(path):
"""Delete file from `public folder`""" """Delete file from `public folder`"""
if path: if path:
if ".." in path.split("/"): if ".." in path.split("/"):
frappe.msgprint(_("It is risky to delete this file: {0}. Please contact your System Manager.").format(path))
frappe.throw(_("It is risky to delete this file: {0}. Please contact your System Manager.").format(path))


parts = os.path.split(path.strip("/")) parts = os.path.split(path.strip("/"))
if parts[0]=="files": if parts[0]=="files":


+ 109
- 1
frappe/core/doctype/report/test_report.py Parādīt failu

@@ -3,8 +3,10 @@


import frappe, json, os import frappe, json, os
import unittest import unittest
from frappe.desk.query_report import run, save_report
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.custom.doctype.customize_form.customize_form import reset_customization
from frappe.core.doctype.user_permission.test_user_permission import create_user


test_records = frappe.get_test_records('Report') test_records = frappe.get_test_records('Report')
test_dependencies = ['User'] test_dependencies = ['User']
@@ -30,6 +32,60 @@ class TestReport(unittest.TestCase):
self.assertEqual(columns[1].get('label'), 'Module') self.assertEqual(columns[1].get('label'), 'Module')
self.assertTrue('User' in [d.get('name') for d in data]) self.assertTrue('User' in [d.get('name') for d in data])


def test_save_or_delete_report(self):
'''Test for validations when editing / deleting report of type Report Builder'''

try:
report = frappe.get_doc({
'doctype': 'Report',
'ref_doctype': 'User',
'report_name': 'Test Delete Report',
'report_type': 'Report Builder',
'is_standard': 'No',
}).insert()

# Check for PermissionError
create_user("test_report_owner@example.com", "Website Manager")
frappe.set_user("test_report_owner@example.com")
self.assertRaises(frappe.PermissionError, delete_report, report.name)

# Check for Report Type
frappe.set_user("Administrator")
report.db_set("report_type", "Custom Report")
self.assertRaisesRegex(
frappe.ValidationError,
"Only reports of type Report Builder can be deleted",
delete_report,
report.name
)

# Check if creating and deleting works with proper validations
frappe.set_user("test@example.com")
report_name = _save_report(
'Dummy Report',
'User',
json.dumps([{
'fieldname': 'email',
'fieldtype': 'Data',
'label': 'Email',
'insert_after_index': 0,
'link_field': 'name',
'doctype': 'User',
'options': 'Email',
'width': 100,
'id':'email',
'name': 'Email'
}])
)

doc = frappe.get_doc("Report", report_name)
delete_report(doc.name)

finally:
frappe.set_user("Administrator")
frappe.db.rollback()


def test_custom_report(self): def test_custom_report(self):
reset_customization('User') reset_customization('User')
custom_report_name = save_report( custom_report_name = save_report(
@@ -226,3 +282,55 @@ result = [


# Set user back to administrator # Set user back to administrator
frappe.set_user('Administrator') frappe.set_user('Administrator')

def test_add_total_row_for_tree_reports(self):
report_settings = {
'tree': True,
'parent_field': 'parent_value'
}

columns = [
{
"fieldname": "parent_column",
"label": "Parent Column",
"fieldtype": "Data",
"width": 10
},
{
"fieldname": "column_1",
"label": "Column 1",
"fieldtype": "Float",
"width": 10
},
{
"fieldname": "column_2",
"label": "Column 2",
"fieldtype": "Float",
"width": 10
}
]

result = [
{
"parent_column": "Parent 1",
"column_1": 200,
"column_2": 150.50
},
{
"parent_column": "Child 1",
"column_1": 100,
"column_2": 75.25,
"parent_value": "Parent 1"
},
{
"parent_column": "Child 2",
"column_1": 100,
"column_2": 75.25,
"parent_value": "Parent 1"
}
]

result = add_total_row(result, columns, meta=None, report_settings=report_settings)
self.assertEqual(result[-1][0], "Total")
self.assertEqual(result[-1][1], 200)
self.assertEqual(result[-1][2], 150.50)

+ 2
- 1
frappe/core/page/permission_manager/permission_manager.js Parādīt failu

@@ -347,6 +347,7 @@ frappe.PermissionEngine = class PermissionEngine {
} }


add_check_events() { add_check_events() {
let me = this;
this.body.on("click", ".show-user-permissions", () => { this.body.on("click", ".show-user-permissions", () => {
frappe.route_options = { allow: this.get_doctype() || "" }; frappe.route_options = { allow: this.get_doctype() || "" };
frappe.set_route('List', 'User Permission'); frappe.set_route('List', 'User Permission');
@@ -373,7 +374,7 @@ frappe.PermissionEngine = class PermissionEngine {
// exception: reverse // exception: reverse
chk.prop("checked", !chk.prop("checked")); chk.prop("checked", !chk.prop("checked"));
} else { } else {
this.get_perm(args.role)[args.ptype] = args.value;
me.get_perm(args.role)[args.ptype] = args.value;
} }
} }
}); });


+ 1
- 0
frappe/coverage.py Parādīt failu

@@ -29,6 +29,7 @@ FRAPPE_EXCLUSIONS = [
"*/commands/*", "*/commands/*",
"*/frappe/change_log/*", "*/frappe/change_log/*",
"*/frappe/exceptions*", "*/frappe/exceptions*",
"*/frappe/coverage.py",
"*frappe/setup.py", "*frappe/setup.py",
"*/doctype/*/*_dashboard.py", "*/doctype/*/*_dashboard.py",
"*/patches/*", "*/patches/*",


+ 466
- 456
frappe/custom/doctype/custom_field/custom_field.json Parādīt failu

@@ -1,458 +1,468 @@
{ {
"actions": [],
"allow_import": 1,
"creation": "2013-01-10 16:34:01",
"description": "Adds a custom field to a DocType",
"doctype": "DocType",
"document_type": "Setup",
"engine": "InnoDB",
"field_order": [
"dt",
"module",
"label",
"label_help",
"fieldname",
"insert_after",
"length",
"column_break_6",
"fieldtype",
"precision",
"hide_seconds",
"hide_days",
"options",
"fetch_from",
"fetch_if_empty",
"options_help",
"section_break_11",
"collapsible",
"collapsible_depends_on",
"default",
"depends_on",
"mandatory_depends_on",
"read_only_depends_on",
"properties",
"non_negative",
"reqd",
"unique",
"read_only",
"ignore_user_permissions",
"hidden",
"print_hide",
"print_hide_if_no_value",
"print_width",
"no_copy",
"allow_on_submit",
"in_list_view",
"in_standard_filter",
"in_global_search",
"in_preview",
"bold",
"report_hide",
"search_index",
"allow_in_quick_entry",
"ignore_xss_filter",
"translatable",
"hide_border",
"description",
"permlevel",
"width",
"columns"
],
"fields": [{
"bold": 1,
"fieldname": "dt",
"fieldtype": "Link",
"in_filter": 1,
"in_list_view": 1,
"label": "Document",
"oldfieldname": "dt",
"oldfieldtype": "Link",
"options": "DocType",
"reqd": 1,
"search_index": 1
},
{
"bold": 1,
"fieldname": "label",
"fieldtype": "Data",
"in_filter": 1,
"label": "Label",
"no_copy": 1,
"oldfieldname": "label",
"oldfieldtype": "Data"
},
{
"fieldname": "label_help",
"fieldtype": "HTML",
"label": "Label Help",
"oldfieldtype": "HTML"
},
{
"fieldname": "fieldname",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Fieldname",
"no_copy": 1,
"oldfieldname": "fieldname",
"oldfieldtype": "Data",
"read_only": 1
},
{
"description": "Select the label after which you want to insert new field.",
"fieldname": "insert_after",
"fieldtype": "Select",
"label": "Insert After",
"no_copy": 1,
"oldfieldname": "insert_after",
"oldfieldtype": "Select"
},
{
"fieldname": "column_break_6",
"fieldtype": "Column Break"
},
{
"bold": 1,
"default": "Data",
"fieldname": "fieldtype",
"fieldtype": "Select",
"in_filter": 1,
"in_list_view": 1,
"label": "Field Type",
"oldfieldname": "fieldtype",
"oldfieldtype": "Select",
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature\nTab Break",
"reqd": 1
},
{
"depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
"description": "Set non-standard precision for a Float or Currency field",
"fieldname": "precision",
"fieldtype": "Select",
"label": "Precision",
"options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
},
{
"fieldname": "options",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Options",
"oldfieldname": "options",
"oldfieldtype": "Text"
},
{
"fieldname": "fetch_from",
"fieldtype": "Small Text",
"label": "Fetch From"
},
{
"default": "0",
"description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
"fieldname": "fetch_if_empty",
"fieldtype": "Check",
"label": "Fetch If Empty"
},
{
"fieldname": "options_help",
"fieldtype": "HTML",
"label": "Options Help",
"oldfieldtype": "HTML"
},
{
"fieldname": "section_break_11",
"fieldtype": "Section Break"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
"fieldname": "collapsible",
"fieldtype": "Check",
"label": "Collapsible"
},
{
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
"fieldname": "collapsible_depends_on",
"fieldtype": "Code",
"label": "Collapsible Depends On"
},
{
"fieldname": "default",
"fieldtype": "Text",
"label": "Default Value",
"oldfieldname": "default",
"oldfieldtype": "Text"
},
{
"fieldname": "depends_on",
"fieldtype": "Code",
"label": "Depends On",
"length": 255
},
{
"fieldname": "description",
"fieldtype": "Text",
"label": "Field Description",
"oldfieldname": "description",
"oldfieldtype": "Text",
"print_width": "300px",
"width": "300px"
},
{
"default": "0",
"fieldname": "permlevel",
"fieldtype": "Int",
"label": "Permission Level",
"oldfieldname": "permlevel",
"oldfieldtype": "Int"
},
{
"fieldname": "width",
"fieldtype": "Data",
"label": "Width",
"oldfieldname": "width",
"oldfieldtype": "Data"
},
{
"description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)",
"fieldname": "columns",
"fieldtype": "Int",
"label": "Columns"
},
{
"fieldname": "properties",
"fieldtype": "Column Break",
"oldfieldtype": "Column Break",
"print_width": "50%",
"width": "50%"
},
{
"default": "0",
"fieldname": "reqd",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Is Mandatory Field",
"oldfieldname": "reqd",
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "unique",
"fieldtype": "Check",
"label": "Unique"
},
{
"default": "0",
"fieldname": "read_only",
"fieldtype": "Check",
"label": "Read Only"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype===\"Link\"",
"fieldname": "ignore_user_permissions",
"fieldtype": "Check",
"label": "Ignore User Permissions"
},
{
"default": "0",
"fieldname": "hidden",
"fieldtype": "Check",
"label": "Hidden"
},
{
"default": "0",
"fieldname": "print_hide",
"fieldtype": "Check",
"label": "Print Hide",
"oldfieldname": "print_hide",
"oldfieldtype": "Check"
},
{
"default": "0",
"depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
"fieldname": "print_hide_if_no_value",
"fieldtype": "Check",
"label": "Print Hide If No Value"
},
{
"fieldname": "print_width",
"fieldtype": "Data",
"hidden": 1,
"label": "Print Width",
"no_copy": 1,
"print_hide": 1
},
{
"default": "0",
"fieldname": "no_copy",
"fieldtype": "Check",
"label": "No Copy",
"oldfieldname": "no_copy",
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "allow_on_submit",
"fieldtype": "Check",
"label": "Allow on Submit",
"oldfieldname": "allow_on_submit",
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "in_list_view",
"fieldtype": "Check",
"label": "In List View"
},
{
"default": "0",
"fieldname": "in_standard_filter",
"fieldtype": "Check",
"label": "In Standard Filter"
},
{
"default": "0",
"depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
"fieldname": "in_global_search",
"fieldtype": "Check",
"label": "In Global Search"
},
{
"default": "0",
"fieldname": "bold",
"fieldtype": "Check",
"label": "Bold"
},
{
"default": "0",
"fieldname": "report_hide",
"fieldtype": "Check",
"label": "Report Hide",
"oldfieldname": "report_hide",
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "search_index",
"fieldtype": "Check",
"hidden": 1,
"label": "Index",
"no_copy": 1,
"print_hide": 1
},
{
"default": "0",
"description": "Don't HTML Encode HTML tags like &lt;script&gt; or just characters like &lt; or &gt;, as they could be intentionally used in this field",
"fieldname": "ignore_xss_filter",
"fieldtype": "Check",
"label": "Ignore XSS Filter"
},
{
"default": "1",
"depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
"fieldname": "translatable",
"fieldtype": "Check",
"label": "Translatable"
},
{
"depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)",
"fieldname": "length",
"fieldtype": "Int",
"label": "Length"
},
{
"fieldname": "mandatory_depends_on",
"fieldtype": "Code",
"label": "Mandatory Depends On",
"length": 255
},
{
"fieldname": "read_only_depends_on",
"fieldtype": "Code",
"label": "Read Only Depends On",
"length": 255
},
{
"default": "0",
"fieldname": "allow_in_quick_entry",
"fieldtype": "Check",
"label": "Allow in Quick Entry"
},
{
"default": "0",
"depends_on": "eval:!in_list(['Table', 'Table MultiSelect'], doc.fieldtype);",
"fieldname": "in_preview",
"fieldtype": "Check",
"label": "In Preview"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype=='Duration'",
"fieldname": "hide_seconds",
"fieldtype": "Check",
"label": "Hide Seconds"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype=='Duration'",
"fieldname": "hide_days",
"fieldtype": "Check",
"label": "Hide Days"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype=='Section Break'",
"fieldname": "hide_border",
"fieldtype": "Check",
"label": "Hide Border"
},
{
"default": "0",
"depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)",
"fieldname": "non_negative",
"fieldtype": "Check",
"label": "Non Negative"
},
{
"fieldname": "module",
"fieldtype": "Link",
"label": "Module (for export)",
"options": "Module Def"
}
],
"icon": "fa fa-glass",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-09-04 12:45:23.810120",
"modified_by": "Administrator",
"module": "Custom",
"name": "Custom Field",
"owner": "Administrator",
"permissions": [{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Administrator",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"search_fields": "dt,label,fieldtype,options",
"sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1
"actions": [],
"allow_import": 1,
"creation": "2013-01-10 16:34:01",
"description": "Adds a custom field to a DocType",
"doctype": "DocType",
"document_type": "Setup",
"engine": "InnoDB",
"field_order": [
"dt",
"module",
"label",
"label_help",
"fieldname",
"insert_after",
"length",
"column_break_6",
"fieldtype",
"precision",
"hide_seconds",
"hide_days",
"options",
"fetch_from",
"fetch_if_empty",
"options_help",
"section_break_11",
"collapsible",
"collapsible_depends_on",
"default",
"depends_on",
"mandatory_depends_on",
"read_only_depends_on",
"properties",
"non_negative",
"reqd",
"unique",
"is_virtual",
"read_only",
"ignore_user_permissions",
"hidden",
"print_hide",
"print_hide_if_no_value",
"print_width",
"no_copy",
"allow_on_submit",
"in_list_view",
"in_standard_filter",
"in_global_search",
"in_preview",
"bold",
"report_hide",
"search_index",
"allow_in_quick_entry",
"ignore_xss_filter",
"translatable",
"hide_border",
"description",
"permlevel",
"width",
"columns"
],
"fields": [
{
"bold": 1,
"fieldname": "dt",
"fieldtype": "Link",
"in_filter": 1,
"in_list_view": 1,
"label": "Document",
"oldfieldname": "dt",
"oldfieldtype": "Link",
"options": "DocType",
"reqd": 1,
"search_index": 1
},
{
"bold": 1,
"fieldname": "label",
"fieldtype": "Data",
"in_filter": 1,
"label": "Label",
"no_copy": 1,
"oldfieldname": "label",
"oldfieldtype": "Data"
},
{
"fieldname": "label_help",
"fieldtype": "HTML",
"label": "Label Help",
"oldfieldtype": "HTML"
},
{
"fieldname": "fieldname",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Fieldname",
"no_copy": 1,
"oldfieldname": "fieldname",
"oldfieldtype": "Data",
"read_only": 1
},
{
"description": "Select the label after which you want to insert new field.",
"fieldname": "insert_after",
"fieldtype": "Select",
"label": "Insert After",
"no_copy": 1,
"oldfieldname": "insert_after",
"oldfieldtype": "Select"
},
{
"fieldname": "column_break_6",
"fieldtype": "Column Break"
},
{
"bold": 1,
"default": "Data",
"fieldname": "fieldtype",
"fieldtype": "Select",
"in_filter": 1,
"in_list_view": 1,
"label": "Field Type",
"oldfieldname": "fieldtype",
"oldfieldtype": "Select",
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature\nTab Break",
"reqd": 1
},
{
"depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
"description": "Set non-standard precision for a Float or Currency field",
"fieldname": "precision",
"fieldtype": "Select",
"label": "Precision",
"options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
},
{
"fieldname": "options",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Options",
"oldfieldname": "options",
"oldfieldtype": "Text"
},
{
"fieldname": "fetch_from",
"fieldtype": "Small Text",
"label": "Fetch From"
},
{
"default": "0",
"description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
"fieldname": "fetch_if_empty",
"fieldtype": "Check",
"label": "Fetch If Empty"
},
{
"fieldname": "options_help",
"fieldtype": "HTML",
"label": "Options Help",
"oldfieldtype": "HTML"
},
{
"fieldname": "section_break_11",
"fieldtype": "Section Break"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
"fieldname": "collapsible",
"fieldtype": "Check",
"label": "Collapsible"
},
{
"depends_on": "eval:doc.fieldtype==\"Section Break\"",
"fieldname": "collapsible_depends_on",
"fieldtype": "Code",
"label": "Collapsible Depends On"
},
{
"fieldname": "default",
"fieldtype": "Text",
"label": "Default Value",
"oldfieldname": "default",
"oldfieldtype": "Text"
},
{
"fieldname": "depends_on",
"fieldtype": "Code",
"label": "Depends On",
"length": 255
},
{
"fieldname": "description",
"fieldtype": "Text",
"label": "Field Description",
"oldfieldname": "description",
"oldfieldtype": "Text",
"print_width": "300px",
"width": "300px"
},
{
"default": "0",
"fieldname": "permlevel",
"fieldtype": "Int",
"label": "Permission Level",
"oldfieldname": "permlevel",
"oldfieldtype": "Int"
},
{
"fieldname": "width",
"fieldtype": "Data",
"label": "Width",
"oldfieldname": "width",
"oldfieldtype": "Data"
},
{
"description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)",
"fieldname": "columns",
"fieldtype": "Int",
"label": "Columns"
},
{
"fieldname": "properties",
"fieldtype": "Column Break",
"oldfieldtype": "Column Break",
"print_width": "50%",
"width": "50%"
},
{
"default": "0",
"fieldname": "reqd",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Is Mandatory Field",
"oldfieldname": "reqd",
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "unique",
"fieldtype": "Check",
"label": "Unique"
},
{
"default": "0",
"fieldname": "is_virtual",
"fieldtype": "Check",
"label": "Is Virtual"
},
{
"default": "0",
"fieldname": "read_only",
"fieldtype": "Check",
"label": "Read Only"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype===\"Link\"",
"fieldname": "ignore_user_permissions",
"fieldtype": "Check",
"label": "Ignore User Permissions"
},
{
"default": "0",
"fieldname": "hidden",
"fieldtype": "Check",
"label": "Hidden"
},
{
"default": "0",
"fieldname": "print_hide",
"fieldtype": "Check",
"label": "Print Hide",
"oldfieldname": "print_hide",
"oldfieldtype": "Check"
},
{
"default": "0",
"depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
"fieldname": "print_hide_if_no_value",
"fieldtype": "Check",
"label": "Print Hide If No Value"
},
{
"fieldname": "print_width",
"fieldtype": "Data",
"hidden": 1,
"label": "Print Width",
"no_copy": 1,
"print_hide": 1
},
{
"default": "0",
"fieldname": "no_copy",
"fieldtype": "Check",
"label": "No Copy",
"oldfieldname": "no_copy",
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "allow_on_submit",
"fieldtype": "Check",
"label": "Allow on Submit",
"oldfieldname": "allow_on_submit",
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "in_list_view",
"fieldtype": "Check",
"label": "In List View"
},
{
"default": "0",
"fieldname": "in_standard_filter",
"fieldtype": "Check",
"label": "In Standard Filter"
},
{
"default": "0",
"depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
"fieldname": "in_global_search",
"fieldtype": "Check",
"label": "In Global Search"
},
{
"default": "0",
"fieldname": "bold",
"fieldtype": "Check",
"label": "Bold"
},
{
"default": "0",
"fieldname": "report_hide",
"fieldtype": "Check",
"label": "Report Hide",
"oldfieldname": "report_hide",
"oldfieldtype": "Check"
},
{
"default": "0",
"fieldname": "search_index",
"fieldtype": "Check",
"hidden": 1,
"label": "Index",
"no_copy": 1,
"print_hide": 1
},
{
"default": "0",
"description": "Don't HTML Encode HTML tags like &lt;script&gt; or just characters like &lt; or &gt;, as they could be intentionally used in this field",
"fieldname": "ignore_xss_filter",
"fieldtype": "Check",
"label": "Ignore XSS Filter"
},
{
"default": "1",
"depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
"fieldname": "translatable",
"fieldtype": "Check",
"label": "Translatable"
},
{
"depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)",
"fieldname": "length",
"fieldtype": "Int",
"label": "Length"
},
{
"fieldname": "mandatory_depends_on",
"fieldtype": "Code",
"label": "Mandatory Depends On",
"length": 255
},
{
"fieldname": "read_only_depends_on",
"fieldtype": "Code",
"label": "Read Only Depends On",
"length": 255
},
{
"default": "0",
"fieldname": "allow_in_quick_entry",
"fieldtype": "Check",
"label": "Allow in Quick Entry"
},
{
"default": "0",
"depends_on": "eval:!in_list(['Table', 'Table MultiSelect'], doc.fieldtype);",
"fieldname": "in_preview",
"fieldtype": "Check",
"label": "In Preview"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype=='Duration'",
"fieldname": "hide_seconds",
"fieldtype": "Check",
"label": "Hide Seconds"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype=='Duration'",
"fieldname": "hide_days",
"fieldtype": "Check",
"label": "Hide Days"
},
{
"default": "0",
"depends_on": "eval:doc.fieldtype=='Section Break'",
"fieldname": "hide_border",
"fieldtype": "Check",
"label": "Hide Border"
},
{
"default": "0",
"depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)",
"fieldname": "non_negative",
"fieldtype": "Check",
"label": "Non Negative"
},
{
"fieldname": "module",
"fieldtype": "Link",
"label": "Module (for export)",
"options": "Module Def"
}
],
"icon": "fa fa-glass",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-01-27 21:47:01.065556",
"modified_by": "Administrator",
"module": "Custom",
"name": "Custom Field",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Administrator",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"search_fields": "dt,label,fieldtype,options",
"sort_field": "modified",
"sort_order": "ASC",
"states": [],
"track_changes": 1
} }

+ 2
- 2
frappe/custom/doctype/custom_field/custom_field.py Parādīt failu

@@ -54,7 +54,7 @@ class CustomField(Document):
old_fieldtype = self.db_get('fieldtype') old_fieldtype = self.db_get('fieldtype')
is_fieldtype_changed = (not self.is_new()) and (old_fieldtype != self.fieldtype) is_fieldtype_changed = (not self.is_new()) and (old_fieldtype != self.fieldtype)


if is_fieldtype_changed and not CustomizeForm.allow_fieldtype_change(old_fieldtype, self.fieldtype):
if not self.is_virtual and is_fieldtype_changed and not CustomizeForm.allow_fieldtype_change(old_fieldtype, self.fieldtype):
frappe.throw(_("Fieldtype cannot be changed from {0} to {1}").format(old_fieldtype, self.fieldtype)) frappe.throw(_("Fieldtype cannot be changed from {0} to {1}").format(old_fieldtype, self.fieldtype))


if not self.fieldname: if not self.fieldname:
@@ -65,7 +65,7 @@ class CustomField(Document):


if not self.flags.ignore_validate: if not self.flags.ignore_validate:
from frappe.core.doctype.doctype.doctype import check_fieldname_conflicts from frappe.core.doctype.doctype.doctype import check_fieldname_conflicts
check_fieldname_conflicts(self.dt, self.fieldname)
check_fieldname_conflicts(self)


def on_update(self): def on_update(self):
if not frappe.flags.in_setup_wizard: if not frappe.flags.in_setup_wizard:


+ 8
- 1
frappe/custom/doctype/customize_form/customize_form.json Parādīt failu

@@ -27,6 +27,7 @@
"autoname", "autoname",
"view_settings_section", "view_settings_section",
"title_field", "title_field",
"show_title_field_in_link",
"image_field", "image_field",
"default_print_format", "default_print_format",
"column_break_29", "column_break_29",
@@ -296,6 +297,12 @@
"fieldtype": "Table", "fieldtype": "Table",
"label": "States", "label": "States",
"options": "DocType State" "options": "DocType State"
},
{
"default": "0",
"fieldname": "show_title_field_in_link",
"fieldtype": "Check",
"label": "Show Title in Link Fields"
} }
], ],
"hide_toolbar": 1, "hide_toolbar": 1,
@@ -304,7 +311,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2021-12-14 16:45:04.308690",
"modified": "2022-01-07 16:07:06.196534",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Custom", "module": "Custom",
"name": "Customize Form", "name": "Customize Form",


+ 9
- 3
frappe/custom/doctype/customize_form/customize_form.py Parādīt failu

@@ -418,6 +418,9 @@ class CustomizeForm(Document):
return property_value return property_value


def validate_fieldtype_change(self, df, old_value, new_value): def validate_fieldtype_change(self, df, old_value, new_value):
if df.is_virtual:
return

allowed = self.allow_fieldtype_change(old_value, new_value) allowed = self.allow_fieldtype_change(old_value, new_value)
if allowed: if allowed:
old_value_length = cint(frappe.db.type_map.get(old_value)[1]) old_value_length = cint(frappe.db.type_map.get(old_value)[1])
@@ -430,7 +433,8 @@ class CustomizeForm(Document):
self.validate_fieldtype_length() self.validate_fieldtype_length()
else: else:
self.flags.update_db = True self.flags.update_db = True
if not allowed:

else:
frappe.throw(_("Fieldtype cannot be changed from {0} to {1} in row {2}").format(old_value, new_value, df.idx)) frappe.throw(_("Fieldtype cannot be changed from {0} to {1} in row {2}").format(old_value, new_value, df.idx))


def validate_fieldtype_length(self): def validate_fieldtype_length(self):
@@ -512,7 +516,8 @@ doctype_properties = {
'email_append_to': 'Check', 'email_append_to': 'Check',
'subject_field': 'Data', 'subject_field': 'Data',
'sender_field': 'Data', 'sender_field': 'Data',
'autoname': 'Data'
'autoname': 'Data',
'show_title_field_in_link': 'Check'
} }


docfield_properties = { docfield_properties = {
@@ -558,7 +563,8 @@ docfield_properties = {
'allow_in_quick_entry': 'Check', 'allow_in_quick_entry': 'Check',
'hide_border': 'Check', 'hide_border': 'Check',
'hide_days': 'Check', 'hide_days': 'Check',
'hide_seconds': 'Check'
'hide_seconds': 'Check',
'is_virtual': 'Check',
} }


doctype_link_properties = { doctype_link_properties = {


+ 8
- 1
frappe/custom/doctype/customize_form_field/customize_form_field.json Parādīt failu

@@ -14,6 +14,7 @@
"non_negative", "non_negative",
"reqd", "reqd",
"unique", "unique",
"is_virtual",
"in_list_view", "in_list_view",
"in_standard_filter", "in_standard_filter",
"in_global_search", "in_global_search",
@@ -115,6 +116,12 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Unique" "label": "Unique"
}, },
{
"default": "0",
"fieldname": "is_virtual",
"fieldtype": "Check",
"label": "Is Virtual"
},
{ {
"default": "0", "default": "0",
"fieldname": "in_list_view", "fieldname": "in_list_view",
@@ -436,7 +443,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-01-03 14:50:32.035768",
"modified": "2022-01-27 21:45:22.349776",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Custom", "module": "Custom",
"name": "Customize Form Field", "name": "Customize Form Field",


+ 11
- 44
frappe/custom/doctype/property_setter/property_setter.py Parādīt failu

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


import frappe import frappe
@@ -18,53 +18,19 @@ class PropertySetter(Document):


def validate(self): def validate(self):
self.validate_fieldtype_change() self.validate_fieldtype_change()

if self.is_new(): if self.is_new():
delete_property_setter(self.doc_type, self.property, self.field_name, self.row_name) delete_property_setter(self.doc_type, self.property, self.field_name, self.row_name)

# clear cache
frappe.clear_cache(doctype = self.doc_type) frappe.clear_cache(doctype = self.doc_type)


def validate_fieldtype_change(self): def validate_fieldtype_change(self):
if self.field_name in not_allowed_fieldtype_change and \
self.property == 'fieldtype':
frappe.throw(_("Field type cannot be changed for {0}").format(self.field_name))

def get_property_list(self, dt):
return frappe.db.get_all('DocField',
fields=['fieldname', 'label', 'fieldtype'],
filters={
'parent': dt,
'fieldtype': ['not in', ('Section Break', 'Column Break', 'Tab Break', 'HTML', 'Read Only', 'Fold') + frappe.model.table_fields],
'fieldname': ['!=', '']
},
order_by='label asc',
as_dict=1
)

def get_setup_data(self):
return {
'doctypes': frappe.get_all("DocType", pluck="name"),
'dt_properties': self.get_property_list('DocType'),
'df_properties': self.get_property_list('DocField')
}

def get_field_ids(self):
return frappe.db.get_values(
"DocField",
filters={"parent": self.doc_type},
fieldname=["name", "fieldtype", "label", "fieldname"],
as_dict=True,
)

def get_defaults(self):
if not self.field_name:
return frappe.get_all("DocType", filters={"name": self.doc_type}, fields="*")[0]
else:
return frappe.db.get_values(
"DocField",
filters={"fieldname": self.field_name, "parent": self.doc_type},
fieldname="*",
)[0]
if (
self.property == 'fieldtype'
and self.field_name in not_allowed_fieldtype_change
):
frappe.throw(
_("Field type cannot be changed for {0}").format(self.field_name)
)


def on_update(self): def on_update(self):
if frappe.flags.in_patch: if frappe.flags.in_patch:
@@ -74,6 +40,7 @@ class PropertySetter(Document):
from frappe.core.doctype.doctype.doctype import validate_fields_for_doctype from frappe.core.doctype.doctype.doctype import validate_fields_for_doctype
validate_fields_for_doctype(self.doc_type) validate_fields_for_doctype(self.doc_type)



def make_property_setter(doctype, fieldname, property, value, property_type, for_doctype = False, def make_property_setter(doctype, fieldname, property, value, property_type, for_doctype = False,
validate_fields_for_doctype=True): validate_fields_for_doctype=True):
# WARNING: Ignores Permissions # WARNING: Ignores Permissions
@@ -91,6 +58,7 @@ def make_property_setter(doctype, fieldname, property, value, property_type, for
property_setter.insert() property_setter.insert()
return property_setter return property_setter



def delete_property_setter(doc_type, property, field_name=None, row_name=None): def delete_property_setter(doc_type, property, field_name=None, row_name=None):
"""delete other property setters on this, if this is new""" """delete other property setters on this, if this is new"""
filters = dict(doc_type=doc_type, property=property) filters = dict(doc_type=doc_type, property=property)
@@ -100,4 +68,3 @@ def delete_property_setter(doc_type, property, field_name=None, row_name=None):
filters["row_name"] = row_name filters["row_name"] = row_name


frappe.db.delete('Property Setter', filters) frappe.db.delete('Property Setter', filters)


+ 2
- 0
frappe/database/database.py Parādīt failu

@@ -177,6 +177,8 @@ class Database(object):
raise frappe.QueryTimeoutError(e) raise frappe.QueryTimeoutError(e)


elif frappe.conf.db_type == 'postgres': elif frappe.conf.db_type == 'postgres':
# TODO: added temporarily
print(e)
raise raise


if ignore_ddl and (self.is_missing_column(e) or self.is_missing_table(e) or self.cant_drop_field_or_key(e)): if ignore_ddl and (self.is_missing_column(e) or self.is_missing_table(e) or self.cant_drop_field_or_key(e)):


+ 1
- 0
frappe/database/mariadb/framework_mariadb.sql Parādīt failu

@@ -224,6 +224,7 @@ CREATE TABLE `tabDocType` (
`email_append_to` int(1) NOT NULL DEFAULT 0, `email_append_to` int(1) NOT NULL DEFAULT 0,
`subject_field` varchar(255) DEFAULT NULL, `subject_field` varchar(255) DEFAULT NULL,
`sender_field` varchar(255) DEFAULT NULL, `sender_field` varchar(255) DEFAULT NULL,
`show_title_field_in_link` int(1) NOT NULL DEFAULT 0,
`migration_hash` varchar(255) DEFAULT NULL, `migration_hash` varchar(255) DEFAULT NULL,
PRIMARY KEY (`name`) PRIMARY KEY (`name`)
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;


+ 1
- 0
frappe/database/postgres/framework_postgres.sql Parādīt failu

@@ -229,6 +229,7 @@ CREATE TABLE "tabDocType" (
"email_append_to" smallint NOT NULL DEFAULT 0, "email_append_to" smallint NOT NULL DEFAULT 0,
"subject_field" varchar(255) DEFAULT NULL, "subject_field" varchar(255) DEFAULT NULL,
"sender_field" varchar(255) DEFAULT NULL, "sender_field" varchar(255) DEFAULT NULL,
"show_title_field_in_link" smallint NOT NULL DEFAULT 0,
"migration_hash" varchar(255) DEFAULT NULL, "migration_hash" varchar(255) DEFAULT NULL,
PRIMARY KEY ("name") PRIMARY KEY ("name")
) ; ) ;


+ 4
- 1
frappe/database/schema.py Parādīt failu

@@ -67,7 +67,7 @@ class DBTable:
""" """
get columns from docfields and custom fields get columns from docfields and custom fields
""" """
fields = self.meta.get_fieldnames_with_value(True)
fields = self.meta.get_fieldnames_with_value(with_field_meta=True)


# optional fields like _comments # optional fields like _comments
if not self.meta.get('istable'): if not self.meta.get('istable'):
@@ -85,6 +85,9 @@ class DBTable:
}) })


for field in fields: for field in fields:
if field.get("is_virtual"):
continue

self.columns[field.get('fieldname')] = DbColumn( self.columns[field.get('fieldname')] = DbColumn(
self, self,
field.get('fieldname'), field.get('fieldname'),


+ 16
- 0
frappe/desk/doctype/dashboard/dashboard_list.js Parādīt failu

@@ -0,0 +1,16 @@
frappe.listview_settings['Dashboard'] = {
button: {
show(doc) {
return doc.name;
},
get_label() {
return frappe.utils.icon("dashboard-list", "sm");
},
get_description(doc) {
return __('View {0}', [`${doc.name}`]);
},
action(doc) {
frappe.set_route('dashboard-view', doc.name);
}
},
};

+ 56
- 1
frappe/desk/form/load.py Parādīt failu

@@ -49,7 +49,7 @@ def getdoc(doctype, name, user=None):
raise raise


doc.add_seen() doc.add_seen()
set_link_titles(doc)
frappe.response.docs.append(doc) frappe.response.docs.append(doc)


@frappe.whitelist() @frappe.whitelist()
@@ -367,6 +367,60 @@ def get_additional_timeline_content(doctype, docname):


return contents return contents


def set_link_titles(doc):
link_titles = {}
link_titles.update(get_title_values_for_link_and_dynamic_link_fields(doc))
link_titles.update(get_title_values_for_table_and_multiselect_fields(doc))

send_link_titles(link_titles)

def get_title_values_for_link_and_dynamic_link_fields(doc, link_fields=None):
link_titles = {}

if not link_fields:
meta = frappe.get_meta(doc.doctype)
link_fields = meta.get_link_fields() + meta.get_dynamic_link_fields()

for field in link_fields:
if not doc.get(field.fieldname):
continue

doctype = field.options if field.fieldtype == "Link" else doc.get(field.options)

meta = frappe.get_meta(doctype)
if not meta or not (meta.title_field and meta.show_title_field_in_link):
continue

link_title = frappe.db.get_value(
doctype, doc.get(field.fieldname), meta.title_field, cache=True
)
link_titles.update({doctype + "::" + doc.get(field.fieldname): link_title})

return link_titles

def get_title_values_for_table_and_multiselect_fields(doc, table_fields=None):
link_titles = {}

if not table_fields:
meta = frappe.get_meta(doc.doctype)
table_fields = meta.get_table_fields()

for field in table_fields:
if not doc.get(field.fieldname):
continue

for value in doc.get(field.fieldname):
link_titles.update(get_title_values_for_link_and_dynamic_link_fields(value))

return link_titles

def send_link_titles(link_titles):
"""Append link titles dict in `frappe.local.response`."""
if "_link_titles" not in frappe.local.response:
frappe.local.response["_link_titles"] = {}

frappe.local.response["_link_titles"].update(link_titles)

def update_user_info(docinfo): def update_user_info(docinfo):
for d in docinfo.communications: for d in docinfo.communications:
frappe.utils.add_user_info(d.sender, docinfo.user_info) frappe.utils.add_user_info(d.sender, docinfo.user_info)
@@ -387,3 +441,4 @@ def get_user_info_for_viewers(users):
frappe.utils.add_user_info(user, user_info) frappe.utils.add_user_info(user, user_info)


return user_info return user_info


+ 17
- 7
frappe/desk/query_report.py Parādīt failu

@@ -73,7 +73,7 @@ def get_report_result(report, filters):
return res return res


@frappe.read_only() @frappe.read_only()
def generate_report_result(report, filters=None, user=None, custom_columns=None):
def generate_report_result(report, filters=None, user=None, custom_columns=None, report_settings=None):
user = user or frappe.session.user user = user or frappe.session.user
filters = filters or [] filters = filters or []


@@ -108,7 +108,7 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None)
result = get_filtered_data(report.ref_doctype, columns, result, user) result = get_filtered_data(report.ref_doctype, columns, result, user)


if cint(report.add_total_row) and result and not skip_total_row: if cint(report.add_total_row) and result and not skip_total_row:
result = add_total_row(result, columns)
result = add_total_row(result, columns, report_settings=report_settings)


return { return {
"result": result, "result": result,
@@ -210,7 +210,7 @@ def get_script(report_name):


@frappe.whitelist() @frappe.whitelist()
@frappe.read_only() @frappe.read_only()
def run(report_name, filters=None, user=None, ignore_prepared_report=False, custom_columns=None):
def run(report_name, filters=None, user=None, ignore_prepared_report=False, custom_columns=None, report_settings=None):
report = get_report_doc(report_name) report = get_report_doc(report_name)
if not user: if not user:
user = frappe.session.user user = frappe.session.user
@@ -238,7 +238,7 @@ def run(report_name, filters=None, user=None, ignore_prepared_report=False, cust
dn = "" dn = ""
result = get_prepared_report_result(report, filters, dn, user) result = get_prepared_report_result(report, filters, dn, user)
else: else:
result = generate_report_result(report, filters, user, custom_columns)
result = generate_report_result(report, filters, user, custom_columns, report_settings)


result["add_total_row"] = report.add_total_row and not result.get( result["add_total_row"] = report.add_total_row and not result.get(
"skip_total_row", False "skip_total_row", False
@@ -435,9 +435,19 @@ def build_xlsx_data(columns, data, visible_idx, include_indentation, ignore_visi
return result, column_widths return result, column_widths




def add_total_row(result, columns, meta=None):
def add_total_row(result, columns, meta=None, report_settings=None):
total_row = [""] * len(columns) total_row = [""] * len(columns)
has_percent = [] has_percent = []
is_tree = False
parent_field = ''

if report_settings:
if isinstance(report_settings, (str,)):
report_settings = json.loads(report_settings)

is_tree = report_settings.get('tree')
parent_field = report_settings.get('parent_field')

for i, col in enumerate(columns): for i, col in enumerate(columns):
fieldtype, options, fieldname = None, None, None fieldtype, options, fieldname = None, None, None
if isinstance(col, str): if isinstance(col, str):
@@ -464,12 +474,12 @@ def add_total_row(result, columns, meta=None):
for row in result: for row in result:
if i >= len(row): if i >= len(row):
continue continue

cell = row.get(fieldname) if isinstance(row, dict) else row[i] cell = row.get(fieldname) if isinstance(row, dict) else row[i]
if fieldtype in ["Currency", "Int", "Float", "Percent", "Duration"] and flt( if fieldtype in ["Currency", "Int", "Float", "Percent", "Duration"] and flt(
cell cell
): ):
total_row[i] = flt(total_row[i]) + flt(cell)
if not (is_tree and row.get(parent_field)):
total_row[i] = flt(total_row[i]) + flt(cell)


if fieldtype == "Percent" and i not in has_percent: if fieldtype == "Percent" and i not in has_percent:
has_percent.append(i) has_percent.append(i)


+ 59
- 15
frappe/desk/reportview.py Parādīt failu

@@ -262,22 +262,66 @@ def compress(data, args=None):
} }


@frappe.whitelist() @frappe.whitelist()
def save_report():
"""save report"""

data = frappe.local.form_dict
if frappe.db.exists('Report', data['name']):
d = frappe.get_doc('Report', data['name'])
def save_report(name, doctype, report_settings):
"""Save reports of type Report Builder from Report View"""

if frappe.db.exists('Report', name):
report = frappe.get_doc('Report', name)
if report.is_standard == "Yes":
frappe.throw(_("Standard Reports cannot be edited"))

if report.report_type != "Report Builder":
frappe.throw(_("Only reports of type Report Builder can be edited"))

if (
report.owner != frappe.session.user
and not frappe.has_permission("Report", "write")
):
frappe.throw(
_("Insufficient Permissions for editing Report"),
frappe.PermissionError
)
else: else:
d = frappe.new_doc('Report')
d.report_name = data['name']
d.ref_doctype = data['doctype']

d.report_type = "Report Builder"
d.json = data['json']
frappe.get_doc(d).save()
frappe.msgprint(_("{0} is saved").format(d.name), alert=True)
return d.name
report = frappe.new_doc('Report')
report.report_name = name
report.ref_doctype = doctype

report.report_type = "Report Builder"
report.json = report_settings
report.save(ignore_permissions=True)
frappe.msgprint(
_("Report {0} saved").format(frappe.bold(report.name)),
indicator="green",
alert=True,
)
return report.name

@frappe.whitelist()
def delete_report(name):
"""Delete reports of type Report Builder from Report View"""

report = frappe.get_doc("Report", name)
if report.is_standard == "Yes":
frappe.throw(_("Standard Reports cannot be deleted"))

if report.report_type != "Report Builder":
frappe.throw(_("Only reports of type Report Builder can be deleted"))

if (
report.owner != frappe.session.user
and not frappe.has_permission("Report", "delete")
):
frappe.throw(
_("Insufficient Permissions for deleting Report"),
frappe.PermissionError
)

report.delete(ignore_permissions=True)
frappe.msgprint(
_("Report {0} deleted").format(frappe.bold(report.name)),
indicator="green",
alert=True,
)


@frappe.whitelist() @frappe.whitelist()
@frappe.read_only() @frappe.read_only()


+ 50
- 6
frappe/desk/search.py Parādīt failu

@@ -49,8 +49,10 @@ def sanitize_searchfield(searchfield):
# this is called by the Link Field # this is called by the Link Field
@frappe.whitelist() @frappe.whitelist()
def search_link(doctype, txt, query=None, filters=None, page_length=20, searchfield=None, reference_doctype=None, ignore_user_permissions=False): def search_link(doctype, txt, query=None, filters=None, page_length=20, searchfield=None, reference_doctype=None, ignore_user_permissions=False):
search_widget(doctype, txt.strip(), query, searchfield=searchfield, page_length=page_length, filters=filters, reference_doctype=reference_doctype, ignore_user_permissions=ignore_user_permissions)
frappe.response['results'] = build_for_autosuggest(frappe.response["values"])
search_widget(doctype, txt.strip(), query, searchfield=searchfield, page_length=page_length, filters=filters,
reference_doctype=reference_doctype, ignore_user_permissions=ignore_user_permissions)

frappe.response["results"] = build_for_autosuggest(frappe.response["values"], doctype=doctype)
del frappe.response["values"] del frappe.response["values"]


# this is called by the search box # this is called by the search box
@@ -138,6 +140,12 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0,
fields = list(set(fields + json.loads(filter_fields))) fields = list(set(fields + json.loads(filter_fields)))
formatted_fields = ['`tab%s`.`%s`' % (meta.name, f.strip()) for f in fields] formatted_fields = ['`tab%s`.`%s`' % (meta.name, f.strip()) for f in fields]


title_field_query = get_title_field_query(meta)

# Insert title field query after name
if title_field_query:
formatted_fields.insert(1, title_field_query)

# find relevance as location of search term from the beginning of string `name`. used for sorting results. # find relevance as location of search term from the beginning of string `name`. used for sorting results.
formatted_fields.append("""locate({_txt}, `tab{doctype}`.`name`) as `_relevance`""".format( formatted_fields.append("""locate({_txt}, `tab{doctype}`.`name`) as `_relevance`""".format(
_txt=frappe.db.escape((txt or "").replace("%", "").replace("@", "")), doctype=doctype)) _txt=frappe.db.escape((txt or "").replace("%", "").replace("@", "")), doctype=doctype))
@@ -205,11 +213,38 @@ def get_std_fields_list(meta, key):


return sflist return sflist


def build_for_autosuggest(res):
def get_title_field_query(meta):
title_field = meta.title_field if meta.title_field else None
show_title_field_in_link = meta.show_title_field_in_link if meta.show_title_field_in_link else None
field = None

if title_field and show_title_field_in_link:
field = "`tab{0}`.{1} as `label`".format(meta.name, title_field)

return field

def build_for_autosuggest(res, doctype):
results = [] results = []
for r in res:
out = {"value": r[0], "description": ", ".join(unique(cstr(d) for d in r if d)[1:])}
results.append(out)
meta = frappe.get_meta(doctype)
if not (meta.title_field and meta.show_title_field_in_link):
for r in res:
r = list(r)
results.append({
"value": r[0],
"description": ", ".join(unique(cstr(d) for d in r[1:] if d))
})

else:
title_field_exists = meta.title_field and meta.show_title_field_in_link
_from = 2 if title_field_exists else 1 # to exclude title from description if title_field_exists
for r in res:
r = list(r)
results.append({
"value": r[0],
"label": r[1] if title_field_exists else None,
"description": ", ".join(unique(cstr(d) for d in r[_from:] if d))
})

return results return results


def scrub_custom_query(query, key, txt): def scrub_custom_query(query, key, txt):
@@ -272,3 +307,12 @@ def get_user_groups():
return frappe.get_all('User Group', fields=['name as id', 'name as value'], update={ return frappe.get_all('User Group', fields=['name as id', 'name as value'], update={
'is_group': True 'is_group': True
}) })

@frappe.whitelist()
def get_link_title(doctype, docname):
meta = frappe.get_meta(doctype)

if meta.title_field and meta.show_title_field_in_link:
return frappe.db.get_value(doctype, docname, meta.title_field)

return docname

+ 1
- 2
frappe/installer.py Parādīt failu

@@ -529,10 +529,9 @@ def extract_sql_gzip(sql_gz_path):
import subprocess import subprocess


try: try:
# dvf - decompress, verbose, force
original_file = sql_gz_path original_file = sql_gz_path
decompressed_file = original_file.rstrip(".gz") decompressed_file = original_file.rstrip(".gz")
cmd = 'gzip -dvf < {0} > {1}'.format(original_file, decompressed_file)
cmd = 'gzip --decompress --force < {0} > {1}'.format(original_file, decompressed_file)
subprocess.check_call(cmd, shell=True) subprocess.check_call(cmd, shell=True)
except Exception: except Exception:
raise raise


+ 130
- 59
frappe/migrate.py Parādīt failu

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


import json import json
import os import os
import sys
from textwrap import dedent

import frappe import frappe
import frappe.translate
import frappe.modules.patch_handler
import frappe.model.sync import frappe.model.sync
from frappe.utils.fixtures import sync_fixtures
from frappe.utils.connections import check_connection
from frappe.utils.dashboard import sync_dashboards
import frappe.modules.patch_handler
import frappe.translate
from frappe.cache_manager import clear_global_cache from frappe.cache_manager import clear_global_cache
from frappe.desk.notifications import clear_notifications
from frappe.website.utils import clear_website_cache
from frappe.core.doctype.language.language import sync_languages from frappe.core.doctype.language.language import sync_languages
from frappe.modules.utils import sync_customizations
from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
from frappe.search.website_search import build_index_for_all_routes
from frappe.database.schema import add_column from frappe.database.schema import add_column
from frappe.desk.notifications import clear_notifications
from frappe.modules.patch_handler import PatchType from frappe.modules.patch_handler import PatchType
from frappe.modules.utils import sync_customizations
from frappe.search.website_search import build_index_for_all_routes
from frappe.utils.connections import check_connection
from frappe.utils.dashboard import sync_dashboards
from frappe.utils.fixtures import sync_fixtures
from frappe.website.utils import clear_website_cache

BENCH_START_MESSAGE = dedent(
"""
Cannot run bench migrate without the services running.
If you are running bench in development mode, make sure that bench is running:

$ bench start


Otherwise, check the server logs and ensure that all the required services are running.
"""
)




def migrate(verbose=True, skip_failing=False, skip_search_index=False):
'''Migrate all apps to the current version, will:
def atomic(method):
def wrapper(*args, **kwargs):
try:
ret = method(*args, **kwargs)
frappe.db.commit()
return ret
except Exception:
frappe.db.rollback()
raise

return wrapper


class SiteMigration:
"""Migrate all apps to the current version, will:
- run before migrate hooks - run before migrate hooks
- run patches - run patches
- sync doctypes (schema) - sync doctypes (schema)
@@ -35,70 +59,117 @@ def migrate(verbose=True, skip_failing=False, skip_search_index=False):
- sync languages - sync languages
- sync web pages (from /www) - sync web pages (from /www)
- run after migrate hooks - run after migrate hooks
'''

service_status = check_connection(redis_services=["redis_cache"])
if False in service_status.values():
for service in service_status:
if not service_status.get(service, True):
print("{} service is not running.".format(service))
print("""Cannot run bench migrate without the services running.
If you are running bench in development mode, make sure that bench is running:
"""


$ bench start
def __init__(self, skip_failing: bool = False, skip_search_index: bool = False) -> None:
self.skip_failing = skip_failing
self.skip_search_index = skip_search_index


Otherwise, check the server logs and ensure that all the required services are running.""")
sys.exit(1)
def setUp(self):
"""Complete setup required for site migration
"""
frappe.flags.touched_tables = set()
self.touched_tables_file = frappe.get_site_path("touched_tables.json")
add_column(doctype="DocType", column_name="migration_hash", fieldtype="Data")
clear_global_cache()


touched_tables_file = frappe.get_site_path('touched_tables.json')
if os.path.exists(touched_tables_file):
os.remove(touched_tables_file)
if os.path.exists(self.touched_tables_file):
os.remove(self.touched_tables_file)


try:
add_column(doctype="DocType", column_name="migration_hash", fieldtype="Data")
frappe.flags.touched_tables = set()
frappe.flags.in_migrate = True frappe.flags.in_migrate = True


clear_global_cache()
def tearDown(self):
"""Run operations that should be run post schema updation processes
This should be executed irrespective of outcome
"""
frappe.translate.clear_cache()
clear_website_cache()
clear_notifications()

with open(self.touched_tables_file, "w") as f:
json.dump(list(frappe.flags.touched_tables), f, sort_keys=True, indent=4)

if not self.skip_search_index:
print(f"Building search index for {frappe.local.site}")
build_index_for_all_routes()

frappe.publish_realtime("version-update")
frappe.flags.touched_tables.clear()
frappe.flags.in_migrate = False


@atomic
def pre_schema_updates(self):
"""Executes `before_migrate` hooks
"""
for app in frappe.get_installed_apps(): for app in frappe.get_installed_apps():
for fn in frappe.get_hooks('before_migrate', app_name=app):
for fn in frappe.get_hooks("before_migrate", app_name=app):
frappe.get_attr(fn)() frappe.get_attr(fn)()


frappe.modules.patch_handler.run_all(skip_failing=skip_failing, patch_type=PatchType.pre_model_sync)
@atomic
def run_schema_updates(self):
"""Run patches as defined in patches.txt, sync schema changes as defined in the {doctype}.json files
"""
frappe.modules.patch_handler.run_all(skip_failing=self.skip_failing, patch_type=PatchType.pre_model_sync)
frappe.model.sync.sync_all() frappe.model.sync.sync_all()
frappe.modules.patch_handler.run_all(skip_failing=skip_failing, patch_type=PatchType.post_model_sync)
frappe.translate.clear_cache()
frappe.modules.patch_handler.run_all(skip_failing=self.skip_failing, patch_type=PatchType.post_model_sync)

@atomic
def post_schema_updates(self):
"""Execute pending migration tasks post patches execution & schema sync
This includes:
* Sync `Scheduled Job Type` and scheduler events defined in hooks
* Sync fixtures & custom scripts
* Sync in-Desk Module Dashboards
* Sync customizations: Custom Fields, Property Setters, Custom Permissions
* Sync Frappe's internal language master
* Sync Portal Menu Items
* Sync Installed Applications Version History
* Execute `after_migrate` hooks
"""
sync_jobs() sync_jobs()
sync_fixtures() sync_fixtures()
sync_dashboards() sync_dashboards()
sync_customizations() sync_customizations()
sync_languages() sync_languages()


frappe.get_doc('Portal Settings', 'Portal Settings').sync_menu()

# syncs static files
clear_website_cache()

# updating installed applications data
frappe.get_single('Installed Applications').update_versions()
frappe.get_single("Portal Settings").sync_menu()
frappe.get_single("Installed Applications").update_versions()


for app in frappe.get_installed_apps(): for app in frappe.get_installed_apps():
for fn in frappe.get_hooks('after_migrate', app_name=app):
for fn in frappe.get_hooks("after_migrate", app_name=app):
frappe.get_attr(fn)() frappe.get_attr(fn)()


if not skip_search_index:
# Run this last as it updates the current session
print('Building search index for {}'.format(frappe.local.site))
build_index_for_all_routes()

frappe.db.commit()

clear_notifications()

frappe.publish_realtime("version-update")
frappe.flags.in_migrate = False
finally:
with open(touched_tables_file, 'w') as f:
json.dump(list(frappe.flags.touched_tables), f, sort_keys=True, indent=4)
frappe.flags.touched_tables.clear()
def required_services_running(self) -> bool:
"""Returns True if all required services are running. Returns False and prints
instructions to stdout when required services are not available.
"""
service_status = check_connection(redis_services=["redis_cache"])
are_services_running = all(service_status.values())

if not are_services_running:
for service in service_status:
if not service_status.get(service, True):
print(f"Service {service} is not running.")
print(BENCH_START_MESSAGE)

return are_services_running

def run(self, site: str):
"""Run Migrate operation on site specified. This method initializes
and destroys connections to the site database.
"""
if not self.required_services_running():
raise SystemExit(1)

if site:
frappe.init(site=site)
frappe.connect()

self.setUp()
try:
self.pre_schema_updates()
self.run_schema_updates()
finally:
self.post_schema_updates()
self.tearDown()
frappe.destroy()

+ 84
- 37
frappe/model/base_document.py Parādīt failu

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


import frappe import frappe
import datetime
from frappe import _ from frappe import _
from frappe.model import default_fields, table_fields, child_table_fields
from frappe.model import child_table_fields, default_fields, display_fieldtypes, table_fields
from frappe.model.naming import set_new_name from frappe.model.naming import set_new_name
from frappe.model.utils.link_count import notify_link_count from frappe.model.utils.link_count import notify_link_count
from frappe.modules import load_doctype_module from frappe.modules import load_doctype_module
from frappe.model import display_fieldtypes
from frappe.utils import (cint, flt, now, cstr, strip_html,
sanitize_html, sanitize_email, cast_fieldtype)
from frappe.utils import cast_fieldtype, cint, cstr, flt, now, sanitize_html, strip_html
from frappe.utils.html_utils import unescape_html from frappe.utils.html_utils import unescape_html
from frappe.model.docstatus import DocStatus from frappe.model.docstatus import DocStatus


@@ -35,13 +33,12 @@ def get_controller(doctype):


module_name, custom = frappe.db.get_value( module_name, custom = frappe.db.get_value(
"DocType", doctype, ("module", "custom"), cache=True "DocType", doctype, ("module", "custom"), cache=True
) or ["Core", False]
) or ("Core", False)


if custom: if custom:
if frappe.db.field_exists("DocType", "is_tree"):
is_tree = frappe.db.get_value("DocType", doctype, "is_tree", cache=True)
else:
is_tree = False
is_tree = frappe.db.get_value(
"DocType", doctype, "is_tree", ignore=True, cache=True
)
_class = NestedSet if is_tree else Document _class = NestedSet if is_tree else Document
else: else:
class_overrides = frappe.get_hooks('override_doctype_class') class_overrides = frappe.get_hooks('override_doctype_class')
@@ -75,9 +72,12 @@ def get_controller(doctype):
return site_controllers[doctype] return site_controllers[doctype]


class BaseDocument(object): class BaseDocument(object):
ignore_in_getter = ("doctype", "_meta", "meta", "_table_fields", "_valid_columns")
ignore_in_setter = ("doctype", "_meta", "meta", "_table_fields", "_valid_columns")


def __init__(self, d): def __init__(self, d):
if d.get("doctype"):
self.doctype = d["doctype"]

self.update(d) self.update(d)
self.dont_update_if_missing = [] self.dont_update_if_missing = []


@@ -105,10 +105,9 @@ class BaseDocument(object):
}) })
""" """


# first set default field values of base document
for key in default_fields:
if key in d:
self.set(key, d[key])
# set name first, as it is used a reference in child document
if "name" in d:
self.name = d["name"]


for key, value in d.items(): for key, value in d.items():
self.set(key, value) self.set(key, value)
@@ -143,10 +142,14 @@ class BaseDocument(object):
else: else:
value = self.__dict__.get(key, default) value = self.__dict__.get(key, default)


if value is None and key not in self.ignore_in_getter \
and key in (d.fieldname for d in self.meta.get_table_fields()):
self.set(key, [])
value = self.__dict__.get(key)
if value is None and key in (
d.fieldname for d in self.meta.get_table_fields()
):
value = []
self.set(key, value)

if limit and isinstance(value, (list, tuple)) and len(value) > limit:
value = value[:limit]


return value return value
else: else:
@@ -156,6 +159,9 @@ class BaseDocument(object):
return self.get(key, filters=filters, limit=1)[0] return self.get(key, filters=filters, limit=1)[0]


def set(self, key, value, as_value=False): def set(self, key, value, as_value=False):
if key in self.ignore_in_setter:
return

if isinstance(value, list) and not as_value: if isinstance(value, list) and not as_value:
self.__dict__[key] = [] self.__dict__[key] = []
self.extend(key, value) self.extend(key, value)
@@ -181,6 +187,7 @@ class BaseDocument(object):
if isinstance(value, (dict, BaseDocument)): if isinstance(value, (dict, BaseDocument)):
if not self.__dict__.get(key): if not self.__dict__.get(key):
self.__dict__[key] = [] self.__dict__[key] = []

value = self._init_child(value, key) value = self._init_child(value, key)
self.__dict__[key].append(value) self.__dict__[key].append(value)


@@ -217,11 +224,11 @@ class BaseDocument(object):
def _init_child(self, value, key): def _init_child(self, value, key):
if not self.doctype: if not self.doctype:
return value return value

if not isinstance(value, BaseDocument): if not isinstance(value, BaseDocument):
if "doctype" not in value or value['doctype'] is None:
value["doctype"] = self.get_table_field_doctype(key)
if not value["doctype"]:
raise AttributeError(key)
value["doctype"] = self.get_table_field_doctype(key)
if not value["doctype"]:
raise AttributeError(key)


value = get_controller(value["doctype"])(value) value = get_controller(value["doctype"])(value)
value.init_valid_columns() value.init_valid_columns()
@@ -241,7 +248,7 @@ class BaseDocument(object):


return value return value


def get_valid_dict(self, sanitize=True, convert_dates_to_str=False, ignore_nulls = False):
def get_valid_dict(self, sanitize=True, convert_dates_to_str=False, ignore_nulls=False, ignore_virtual=False):
d = frappe._dict() d = frappe._dict()
for fieldname in self.meta.get_valid_columns(): for fieldname in self.meta.get_valid_columns():
d[fieldname] = self.get(fieldname) d[fieldname] = self.get(fieldname)
@@ -251,7 +258,26 @@ class BaseDocument(object):
continue continue


df = self.meta.get_field(fieldname) df = self.meta.get_field(fieldname)
if df:

if df and df.get("is_virtual"):
if ignore_virtual:
del d[fieldname]
continue

from frappe.utils.safe_exec import get_safe_globals

if d[fieldname] is None:
if df.get("options"):
d[fieldname] = frappe.safe_eval(
code=df.get("options"),
eval_globals=get_safe_globals(),
eval_locals={"doc": self},
)
else:
_val = getattr(self, fieldname, None)
if _val and not callable(_val):
d[fieldname] = _val
elif df:
if df.fieldtype=="Check": if df.fieldtype=="Check":
d[fieldname] = 1 if cint(d[fieldname]) else 0 d[fieldname] = 1 if cint(d[fieldname]) else 0


@@ -325,6 +351,7 @@ class BaseDocument(object):
def as_dict(self, no_nulls=False, no_default_fields=False, convert_dates_to_str=False, no_child_table_fields=False): def as_dict(self, no_nulls=False, no_default_fields=False, convert_dates_to_str=False, no_child_table_fields=False):
doc = self.get_valid_dict(convert_dates_to_str=convert_dates_to_str) doc = self.get_valid_dict(convert_dates_to_str=convert_dates_to_str)
doc["doctype"] = self.doctype doc["doctype"] = self.doctype

for df in self.meta.get_table_fields(): for df in self.meta.get_table_fields():
children = self.get(df.fieldname) or [] children = self.get(df.fieldname) or []
doc[df.fieldname] = [ doc[df.fieldname] = [
@@ -372,26 +399,43 @@ class BaseDocument(object):
fieldname = [df.fieldname for df in self.meta.get_table_fields() if df.options==doctype] fieldname = [df.fieldname for df in self.meta.get_table_fields() if df.options==doctype]
return fieldname[0] if fieldname else None return fieldname[0] if fieldname else None


def db_insert(self):
"""INSERT the document (with valid columns) in the database."""
def db_insert(self, ignore_if_duplicate=False):
"""INSERT the document (with valid columns) in the database.

args:
ignore_if_duplicate: ignore primary key collision
at database level (postgres)
in python (mariadb)
"""
if not self.name: if not self.name:
# name will be set by document class in most cases # name will be set by document class in most cases
set_new_name(self) set_new_name(self)


conflict_handler = ""
# On postgres we can't implcitly ignore PK collision
# So instruct pg to ignore `name` field conflicts
if ignore_if_duplicate and frappe.db.db_type == "postgres":
conflict_handler = "on conflict (name) do nothing"

if not self.creation: if not self.creation:
self.creation = self.modified = now() self.creation = self.modified = now()
self.created_by = self.modified_by = frappe.session.user self.created_by = self.modified_by = frappe.session.user


# if doctype is "DocType", don't insert null values as we don't know who is valid yet # if doctype is "DocType", don't insert null values as we don't know who is valid yet
d = self.get_valid_dict(convert_dates_to_str=True, ignore_nulls = self.doctype in DOCTYPES_FOR_DOCTYPE)
d = self.get_valid_dict(
convert_dates_to_str=True,
ignore_nulls=self.doctype in DOCTYPES_FOR_DOCTYPE,
ignore_virtual=True,
)


columns = list(d) columns = list(d)
try: try:
frappe.db.sql("""INSERT INTO `tab{doctype}` ({columns}) frappe.db.sql("""INSERT INTO `tab{doctype}` ({columns})
VALUES ({values})""".format(
doctype = self.doctype,
columns = ", ".join("`"+c+"`" for c in columns),
values = ", ".join(["%s"] * len(columns))
VALUES ({values}) {conflict_handler}""".format(
doctype=self.doctype,
columns=", ".join("`"+c+"`" for c in columns),
values=", ".join(["%s"] * len(columns)),
conflict_handler=conflict_handler
), list(d.values())) ), list(d.values()))
except Exception as e: except Exception as e:
if frappe.db.is_primary_key_violation(e): if frappe.db.is_primary_key_violation(e):
@@ -404,8 +448,11 @@ class BaseDocument(object):
self.db_insert() self.db_insert()
return return


frappe.msgprint(_("{0} {1} already exists").format(self.doctype, frappe.bold(self.name)), title=_("Duplicate Name"), indicator="red")
raise frappe.DuplicateEntryError(self.doctype, self.name, e)
if not ignore_if_duplicate:
frappe.msgprint(_("{0} {1} already exists")
.format(self.doctype, frappe.bold(self.name)),
title=_("Duplicate Name"), indicator="red")
raise frappe.DuplicateEntryError(self.doctype, self.name, e)


elif frappe.db.is_unique_key_violation(e): elif frappe.db.is_unique_key_violation(e):
# unique constraint # unique constraint
@@ -733,7 +780,7 @@ class BaseDocument(object):


type_map = frappe.db.type_map type_map = frappe.db.type_map


for fieldname, value in self.get_valid_dict().items():
for fieldname, value in self.get_valid_dict(ignore_virtual=True).items():
df = self.meta.get_field(fieldname) df = self.meta.get_field(fieldname)


if not df or df.fieldtype == 'Check': if not df or df.fieldtype == 'Check':
@@ -811,7 +858,7 @@ class BaseDocument(object):
if frappe.flags.in_install: if frappe.flags.in_install:
return return


for fieldname, value in self.get_valid_dict().items():
for fieldname, value in self.get_valid_dict(ignore_virtual=True).items():
if not value or not isinstance(value, str): if not value or not isinstance(value, str):
continue continue




+ 1
- 5
frappe/model/document.py Parādīt failu

@@ -249,11 +249,7 @@ class Document(BaseDocument):
if getattr(self.meta, "issingle", 0): if getattr(self.meta, "issingle", 0):
self.update_single(self.get_valid_dict()) self.update_single(self.get_valid_dict())
else: else:
try:
self.db_insert()
except frappe.DuplicateEntryError as e:
if not ignore_if_duplicate:
raise e
self.db_insert(ignore_if_duplicate=ignore_if_duplicate)


# children # children
for d in self.get_all_children(): for d in self.get_all_children():


+ 28
- 9
frappe/model/meta.py Parādīt failu

@@ -14,16 +14,28 @@ Example:




''' '''
import json
import os
from datetime import datetime from datetime import datetime

import click import click
import frappe, json, os
from frappe.utils import cstr, cint, cast
from frappe.model import default_fields, no_value_fields, optional_fields, data_fieldtypes, table_fields, child_table_fields
from frappe.model.document import Document

import frappe
from frappe import _
from frappe.model import (
child_table_fields,
data_fieldtypes,
default_fields,
no_value_fields,
optional_fields,
table_fields,
)
from frappe.model.base_document import BaseDocument from frappe.model.base_document import BaseDocument
from frappe.modules import load_doctype_module
from frappe.model.document import Document
from frappe.model.workflow import get_workflow_name from frappe.model.workflow import get_workflow_name
from frappe import _
from frappe.modules import load_doctype_module
from frappe.utils import cast, cint, cstr



def get_meta(doctype, cached=True): def get_meta(doctype, cached=True):
if cached: if cached:
@@ -444,9 +456,16 @@ class Meta(Document):
self.permissions = [Document(d) for d in custom_perms] self.permissions = [Document(d) for d in custom_perms]


def get_fieldnames_with_value(self, with_field_meta=False): def get_fieldnames_with_value(self, with_field_meta=False):
return [df if with_field_meta else df.fieldname \
for df in self.fields if df.fieldtype not in no_value_fields]
def is_value_field(docfield):
return not (
docfield.get("is_virtual")
or docfield.fieldtype in no_value_fields
)

if with_field_meta:
return [df for df in self.fields if is_value_field(df)]


return [df.fieldname for df in self.fields if is_value_field(df)]


def get_fields_to_check_permissions(self, user_permission_doctypes): def get_fields_to_check_permissions(self, user_permission_doctypes):
fields = self.get("fields", { fields = self.get("fields", {
@@ -546,7 +565,7 @@ class Meta(Document):
# For internal links parent doctype will be the key # For internal links parent doctype will be the key
doctype = link.parent_doctype or link.link_doctype doctype = link.parent_doctype or link.link_doctype
# group found # group found
if link.group and group.label == link.group:
if link.group and _(group.label) == _(link.group):
if doctype not in group.get('items'): if doctype not in group.get('items'):
group.get('items').append(doctype) group.get('items').append(doctype)
link.added = True link.added = True


+ 1
- 0
frappe/patches.txt Parādīt failu

@@ -184,6 +184,7 @@ frappe.patches.v13_0.queryreport_columns
frappe.patches.v13_0.jinja_hook frappe.patches.v13_0.jinja_hook
frappe.patches.v13_0.update_notification_channel_if_empty frappe.patches.v13_0.update_notification_channel_if_empty
frappe.patches.v13_0.set_first_day_of_the_week frappe.patches.v13_0.set_first_day_of_the_week
execute:frappe.reload_doc('custom', 'doctype', 'custom_field')
frappe.patches.v14_0.update_workspace2 # 20.09.2021 frappe.patches.v14_0.update_workspace2 # 20.09.2021
frappe.patches.v14_0.save_ratings_in_fraction #23-12-2021 frappe.patches.v14_0.save_ratings_in_fraction #23-12-2021
frappe.patches.v14_0.transform_todo_schema frappe.patches.v14_0.transform_todo_schema


+ 7
- 0
frappe/public/icons/timeless/symbol-defs.svg Parādīt failu

@@ -814,6 +814,13 @@
<path d="M16.814 13.3304L17.9274 12.6875" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/> <path d="M16.814 13.3304L17.9274 12.6875" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
</symbol> </symbol>


<symbol viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-dashboard-list">
<path d="M7.5 2.5H4.5C3.94772 2.5 3.5 2.94772 3.5 3.5V9.5C3.5 10.0523 3.94772 10.5 4.5 10.5H7.5C8.05228 10.5 8.5 10.0523 8.5 9.5V3.5C8.5 2.94772 8.05228 2.5 7.5 2.5Z" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.5 13.5H4.5C3.94772 13.5 3.5 13.9477 3.5 14.5V16.5C3.5 17.0523 3.94772 17.5 4.5 17.5H7.5C8.05228 17.5 8.5 17.0523 8.5 16.5V14.5C8.5 13.9477 8.05228 13.5 7.5 13.5Z" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15.5 2.5H12.5C11.9477 2.5 11.5 2.94772 11.5 3.5V6.5C11.5 7.05228 11.9477 7.5 12.5 7.5H15.5C16.0523 7.5 16.5 7.05228 16.5 6.5V3.5C16.5 2.94772 16.0523 2.5 15.5 2.5Z" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15.5 10.5H12.5C11.9477 10.5 11.5 10.9477 11.5 11.5V16.5C11.5 17.0523 11.9477 17.5 12.5 17.5H15.5C16.0523 17.5 16.5 17.0523 16.5 16.5V11.5C16.5 10.9477 16.0523 10.5 15.5 10.5Z" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
</symbol>

<symbol viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-text"> <symbol viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-text">
<path d="M5 4V6.4H9V16H11.4V6.4H15.4V4H5Z" fill="var(--icon-stroke)" stroke="none"/> <path d="M5 4V6.4H9V16H11.4V6.4H15.4V4H5Z" fill="var(--icon-stroke)" stroke="none"/>
</symbol> </symbol>


+ 4
- 1
frappe/public/js/frappe/form/controls/base_control.js Parādīt failu

@@ -39,6 +39,9 @@ frappe.ui.form.Control = class BaseControl {
if (this.df.get_status) { if (this.df.get_status) {
return this.df.get_status(this); return this.df.get_status(this);
} }
if (this.df.is_virtual) {
return "Read";
}


if ((!this.doctype && !this.docname) || this.df.parenttype === 'Web Form' || this.df.is_web_form) { if ((!this.doctype && !this.docname) || this.df.parenttype === 'Web Form' || this.df.is_web_form) {
// like in case of a dialog box // like in case of a dialog box
@@ -52,7 +55,7 @@ frappe.ui.form.Control = class BaseControl {
if(explain) console.log("By Hidden Dependency: None"); // eslint-disable-line no-console if(explain) console.log("By Hidden Dependency: None"); // eslint-disable-line no-console
return "None"; return "None";


} else if (cint(this.df.read_only)) {
} else if (cint(this.df.read_only || this.df.is_virtual)) {
// eslint-disable-next-line // eslint-disable-next-line
if (explain) console.log("By Read Only: Read"); // eslint-disable-line no-console if (explain) console.log("By Read Only: Read"); // eslint-disable-line no-console
return "Read"; return "Read";


+ 71
- 10
frappe/public/js/frappe/form/controls/link.js Parādīt failu

@@ -29,7 +29,7 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
setTimeout(function() { setTimeout(function() {
if(me.$input.val() && me.get_options()) { if(me.$input.val() && me.get_options()) {
let doctype = me.get_options(); let doctype = me.get_options();
let name = me.$input.val();
let name = me.get_input_value();
me.$link.toggle(true); me.$link.toggle(true);
me.$link_open.attr('href', frappe.utils.get_form_link(doctype, name)); me.$link_open.attr('href', frappe.utils.get_form_link(doctype, name));
} }
@@ -69,6 +69,59 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
this.$input_area.find(".link-btn").remove(); this.$input_area.find(".link-btn").remove();
} }
} }
set_formatted_input(value) {
super.set_formatted_input();
if (!value) return;

if (!this.title_value_map) {
this.title_value_map = {};
}
this.set_link_title(value);
}
set_link_title(value) {
let doctype = this.get_options();

if (!doctype) return;

if (in_list(frappe.boot.link_title_doctypes, doctype)) {
let link_title = frappe.utils.get_link_title(doctype, value);
if (!link_title) {
link_title = frappe.utils
.fetch_link_title(doctype, value)
.then(link_title => {
this.set_input_value(link_title);
this.title_value_map[link_title] = value;
});
} else {
this.set_input_value(link_title);
this.title_value_map[link_title] = value;
}
} else {
this.set_input_value(value);
}
}
parse_validate_and_set_in_model(value, e, label) {
if (this.parse) value = this.parse(value, label);
if (label) {
this.label = label;
frappe.utils.add_link_title(this.df.options, value, label);
}

return this.validate_and_set_in_model(value, e);
}
get_input_value() {
if (this.$input) {
const input_value = this.$input.val();
return this.title_value_map?.[input_value] || input_value;
}
return null;
}
get_label_value() {
return this.$input ? this.$input.val() : "";
}
set_input_value(value) {
this.$input && this.$input.val(value);
}
open_advanced_search() { open_advanced_search() {
var doctype = this.get_options(); var doctype = this.get_options();
if(!doctype) return; if(!doctype) return;
@@ -98,7 +151,7 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
} }


// partially entered name field // partially entered name field
frappe.route_options.name_field = this.get_value();
frappe.route_options.name_field = this.get_label_value();


// reference to calling link // reference to calling link
frappe._from_link = this; frappe._from_link = this;
@@ -120,6 +173,11 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
maxItems: 99, maxItems: 99,
autoFirst: true, autoFirst: true,
list: [], list: [],
replace: function (suggestion) {
// Override Awesomeplete replace function as it is used to set the input value
// https://github.com/LeaVerou/awesomplete/issues/17104#issuecomment-359185403
this.input.value = suggestion.label || suggestion.value;
},
data: function (item) { data: function (item) {
return { return {
label: item.label || item.value, label: item.label || item.value,
@@ -236,9 +294,11 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
me.selected = false; me.selected = false;
return; return;
} }
var value = me.get_input_value();
if(value!==me.last_value) {
me.parse_validate_and_set_in_model(value);
let value = me.get_input_value();
let label = me.get_label_value();

if (value !== me.last_value || me.label !== label) {
me.parse_validate_and_set_in_model(value, null, label);
} }
}); });


@@ -258,14 +318,15 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat


// prevent selection on tab // prevent selection on tab
var TABKEY = 9; var TABKEY = 9;
if(e.keyCode === TABKEY) {
if (e.keyCode === TABKEY) {
e.preventDefault(); e.preventDefault();
me.awesomplete.close(); me.awesomplete.close();
return false; return false;
} }


if(item.action) {
if (item.action) {
item.value = ""; item.value = "";
item.label = "";
item.action.apply(me); item.action.apply(me);
} }


@@ -277,12 +338,12 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
frappe.boot.user.last_selected_values[me.df.options] = item.value; frappe.boot.user.last_selected_values[me.df.options] = item.value;
} }


me.parse_validate_and_set_in_model(item.value);
me.parse_validate_and_set_in_model(item.value, null, item.label);
}); });


this.$input.on("awesomplete-selectcomplete", function(e) { this.$input.on("awesomplete-selectcomplete", function(e) {
var o = e.originalEvent;
if(o.text.value.indexOf("__link_option") !== -1) {
let o = e.originalEvent;
if (o.text.value.indexOf("__link_option") !== -1) {
me.$input.val(""); me.$input.val("");
} }
}); });


+ 7
- 1
frappe/public/js/frappe/form/controls/multiselect_pills.js Parādīt failu

@@ -83,15 +83,21 @@ frappe.ui.form.ControlMultiSelectPills = class ControlMultiSelectPills extends f
} }


get_pill_html(value) { get_pill_html(value) {
const label = this.get_label(value);
const encoded_value = encodeURIComponent(value); const encoded_value = encodeURIComponent(value);
return ` return `
<button class="data-pill btn tb-selected-value" data-value="${encoded_value}"> <button class="data-pill btn tb-selected-value" data-value="${encoded_value}">
<span class="btn-link-to-form">${__(value)}</span>
<span class="btn-link-to-form">${__(label || value)}</span>
<span class="btn-remove">${frappe.utils.icon('close')}</span> <span class="btn-remove">${frappe.utils.icon('close')}</span>
</button> </button>
`; `;
} }


get_label(value) {
const item = this._data?.find(d => d.value === value);
return item ? item.label || item.value : null;
}

get_awesomplete_settings() { get_awesomplete_settings() {
const settings = super.get_awesomplete_settings(); const settings = super.get_awesomplete_settings();




+ 5
- 2
frappe/public/js/frappe/form/controls/table_multiselect.js Parādīt failu

@@ -49,7 +49,7 @@ frappe.ui.form.ControlTableMultiSelect = class ControlTableMultiSelect extends f
setup_buttons() { setup_buttons() {
this.$input_area.find('.link-btn').remove(); this.$input_area.find('.link-btn').remove();
} }
parse(value) {
parse(value, label) {
const link_field = this.get_link_field(); const link_field = this.get_link_field();


if (value) { if (value) {
@@ -62,6 +62,7 @@ frappe.ui.form.ControlTableMultiSelect = class ControlTableMultiSelect extends f
[link_field.fieldname]: value [link_field.fieldname]: value
}); });
} }
frappe.utils.add_link_title(link_field.options, value, label);
} }
this._rows_list = this.rows.map(row => row[link_field.fieldname]); this._rows_list = this.rows.map(row => row[link_field.fieldname]);
return this.rows; return this.rows;
@@ -126,10 +127,12 @@ frappe.ui.form.ControlTableMultiSelect = class ControlTableMultiSelect extends f
this.$input_area.prepend(html); this.$input_area.prepend(html);
} }
get_pill_html(value) { get_pill_html(value) {
const link_field = this.get_link_field();
const encoded_value = encodeURIComponent(value); const encoded_value = encodeURIComponent(value);
const pill_name = frappe.utils.get_link_title(link_field.options, value) || value;
return ` return `
<button class="data-pill btn tb-selected-value" data-value="${encoded_value}"> <button class="data-pill btn tb-selected-value" data-value="${encoded_value}">
<span class="btn-link-to-form">${__(value)}</span>
<span class="btn-link-to-form">${__(pill_name)}</span>
<span class="btn-remove">${frappe.utils.icon('close')}</span> <span class="btn-remove">${frappe.utils.icon('close')}</span>
</button> </button>
`; `;


+ 21
- 10
frappe/public/js/frappe/form/form.js Parādīt failu

@@ -334,7 +334,7 @@ frappe.ui.form.Form = class FrappeForm {
this.doc = frappe.get_doc(this.doctype, this.docname); this.doc = frappe.get_doc(this.doctype, this.docname);


// check permissions // check permissions
if(!this.has_read_permission()) {
if (!this.has_read_permission()) {
frappe.show_not_permitted(__(this.doctype) + " " + __(this.docname)); frappe.show_not_permitted(__(this.doctype) + " " + __(this.docname));
return; return;
} }
@@ -1363,6 +1363,7 @@ frappe.ui.form.Form = class FrappeForm {


set_df_property(fieldname, property, value, docname, table_field, table_row_name=null) { set_df_property(fieldname, property, value, docname, table_field, table_row_name=null) {
let df; let df;

if (!docname || !table_field) { if (!docname || !table_field) {
df = this.get_docfield(fieldname); df = this.get_docfield(fieldname);
} else { } else {
@@ -1372,8 +1373,10 @@ frappe.ui.form.Form = class FrappeForm {
df = frappe.meta.get_docfield(filtered_fields[0].parent, table_field, table_row_name); df = frappe.meta.get_docfield(filtered_fields[0].parent, table_field, table_row_name);
} }
} }

if (df && df[property] != value) { if (df && df[property] != value) {
df[property] = value; df[property] = value;

if (table_field && table_row_name) { if (table_field && table_row_name) {
if (this.fields_dict[fieldname].grid.grid_rows_by_docname[table_row_name]) { if (this.fields_dict[fieldname].grid.grid_rows_by_docname[table_row_name]) {
this.fields_dict[fieldname].grid.grid_rows_by_docname[table_row_name].refresh_field(fieldname); this.fields_dict[fieldname].grid.grid_rows_by_docname[table_row_name].refresh_field(fieldname);
@@ -1661,23 +1664,17 @@ frappe.ui.form.Form = class FrappeForm {
// make new doctype from the current form // make new doctype from the current form
// will handover to `make_methods` if defined // will handover to `make_methods` if defined
// or will create and match link fields // or will create and match link fields
var me = this;
let me = this;
if(this.make_methods && this.make_methods[doctype]) { if(this.make_methods && this.make_methods[doctype]) {
return this.make_methods[doctype](this); return this.make_methods[doctype](this);
} else if(this.custom_make_buttons && this.custom_make_buttons[doctype]) { } else if(this.custom_make_buttons && this.custom_make_buttons[doctype]) {
this.custom_buttons[__(this.custom_make_buttons[doctype])].trigger('click'); this.custom_buttons[__(this.custom_make_buttons[doctype])].trigger('click');
} else { } else {
frappe.model.with_doctype(doctype, function() { frappe.model.with_doctype(doctype, function() {
var new_doc = frappe.model.get_new_doc(doctype);
let new_doc = frappe.model.get_new_doc(doctype, null, null, true);


// set link fields (if found) // set link fields (if found)
frappe.get_meta(doctype).fields.forEach(function(df) {
if(df.fieldtype==='Link' && df.options===me.doctype) {
new_doc[df.fieldname] = me.doc.name;
} else if (['Link', 'Dynamic Link'].includes(df.fieldtype) && me.doc[df.fieldname]) {
new_doc[df.fieldname] = me.doc[df.fieldname];
}
});
me.set_link_field(doctype, new_doc);


frappe.ui.form.make_quick_entry(doctype, null, null, new_doc); frappe.ui.form.make_quick_entry(doctype, null, null, new_doc);
// frappe.set_route('Form', doctype, new_doc.name); // frappe.set_route('Form', doctype, new_doc.name);
@@ -1685,6 +1682,20 @@ frappe.ui.form.Form = class FrappeForm {
} }
} }


set_link_field(doctype, new_doc) {
let me = this;
frappe.get_meta(doctype).fields.forEach(function(df) {
if (df.fieldtype === 'Link' && df.options === me.doctype) {
new_doc[df.fieldname] = me.doc.name;
} else if (['Link', 'Dynamic Link'].includes(df.fieldtype) && me.doc[df.fieldname]) {
new_doc[df.fieldname] = me.doc[df.fieldname];
} else if (df.fieldtype === 'Table' && df.options && df.reqd) {
let row = new_doc[df.fieldname][0];
me.set_link_field(df.options, row);
}
});
}

update_in_all_rows(table_fieldname, fieldname, value) { update_in_all_rows(table_fieldname, fieldname, value) {
// update the child value in all tables where it is missing // update the child value in all tables where it is missing
if(!value) return; if(!value) return;


+ 8
- 5
frappe/public/js/frappe/form/formatters.js Parādīt failu

@@ -110,12 +110,14 @@ frappe.form.formatters = {
Link: function(value, docfield, options, doc) { Link: function(value, docfield, options, doc) {
var doctype = docfield._options || docfield.options; var doctype = docfield._options || docfield.options;
var original_value = value; var original_value = value;
let link_title = frappe.utils.get_link_title(doctype, value);

if(value && value.match && value.match(/^['"].*['"]$/)) { if(value && value.match && value.match(/^['"].*['"]$/)) {
value.replace(/^.(.*).$/, "$1"); value.replace(/^.(.*).$/, "$1");
} }


if(options && (options.for_print || options.only_value)) { if(options && (options.for_print || options.only_value)) {
return value;
return link_title || value;
} }


if(frappe.form.link_formatters[doctype]) { if(frappe.form.link_formatters[doctype]) {
@@ -139,13 +141,14 @@ frappe.form.formatters = {
return `<a return `<a
href="/app/${encodeURIComponent(frappe.router.slug(doctype))}/${encodeURIComponent(original_value)}" href="/app/${encodeURIComponent(frappe.router.slug(doctype))}/${encodeURIComponent(original_value)}"
data-doctype="${doctype}" data-doctype="${doctype}"
data-name="${original_value}">
${__(options && options.label || value)}</a>`;
data-name="${original_value}"
data-value="${original_value}">
${__(options && options.label || link_title || value)}</a>`;
} else { } else {
return value;
return link_title || value;
} }
} else { } else {
return value;
return link_title || value;
} }
}, },
Date: function(value) { Date: function(value) {


+ 6
- 7
frappe/public/js/frappe/form/grid.js Parādīt failu

@@ -502,10 +502,9 @@ export default class Grid {


set_column_disp(fieldname, show) { set_column_disp(fieldname, show) {
if ($.isArray(fieldname)) { if ($.isArray(fieldname)) {
for (var i = 0, l = fieldname.length; i < l; i++) {
var fname = fieldname[i];
this.get_docfield(fname).hidden = show ? 0 : 1;
this.set_editable_grid_column_disp(fname, show);
for (let field of fieldname) {
this.update_docfield_property(field, "hidden", show);
this.set_editable_grid_column_disp(field, show);
} }
} else { } else {
this.get_docfield(fieldname).hidden = show ? 0 : 1; this.get_docfield(fieldname).hidden = show ? 0 : 1;
@@ -555,17 +554,17 @@ export default class Grid {
} }


toggle_reqd(fieldname, reqd) { toggle_reqd(fieldname, reqd) {
this.get_docfield(fieldname).reqd = reqd;
this.update_docfield_property(fieldname, "reqd", reqd);
this.debounced_refresh(); this.debounced_refresh();
} }


toggle_enable(fieldname, enable) { toggle_enable(fieldname, enable) {
this.get_docfield(fieldname).read_only = enable ? 0 : 1;
this.update_docfield_property(fieldname, "read_only", enable ? 0 : 1);
this.debounced_refresh(); this.debounced_refresh();
} }


toggle_display(fieldname, show) { toggle_display(fieldname, show) {
this.get_docfield(fieldname).hidden = show ? 0 : 1;
this.update_docfield_property(fieldname, "hidden", show ? 0 : 1);
this.debounced_refresh(); this.debounced_refresh();
} }




+ 22
- 5
frappe/public/js/frappe/form/grid_row.js Parādīt failu

@@ -5,11 +5,7 @@ export default class GridRow {
this.on_grid_fields_dict = {}; this.on_grid_fields_dict = {};
this.on_grid_fields = []; this.on_grid_fields = [];
$.extend(this, opts); $.extend(this, opts);
if (this.doc && this.parent_df.options) {
frappe.meta.make_docfield_copy_for(this.parent_df.options, this.doc.name, this.docfields);
const docfields = frappe.meta.get_docfields(this.parent_df.options, this.doc.name);
this.docfields = docfields.length ? docfields : opts.docfields;
}
this.set_docfields();
this.columns = {}; this.columns = {};
this.columns_list = []; 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 pull-left">';
@@ -41,6 +37,22 @@ export default class GridRow {
this.set_data(); this.set_data();
} }
} }

set_docfields(update=false) {
if (this.doc && this.parent_df.options) {
frappe.meta.make_docfield_copy_for(this.parent_df.options, this.doc.name, this.docfields);
const docfields = frappe.meta.get_docfields(this.parent_df.options, this.doc.name);
if (update) {
// to maintain references
this.docfields.forEach(df => {
Object.assign(df, docfields.find(d => d.fieldname === df.fieldname));
});
} else {
this.docfields = docfields;
}
}
}

set_data() { set_data() {
this.wrapper.data({ this.wrapper.data({
"doc": this.doc "doc": this.doc
@@ -148,6 +160,11 @@ export default class GridRow {
}, __('Move To'), 'Update'); }, __('Move To'), 'Update');
} }
refresh() { refresh() {
// update docfields for new record
if (this.frm && this.doc && this.doc.__islocal) {
this.set_docfields(true);
}

if(this.frm && this.doc) { if(this.frm && this.doc) {
this.doc = locals[this.doc.doctype][this.doc.name]; this.doc = locals[this.doc.doctype][this.doc.name];
} }


+ 11
- 7
frappe/public/js/frappe/form/layout.js Parādīt failu

@@ -547,24 +547,28 @@ frappe.ui.form.Layout = class Layout {
} }


refresh_dependency() { refresh_dependency() {
// Resolve "depends_on" and show / hide accordingly
/**
Resolve "depends_on" and show / hide accordingly
build dependants' dictionary
*/


// build dependants' dictionary
let has_dep = false; let has_dep = false;


for (let fkey in this.fields_list) {
let f = this.fields_list[fkey];
f.dependencies_clear = true;
const fields = this.fields_list.concat(this.tabs);

for (let fkey in fields) {
let f = fields[fkey];
if (f.df.depends_on || f.df.mandatory_depends_on || f.df.read_only_depends_on) { if (f.df.depends_on || f.df.mandatory_depends_on || f.df.read_only_depends_on) {
has_dep = true; has_dep = true;
break;
} }
} }


if (!has_dep) return; if (!has_dep) return;


// show / hide based on values // show / hide based on values
for (let i = this.fields_list.length - 1; i >= 0; i--) {
let f = this.fields_list[i];
for (let i = fields.length - 1; i >= 0; i--) {
let f = fields[i];
f.guardian_has_value = true; f.guardian_has_value = true;
if (f.df.depends_on) { if (f.df.depends_on) {
// evaluate guardian // evaluate guardian


+ 12
- 13
frappe/public/js/frappe/form/multi_select_dialog.js Parādīt failu

@@ -1,6 +1,6 @@
frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
constructor(opts) { constructor(opts) {
/* Options: doctype, target, setters, get_query, action, add_filters_group, data_fields, primary_action_label */
/* Options: doctype, target, setters, get_query, action, add_filters_group, data_fields, primary_action_label, columns */
Object.assign(this, opts); Object.assign(this, opts);
this.for_select = this.doctype == "[Select]"; this.for_select = this.doctype == "[Select]";
if (!this.for_select) { if (!this.for_select) {
@@ -400,23 +400,22 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
return this.results.filter(res => checked_values.includes(res.name)); return this.results.filter(res => checked_values.includes(res.name));
} }


get_datatable_columns() {
if (this.get_query && this.get_query().query && this.columns) return this.columns;

if (Array.isArray(this.setters))
return ["name", ...this.setters.map(df => df.fieldname)];

return ["name", ...Object.keys(this.setters)];
}

make_list_row(result = {}) { make_list_row(result = {}) {
var me = this; var me = this;
// Make a head row by default (if result not passed) // Make a head row by default (if result not passed)
let head = Object.keys(result).length === 0; let head = Object.keys(result).length === 0;


let contents = ``; let contents = ``;
let columns = ["name"];

if ($.isArray(this.setters)) {
for (let df of this.setters) {
columns.push(df.fieldname);
}
} else {
columns = columns.concat(Object.keys(this.setters));
}

columns.forEach(function (column) {
this.get_datatable_columns().forEach(function (column) {
contents += `<div class="list-item__content ellipsis"> contents += `<div class="list-item__content ellipsis">
${ ${
head ? `<span class="ellipsis text-muted" title="${__(frappe.model.unscrub(column))}">${__(frappe.model.unscrub(column))}</span>` head ? `<span class="ellipsis text-muted" title="${__(frappe.model.unscrub(column))}">${__(frappe.model.unscrub(column))}</span>`
@@ -486,7 +485,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {


get_filters_from_setters() { get_filters_from_setters() {
let me = this; let me = this;
let filters = this.get_query ? this.get_query().filters : {} || {};
let filters = (this.get_query ? this.get_query().filters : {}) || {};
let filter_fields = []; let filter_fields = [];


if ($.isArray(this.setters)) { if ($.isArray(this.setters)) {


+ 1
- 1
frappe/public/js/frappe/form/quick_entry.js Parādīt failu

@@ -55,7 +55,7 @@ frappe.ui.form.QuickEntryForm = class QuickEntryForm {


// prepare a list of mandatory, bold and allow in quick entry fields // prepare a list of mandatory, bold and allow in quick entry fields
this.mandatory = fields.filter(df => { this.mandatory = fields.filter(df => {
return ((df.reqd || df.bold || df.allow_in_quick_entry) && !df.read_only);
return ((df.reqd || df.bold || df.allow_in_quick_entry) && !df.read_only && !df.is_virtual);
}); });
} }




+ 30
- 21
frappe/public/js/frappe/form/save.js Parādīt failu

@@ -249,30 +249,39 @@ frappe.ui.form.update_calling_link = (newdoc) => {
}; };


if (is_valid_doctype()) { if (is_valid_doctype()) {
// set value
if (doc && doc.parentfield) {
//update values for child table
$.each(frappe._from_link.frm.fields_dict[doc.parentfield].grid.grid_rows, function (index, field) {
if (field.doc && field.doc.name === frappe._from_link.docname) {
frappe._from_link.set_value(newdoc.name);
frappe.model.with_doctype(newdoc.doctype, () => {
let meta = frappe.get_meta(newdoc.doctype);
// set value
if (doc && doc.parentfield) {
//update values for child table
$.each(frappe._from_link.frm.fields_dict[doc.parentfield].grid.grid_rows, function (index, field) {
if (field.doc && field.doc.name === frappe._from_link.docname) {
if (meta.title_field && meta.show_title_field_in_link) {
frappe.utils.add_link_title(newdoc.doctype, newdoc.name, newdoc[meta.title_field]);
}
frappe._from_link.set_value(newdoc.name);
}
});
} else {
if (meta.title_field && meta.show_title_field_in_link) {
frappe.utils.add_link_title(newdoc.doctype, newdoc.name, newdoc[meta.title_field]);
} }
});
} else {
frappe._from_link.set_value(newdoc.name);
}
frappe._from_link.set_value(newdoc.name);
}


// refresh field
frappe._from_link.refresh();
// refresh field
frappe._from_link.refresh();


// if from form, switch
if (frappe._from_link.frm) {
frappe.set_route("Form",
frappe._from_link.frm.doctype, frappe._from_link.frm.docname)
.then(() => {
frappe.utils.scroll_to(frappe._from_link_scrollY);
});
}
// if from form, switch
if (frappe._from_link.frm) {
frappe.set_route("Form",
frappe._from_link.frm.doctype, frappe._from_link.frm.docname)
.then(() => {
frappe.utils.scroll_to(frappe._from_link_scrollY);
});
}


frappe._from_link = null;
frappe._from_link = null;
});
} }
} }

+ 12
- 3
frappe/public/js/frappe/form/script_manager.js Parādīt failu

@@ -192,9 +192,18 @@ frappe.ui.form.ScriptManager = class ScriptManager {
} }


function setup_add_fetch(df) { function setup_add_fetch(df) {
if ((['Data', 'Read Only', 'Text', 'Small Text', 'Currency', 'Check', 'Attach Image',
'Text Editor', 'Code', 'Link', 'Float', 'Int', 'Date', 'Select', 'Duration'].includes(df.fieldtype) || df.read_only==1)
&& df.fetch_from && df.fetch_from.indexOf(".")!=-1) {
let is_read_only_field = (
['Data', 'Read Only', 'Text', 'Small Text', 'Currency', 'Check', 'Text Editor', 'Attach Image',
'Code', 'Link', 'Float', 'Int', 'Date', 'Select', 'Duration'].includes(df.fieldtype)
|| df.read_only == 1
|| df.is_virtual == 1
)

if (
is_read_only_field
&& df.fetch_from
&& df.fetch_from.indexOf(".") != -1
) {
var parts = df.fetch_from.split("."); var parts = df.fetch_from.split(".");
me.frm.add_fetch(parts[0], parts[1], df.fieldname, df.parent); me.frm.add_fetch(parts[0], parts[1], df.fieldname, df.parent);
} }


+ 1
- 1
frappe/public/js/frappe/form/tab.js Parādīt failu

@@ -40,7 +40,7 @@ export default class Tab {
hide = true; hide = true;
} }


hide && this.toggle(false);
this.toggle(!hide);
} }


toggle(show) { toggle(show) {


+ 2
- 2
frappe/public/js/frappe/list/base_list.js Parādīt failu

@@ -391,10 +391,10 @@ frappe.views.BaseList = class BaseList {
$this.addClass("btn-info"); $this.addClass("btn-info");


this.start = 0; this.start = 0;
this.page_length = $this.data().value;
this.page_length = this.selected_page_count = $this.data().value;
} else if ($this.is(".btn-more")) { } else if ($this.is(".btn-more")) {
this.start = this.start + this.page_length; this.start = this.start + this.page_length;
this.page_length = 20;
this.page_length = this.selected_page_count || 20;
} }
this.refresh(); this.refresh();
}); });


+ 3
- 2
frappe/public/js/frappe/list/list_view.js Parādīt failu

@@ -1483,7 +1483,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
return [ return [
filter[1], filter[1],
"=", "=",
JSON.stringify([filter[2], filter[3]]),
encodeURIComponent(JSON.stringify([filter[2], filter[3]])),
].join(""); ].join("");
}) })
.join("&"); .join("&");
@@ -1672,7 +1672,8 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
frappe.model.is_value_type(field_doc) && frappe.model.is_value_type(field_doc) &&
field_doc.fieldtype !== "Read Only" && field_doc.fieldtype !== "Read Only" &&
!field_doc.hidden && !field_doc.hidden &&
!field_doc.read_only
!field_doc.read_only &&
!field_doc.is_virtual
); );
}; };




+ 8
- 0
frappe/public/js/frappe/request.js Parādīt failu

@@ -260,6 +260,14 @@ frappe.request.call = function(opts) {
$.extend(frappe._messages, data.__messages); $.extend(frappe._messages, data.__messages);
} }


// sync link titles
if (data._link_titles) {
if (!frappe._link_titles) {
frappe._link_titles = {};
}
$.extend(frappe._link_titles, data._link_titles);
}

// callbacks // callbacks
var status_code_handler = statusCode[xhr.statusCode().status]; var status_code_handler = statusCode[xhr.statusCode().status];
if (status_code_handler) { if (status_code_handler) {


+ 11
- 1
frappe/public/js/frappe/ui/filters/filter.js Parādīt failu

@@ -314,6 +314,10 @@ frappe.ui.Filter = class {
return this.utils.get_selected_value(this.field, this.get_condition()); return this.utils.get_selected_value(this.field, this.get_condition());
} }


get_selected_label() {
return this.utils.get_selected_label(this.field);
}

get_condition() { get_condition() {
return this.filter_edit_area.find('.condition').val(); return this.filter_edit_area.find('.condition').val();
} }
@@ -361,7 +365,7 @@ frappe.ui.Filter = class {
get_filter_button_text() { get_filter_button_text() {
let value = this.utils.get_formatted_value( let value = this.utils.get_formatted_value(
this.field, this.field,
this.get_selected_value()
this.get_selected_label() || this.get_selected_value()
); );
return `${__(this.field.df.label)} ${__(this.get_condition())} ${__( return `${__(this.field.df.label)} ${__(this.get_condition())} ${__(
value value
@@ -449,6 +453,12 @@ frappe.ui.filter_utils = {
return val; return val;
}, },


get_selected_label(field) {
if (in_list(["Link", "Dynamic Link"], field.df.fieldtype)) {
return field.get_label_value();
}
},

get_default_condition(df) { get_default_condition(df) {
if (df.fieldtype == 'Data') { if (df.fieldtype == 'Data') {
return 'like'; return 'like';


+ 8
- 5
frappe/public/js/frappe/utils/common.js Parādīt failu

@@ -259,8 +259,16 @@ frappe.utils.xss_sanitise = function (string, options) {
'/': '&#x2F;' '/': '&#x2F;'
}; };
const REGEX_SCRIPT = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi; // used in jQuery 1.7.2 src/ajax.js Line 14 const REGEX_SCRIPT = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi; // used in jQuery 1.7.2 src/ajax.js Line 14
const REGEX_ALERT = /confirm\(.*\)|alert\(.*\)|prompt\(.*\)/gi; // captures alert, confirm, prompt
options = Object.assign({}, DEFAULT_OPTIONS, options); // don't deep copy, immutable beauty. options = Object.assign({}, DEFAULT_OPTIONS, options); // don't deep copy, immutable beauty.


// Rule 3 - TODO: Check event handlers?
// script and alert should be checked first or else it will be escaped
if (options.strategies.includes('js')) {
sanitised = sanitised.replace(REGEX_SCRIPT, "");
sanitised = sanitised.replace(REGEX_ALERT, "");
}

// Rule 1 // Rule 1
if (options.strategies.includes('html')) { if (options.strategies.includes('html')) {
for (let char in HTML_ESCAPE_MAP) { for (let char in HTML_ESCAPE_MAP) {
@@ -270,11 +278,6 @@ frappe.utils.xss_sanitise = function (string, options) {
} }
} }


// Rule 3 - TODO: Check event handlers?
if (options.strategies.includes('js')) {
sanitised = sanitised.replace(REGEX_SCRIPT, "");
}

return sanitised; return sanitised;
} }




+ 37
- 0
frappe/public/js/frappe/utils/utils.js Parādīt failu

@@ -1416,5 +1416,42 @@ Object.assign(frappe.utils, {
arr.push(i); arr.push(i);
} }
return arr; return arr;
},

get_link_title(doctype, name) {
if (!doctype || !name || !frappe._link_titles) {
return;
}

return frappe._link_titles[doctype + "::" + name];
},

add_link_title(doctype, name, value) {
if (!doctype || !name) {
return;
}

if (!frappe._link_titles) {
// for link titles
frappe._link_titles = {};
}
frappe._link_titles[doctype + "::" + name] = value;
},

fetch_link_title(doctype, name) {
try {
return frappe.xcall("frappe.desk.search.get_link_title", {
"doctype": doctype,
"docname": name
}).then(title => {
frappe.utils.add_link_title(doctype, name, title);
return title;
});
} catch (error) {
console.log('Error while fetching link title.'); // eslint-disable-line
console.log(error); // eslint-disable-line
return Promise.resolve(name);
}
} }
}); });

+ 3
- 2
frappe/public/js/frappe/views/reports/query_report.js Parādīt failu

@@ -578,6 +578,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
args: { args: {
report_name: this.report_name, report_name: this.report_name,
filters: filters, filters: filters,
report_settings: this.report_settings
}, },
callback: resolve, callback: resolve,
always: () => this.page.btn_secondary.prop('disabled', false) always: () => this.page.btn_secondary.prop('disabled', false)
@@ -834,7 +835,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
let data = this.data; let data = this.data;
let columns = this.columns.filter((col) => !col.hidden); let columns = this.columns.filter((col) => !col.hidden);


if (this.raw_data.add_total_row) {
if (this.raw_data.add_total_row && !this.report_settings.tree) {
data = data.slice(); data = data.slice();
data.splice(-1, 1); data.splice(-1, 1);
} }
@@ -854,7 +855,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
treeView: this.tree_report, treeView: this.tree_report,
layout: 'fixed', layout: 'fixed',
cellHeight: 33, cellHeight: 33,
showTotalRow: this.raw_data.add_total_row,
showTotalRow: this.raw_data.add_total_row && !this.report_settings.tree,
direction: frappe.utils.is_rtl() ? 'rtl' : 'ltr', direction: frappe.utils.is_rtl() ? 'rtl' : 'ltr',
hooks: { hooks: {
columnTotal: frappe.utils.report_column_total columnTotal: frappe.utils.report_column_total


+ 55
- 9
frappe/public/js/frappe/views/reports/report_view.js Parādīt failu

@@ -18,7 +18,6 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
setup_defaults() { setup_defaults() {
super.setup_defaults(); super.setup_defaults();
this.page_title = __('Report:') + ' ' + this.page_title; this.page_title = __('Report:') + ' ' + this.page_title;
this.menu_items = this.report_menu_items();
this.view = 'Report'; this.view = 'Report';


const route = frappe.get_route(); const route = frappe.get_route();
@@ -52,6 +51,11 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
this.page.main.addClass('report-view'); this.page.main.addClass('report-view');
} }


setup_page() {
this.menu_items = this.report_menu_items();
super.setup_page();
}

toggle_side_bar() { toggle_side_bar() {
super.toggle_side_bar(); super.toggle_side_bar();
// refresh datatable when sidebar is toggled to accomodate extra space // refresh datatable when sidebar is toggled to accomodate extra space
@@ -644,6 +648,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
// not a cancelled doc // not a cancelled doc
&& data.docstatus !== 2 && data.docstatus !== 2
&& !df.read_only && !df.read_only
&& !df.is_virtual
&& !df.hidden && !df.hidden
// not a standard field i.e., owner, modified_by, etc. // not a standard field i.e., owner, modified_by, etc.
&& frappe.model.is_non_std_field(df.fieldname)) && frappe.model.is_non_std_field(df.fieldname))
@@ -1025,7 +1030,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
title += ` (${__(doctype)})`; title += ` (${__(doctype)})`;
} }


const editable = frappe.model.is_non_std_field(fieldname) && !docfield.read_only;
const editable = frappe.model.is_non_std_field(fieldname) && !docfield.read_only && !docfield.is_virtual;


const align = (() => { const align = (() => {
const is_numeric = frappe.model.is_numeric_field(docfield); const is_numeric = frappe.model.is_numeric_field(docfield);
@@ -1207,7 +1212,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
args: { args: {
name: name, name: name,
doctype: this.doctype, doctype: this.doctype,
json: JSON.stringify(report_settings)
report_settings: JSON.stringify(report_settings)
}, },
callback:(r) => { callback:(r) => {
if(r.exc) { if(r.exc) {
@@ -1244,6 +1249,17 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
} }
} }


delete_report() {
return frappe.call({
method: 'frappe.desk.reportview.delete_report',
args: { name: this.report_name },
callback(response) {
if (response.exc) return;
window.history.back();
}
});
}

get_column_widths() { get_column_widths() {
if (this.datatable) { if (this.datatable) {
return this.datatable return this.datatable
@@ -1465,12 +1481,42 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
} }
}); });


// save buttons
if(frappe.user.is_report_manager()) {
items = items.concat([
{ label: __('Save'), action: () => this.save_report('save') },
{ label: __('Save As'), action: () => this.save_report('save_as') }
]);
const can_edit_or_delete = (action) => {
const method = action == "delete" ? "can_delete" : "can_write";
return (
this.report_doc
&& this.report_doc.is_standard !== "Yes"
&& (
frappe.model[method]("Report")
|| this.report_doc.owner === frappe.session.user
)
);
};

// A user with role Report Manager or Report Owner can save
if (can_edit_or_delete()) {
items.push({
label: __("Save"),
action: () => this.save_report('save')
});
}

// anyone can save as
items.push({
label: __('Save As'),
action: () => this.save_report('save_as')
});

// A user with role Report Manager or Report Owner can delete
if (can_edit_or_delete("delete")) {
items.push({
label: __("Delete"),
action: () => frappe.confirm(
"Are you sure you want to delete this report?",
() => this.delete_report(),
),
shortcut: "Shift+Ctrl+D"
});
} }


// user permissions // user permissions


+ 1
- 1
frappe/public/js/frappe/views/treeview.js Parādīt failu

@@ -343,7 +343,7 @@ frappe.views.TreeView = class TreeView {
this.ignore_fields = this.opts.ignore_fields || []; this.ignore_fields = this.opts.ignore_fields || [];


var mandatory_fields = $.map(me.opts.meta.fields, function(d) { var mandatory_fields = $.map(me.opts.meta.fields, function(d) {
return (d.reqd || d.bold && !d.read_only) ? d : null });
return (d.reqd || d.bold && !d.read_only && !!d.is_virtual) ? d : null });


var opts_field_names = this.fields.map(function(d) { var opts_field_names = this.fields.map(function(d) {
return d.fieldname return d.fieldname


+ 2
- 0
frappe/public/scss/common/css_variables.scss Parādīt failu

@@ -136,6 +136,8 @@
--shadow-md: 0px 8px 14px rgba(25, 39, 52, 0.08), 0px 2px 6px rgba(25, 39, 52, 0.04); --shadow-md: 0px 8px 14px rgba(25, 39, 52, 0.08), 0px 2px 6px rgba(25, 39, 52, 0.04);
--shadow-lg: 0px 18px 22px rgba(25, 39, 52, 0.1), 0px 1px 10px rgba(0, 0, 0, 0.06), 0px 0.5px 5px rgba(25, 39, 52, 0.04); --shadow-lg: 0px 18px 22px rgba(25, 39, 52, 0.1), 0px 1px 10px rgba(0, 0, 0, 0.06), 0px 0.5px 5px rgba(25, 39, 52, 0.04);


--drop-shadow: 0px 0.5px 0px rgba(0, 0, 0, 0.05), 0px 0px 0px rgba(0, 0, 0, 0), 0px 2px 4px rgba(0, 0, 0, 0.05);

--modal-shadow: var(--shadow-md); --modal-shadow: var(--shadow-md);
--card-shadow: var(--shadow-sm); --card-shadow: var(--shadow-sm);
--btn-shadow: var(--shadow-xs); --btn-shadow: var(--shadow-xs);


+ 25
- 1
frappe/public/scss/desk/list.scss Parādīt failu

@@ -187,7 +187,31 @@ $level-margin-right: 8px;
} }


.list-paging-area, .footnote-area { .list-paging-area, .footnote-area {
border-top: 1px sol var(--border-color);
border-top: 1px solid var(--border-color);

.btn-group {
box-shadow: var(--drop-shadow);
border-radius: var(--border-radius-md);

&> .btn:nth-child(2) {
border-left: none;
border-right: none;
}

.btn-paging {
box-shadow: none;
margin-left: 0px !important;
border: 1px solid var(--dark-border-color);
&.btn-info {
background-color: var(--gray-400);
border-color: var(--gray-400);
color: var(--white);
font-weight: var(--text-bold);
}
}
}

} }


.frappe-card { .frappe-card {


+ 1
- 1
frappe/public/scss/desk/sidebar.scss Parādīt failu

@@ -104,10 +104,10 @@ body[data-route^="Module"] .main-menu {
} }


.sidebar-image-section { .sidebar-image-section {
width: min(100%, 170px);
cursor: pointer; cursor: pointer;


.sidebar-image { .sidebar-image {
width: min(100%, 170px);
height: auto; height: auto;
max-height: 170px; max-height: 170px;
object-fit: cover; object-fit: cover;


+ 1
- 1
frappe/templates/print_formats/standard.html Parādīt failu

@@ -3,7 +3,7 @@
{% for page in layout %} {% for page in layout %}
<div class="page-break"> <div class="page-break">
<div {% if print_settings.repeat_header_footer %} id="header-html" class="hidden-pdf" {% endif %}> <div {% if print_settings.repeat_header_footer %} id="header-html" class="hidden-pdf" {% endif %}>
{{ add_header(loop.index, layout|len, doc, letter_head, no_letterhead, footer, print_settings) }}
{{ add_header(loop.index, layout|len, doc, letter_head, no_letterhead, footer, print_settings, print_heading_template) }}
</div> </div>


{% if print_settings.repeat_header_footer %} {% if print_settings.repeat_header_footer %}


+ 3
- 3
frappe/templates/print_formats/standard_macros.html Parādīt failu

@@ -186,12 +186,12 @@ data-fieldname="{{ df.fieldname }}" data-fieldtype="{{ df.fieldtype }}"
{%- endif -%} {%- endif -%}
{% endmacro %} {% endmacro %}


{%- macro add_header(page_num, max_pages, doc, letter_head, no_letterhead, footer, print_settings=None) -%}
{%- macro add_header(page_num, max_pages, doc, letter_head, no_letterhead, footer, print_settings=None, print_heading_template=None) -%}
{% if letter_head and not no_letterhead %} {% if letter_head and not no_letterhead %}
<div class="letter-head">{{ letter_head }}</div> <div class="letter-head">{{ letter_head }}</div>
{% endif %} {% endif %}
{% if doc.print_heading_template %}
{{ frappe.render_template(doc.print_heading_template, {"doc":doc}) }}
{% if print_heading_template %}
{{ frappe.render_template(print_heading_template, {"doc":doc}) }}
{% else %} {% else %}
<div class="print-heading"> <div class="print-heading">
<h2> <h2>


+ 166
- 78
frappe/tests/test_api.py Parādīt failu

@@ -1,77 +1,126 @@
import sys
import unittest import unittest
from contextlib import contextmanager
from random import choice from random import choice
from threading import Thread
from typing import Dict, Optional, Tuple
from unittest.mock import patch


import requests import requests
from semantic_version import Version from semantic_version import Version
from werkzeug.test import TestResponse


import frappe import frappe
from frappe.utils import get_site_url
from frappe.utils import get_site_url, get_test_client


try:
_site = frappe.local.site
except Exception:
_site = None

authorization_token = None

@contextmanager
def suppress_stdout():
"""Supress stdout for tests which expectedly make noise
but that you don't need in tests"""
sys.stdout = None
try:
yield
finally:
sys.stdout = sys.__stdout__


def make_request(target: str, args: Optional[Tuple] = None, kwargs: Optional[Dict] = None) -> TestResponse:
t = ThreadWithReturnValue(target=target, args=args, kwargs=kwargs)
t.start()
t.join()
return t._return


def maintain_state(f):
def wrapper(*args, **kwargs):
frappe.db.rollback()
r = f(*args, **kwargs)
frappe.db.commit()
return r


return wrapper
def patch_request_header(key, *args, **kwargs):
if key == "Authorization":
return f"token {authorization_token}"




class TestResourceAPI(unittest.TestCase):
SITE_URL = get_site_url(frappe.local.site)
class ThreadWithReturnValue(Thread):
def __init__(self, group=None, target=None, name=None, args=(), kwargs={}):
Thread.__init__(self, group, target, name, args, kwargs)
self._return = None

def run(self):
if self._target is not None:
with patch("frappe.app.get_site_name", return_value=_site):
header_patch = patch("frappe.get_request_header", new=patch_request_header)
if authorization_token:
header_patch.start()
self._return = self._target(*self._args, **self._kwargs)
if authorization_token:
header_patch.stop()

def join(self, *args):
Thread.join(self, *args)
return self._return


class FrappeAPITestCase(unittest.TestCase):
SITE = frappe.local.site
SITE_URL = get_site_url(SITE)
RESOURCE_URL = f"{SITE_URL}/api/resource" RESOURCE_URL = f"{SITE_URL}/api/resource"
TEST_CLIENT = get_test_client()

@property
def sid(self) -> str:
if not getattr(self, "_sid", None):
r = self.post("/api/method/login", {
"usr": "Administrator",
"pwd": frappe.conf.admin_password or "admin",
})
self._sid = r.headers[2][1].split(";")[0].lstrip("sid=")

return self._sid

def get(self, path: str, params: Optional[Dict] = None) -> TestResponse:
return make_request(target=self.TEST_CLIENT.get, args=(path, ), kwargs={"data": params})

def post(self, path, data) -> TestResponse:
return make_request(target=self.TEST_CLIENT.post, args=(path, ), kwargs={"data": data})

def put(self, path, data) -> TestResponse:
return make_request(target=self.TEST_CLIENT.put, args=(path, ), kwargs={"data": data})

def delete(self, path) -> TestResponse:
return make_request(target=self.TEST_CLIENT.delete, args=(path, ))


class TestResourceAPI(FrappeAPITestCase):
DOCTYPE = "ToDo" DOCTYPE = "ToDo"
GENERATED_DOCUMENTS = [] GENERATED_DOCUMENTS = []


@classmethod @classmethod
@maintain_state
def setUpClass(self):
def setUpClass(cls):
for _ in range(10): for _ in range(10):
doc = frappe.get_doc( doc = frappe.get_doc(
{"doctype": "ToDo", "description": frappe.mock("paragraph")} {"doctype": "ToDo", "description": frappe.mock("paragraph")}
).insert() ).insert()
self.GENERATED_DOCUMENTS.append(doc.name)
cls.GENERATED_DOCUMENTS.append(doc.name)
frappe.db.commit()


@classmethod @classmethod
@maintain_state
def tearDownClass(self):
for name in self.GENERATED_DOCUMENTS:
frappe.delete_doc_if_exists(self.DOCTYPE, name)
def tearDownClass(cls):
for name in cls.GENERATED_DOCUMENTS:
frappe.delete_doc_if_exists(cls.DOCTYPE, name)
frappe.db.commit()


def setUp(self): def setUp(self):
# commit to ensure consistency in session (postgres CI randomly fails) # commit to ensure consistency in session (postgres CI randomly fails)
if frappe.conf.db_type == "postgres": if frappe.conf.db_type == "postgres":
frappe.db.commit() frappe.db.commit()


@property
def sid(self):
if not getattr(self, "_sid", None):
self._sid = requests.post(
f"{self.SITE_URL}/api/method/login",
data={
"usr": "Administrator",
"pwd": frappe.conf.admin_password or "admin",
},
).cookies.get("sid")

return self._sid

def get(self, path, params=""):
return requests.get(f"{self.RESOURCE_URL}/{path}?sid={self.sid}{params}")

def post(self, path, data):
return requests.post(
f"{self.RESOURCE_URL}/{path}?sid={self.sid}", data=frappe.as_json(data)
)

def put(self, path, data):
return requests.put(
f"{self.RESOURCE_URL}/{path}?sid={self.sid}", data=frappe.as_json(data)
)

def delete(self, path):
return requests.delete(f"{self.RESOURCE_URL}/{path}?sid={self.sid}")
if self._testMethodName == "test_auth_cycle":
from frappe.core.doctype.user.user import generate_keys
generate_keys("Administrator")
frappe.db.commit()


def test_unauthorized_call(self): def test_unauthorized_call(self):
# test 1: fetch documents without auth # test 1: fetch documents without auth
@@ -80,88 +129,107 @@ class TestResourceAPI(unittest.TestCase):


def test_get_list(self): def test_get_list(self):
# test 2: fetch documents without params # test 2: fetch documents without params
response = self.get(self.DOCTYPE)
response = self.get(f"/api/resource/{self.DOCTYPE}", {"sid": self.sid})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertIsInstance(response.json(), dict)
self.assertIn("data", response.json())
self.assertIsInstance(response.json, dict)
self.assertIn("data", response.json)


def test_get_list_limit(self): def test_get_list_limit(self):
# test 3: fetch data with limit # test 3: fetch data with limit
response = self.get(self.DOCTYPE, "&limit=2")
response = self.get(f"/api/resource/{self.DOCTYPE}", {"sid": self.sid, "limit": 2})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()["data"]), 2)
self.assertEqual(len(response.json["data"]), 2)


def test_get_list_dict(self): def test_get_list_dict(self):
# test 4: fetch response as (not) dict # test 4: fetch response as (not) dict
response = self.get(self.DOCTYPE, "&as_dict=True")
json = frappe._dict(response.json())
response = self.get(f"/api/resource/{self.DOCTYPE}", {"sid": self.sid, "as_dict": True})
json = frappe._dict(response.json)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertIsInstance(json.data, list) self.assertIsInstance(json.data, list)
self.assertIsInstance(json.data[0], dict) self.assertIsInstance(json.data[0], dict)


response = self.get(self.DOCTYPE, "&as_dict=False")
json = frappe._dict(response.json())
response = self.get(f"/api/resource/{self.DOCTYPE}", {"sid": self.sid, "as_dict": False})
json = frappe._dict(response.json)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertIsInstance(json.data, list) self.assertIsInstance(json.data, list)
self.assertIsInstance(json.data[0], list) self.assertIsInstance(json.data[0], list)


def test_get_list_debug(self): def test_get_list_debug(self):
# test 5: fetch response with debug # test 5: fetch response with debug
response = self.get(self.DOCTYPE, "&debug=true")
response = self.get(f"/api/resource/{self.DOCTYPE}", {"sid": self.sid, "debug": True})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertIn("exc", response.json())
self.assertIsInstance(response.json()["exc"], str)
self.assertIsInstance(eval(response.json()["exc"]), list)
self.assertIn("exc", response.json)
self.assertIsInstance(response.json["exc"], str)
self.assertIsInstance(eval(response.json["exc"]), list)


def test_get_list_fields(self): def test_get_list_fields(self):
# test 6: fetch response with fields # test 6: fetch response with fields
response = self.get(self.DOCTYPE, r'&fields=["description"]')
response = self.get(f"/api/resource/{self.DOCTYPE}", {"sid": self.sid, "fields": '["description"]'})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
json = frappe._dict(response.json())
json = frappe._dict(response.json)
self.assertIn("description", json.data[0]) self.assertIn("description", json.data[0])


def test_create_document(self): def test_create_document(self):
# test 7: POST method on /api/resource to create doc # test 7: POST method on /api/resource to create doc
data = {"description": frappe.mock("paragraph")}
response = self.post(self.DOCTYPE, data)
data = {"description": frappe.mock("paragraph"), "sid": self.sid}
response = self.post(f"/api/resource/{self.DOCTYPE}", data)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
docname = response.json()["data"]["name"]
docname = response.json["data"]["name"]
self.assertIsInstance(docname, str) self.assertIsInstance(docname, str)
self.GENERATED_DOCUMENTS.append(docname) self.GENERATED_DOCUMENTS.append(docname)


def test_update_document(self): def test_update_document(self):
# test 8: PUT method on /api/resource to update doc # test 8: PUT method on /api/resource to update doc
generated_desc = frappe.mock("paragraph") generated_desc = frappe.mock("paragraph")
data = {"description": generated_desc}
data = {"description": generated_desc, "sid": self.sid}
random_doc = choice(self.GENERATED_DOCUMENTS) random_doc = choice(self.GENERATED_DOCUMENTS)
desc_before_update = frappe.db.get_value(self.DOCTYPE, random_doc, "description") desc_before_update = frappe.db.get_value(self.DOCTYPE, random_doc, "description")


response = self.put(f"{self.DOCTYPE}/{random_doc}", data=data)
response = self.put(f"/api/resource/{self.DOCTYPE}/{random_doc}", data=data)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertNotEqual(response.json()["data"]["description"], desc_before_update)
self.assertEqual(response.json()["data"]["description"], generated_desc)
self.assertNotEqual(response.json["data"]["description"], desc_before_update)
self.assertEqual(response.json["data"]["description"], generated_desc)


def test_delete_document(self): def test_delete_document(self):
# test 9: DELETE method on /api/resource # test 9: DELETE method on /api/resource
doc_to_delete = choice(self.GENERATED_DOCUMENTS) doc_to_delete = choice(self.GENERATED_DOCUMENTS)
response = self.delete(f"{self.DOCTYPE}/{doc_to_delete}")
response = self.delete(f"/api/resource/{self.DOCTYPE}/{doc_to_delete}")
self.assertEqual(response.status_code, 202) self.assertEqual(response.status_code, 202)
self.assertDictEqual(response.json(), {"message": "ok"})
self.assertDictEqual(response.json, {"message": "ok"})
self.GENERATED_DOCUMENTS.remove(doc_to_delete) self.GENERATED_DOCUMENTS.remove(doc_to_delete)


non_existent_doc = frappe.generate_hash(length=12) non_existent_doc = frappe.generate_hash(length=12)
response = self.delete(f"{self.DOCTYPE}/{non_existent_doc}")
with suppress_stdout():
response = self.delete(f"/api/resource/{self.DOCTYPE}/{non_existent_doc}")
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
self.assertDictEqual(response.json(), {})
self.assertDictEqual(response.json, {})

def test_run_doc_method(self):
# test 10: Run whitelisted method on doc via /api/resource
# status_code is 403 if no other tests are run before this - it's not logged in
self.post("/api/resource/Website Theme/Standard", {"run_method": "get_apps"})
response = self.get("/api/resource/Website Theme/Standard", {"run_method": "get_apps"})
self.assertIn(response.status_code, (403, 200))

if response.status_code == 403:
self.assertTrue(set(response.json.keys()) == {'exc_type', 'exception', 'exc', '_server_messages'})
self.assertEqual(response.json.get('exc_type'), 'PermissionError')
self.assertEqual(response.json.get('exception'), 'frappe.exceptions.PermissionError: Not permitted')
self.assertIsInstance(response.json.get('exc'), str)


elif response.status_code == 200:
data = response.json.get("data")
self.assertIsInstance(data, list)
self.assertIsInstance(data[0], dict)


class TestMethodAPI(unittest.TestCase):
METHOD_URL = f"{get_site_url(frappe.local.site)}/api/method"

class TestMethodAPI(FrappeAPITestCase):
METHOD_PATH = "/api/method"


def test_version(self): def test_version(self):
# test 1: test for /api/method/version # test 1: test for /api/method/version
response = requests.get(f"{self.METHOD_URL}/version")
json = frappe._dict(response.json())
response = self.get(f"{self.METHOD_PATH}/version")
json = frappe._dict(response.json)


self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertIsInstance(json, dict) self.assertIsInstance(json, dict)
@@ -170,7 +238,27 @@ class TestMethodAPI(unittest.TestCase):


def test_ping(self): def test_ping(self):
# test 2: test for /api/method/ping # test 2: test for /api/method/ping
response = requests.get(f"{self.METHOD_URL}/ping")
response = self.get(f"{self.METHOD_PATH}/ping")
self.assertEqual(response.status_code, 200)
self.assertIsInstance(response.json, dict)
self.assertEqual(response.json["message"], "pong")

def test_get_user_info(self):
# test 3: test for /api/method/frappe.realtime.get_user_info
response = self.get(f"{self.METHOD_PATH}/frappe.realtime.get_user_info")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertIsInstance(response.json(), dict)
self.assertEqual(response.json()['message'], "pong")
self.assertIsInstance(response.json, dict)
self.assertIn(response.json.get("message").get("user"), ("Administrator", "Guest"))

def test_auth_cycle(self):
# test 4: Pass authorization token in request
global authorization_token
user = frappe.get_doc("User", "Administrator")
api_key, api_secret = user.api_key, user.get_password("api_secret")
authorization_token = f"{api_key}:{api_secret}"
response = self.get("/api/method/frappe.auth.get_logged_user")

self.assertEqual(response.status_code, 200)
self.assertEqual(response.json["message"], "Administrator")

authorization_token = None

+ 133
- 25
frappe/tests/test_commands.py Parādīt failu

@@ -3,25 +3,37 @@


# imports - standard imports # imports - standard imports
import gzip import gzip
import importlib
import json import json
import os import os
import shlex import shlex
import shutil import shutil
import subprocess import subprocess
from typing import List
import unittest import unittest
from contextlib import contextmanager
from functools import wraps
from glob import glob from glob import glob
from typing import List, Optional
from unittest.case import skipIf from unittest.case import skipIf
from unittest.mock import patch

# imports - third party imports
import click
from click.testing import CliRunner, Result
from click import Command


# imports - module imports # imports - module imports
import frappe import frappe
import frappe.commands.site
import frappe.commands.utils
import frappe.recorder import frappe.recorder
from frappe.installer import add_to_installed_apps, remove_app from frappe.installer import add_to_installed_apps, remove_app
from frappe.utils import add_to_date, get_bench_path, get_bench_relative_path, now from frappe.utils import add_to_date, get_bench_path, get_bench_relative_path, now
from frappe.utils.backups import fetch_latest_backups from frappe.utils.backups import fetch_latest_backups


# imports - third party imports
import click
_result: Optional[Result] = None
TEST_SITE = "commands-site-O4PN2QKA.test" # added random string tag to avoid collisions
CLI_CONTEXT = frappe._dict(sites=[TEST_SITE])




def clean(value) -> str: def clean(value) -> str:
@@ -76,7 +88,61 @@ def exists_in_backup(doctypes: List, file: os.PathLike) -> bool:
return len(missing_doctypes) == 0 return len(missing_doctypes) == 0




@contextmanager
def maintain_locals():
pre_site = frappe.local.site
pre_flags = frappe.local.flags.copy()
pre_db = frappe.local.db

try:
yield
finally:
post_site = getattr(frappe.local, "site", None)
if not post_site or post_site != pre_site:
frappe.init(site=pre_site)
frappe.local.db = pre_db
frappe.local.flags.update(pre_flags)


def pass_test_context(f):
@wraps(f)
def decorated_function(*args, **kwargs):
return f(CLI_CONTEXT, *args, **kwargs)
return decorated_function


@contextmanager
def cli(cmd: Command, args: Optional[List] = None):
with maintain_locals():
global _result

patch_ctx = patch("frappe.commands.pass_context", pass_test_context)
_module = cmd.callback.__module__
_cmd = cmd.callback.__qualname__

__module = importlib.import_module(_module)
patch_ctx.start()
importlib.reload(__module)
click_cmd = getattr(__module, _cmd)

try:
_result = CliRunner().invoke(click_cmd, args=args)
_result.command = str(cmd)
yield _result
finally:
patch_ctx.stop()
__module = importlib.import_module(_module)
importlib.reload(__module)
importlib.invalidate_caches()


class BaseTestCommands(unittest.TestCase): class BaseTestCommands(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.setup_test_site()
return super().setUpClass()

@classmethod
def execute(self, command, kwargs=None): def execute(self, command, kwargs=None):
site = {"site": frappe.local.site} site = {"site": frappe.local.site}
cmd_input = None cmd_input = None
@@ -102,16 +168,48 @@ class BaseTestCommands(unittest.TestCase):
self.stderr = clean(self._proc.stderr) self.stderr = clean(self._proc.stderr)
self.returncode = clean(self._proc.returncode) self.returncode = clean(self._proc.returncode)


@classmethod
def setup_test_site(cls):
cmd_config = {
"test_site": TEST_SITE,
"admin_password": frappe.conf.admin_password,
"root_login": frappe.conf.root_login,
"root_password": frappe.conf.root_password,
"db_type": frappe.conf.db_type,
}

if not os.path.exists(
os.path.join(TEST_SITE, "site_config.json")
):
cls.execute(
"bench new-site {test_site} --admin-password {admin_password} --db-type"
" {db_type}",
cmd_config,
)

def _formatMessage(self, msg, standardMsg): def _formatMessage(self, msg, standardMsg):
output = super(BaseTestCommands, self)._formatMessage(msg, standardMsg) output = super(BaseTestCommands, self)._formatMessage(msg, standardMsg)

if not hasattr(self, "command") and _result:
command = _result.command
stdout = _result.stdout_bytes.decode() if _result.stdout_bytes else None
stderr = _result.stderr_bytes.decode() if _result.stderr_bytes else None
returncode = _result.exit_code
else:
command = self.command
stdout = self.stdout
stderr = self.stderr
returncode = self.returncode

cmd_execution_summary = "\n".join([ cmd_execution_summary = "\n".join([
"-" * 70, "-" * 70,
"Last Command Execution Summary:", "Last Command Execution Summary:",
"Command: {}".format(self.command) if self.command else "",
"Standard Output: {}".format(self.stdout) if self.stdout else "",
"Standard Error: {}".format(self.stderr) if self.stderr else "",
"Return Code: {}".format(self.returncode) if self.returncode else "",
"Command: {}".format(command) if command else "",
"Standard Output: {}".format(stdout) if stdout else "",
"Standard Error: {}".format(stderr) if stderr else "",
"Return Code: {}".format(returncode) if returncode else "",
]).strip() ]).strip()

return "{}\n\n{}".format(output, cmd_execution_summary) return "{}\n\n{}".format(output, cmd_execution_summary)




@@ -135,6 +233,7 @@ class TestCommands(BaseTestCommands):
self.assertEqual(self.returncode, 0) self.assertEqual(self.returncode, 0)
self.assertEqual(self.stdout[1:-1], frappe.bold(text="DocType")) self.assertEqual(self.stdout[1:-1], frappe.bold(text="DocType"))


@unittest.skip
def test_restore(self): def test_restore(self):
# step 0: create a site to run the test on # step 0: create a site to run the test on
global_config = { global_config = {
@@ -143,35 +242,30 @@ class TestCommands(BaseTestCommands):
"root_password": frappe.conf.root_password, "root_password": frappe.conf.root_password,
"db_type": frappe.conf.db_type, "db_type": frappe.conf.db_type,
} }
site_data = {"another_site": f"{frappe.local.site}-restore.test", **global_config}
site_data = {"test_site": TEST_SITE, **global_config}
for key, value in global_config.items(): for key, value in global_config.items():
if value: if value:
self.execute(f"bench set-config {key} {value} -g") self.execute(f"bench set-config {key} {value} -g")
self.execute(
"bench new-site {another_site} --admin-password {admin_password} --db-type"
" {db_type}",
site_data,
)


# test 1: bench restore from full backup # test 1: bench restore from full backup
self.execute("bench --site {another_site} backup --ignore-backup-conf", site_data)
self.execute("bench --site {test_site} backup --ignore-backup-conf", site_data)
self.execute( self.execute(
"bench --site {another_site} execute frappe.utils.backups.fetch_latest_backups",
"bench --site {test_site} execute frappe.utils.backups.fetch_latest_backups",
site_data, site_data,
) )
site_data.update({"database": json.loads(self.stdout)["database"]}) site_data.update({"database": json.loads(self.stdout)["database"]})
self.execute("bench --site {another_site} restore {database}", site_data)
self.execute("bench --site {test_site} restore {database}", site_data)


# test 2: restore from partial backup # test 2: restore from partial backup
self.execute("bench --site {another_site} backup --exclude 'ToDo'", site_data)
self.execute("bench --site {test_site} backup --exclude 'ToDo'", site_data)
site_data.update({"kw": "\"{'partial':True}\""}) site_data.update({"kw": "\"{'partial':True}\""})
self.execute( self.execute(
"bench --site {another_site} execute"
"bench --site {test_site} execute"
" frappe.utils.backups.fetch_latest_backups --kwargs {kw}", " frappe.utils.backups.fetch_latest_backups --kwargs {kw}",
site_data, site_data,
) )
site_data.update({"database": json.loads(self.stdout)["database"]}) site_data.update({"database": json.loads(self.stdout)["database"]})
self.execute("bench --site {another_site} restore {database}", site_data)
self.execute("bench --site {test_site} restore {database}", site_data)
self.assertEqual(self.returncode, 1) self.assertEqual(self.returncode, 1)


def test_partial_restore(self): def test_partial_restore(self):
@@ -226,7 +320,8 @@ class TestCommands(BaseTestCommands):
def test_list_apps(self): def test_list_apps(self):
# test 1: sanity check for command # test 1: sanity check for command
self.execute("bench --site all list-apps") self.execute("bench --site all list-apps")
self.assertEqual(self.returncode, 0)
self.assertIsNotNone(self.returncode)
self.assertIsInstance(self.stdout or self.stderr, str)


# test 2: bare functionality for single site # test 2: bare functionality for single site
self.execute("bench --site {site} list-apps") self.execute("bench --site {site} list-apps")
@@ -242,14 +337,12 @@ class TestCommands(BaseTestCommands):
self.assertSetEqual(list_apps, installed_apps) self.assertSetEqual(list_apps, installed_apps)


# test 3: parse json format # test 3: parse json format
self.execute("bench --site all list-apps --format json")
self.assertEqual(self.returncode, 0)
self.assertIsInstance(json.loads(self.stdout), dict)

self.execute("bench --site {site} list-apps --format json") self.execute("bench --site {site} list-apps --format json")
self.assertEqual(self.returncode, 0)
self.assertIsInstance(json.loads(self.stdout), dict) self.assertIsInstance(json.loads(self.stdout), dict)


self.execute("bench --site {site} list-apps -f json") self.execute("bench --site {site} list-apps -f json")
self.assertEqual(self.returncode, 0)
self.assertIsInstance(json.loads(self.stdout), dict) self.assertIsInstance(json.loads(self.stdout), dict)


def test_show_config(self): def test_show_config(self):
@@ -358,7 +451,7 @@ class TestCommands(BaseTestCommands):
) )
def test_bench_drop_site_should_archive_site(self): def test_bench_drop_site_should_archive_site(self):
# TODO: Make this test postgres compatible # TODO: Make this test postgres compatible
site = 'test_site.localhost'
site = TEST_SITE


self.execute( self.execute(
f"bench new-site {site} --force --verbose " f"bench new-site {site} --force --verbose "
@@ -585,3 +678,18 @@ class TestRemoveApp(unittest.TestCase):


# nothing to assert, if this fails rest of the test suite will crumble. # nothing to assert, if this fails rest of the test suite will crumble.
remove_app("frappe", dry_run=True, yes=True, no_backup=True) remove_app("frappe", dry_run=True, yes=True, no_backup=True)


class TestSiteMigration(BaseTestCommands):
def test_migrate_cli(self):
with cli(frappe.commands.site.migrate) as result:
self.assertTrue(TEST_SITE in result.stdout)
self.assertEqual(result.exit_code, 0)
self.assertEqual(result.exception, None)


class TestBenchBuild(BaseTestCommands):
def test_build_assets(self):
with cli(frappe.commands.utils.build) as result:
self.assertEqual(result.exit_code, 0)
self.assertEqual(result.exception, None)

+ 10
- 0
frappe/tests/test_db.py Parādīt failu

@@ -291,6 +291,16 @@ class TestDB(unittest.TestCase):


frappe.db.MAX_WRITES_PER_TRANSACTION = Database.MAX_WRITES_PER_TRANSACTION frappe.db.MAX_WRITES_PER_TRANSACTION = Database.MAX_WRITES_PER_TRANSACTION


def test_pk_collision_ignoring(self):
# note has `name` generated from title
for _ in range(3):
frappe.get_doc(doctype="Note", title="duplicate name").insert(ignore_if_duplicate=True)

with savepoint():
self.assertRaises(frappe.DuplicateEntryError, frappe.get_doc(doctype="Note", title="duplicate name").insert)
# recover transaction to continue other tests
raise Exception



@run_only_if(db_type_is.MARIADB) @run_only_if(db_type_is.MARIADB)
class TestDDLCommandsMaria(unittest.TestCase): class TestDDLCommandsMaria(unittest.TestCase):


+ 68
- 6
frappe/tests/test_document.py Parādīt failu

@@ -1,11 +1,20 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import os
import unittest import unittest
from contextlib import contextmanager
from datetime import timedelta
from unittest.mock import patch


import frappe import frappe
from frappe.utils import cint
from frappe.model.naming import revert_series_if_last, make_autoname, parse_naming_series
from frappe.desk.doctype.note.note import Note
from frappe.model.naming import make_autoname, parse_naming_series, revert_series_if_last
from frappe.utils import cint, now_datetime


class CustomTestNote(Note):
@property
def age(self):
return now_datetime() - self.creation




class TestDocument(unittest.TestCase): class TestDocument(unittest.TestCase):
@@ -255,5 +264,58 @@ class TestDocument(unittest.TestCase):


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

# limit with filters
self.assertEquals(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
"""
frappe.db.delete("Custom Field", {"dt": "Note", "fieldname":"age"})
note = frappe.new_doc("Note")
note.content = "some content"
note.title = frappe.generate_hash(length=20)
note.insert()

def patch_note():
return patch("frappe.controllers", new={frappe.local.site: {'Note': CustomTestNote}})

@contextmanager
def customize_note(with_options=False):
options = "frappe.utils.now_datetime() - doc.creation" if with_options else ""
custom_field = frappe.get_doc({
"doctype": "Custom Field",
"dt": "Note",
"fieldname": "age",
"fieldtype": "Data",
"read_only": True,
"is_virtual": True,
"options": options,
})

try:
yield custom_field.insert(ignore_if_duplicate=True)
finally:
custom_field.delete()

with patch_note():
doc = frappe.get_last_doc("Note")
self.assertIsInstance(doc, CustomTestNote)
self.assertIsInstance(doc.age, timedelta)
self.assertIsNone(doc.as_dict().get("age"))
self.assertIsNone(doc.get_valid_dict().get("age"))

with customize_note(), patch_note():
doc = frappe.get_last_doc("Note")
self.assertIsInstance(doc, CustomTestNote)
self.assertIsInstance(doc.age, timedelta)
self.assertIsInstance(doc.as_dict().get("age"), timedelta)
self.assertIsInstance(doc.get_valid_dict().get("age"), timedelta)

with customize_note(with_options=True):
doc = frappe.get_last_doc("Note")
self.assertIsInstance(doc, Note)
self.assertIsInstance(doc.as_dict().get("age"), timedelta)
self.assertIsInstance(doc.get_valid_dict().get("age"), timedelta)

+ 2
- 2
frappe/tests/test_frappe_client.py Parādīt failu

@@ -10,7 +10,8 @@ import requests
import base64 import base64


class TestFrappeClient(unittest.TestCase): class TestFrappeClient(unittest.TestCase):
PASSWORD = "admin"
PASSWORD = frappe.conf.admin_password or "admin"

def test_insert_many(self): def test_insert_many(self):
server = FrappeClient(get_url(), "Administrator", self.PASSWORD, verify=False) server = FrappeClient(get_url(), "Administrator", self.PASSWORD, verify=False)
frappe.db.delete("Note", {"title": ("in", ('Sing','a','song','of','sixpence'))}) frappe.db.delete("Note", {"title": ("in", ('Sing','a','song','of','sixpence'))})
@@ -169,7 +170,6 @@ class TestFrappeClient(unittest.TestCase):
res = requests.post(get_url() + "/api/method/frappe.auth.get_logged_user", headers=header) res = requests.post(get_url() + "/api/method/frappe.auth.get_logged_user", headers=header)
self.assertEqual(res.status_code, 403) self.assertEqual(res.status_code, 403)



# random api key and api secret # random api key and api secret
api_key = "@3djdk3kld" api_key = "@3djdk3kld"
api_secret = "ksk&93nxoe3os" api_secret = "ksk&93nxoe3os"


+ 93
- 0
frappe/tests/test_utils.py Parādīt failu

@@ -419,3 +419,96 @@ class TestXlsxUtils(unittest.TestCase):
val = handle_html("<p>html data &gt;</p>") val = handle_html("<p>html data &gt;</p>")
self.assertIn("html data >", val) self.assertIn("html data >", val)
self.assertEqual("abc", handle_html("abc")) self.assertEqual("abc", handle_html("abc"))


class TestLinkTitle(unittest.TestCase):
def test_link_title_doctypes_in_boot_info(self):
"""
Test that doctypes are added to link_title_map in boot_info
"""
custom_doctype = frappe.get_doc(
{
"doctype": "DocType",
"module": "Core",
"custom": 1,
"fields": [
{
"label": "Test Field",
"fieldname": "test_title_field",
"fieldtype": "Data",
}
],
"show_title_field_in_link": 1,
"title_field": "test_title_field",
"permissions": [{"role": "System Manager", "read": 1}],
"name": "Test Custom Doctype for Link Title",
}
)
custom_doctype.insert()

prop_setter = frappe.get_doc(
{
"doctype": "Property Setter",
"doc_type": "User",
"property": "show_title_field_in_link",
"property_type": "Check",
"doctype_or_field": "DocType",
"value": "1",
}
).insert()

from frappe.boot import get_link_title_doctypes

link_title_doctypes = get_link_title_doctypes()
self.assertTrue("User" in link_title_doctypes)
self.assertTrue("Test Custom Doctype for Link Title" in link_title_doctypes)

prop_setter.delete()
custom_doctype.delete()

def test_link_titles_on_getdoc(self):
"""
Test that link titles are added to the doctype on getdoc
"""
prop_setter = frappe.get_doc(
{
"doctype": "Property Setter",
"doc_type": "User",
"property": "show_title_field_in_link",
"property_type": "Check",
"doctype_or_field": "DocType",
"value": "1",
}
).insert()

user = frappe.get_doc(
{
"doctype": "User",
"user_type": "Website User",
"email": "test_user_for_link_title@example.com",
"send_welcome_email": 0,
"first_name": "Test User for Link Title",
}
).insert(ignore_permissions=True)

todo = frappe.get_doc(
{
"doctype": "ToDo",
"description": "test-link-title-on-getdoc",
"allocated_to": user.name,
}
).insert()

from frappe.desk.form.load import getdoc

getdoc("ToDo", todo.name)
link_titles = frappe.local.response["_link_titles"]

self.assertTrue(f"{user.doctype}::{user.name}" in link_titles)
self.assertEqual(link_titles[f"{user.doctype}::{user.name}"], user.full_name)

todo.delete()
user.delete()
prop_setter.delete()



+ 20
- 0
frappe/tests/ui_test_helpers.py Parādīt failu

@@ -134,6 +134,12 @@ def create_contact_records():
insert_contact('Test Form Contact 2', '54321') insert_contact('Test Form Contact 2', '54321')
insert_contact('Test Form Contact 3', '12345') insert_contact('Test Form Contact 3', '12345')


@frappe.whitelist()
def create_multiple_contact_records():
if frappe.db.get_all('Contact', {'first_name': 'Multiple Contact 1'}):
return
for index in range(1001):
insert_contact('Multiple Contact {}'.format(index+1), '12345{}'.format(index+1))


def insert_contact(first_name, phone_number): def insert_contact(first_name, phone_number):
doc = frappe.get_doc({ doc = frappe.get_doc({
@@ -249,3 +255,17 @@ def update_webform_to_multistep():
_doc.route = "update-profile-duplicate" _doc.route = "update-profile-duplicate"
_doc.is_standard = False _doc.is_standard = False
_doc.save() _doc.save()

@frappe.whitelist()
def update_child_table(name):
doc = frappe.get_doc('DocType', name)
if len(doc.fields) == 1:
doc.append('fields', {
'fieldname': 'doctype_to_link',
'fieldtype': 'Link',
'in_list_view': 1,
'label': 'Doctype to Link',
'options': 'Doctype to Link'
})

doc.save()

+ 10
- 4
frappe/translations/de.csv Parādīt failu

@@ -148,6 +148,8 @@ More Information,Mehr Informationen,
More...,Mehr..., More...,Mehr...,
Move,Bewegen, Move,Bewegen,
My Account,Mein Konto, My Account,Mein Konto,
My Profile,Mein Profil,
My Settings,Meine Einstellungen,
New Address,Neue Adresse, New Address,Neue Adresse,
New Contact,Neuer Kontakt, New Contact,Neuer Kontakt,
Next,Weiter, Next,Weiter,
@@ -406,7 +408,7 @@ Allow Self Approval,Erlaube Selbstgenehmigung,
Allow approval for creator of the document,Genehmigung für den Ersteller des Dokuments zulassen, Allow approval for creator of the document,Genehmigung für den Ersteller des Dokuments zulassen,
Allow events in timeline,Ereignisse in der Zeitleiste zulassen, Allow events in timeline,Ereignisse in der Zeitleiste zulassen,
Allow in Quick Entry,In Schnelleingabe zulassen, Allow in Quick Entry,In Schnelleingabe zulassen,
Allow on Submit,Beim Übertragen zulassen,
Allow on Submit,Änderungen zulassen wenn gebucht,
Allow only one session per user,Nur eine Sitzung pro Benutzer zulassen, Allow only one session per user,Nur eine Sitzung pro Benutzer zulassen,
Allow page break inside tables,Seitenumbruch innerhalb von Tabellen erlauben, Allow page break inside tables,Seitenumbruch innerhalb von Tabellen erlauben,
Allow saving if mandatory fields are not filled,Speichern trotz leerer Pflichtfelder zulassen, Allow saving if mandatory fields are not filled,Speichern trotz leerer Pflichtfelder zulassen,
@@ -734,6 +736,7 @@ Content Hash,Inhalts-Hash,
Content web page.,Inhalt der Webseite., Content web page.,Inhalt der Webseite.,
Conversation Tones,Konversationstöne, Conversation Tones,Konversationstöne,
Copyright,Copyright, Copyright,Copyright,
Copy to Clipboard,In die Zwischenablage,
Core,Kern, Core,Kern,
Core DocTypes cannot be customized.,Core DocTypes können nicht angepasst werden., Core DocTypes cannot be customized.,Core DocTypes können nicht angepasst werden.,
Could not connect to outgoing email server,Konnte keine Verbindung zum Postausgangsserver herstellen, Could not connect to outgoing email server,Konnte keine Verbindung zum Postausgangsserver herstellen,
@@ -957,6 +960,7 @@ Edit {0},Bearbeiten {0},
Editable Grid,Editierbares Raster, Editable Grid,Editierbares Raster,
Editing Row,Zeile bearbeiten, Editing Row,Zeile bearbeiten,
Eg. smsgateway.com/api/send_sms.cgi,z. B. smsgateway.com/api/send_sms.cgi, Eg. smsgateway.com/api/send_sms.cgi,z. B. smsgateway.com/api/send_sms.cgi,
Email,E-Mail,
Email Account Name,E-Mail-Konten-Name, Email Account Name,E-Mail-Konten-Name,
Email Account added multiple times,E-Mail-Konto wurde mehrmals hinzugefügt, Email Account added multiple times,E-Mail-Konto wurde mehrmals hinzugefügt,
Email Addresses,E-Mail-Adressen, Email Addresses,E-Mail-Adressen,
@@ -1222,8 +1226,8 @@ Headers,Headers,
Heading,Überschrift, Heading,Überschrift,
Hello {0},Hallo {0}, Hello {0},Hallo {0},
Hello!,Hallo!, Hello!,Hallo!,
Help Articles,Artikel-Hilfe,
Help Category,Kategorie-Hilfe,
Help Articles,Hilfeartikel,
Help Category,Hilfekategorie,
Help on Search,Hilfe zur Suche, Help on Search,Hilfe zur Suche,
"Help: To link to another record in the system, use ""#Form/Note/[Note Name]"" as the Link URL. (don't use ""http://"")","Hilfe: Um eine Verknüpfung mit einem anderen Datensatz im System zu erstellen, bitte ""#Formular/Anmerkung/[Anmerkungsname]"" als Verknüpfungs-URL verwenden (kein ""http://""!).", "Help: To link to another record in the system, use ""#Form/Note/[Note Name]"" as the Link URL. (don't use ""http://"")","Hilfe: Um eine Verknüpfung mit einem anderen Datensatz im System zu erstellen, bitte ""#Formular/Anmerkung/[Anmerkungsname]"" als Verknüpfungs-URL verwenden (kein ""http://""!).",
Helvetica,Helvetica, Helvetica,Helvetica,
@@ -1451,6 +1455,7 @@ Last User,Letzter Benutzer,
Last Week,Letzte Woche, Last Week,Letzte Woche,
Last Year,Vergangenes Jahr, Last Year,Vergangenes Jahr,
Last synced {0},Zuletzt synchronisiert {0}, Last synced {0},Zuletzt synchronisiert {0},
Learn more,Mehr erfahren,
Leave a Comment,Hinterlasse einen Kommentar, Leave a Comment,Hinterlasse einen Kommentar,
Leave blank to repeat always,"Freilassen, um immer zu wiederholen", Leave blank to repeat always,"Freilassen, um immer zu wiederholen",
Leave this conversation,Benachrichtigungen abbestellen, Leave this conversation,Benachrichtigungen abbestellen,
@@ -1483,7 +1488,8 @@ Linked with {0},Verknüpft mit {0},
Links,Verknüpfungen, Links,Verknüpfungen,
List,Listenansicht, List,Listenansicht,
List Filter,Listenfilter, List Filter,Listenfilter,
List View Setting,List View Setting,
List View,Listenansicht,
List View Setting,Einstellungen zu Listenansicht,
List a document type,Einen Dokumenttyp auflisten, List a document type,Einen Dokumenttyp auflisten,
"List as [{""label"": _(""Jobs""), ""route"":""jobs""}]","Liste als [{ ""label"": _ ( ""Jobs""), ""route"": ""jobs""}]", "List as [{""label"": _(""Jobs""), ""route"":""jobs""}]","Liste als [{ ""label"": _ ( ""Jobs""), ""route"": ""jobs""}]",
List of backups available for download,Datensicherungen herunterladen, List of backups available for download,Datensicherungen herunterladen,


+ 2
- 1
frappe/utils/__init__.py Parādīt failu

@@ -438,7 +438,8 @@ def touch_file(path):
os.utime(path, None) os.utime(path, None)
return path return path


def get_test_client():
def get_test_client() -> Client:
"""Returns an test instance of the Frappe WSGI"""
from frappe.app import application from frappe.app import application
return Client(application) return Client(application)




+ 2
- 1
frappe/utils/backups.py Parādīt failu

@@ -653,7 +653,8 @@ def get_backup_path():


@frappe.whitelist() @frappe.whitelist()
def get_backup_encryption_key(): def get_backup_encryption_key():
return frappe.local.conf.encryption_key
frappe.only_for("System Manager")
return frappe.conf.encryption_key


class Backup: class Backup:
def __init__(self, file_path): def __init__(self, file_path):


+ 21
- 1
frappe/utils/formatters.py Parādīt failu

@@ -88,9 +88,14 @@ def format_value(value, df=None, doc=None, currency=None, translated=False, form
return frappe.utils.markdown(value) return frappe.utils.markdown(value)


elif df.get("fieldtype") == "Table MultiSelect": elif df.get("fieldtype") == "Table MultiSelect":
values = []
meta = frappe.get_meta(df.options) meta = frappe.get_meta(df.options)
link_field = [df for df in meta.fields if df.fieldtype == 'Link'][0] link_field = [df for df in meta.fields if df.fieldtype == 'Link'][0]
values = [v.get(link_field.fieldname, 'asdf') for v in value]
for v in value:
v.update({'__link_titles': doc.get('__link_titles')})
formatted_value = frappe.format_value(v.get(link_field.fieldname, ''), link_field, v)
values.append(formatted_value)

return ', '.join(values) return ', '.join(values)


elif df.get("fieldtype") == "Duration": elif df.get("fieldtype") == "Duration":
@@ -100,4 +105,19 @@ def format_value(value, df=None, doc=None, currency=None, translated=False, form
elif df.get("fieldtype") == "Text Editor": elif df.get("fieldtype") == "Text Editor":
return "<div class='ql-snow'>{}</div>".format(value) return "<div class='ql-snow'>{}</div>".format(value)


elif df.get("fieldtype") in ["Link", "Dynamic Link"]:
if not doc or not doc.get("__link_titles") or not df.options:
return value

doctype = df.options
if df.get("fieldtype") == "Dynamic Link":
if not df.parent:
return value

meta = frappe.get_meta(df.parent)
_field = meta.get_field(df.options)
doctype = _field.options

return doc.__link_titles.get("{0}::{1}".format(doctype, value), value)

return value return value

+ 0
- 212
frappe/utils/minify.py Parādīt failu

@@ -1,212 +0,0 @@

# This code is original from jsmin by Douglas Crockford, it was translated to
# Python by Baruch Even. The original code had the following copyright and
# license.
#
# /* jsmin.c
# 2007-05-22
#
# Copyright (c) 2002 Douglas Crockford (www.crockford.com)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
# of the Software, and to permit persons to whom the Software is furnished to do
# so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# The Software shall be used for Good, not Evil.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# */

from io import StringIO

def jsmin(js):
ins = StringIO(js)
outs = StringIO()
JavascriptMinify().minify(ins, outs)
str = outs.getvalue()
if len(str) > 0 and str[0] == '\n':
str = str[1:]
return str

def isAlphanum(c):
"""return true if the character is a letter, digit, underscore,
dollar sign, or non-ASCII character.
"""
return ((c >= 'a' and c <= 'z') or (c >= '0' and c <= '9') or
(c >= 'A' and c <= 'Z') or c == '_' or c == '$' or c == '\\' or (c is not None and ord(c) > 126));

class UnterminatedComment(Exception):
pass

class UnterminatedStringLiteral(Exception):
pass

class UnterminatedRegularExpression(Exception):
pass

class JavascriptMinify(object):

def _outA(self):
self.outstream.write(self.theA)
def _outB(self):
self.outstream.write(self.theB)

def _get(self):
"""return the next character from stdin. Watch out for lookahead. If
the character is a control character, translate it to a space or
linefeed.
"""
c = self.theLookahead
self.theLookahead = None
if c is None:
c = self.instream.read(1)
if c >= ' ' or c == '\n':
return c
if c == '': # EOF
return '\000'
if c == '\r':
return '\n'
return ' '

def _peek(self):
self.theLookahead = self._get()
return self.theLookahead

def _next(self):
"""get the next character, excluding comments. peek() is used to see
if an unescaped '/' is followed by a '/' or '*'.
"""
c = self._get()
if c == '/' and self.theA != '\\':
p = self._peek()
if p == '/':
c = self._get()
while c > '\n':
c = self._get()
return c
if p == '*':
c = self._get()
while 1:
c = self._get()
if c == '*':
if self._peek() == '/':
self._get()
return ' '
if c == '\000':
raise UnterminatedComment()

return c

def _action(self, action):
"""do something! What you do is determined by the argument:
1 Output A. Copy B to A. Get the next B.
2 Copy B to A. Get the next B. (Delete A).
3 Get the next B. (Delete B).
action treats a string as a single character. Wow!
action recognizes a regular expression if it is preceded by ( or , or =.
"""
if action <= 1:
self._outA()

if action <= 2:
self.theA = self.theB
if self.theA == "'" or self.theA == '"':
while 1:
self._outA()
self.theA = self._get()
if self.theA == self.theB:
break
if self.theA <= '\n':
raise UnterminatedStringLiteral()
if self.theA == '\\':
self._outA()
self.theA = self._get()


if action <= 3:
self.theB = self._next()
if self.theB == '/' and (self.theA == '(' or self.theA == ',' or
self.theA == '=' or self.theA == ':' or
self.theA == '[' or self.theA == '?' or
self.theA == '!' or self.theA == '&' or
self.theA == '|' or self.theA == ';' or
self.theA == '{' or self.theA == '}' or
self.theA == '\n'):
self._outA()
self._outB()
while 1:
self.theA = self._get()
if self.theA == '/':
break
elif self.theA == '\\':
self._outA()
self.theA = self._get()
elif self.theA <= '\n':
raise UnterminatedRegularExpression()
self._outA()
self.theB = self._next()


def _jsmin(self):
"""Copy the input to the output, deleting the characters which are
insignificant to JavaScript. Comments will be removed. Tabs will be
replaced with spaces. Carriage returns will be replaced with linefeeds.
Most spaces and linefeeds will be removed.
"""
self.theA = '\n'
self._action(3)

while self.theA != '\000':
if self.theA == ' ':
if isAlphanum(self.theB):
self._action(1)
else:
self._action(2)
elif self.theA == '\n':
if self.theB in ['{', '[', '(', '+', '-']:
self._action(1)
elif self.theB == ' ':
self._action(3)
else:
if isAlphanum(self.theB):
self._action(1)
else:
self._action(2)
else:
if self.theB == ' ':
if isAlphanum(self.theA):
self._action(1)
else:
self._action(3)
elif self.theB == '\n':
if self.theA in ['}', ']', ')', '+', '-', '"', '\'']:
self._action(1)
else:
if isAlphanum(self.theA):
self._action(1)
else:
self._action(3)
else:
self._action(1)

def minify(self, instream, outstream):
self.instream = instream
self.outstream = outstream
self.theA = '\n'
self.theB = None
self.theLookahead = None

self._jsmin()
self.instream.close()

+ 1
- 1
frappe/website/doctype/web_form/web_form.js Parādīt failu

@@ -60,7 +60,7 @@ frappe.ui.form.on("Web Form", {
options: field.options, options: field.options,
reqd: field.reqd, reqd: field.reqd,
default: field.default, default: field.default,
read_only: field.read_only,
read_only: field.read_only || field.is_virtual,
depends_on: field.depends_on, depends_on: field.depends_on,
mandatory_depends_on: field.mandatory_depends_on, mandatory_depends_on: field.mandatory_depends_on,
read_only_depends_on: field.read_only_depends_on, read_only_depends_on: field.read_only_depends_on,


+ 50
- 9
frappe/www/printview.py Parādīt failu

@@ -151,7 +151,12 @@ def get_rendered_template(doc, name=None, print_format=None, meta=None,


convert_markdown(doc, meta) convert_markdown(doc, meta)


args = {
args = {}
# extract `print_heading_template` from the first field and remove it
if format_data and format_data[0].get("fieldname") == "print_heading_template":
args["print_heading_template"] = format_data.pop(0).get("options")

args.update({
"doc": doc, "doc": doc,
"meta": frappe.get_meta(doc.doctype), "meta": frappe.get_meta(doc.doctype),
"layout": make_layout(doc, meta, format_data), "layout": make_layout(doc, meta, format_data),
@@ -160,7 +165,7 @@ def get_rendered_template(doc, name=None, print_format=None, meta=None,
"letter_head": letter_head.content, "letter_head": letter_head.content,
"footer": letter_head.footer, "footer": letter_head.footer,
"print_settings": print_settings "print_settings": print_settings
}
})


html = template.render(args, filters={"len": len}) html = template.render(args, filters={"len": len})


@@ -169,6 +174,48 @@ def get_rendered_template(doc, name=None, print_format=None, meta=None,


return html return html


def set_link_titles(doc):
# Adds name with title of link field doctype to __link_titles
if not doc.get("__link_titles"):
setattr(doc, "__link_titles", {})

meta = frappe.get_meta(doc.doctype)
set_title_values_for_link_and_dynamic_link_fields(meta, doc)
set_title_values_for_table_and_multiselect_fields(meta, doc)

def set_title_values_for_link_and_dynamic_link_fields(meta, doc, parent_doc=None):
if parent_doc and not parent_doc.get("__link_titles"):
setattr(parent_doc, "__link_titles", {})
elif doc and not doc.get("__link_titles"):
setattr(doc, "__link_titles", {})

for field in meta.get_link_fields() + meta.get_dynamic_link_fields():
if not doc.get(field.fieldname):
continue

# If link field, then get doctype from options
# If dynamic link field, then get doctype from dependent field
doctype = field.options if field.fieldtype == "Link" else doc.get(field.options)

meta = frappe.get_meta(doctype)
if not meta or not (meta.title_field and meta.show_title_field_in_link):
continue

link_title = frappe.get_cached_value(doctype, doc.get(field.fieldname), meta.title_field)
if parent_doc:
parent_doc.__link_titles["{0}::{1}".format(doctype, doc.get(field.fieldname))] = link_title
elif doc:
doc.__link_titles["{0}::{1}".format(doctype, doc.get(field.fieldname))] = link_title

def set_title_values_for_table_and_multiselect_fields(meta, doc):
for field in meta.get_table_fields():
if not doc.get(field.fieldname):
continue

_meta = frappe.get_meta(field.options)
for value in doc.get(field.fieldname):
set_title_values_for_link_and_dynamic_link_fields(_meta, value, doc)

def convert_markdown(doc, meta): def convert_markdown(doc, meta):
'''Convert text field values to markdown if necessary''' '''Convert text field values to markdown if necessary'''
for field in meta.fields: for field in meta.fields:
@@ -190,6 +237,7 @@ def get_html_and_style(doc, name=None, print_format=None, meta=None,
doc = frappe.get_doc(json.loads(doc)) doc = frappe.get_doc(json.loads(doc))


print_format = get_print_format_doc(print_format, meta=meta or frappe.get_meta(doc.doctype)) print_format = get_print_format_doc(print_format, meta=meta or frappe.get_meta(doc.doctype))
set_link_titles(doc)


try: try:
html = get_rendered_template(doc, name=name, print_format=print_format, meta=meta, html = get_rendered_template(doc, name=name, print_format=print_format, meta=meta,
@@ -276,13 +324,6 @@ def make_layout(doc, meta, format_data=None):
layout, page = [], [] layout, page = [], []
layout.append(page) layout.append(page)


if format_data:
# extract print_heading_template from the first field
# and remove the field
if format_data[0].get("fieldname") == "print_heading_template":
doc.print_heading_template = format_data[0].get("options")
format_data = format_data[1:]

def get_new_section(): return {'columns': [], 'has_data': False} def get_new_section(): return {'columns': [], 'has_data': False}


def append_empty_field_dict_to_page_column(page): def append_empty_field_dict_to_page_column(page):


Notiek ielāde…
Atcelt
Saglabāt