Merge branch 'develop' into fix/ux_kb_driven_animations
This commit is contained in:
commit
dc60d7d7f0
90 changed files with 1488 additions and 777 deletions
32
.github/workflows/release.yml
vendored
Normal file
32
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
name: Generate Semantic Release
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- version-13
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Entire Repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Setup Node.js v14
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14
|
||||
- name: Setup dependencies
|
||||
run: |
|
||||
npm install @semantic-release/git @semantic-release/exec --no-save
|
||||
- name: Create Release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
GIT_AUTHOR_NAME: "Frappe PR Bot"
|
||||
GIT_AUTHOR_EMAIL: "developers@frappe.io"
|
||||
GIT_COMMITTER_NAME: "Frappe PR Bot"
|
||||
GIT_COMMITTER_EMAIL: "developers@frappe.io"
|
||||
run: npx semantic-release
|
||||
40
.mergify.yml
40
.mergify.yml
|
|
@ -53,3 +53,43 @@ pull_request_rules:
|
|||
{{ title }} (#{{ number }})
|
||||
|
||||
{{ body }}
|
||||
|
||||
- name: backport to develop
|
||||
conditions:
|
||||
- label="backport develop"
|
||||
actions:
|
||||
backport:
|
||||
branches:
|
||||
- develop
|
||||
assignees:
|
||||
- "{{ author }}"
|
||||
|
||||
- name: backport to version-13-hotfix
|
||||
conditions:
|
||||
- label="backport version-13-hotfix"
|
||||
actions:
|
||||
backport:
|
||||
branches:
|
||||
- version-13-hotfix
|
||||
assignees:
|
||||
- "{{ author }}"
|
||||
|
||||
- name: backport to version-13-pre-release
|
||||
conditions:
|
||||
- label="backport version-13-pre-release"
|
||||
actions:
|
||||
backport:
|
||||
branches:
|
||||
- version-13-pre-release
|
||||
assignees:
|
||||
- "{{ author }}"
|
||||
|
||||
- name: backport to version-12-hotfix
|
||||
conditions:
|
||||
- label="backport version-12-hotfix"
|
||||
actions:
|
||||
backport:
|
||||
branches:
|
||||
- version-12-hotfix
|
||||
assignees:
|
||||
- "{{ author }}"
|
||||
24
.releaserc
Normal file
24
.releaserc
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"branches": ["version-13"],
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer", {
|
||||
"preset": "angular",
|
||||
"releaseRules": [
|
||||
{"breaking": true, "release": false}
|
||||
]
|
||||
},
|
||||
"@semantic-release/release-notes-generator",
|
||||
[
|
||||
"@semantic-release/exec", {
|
||||
"prepareCmd": 'sed -ir "s/[0-9]*\.[0-9]*\.[0-9]*/${nextRelease.version}/" frappe/__init__.py'
|
||||
}
|
||||
],
|
||||
[
|
||||
"@semantic-release/git", {
|
||||
"assets": ["frappe/__init__.py"],
|
||||
"message": "chore(release): Bumped to Version ${nextRelease.version}\n\n${nextRelease.notes}"
|
||||
}
|
||||
],
|
||||
"@semantic-release/github"
|
||||
]
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ context('Control Barcode', () => {
|
|||
it('should generate barcode on setting a value', () => {
|
||||
get_dialog_with_barcode().as('dialog');
|
||||
|
||||
cy.focused().blur();
|
||||
cy.get('.frappe-control[data-fieldname=barcode]').findByRole('textbox')
|
||||
.type('123456789')
|
||||
.blur();
|
||||
|
|
@ -36,6 +37,7 @@ context('Control Barcode', () => {
|
|||
it('should reset when input is cleared', () => {
|
||||
get_dialog_with_barcode().as('dialog');
|
||||
|
||||
cy.focused().blur();
|
||||
cy.get('.frappe-control[data-fieldname=barcode]').findByRole('textbox')
|
||||
.type('123456789')
|
||||
.blur();
|
||||
|
|
|
|||
|
|
@ -1,23 +1,27 @@
|
|||
context('Date Control', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/doctype');
|
||||
return cy.window().its('frappe').then(frappe => {
|
||||
return frappe.xcall('frappe.tests.ui_test_helpers.create_doctype', {
|
||||
name: 'Test Date Control',
|
||||
fields: [
|
||||
{
|
||||
"label": "Date",
|
||||
"fieldname": "date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1
|
||||
},
|
||||
]
|
||||
});
|
||||
});
|
||||
cy.visit('/app');
|
||||
});
|
||||
|
||||
function get_dialog(date_field_options) {
|
||||
return cy.dialog({
|
||||
title: 'Date',
|
||||
fields: [{
|
||||
"label": "Date",
|
||||
"fieldname": "date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
...date_field_options
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
it('Selecting a date from the datepicker', () => {
|
||||
cy.new_form('Test Date Control');
|
||||
cy.clear_dialogs();
|
||||
cy.clear_datepickers();
|
||||
|
||||
get_dialog().as('dialog');
|
||||
cy.get_field('date', 'Date').click();
|
||||
cy.get('.datepicker--nav-title').click();
|
||||
cy.get('.datepicker--nav-title').click({force: true});
|
||||
|
|
@ -28,12 +32,16 @@ context('Date Control', () => {
|
|||
cy.get('.datepicker--months > .datepicker--cells > .datepicker--cell[data-month=0]').click();
|
||||
cy.get('.datepicker--days > .datepicker--cells > .datepicker--cell[data-date=15]').click();
|
||||
|
||||
//Verifying if the selected date is displayed in the date field
|
||||
cy.get_field('date', 'Date').should('have.value', '01-15-2020');
|
||||
// Verify if the selected date is set the date field
|
||||
cy.window().its('cur_dialog.fields_dict.date.value').should('be.equal', '2020-01-15');
|
||||
});
|
||||
|
||||
it('Checking next and previous button', () => {
|
||||
cy.get_field('date', 'Date').click();
|
||||
cy.clear_dialogs();
|
||||
cy.clear_datepickers();
|
||||
|
||||
get_dialog({ default: '2020-01-15' }).as('dialog');
|
||||
cy.get_field('date', 'Date').click();
|
||||
|
||||
//Clicking on the next button in the datepicker
|
||||
cy.get('.datepicker--nav-action[data-action=next]').click();
|
||||
|
|
@ -42,7 +50,7 @@ context('Date Control', () => {
|
|||
cy.get('.datepicker--cell[data-date=15]').click({force: true});
|
||||
|
||||
//Verifying if the selected date has been displayed in the date field
|
||||
cy.get_field('date', 'Date').should('have.value', '02-15-2020');
|
||||
cy.window().its('cur_dialog.fields_dict.date.value').should('be.equal', '2020-02-15');
|
||||
cy.wait(500);
|
||||
cy.get_field('date', 'Date').click();
|
||||
|
||||
|
|
@ -53,19 +61,22 @@ context('Date Control', () => {
|
|||
cy.get('.datepicker--cell[data-date=15]').click({force: true});
|
||||
|
||||
//Verifying if the selected date has been displayed in the date field
|
||||
cy.get_field('date', 'Date').should('have.value', '01-15-2020');
|
||||
cy.window().its('cur_dialog.fields_dict.date.value').should('be.equal', '2020-01-15');
|
||||
});
|
||||
|
||||
it('Clicking on "Today" button gives todays date', () => {
|
||||
cy.get_field('date', 'Date').click();
|
||||
cy.clear_dialogs();
|
||||
cy.clear_datepickers();
|
||||
|
||||
get_dialog().as('dialog');
|
||||
cy.get_field('date', 'Date').click();
|
||||
|
||||
//Clicking on "Today" button
|
||||
cy.get('.datepicker--button').click();
|
||||
|
||||
//Picking up the todays date
|
||||
const todays_date = Cypress.moment().format('MM-DD-YYYY');
|
||||
|
||||
//Verifying if clicking on "Today" button matches today's date
|
||||
cy.get_field('date', 'Date').should('have.value', todays_date);
|
||||
cy.window().then(win => {
|
||||
expect(win.cur_dialog.fields_dict.date.value).to.be.equal(win.frappe.datetime.get_today());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -16,7 +16,7 @@ context("Control Markdown Editor", () => {
|
|||
cy.click_modal_primary_button("Upload");
|
||||
cy.get_field("main_section_md", "Markdown Editor").should(
|
||||
"contain",
|
||||
""
|
||||
";
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ context('Form', () => {
|
|||
|
||||
cy.clear_filters();
|
||||
cy.get('.standard-filter-section [data-fieldname="name"] input').type('Test Form Contact 3').blur();
|
||||
cy.click_listview_row_item(0);
|
||||
cy.click_listview_row_item_with_text('Test Form Contact 3');
|
||||
|
||||
cy.get('#page-Contact .page-head').findByTitle('Test Form Contact 3').should('exist');
|
||||
cy.get('.prev-doc').should('be.visible').click();
|
||||
|
|
|
|||
87
cypress/integration/kanban.js
Normal file
87
cypress/integration/kanban.js
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
context('Kanban Board', () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app');
|
||||
});
|
||||
|
||||
it('Create ToDo Kanban', () => {
|
||||
cy.visit('/app/todo');
|
||||
|
||||
cy.get('.page-actions .custom-btn-group button').click();
|
||||
cy.get('.page-actions .custom-btn-group ul.dropdown-menu li').contains('Kanban').click();
|
||||
|
||||
cy.focused().blur();
|
||||
cy.fill_field('board_name', 'ToDo Kanban', 'Data');
|
||||
cy.fill_field('field_name', 'Status', 'Select');
|
||||
cy.click_modal_primary_button('Save');
|
||||
|
||||
cy.get('.title-text').should('contain', 'ToDo Kanban');
|
||||
});
|
||||
|
||||
it('Create ToDo from kanban', () => {
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
url: 'api/method/frappe.client.save'
|
||||
}).as('save-todo');
|
||||
|
||||
cy.click_listview_primary_button('Add ToDo');
|
||||
|
||||
cy.fill_field('description', 'Test Kanban ToDo', 'Text Editor');
|
||||
cy.get('.modal-footer .btn-primary').last().click();
|
||||
|
||||
cy.wait('@save-todo');
|
||||
});
|
||||
|
||||
it('Add and Remove fields', () => {
|
||||
cy.visit('/app/todo/view/kanban/ToDo Kanban');
|
||||
|
||||
cy.intercept('POST', '/api/method/frappe.desk.doctype.kanban_board.kanban_board.save_settings').as('save-kanban');
|
||||
cy.intercept('POST', '/api/method/frappe.desk.doctype.kanban_board.kanban_board.update_order').as('update-order');
|
||||
|
||||
cy.get('.page-actions .menu-btn-group > .btn').click();
|
||||
cy.get('.page-actions .menu-btn-group .dropdown-menu li').contains('Kanban Settings').click();
|
||||
cy.get('.add-new-fields').click();
|
||||
|
||||
cy.get('.checkbox-options .checkbox').contains('ID').click();
|
||||
cy.get('.checkbox-options .checkbox').contains('Status').first().click();
|
||||
cy.get('.checkbox-options .checkbox').contains('Priority').click();
|
||||
|
||||
cy.get('.modal-footer .btn-primary').last().click();
|
||||
|
||||
cy.get('.frappe-control .label-area').contains('Show Labels').click();
|
||||
cy.click_modal_primary_button('Save');
|
||||
|
||||
cy.wait('@save-kanban');
|
||||
|
||||
cy.get('.kanban-column[data-column-value="Open"] .kanban-cards').as('open-cards');
|
||||
cy.get('@open-cards').find('.kanban-card .kanban-card-doc').first().should('contain', 'ID:');
|
||||
cy.get('@open-cards').find('.kanban-card .kanban-card-doc').first().should('contain', 'Status:');
|
||||
cy.get('@open-cards').find('.kanban-card .kanban-card-doc').first().should('contain', 'Priority:');
|
||||
|
||||
cy.get('.page-actions .menu-btn-group > .btn').click();
|
||||
cy.get('.page-actions .menu-btn-group .dropdown-menu li').contains('Kanban Settings').click();
|
||||
cy.get_open_dialog().find('.frappe-control[data-fieldname="fields_html"] div[data-label="ID"] .remove-field').click();
|
||||
|
||||
cy.wait('@update-order');
|
||||
cy.get_open_dialog().find('.frappe-control .label-area').contains('Show Labels').click();
|
||||
cy.get('.modal-footer .btn-primary').last().click();
|
||||
|
||||
cy.wait('@save-kanban');
|
||||
|
||||
cy.get('@open-cards').find('.kanban-card .kanban-card-doc').first().should('not.contain', 'ID:');
|
||||
|
||||
});
|
||||
|
||||
// it('Drag todo', () => {
|
||||
// cy.intercept({
|
||||
// method: 'POST',
|
||||
// url: 'api/method/frappe.desk.doctype.kanban_board.kanban_board.update_order_for_single_card'
|
||||
// }).as('drag-completed');
|
||||
|
||||
// cy.get('.kanban-card-body')
|
||||
// .contains('Test Kanban ToDo').first()
|
||||
// .drag('[data-column-value="Closed"] .kanban-cards', { force: true });
|
||||
|
||||
// cy.wait('@drag-completed');
|
||||
// });
|
||||
});
|
||||
|
|
@ -16,7 +16,7 @@ context('Timeline Email', () => {
|
|||
|
||||
it('Adding email and verifying timeline content for email attachment', () => {
|
||||
cy.visit('/app/todo');
|
||||
cy.get('.list-row > .level-left > .list-subject').eq(0).click();
|
||||
cy.click_listview_row_item_with_text('Test ToDo');
|
||||
|
||||
//Creating a new email
|
||||
cy.get('.timeline-actions > .timeline-item > .action-buttons > .action-btn').click();
|
||||
|
|
@ -47,7 +47,7 @@ context('Timeline Email', () => {
|
|||
|
||||
it('Deleting attachment and ToDo', () => {
|
||||
cy.visit('/app/todo');
|
||||
cy.get('.list-row > .level-left > .list-subject > .level-item.ellipsis > .ellipsis').eq(0).click();
|
||||
cy.click_listview_row_item_with_text('Test ToDo');
|
||||
|
||||
//Removing the added attachment
|
||||
cy.get('.attachment-row > .data-pill > .remove-btn > .icon').click();
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ context('Workspace 2.0', () => {
|
|||
before(() => {
|
||||
cy.visit('/login');
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
});
|
||||
|
||||
it('Navigate to page from sidebar', () => {
|
||||
|
|
@ -13,6 +12,11 @@ context('Workspace 2.0', () => {
|
|||
});
|
||||
|
||||
it('Create Private Page', () => {
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
url: 'api/method/frappe.desk.doctype.workspace.workspace.new_page'
|
||||
}).as('new_page');
|
||||
|
||||
cy.get('.codex-editor__redactor .ce-block');
|
||||
cy.get('.custom-actions button[data-label="Create%20Workspace"]').click();
|
||||
cy.fill_field('title', 'Test Private Page', 'Data');
|
||||
|
|
@ -27,12 +31,100 @@ context('Workspace 2.0', () => {
|
|||
cy.wait(300);
|
||||
cy.get('.sidebar-item-container[item-name="Test Private Page"]').should('have.attr', 'item-public', '0');
|
||||
|
||||
cy.wait(500);
|
||||
cy.wait('@new_page');
|
||||
});
|
||||
|
||||
it('Create Child Page', () => {
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
url: 'api/method/frappe.desk.doctype.workspace.workspace.new_page'
|
||||
}).as('new_page');
|
||||
|
||||
cy.get('.codex-editor__redactor .ce-block');
|
||||
cy.get('.custom-actions button[data-label="Create%20Workspace"]').click();
|
||||
cy.fill_field('title', 'Test Child Page', 'Data');
|
||||
cy.fill_field('parent', 'Test Private Page', 'Select');
|
||||
cy.fill_field('icon', 'edit', 'Icon');
|
||||
cy.get_open_dialog().find('.modal-header').click();
|
||||
cy.get_open_dialog().find('.btn-primary').click();
|
||||
|
||||
// check if sidebar item is added in pubic section
|
||||
cy.get('.sidebar-item-container[item-name="Test Child Page"]').should('have.attr', 'item-public', '0');
|
||||
|
||||
cy.get('.standard-actions .btn-primary[data-label="Save"]').click();
|
||||
cy.wait(300);
|
||||
cy.get('.sidebar-item-container[item-name="Test Child Page"]').should('have.attr', 'item-public', '0');
|
||||
|
||||
cy.wait('@new_page');
|
||||
});
|
||||
|
||||
it('Duplicate Page', () => {
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
url: 'api/method/frappe.desk.doctype.workspace.workspace.duplicate_page'
|
||||
}).as('page_duplicated');
|
||||
|
||||
cy.get('.codex-editor__redactor .ce-block');
|
||||
cy.get('.standard-actions .btn-secondary[data-label=Edit]').click();
|
||||
|
||||
cy.get('.sidebar-item-container[item-name="Test Private Page"]').as('sidebar-item');
|
||||
|
||||
cy.get('@sidebar-item').find('.standard-sidebar-item').first().click();
|
||||
cy.get('@sidebar-item').find('.dropdown-btn').first().click();
|
||||
cy.get('@sidebar-item').find('.dropdown-list .dropdown-item').contains('Duplicate').first().click({force: true});
|
||||
|
||||
cy.get_open_dialog().fill_field('title', 'Duplicate Page', 'Data');
|
||||
cy.click_modal_primary_button('Duplicate');
|
||||
|
||||
cy.wait('@page_duplicated');
|
||||
});
|
||||
|
||||
it('Drag Sidebar Item', () => {
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
url: 'api/method/frappe.desk.doctype.workspace.workspace.sort_pages'
|
||||
}).as('page_sorted');
|
||||
|
||||
cy.get('.sidebar-item-container[item-name="Duplicate Page"]').as('sidebar-item');
|
||||
|
||||
cy.get('@sidebar-item').find('.standard-sidebar-item').first().click();
|
||||
cy.get('@sidebar-item').find('.drag-handle').first().move({ deltaX: 0, deltaY: 100 });
|
||||
|
||||
cy.get('.sidebar-item-container[item-name="Build"]').as('sidebar-item');
|
||||
|
||||
cy.get('@sidebar-item').find('.standard-sidebar-item').first().click();
|
||||
cy.get('@sidebar-item').find('.drag-handle').first().move({ deltaX: 0, deltaY: 100 });
|
||||
|
||||
cy.wait('@page_sorted');
|
||||
});
|
||||
|
||||
it('Edit Page Detail', () => {
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
url: 'api/method/frappe.desk.doctype.workspace.workspace.update_page'
|
||||
}).as('page_updated');
|
||||
|
||||
cy.get('.sidebar-item-container[item-name="Test Private Page"]').as('sidebar-item');
|
||||
|
||||
cy.get('@sidebar-item').find('.standard-sidebar-item').first().click();
|
||||
cy.get('@sidebar-item').find('.dropdown-btn').first().click();
|
||||
cy.get('@sidebar-item').find('.dropdown-list .dropdown-item').contains('Edit').first().click({force: true});
|
||||
|
||||
cy.get_open_dialog().fill_field('title', ' 1', 'Data');
|
||||
cy.get_open_dialog().find('input[data-fieldname="is_public"]').check();
|
||||
cy.click_modal_primary_button('Update');
|
||||
|
||||
cy.get('.standard-sidebar-section:first .sidebar-item-container[item-name="Test Private Page"]').should('not.exist');
|
||||
cy.get('.standard-sidebar-section:last .sidebar-item-container[item-name="Test Private Page 1"]').should('exist');
|
||||
|
||||
cy.wait('@page_updated');
|
||||
});
|
||||
|
||||
it('Add New Block', () => {
|
||||
cy.get('.sidebar-item-container[item-name="Duplicate Page"]').as('sidebar-item');
|
||||
|
||||
cy.get('@sidebar-item').find('.standard-sidebar-item').first().click();
|
||||
|
||||
cy.get('.ce-block').click().type('{enter}');
|
||||
cy.get('.block-list-container .block-list-item').contains('Heading').click();
|
||||
cy.get(":focus").type('Header');
|
||||
|
|
@ -70,19 +162,24 @@ context('Workspace 2.0', () => {
|
|||
cy.get('.standard-actions .btn-primary[data-label="Save"]').click();
|
||||
});
|
||||
|
||||
it('Delete Private Page', () => {
|
||||
it('Delete Duplicate Page', () => {
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
url: 'api/method/frappe.desk.doctype.workspace.workspace.delete_page'
|
||||
}).as('page_deleted');
|
||||
|
||||
cy.get('.codex-editor__redactor .ce-block');
|
||||
cy.get('.standard-actions .btn-secondary[data-label=Edit]').click();
|
||||
|
||||
cy.get('.sidebar-item-container[item-name="Test Private Page"]')
|
||||
cy.get('.sidebar-item-container[item-name="Duplicate Page"]')
|
||||
.find('.sidebar-item-control .setting-btn').click();
|
||||
cy.get('.sidebar-item-container[item-name="Test Private Page"]')
|
||||
cy.get('.sidebar-item-container[item-name="Duplicate Page"]')
|
||||
.find('.dropdown-item[title="Delete Workspace"]').click({force: true});
|
||||
cy.wait(300);
|
||||
cy.get('.modal-footer > .standard-actions > .btn-modal-primary:visible').first().click();
|
||||
cy.get('.standard-actions .btn-primary[data-label="Save"]').click();
|
||||
cy.get('.codex-editor__redactor .ce-block');
|
||||
cy.get('.sidebar-item-container[item-name="Test Private Page"]').should('not.exist');
|
||||
cy.get('.sidebar-item-container[item-name="Duplicate Page"]').should('not.exist');
|
||||
|
||||
cy.wait('@page_deleted');
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import 'cypress-file-upload';
|
||||
import '@testing-library/cypress/add-commands';
|
||||
import '@4tw/cypress-drag-drop';
|
||||
// ***********************************************
|
||||
// This example commands.js shows you how to
|
||||
// create various custom commands and overwrite
|
||||
|
|
@ -240,8 +241,20 @@ Cypress.Commands.add('clear_cache', () => {
|
|||
});
|
||||
|
||||
Cypress.Commands.add('dialog', opts => {
|
||||
return cy.window().then(win => {
|
||||
var d = new win.frappe.ui.Dialog(opts);
|
||||
return cy.window({ log: false }).its('frappe', { log: false }).then(frappe => {
|
||||
Cypress.log({
|
||||
name: "dialog",
|
||||
displayName: "dialog",
|
||||
message: 'frappe.ui.Dialog',
|
||||
consoleProps: () => {
|
||||
return {
|
||||
options: opts,
|
||||
dialog: d
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var d = new frappe.ui.Dialog(opts);
|
||||
d.show();
|
||||
return d;
|
||||
});
|
||||
|
|
@ -257,6 +270,20 @@ Cypress.Commands.add('hide_dialog', () => {
|
|||
cy.get('.modal:visible').should('not.exist');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('clear_dialogs', () => {
|
||||
cy.window().then((win) => {
|
||||
win.$('.modal, .modal-backdrop').remove();
|
||||
});
|
||||
cy.get('.modal').should('not.exist');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('clear_datepickers', () => {
|
||||
cy.window().then((win) => {
|
||||
win.$('.datepicker').remove();
|
||||
});
|
||||
cy.get('.datepicker').should('not.exist');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('insert_doc', (doctype, args, ignore_duplicate) => {
|
||||
return cy
|
||||
.window()
|
||||
|
|
@ -325,6 +352,13 @@ Cypress.Commands.add('click_listview_row_item', (row_no) => {
|
|||
cy.get('.list-row > .level-left > .list-subject > .level-item > .ellipsis').eq(row_no).click({force: true});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('click_listview_row_item_with_text', (text) => {
|
||||
cy.get('.list-row > .level-left > .list-subject > .level-item > .ellipsis')
|
||||
.contains(text)
|
||||
.first()
|
||||
.click({force: true});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('click_filter_button', () => {
|
||||
cy.get('.filter-selector > .btn').click();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -217,7 +217,6 @@ def init(site, sites_path=None, new_site=False):
|
|||
|
||||
local.module_app = None
|
||||
local.app_modules = None
|
||||
local.system_settings = _dict()
|
||||
|
||||
local.user = None
|
||||
local.user_perms = None
|
||||
|
|
@ -355,11 +354,11 @@ def cache() -> "RedisWrapper":
|
|||
return redis_server
|
||||
|
||||
|
||||
def get_traceback():
|
||||
def get_traceback(with_context=False):
|
||||
"""Returns error traceback."""
|
||||
from frappe.utils import get_traceback
|
||||
|
||||
return get_traceback()
|
||||
return get_traceback(with_context=with_context)
|
||||
|
||||
|
||||
def errprint(msg):
|
||||
|
|
@ -1211,18 +1210,35 @@ def reload_doc(module, dt=None, dn=None, force=False, reset_permissions=False):
|
|||
|
||||
|
||||
@whitelist()
|
||||
def rename_doc(*args, **kwargs):
|
||||
def rename_doc(
|
||||
doctype: str,
|
||||
old: str,
|
||||
new: str,
|
||||
force: bool = False,
|
||||
merge: bool = False,
|
||||
*,
|
||||
ignore_if_exists: bool = False,
|
||||
show_alert: bool = True,
|
||||
rebuild_search: bool = True,
|
||||
) -> str:
|
||||
"""
|
||||
Renames a doc(dt, old) to doc(dt, new) and updates all linked fields of type "Link"
|
||||
|
||||
Calls `frappe.model.rename_doc.rename_doc`
|
||||
"""
|
||||
kwargs.pop("ignore_permissions", None)
|
||||
kwargs.pop("cmd", None)
|
||||
|
||||
from frappe.model.rename_doc import rename_doc
|
||||
|
||||
return rename_doc(*args, **kwargs)
|
||||
return rename_doc(
|
||||
doctype=doctype,
|
||||
old=old,
|
||||
new=new,
|
||||
force=force,
|
||||
merge=merge,
|
||||
ignore_if_exists=ignore_if_exists,
|
||||
show_alert=show_alert,
|
||||
rebuild_search=rebuild_search,
|
||||
)
|
||||
|
||||
|
||||
def get_module(modulename):
|
||||
|
|
@ -2068,25 +2084,30 @@ def logger(
|
|||
)
|
||||
|
||||
|
||||
def log_error(message=None, title=_("Error")):
|
||||
def log_error(title=None, message=None, reference_doctype=None, reference_name=None):
|
||||
"""Log error to Error Log"""
|
||||
|
||||
# AI ALERT:
|
||||
# Parameter ALERT:
|
||||
# the title and message may be swapped
|
||||
# the better API for this is log_error(title, message), and used in many cases this way
|
||||
# this hack tries to be smart about whats a title (single line ;-)) and fixes it
|
||||
|
||||
traceback = None
|
||||
if message:
|
||||
if "\n" in title:
|
||||
error, title = title, message
|
||||
if "\n" in title: # traceback sent as title
|
||||
traceback, title = title, message
|
||||
else:
|
||||
error = message
|
||||
else:
|
||||
error = get_traceback()
|
||||
traceback = message
|
||||
|
||||
return get_doc(dict(doctype="Error Log", error=as_unicode(error), method=title)).insert(
|
||||
ignore_permissions=True
|
||||
)
|
||||
title = title or "Error"
|
||||
traceback = as_unicode(traceback or get_traceback(with_context=True))
|
||||
|
||||
return get_doc(
|
||||
doctype="Error Log",
|
||||
error=traceback,
|
||||
method=title,
|
||||
reference_doctype=reference_doctype,
|
||||
reference_name=reference_name,
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
|
||||
def get_desk_link(doctype, name):
|
||||
|
|
@ -2139,9 +2160,7 @@ def safe_eval(code, eval_globals=None, eval_locals=None):
|
|||
|
||||
|
||||
def get_system_settings(key):
|
||||
if key not in local.system_settings:
|
||||
local.system_settings.update({key: db.get_single_value("System Settings", key)})
|
||||
return local.system_settings.get(key)
|
||||
return db.get_single_value("System Settings", key, cache=True)
|
||||
|
||||
|
||||
def get_active_domains():
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ class AutoRepeat(Document):
|
|||
if self.notify_by_email and self.recipients:
|
||||
self.send_notification(new_doc)
|
||||
except Exception:
|
||||
error_log = frappe.log_error(frappe.get_traceback(), _("Auto Repeat Document Creation Failure"))
|
||||
error_log = self.log_error("Auto repeat failed")
|
||||
|
||||
self.disable_auto_repeat()
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import json
|
|||
|
||||
import frappe
|
||||
from frappe.desk.notifications import clear_notifications, delete_notification_count_for
|
||||
from frappe.model.document import Document
|
||||
|
||||
common_default_keys = ["__default", "__global"]
|
||||
|
||||
|
|
|
|||
|
|
@ -189,7 +189,10 @@ def insert(doc=None):
|
|||
if isinstance(doc, str):
|
||||
doc = json.loads(doc)
|
||||
|
||||
if doc.get("parenttype"):
|
||||
doc = frappe._dict(doc)
|
||||
if frappe.is_table(doc.doctype):
|
||||
if not (doc.parenttype and doc.parent and doc.parentfield):
|
||||
frappe.throw(_("parenttype, parent and parentfield are required to insert a child record"))
|
||||
# inserting a child record
|
||||
parent = frappe.get_doc(doc.parenttype, doc.parent)
|
||||
parent.append(doc.parentfield, doc)
|
||||
|
|
|
|||
|
|
@ -870,7 +870,7 @@ def run_ui_tests(
|
|||
# install cypress
|
||||
click.secho("Installing Cypress...", fg="yellow")
|
||||
frappe.commands.popen(
|
||||
"yarn add cypress@^6 cypress-file-upload@^5 @testing-library/cypress@^8 @cypress/code-coverage@^3 --no-lockfile"
|
||||
"yarn add cypress@^6 cypress-file-upload@^5 @4tw/cypress-drag-drop@^2 @testing-library/cypress@^8 @cypress/code-coverage@^3 --no-lockfile"
|
||||
)
|
||||
|
||||
# run for headless mode
|
||||
|
|
@ -1024,6 +1024,7 @@ def set_config(context, key, value, global_=False, parse=False, as_dict=False):
|
|||
def get_version(output):
|
||||
"""Show the versions of all the installed apps."""
|
||||
from git import Repo
|
||||
from git.exc import InvalidGitRepositoryError
|
||||
|
||||
from frappe.utils.change_log import get_app_branch
|
||||
from frappe.utils.commands import render_table
|
||||
|
|
@ -1034,12 +1035,16 @@ def get_version(output):
|
|||
for app in sorted(frappe.get_all_apps()):
|
||||
module = frappe.get_module(app)
|
||||
app_hooks = frappe.get_module(app + ".hooks")
|
||||
repo = Repo(frappe.get_app_path(app, ".."))
|
||||
|
||||
app_info = frappe._dict()
|
||||
|
||||
try:
|
||||
app_info.commit = Repo(frappe.get_app_path(app, "..")).head.object.hexsha[:7]
|
||||
except InvalidGitRepositoryError:
|
||||
app_info.commit = ""
|
||||
|
||||
app_info.app = app
|
||||
app_info.branch = get_app_branch(app)
|
||||
app_info.commit = repo.head.object.hexsha[:7]
|
||||
app_info.version = getattr(app_hooks, f"{app_info.branch}_version", None) or module.__version__
|
||||
|
||||
data.append(app_info)
|
||||
|
|
|
|||
|
|
@ -450,8 +450,7 @@ def get_contacts(email_strings: List[str], auto_create_contact=False) -> List[st
|
|||
contact.insert(ignore_permissions=True)
|
||||
contact_name = contact.name
|
||||
except Exception:
|
||||
traceback = frappe.get_traceback()
|
||||
frappe.log_error(traceback)
|
||||
contact.log_error("Unable to add contact")
|
||||
|
||||
if contact_name:
|
||||
contacts.append(contact_name)
|
||||
|
|
|
|||
|
|
@ -248,7 +248,7 @@ def mark_email_as_seen(name: str = None):
|
|||
frappe.db.commit() # nosemgrep: this will be called in a GET request
|
||||
|
||||
except Exception:
|
||||
frappe.log_error(frappe.get_traceback())
|
||||
frappe.log_error("Unable to mark as seen", None, "Communication", name)
|
||||
|
||||
finally:
|
||||
frappe.response.update(
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ def start_import(data_import):
|
|||
except Exception:
|
||||
frappe.db.rollback()
|
||||
data_import.db_set("status", "Error")
|
||||
frappe.log_error(title=data_import.name)
|
||||
data_import.log_error("Data import failed")
|
||||
finally:
|
||||
frappe.flags.in_import = False
|
||||
|
||||
|
|
|
|||
|
|
@ -347,7 +347,7 @@
|
|||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field",
|
||||
"description": "Don't encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field",
|
||||
"fieldname": "ignore_xss_filter",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore XSS Filter"
|
||||
|
|
@ -547,7 +547,7 @@
|
|||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-03-02 17:07:32.117897",
|
||||
"modified": "2022-04-19 12:27:28.641580",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "DocField",
|
||||
|
|
|
|||
|
|
@ -1,148 +1,76 @@
|
|||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"autoname": "",
|
||||
"beta": 0,
|
||||
"creation": "2013-01-16 13:09:40",
|
||||
"custom": 0,
|
||||
"description": "Log of Scheduler Errors",
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "System",
|
||||
"editable_grid": 0,
|
||||
"engine": "MyISAM",
|
||||
"actions": [],
|
||||
"creation": "2013-01-16 13:09:40",
|
||||
"doctype": "DocType",
|
||||
"document_type": "System",
|
||||
"engine": "MyISAM",
|
||||
"field_order": [
|
||||
"seen",
|
||||
"method",
|
||||
"error",
|
||||
"reference_doctype",
|
||||
"reference_name"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "0",
|
||||
"fieldname": "seen",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Seen",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"default": "0",
|
||||
"fieldname": "seen",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Seen"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "method",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Title",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "method",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Title",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "error",
|
||||
"fieldtype": "Code",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Error",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"fieldname": "error",
|
||||
"fieldtype": "Code",
|
||||
"label": "Error",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_doctype",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Reference DocType",
|
||||
"options": "DocType",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Reference Name"
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"icon": "fa fa-warning-sign",
|
||||
"idx": 1,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2021-10-25 12:21:44.292471",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Error Log",
|
||||
"owner": "Administrator",
|
||||
],
|
||||
"icon": "fa fa-warning-sign",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2022-04-18 17:25:47.406873",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Error Log",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"apply_user_permissions": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 0,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_order": "ASC",
|
||||
"track_seen": 0
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"title_field": "method"
|
||||
}
|
||||
|
|
@ -9,4 +9,8 @@ import frappe
|
|||
|
||||
|
||||
class TestErrorLog(unittest.TestCase):
|
||||
pass
|
||||
def test_error_log(self):
|
||||
"""let's do an error log on error log?"""
|
||||
doc = frappe.new_doc("Error Log")
|
||||
error = doc.log_error("This is an error")
|
||||
self.assertEqual(error.doctype, "Error Log")
|
||||
|
|
|
|||
|
|
@ -1043,7 +1043,7 @@ def attach_files_to_document(doc, event):
|
|||
):
|
||||
return
|
||||
|
||||
frappe.get_doc(
|
||||
file_doc = frappe.get_doc(
|
||||
doctype="File",
|
||||
file_url=value,
|
||||
attached_to_name=doc.name,
|
||||
|
|
@ -1052,4 +1052,4 @@ def attach_files_to_document(doc, event):
|
|||
folder="Home/Attachments",
|
||||
).insert()
|
||||
except Exception:
|
||||
frappe.log_error(title=_("Error Attaching File"))
|
||||
file_doc.log_error("Error Attaching File")
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ def run_background(prepared_report):
|
|||
instance.save(ignore_permissions=True)
|
||||
|
||||
except Exception:
|
||||
frappe.log_error(frappe.get_traceback())
|
||||
report.log_error("Prepared report failed")
|
||||
instance = frappe.get_doc("Prepared Report", prepared_report)
|
||||
instance.status = "Error"
|
||||
instance.error_message = frappe.get_traceback()
|
||||
|
|
|
|||
|
|
@ -68,6 +68,8 @@
|
|||
"prepared_report_section",
|
||||
"enable_prepared_report_auto_deletion",
|
||||
"prepared_report_expiry_period",
|
||||
"column_break_64",
|
||||
"max_auto_email_report_per_user",
|
||||
"system_updates_section",
|
||||
"disable_system_update_notification"
|
||||
],
|
||||
|
|
@ -445,7 +447,7 @@
|
|||
"collapsible": 1,
|
||||
"fieldname": "prepared_report_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Prepared Report"
|
||||
"label": "Reports"
|
||||
},
|
||||
{
|
||||
"default": "Frappe",
|
||||
|
|
@ -485,12 +487,22 @@
|
|||
"fieldtype": "Select",
|
||||
"label": "First Day of the Week",
|
||||
"options": "Sunday\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_64",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "20",
|
||||
"fieldname": "max_auto_email_report_per_user",
|
||||
"fieldtype": "Int",
|
||||
"label": "Max auto email report per user"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2022-01-04 11:28:34.881192",
|
||||
"modified": "2022-04-21 09:11:35.218721",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "System Settings",
|
||||
|
|
|
|||
|
|
@ -53,7 +53,6 @@ class SystemSettings(Document):
|
|||
|
||||
frappe.cache().delete_value("system_settings")
|
||||
frappe.cache().delete_value("time_zone")
|
||||
frappe.local.system_settings = {}
|
||||
|
||||
if frappe.flags.update_last_reset_password_date:
|
||||
update_last_reset_password_date()
|
||||
|
|
|
|||
|
|
@ -265,7 +265,7 @@ class User(Document):
|
|||
|
||||
except frappe.OutgoingEmailError:
|
||||
# email server not set, don't send email
|
||||
frappe.log_error(frappe.get_traceback())
|
||||
self.log_error("Unable to send new password notification")
|
||||
|
||||
@Document.hook
|
||||
def validate_reset_password(self):
|
||||
|
|
|
|||
|
|
@ -22,11 +22,7 @@ JOB_COLORS = {"queued": "orange", "failed": "red", "started": "blue", "finished"
|
|||
def get_info(view=None, queue_timeout=None, job_status=None) -> List[Dict]:
|
||||
jobs = []
|
||||
|
||||
def add_job(job: "Job", name: str) -> None:
|
||||
if job_status != "all" and job.get_status() != job_status:
|
||||
return
|
||||
if queue_timeout != "all" and not name.endswith(f":{queue_timeout}"):
|
||||
return
|
||||
def add_job(job: "Job", queue: str) -> None:
|
||||
|
||||
if job.kwargs.get("site") == frappe.local.site:
|
||||
job_info = {
|
||||
|
|
@ -34,7 +30,7 @@ def get_info(view=None, queue_timeout=None, job_status=None) -> List[Dict]:
|
|||
or job.kwargs.get("kwargs", {}).get("job_type")
|
||||
or str(job.kwargs.get("job_name")),
|
||||
"status": job.get_status(),
|
||||
"queue": name,
|
||||
"queue": queue,
|
||||
"creation": convert_utc_to_user_timezone(job.created_at),
|
||||
"color": JOB_COLORS[job.get_status()],
|
||||
}
|
||||
|
|
@ -48,14 +44,21 @@ def get_info(view=None, queue_timeout=None, job_status=None) -> List[Dict]:
|
|||
queues = get_queues()
|
||||
for queue in queues:
|
||||
for job in queue.jobs:
|
||||
if job_status != "all" and job.get_status() != job_status:
|
||||
return
|
||||
if queue_timeout != "all" and not queue.name.endswith(f":{queue_timeout}"):
|
||||
return
|
||||
add_job(job, queue.name)
|
||||
|
||||
elif view == "Workers":
|
||||
workers = get_workers()
|
||||
for worker in workers:
|
||||
current_job = worker.get_current_job()
|
||||
if current_job and current_job.kwargs.get("site") == frappe.local.site:
|
||||
add_job(current_job, job.origin)
|
||||
if current_job:
|
||||
if hasattr(current_job, "kwargs") and current_job.kwargs.get("site") == frappe.local.site:
|
||||
add_job(current_job, current_job.origin)
|
||||
else:
|
||||
jobs.append({"queue": worker.name, "job_name": "busy", "status": "", "creation": ""})
|
||||
else:
|
||||
jobs.append({"queue": worker.name, "job_name": "idle", "status": "", "creation": ""})
|
||||
|
||||
|
|
|
|||
|
|
@ -596,6 +596,7 @@ docfield_properties = {
|
|||
"in_preview": "Check",
|
||||
"bold": "Check",
|
||||
"no_copy": "Check",
|
||||
"ignore_xss_filter": "Check",
|
||||
"hidden": "Check",
|
||||
"collapsible": "Check",
|
||||
"collapsible_depends_on": "Data",
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@
|
|||
"report_hide",
|
||||
"remember_last_selected_value",
|
||||
"hide_border",
|
||||
"ignore_xss_filter",
|
||||
"property_depends_on_section",
|
||||
"mandatory_depends_on",
|
||||
"column_break_33",
|
||||
|
|
@ -453,13 +454,20 @@
|
|||
"hidden": 1,
|
||||
"label": "Is System Generated",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Don't encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field",
|
||||
"fieldname": "ignore_xss_filter",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore XSS Filter"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-03-31 12:05:11.799654",
|
||||
"modified": "2022-04-13 22:31:14.162661",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Customize Form Field",
|
||||
|
|
|
|||
|
|
@ -1066,7 +1066,7 @@ class Database(object):
|
|||
now_datetime() - relativedelta(minutes=minutes),
|
||||
)[0][0]
|
||||
|
||||
def get_db_table_columns(self, table):
|
||||
def get_db_table_columns(self, table) -> List[str]:
|
||||
"""Returns list of column names from given table."""
|
||||
columns = frappe.cache().hget("table_columns", table)
|
||||
if columns is None:
|
||||
|
|
@ -1146,18 +1146,13 @@ class Database(object):
|
|||
return frappe.db.is_missing_column(e)
|
||||
|
||||
def get_descendants(self, doctype, name):
|
||||
"""Return descendants of the current record"""
|
||||
node_location_indexes = self.get_value(doctype, name, ("lft", "rgt"))
|
||||
if node_location_indexes:
|
||||
lft, rgt = node_location_indexes
|
||||
return self.sql_list(
|
||||
"""select name from `tab{doctype}`
|
||||
where lft > {lft} and rgt < {rgt}""".format(
|
||||
doctype=doctype, lft=lft, rgt=rgt
|
||||
)
|
||||
)
|
||||
else:
|
||||
# when document does not exist
|
||||
"""Return descendants of the group node in tree"""
|
||||
from frappe.utils.nestedset import get_descendants_of
|
||||
|
||||
try:
|
||||
return get_descendants_of(doctype, name, ignore_permissions=True)
|
||||
except Exception:
|
||||
# Can only happen if document doesn't exists - kept for backward compatibility
|
||||
return []
|
||||
|
||||
def is_missing_table_or_column(self, e):
|
||||
|
|
@ -1228,7 +1223,7 @@ class Database(object):
|
|||
frappe.flags.touched_tables = set()
|
||||
frappe.flags.touched_tables.update(tables)
|
||||
|
||||
def bulk_insert(self, doctype, fields, values, ignore_duplicates=False):
|
||||
def bulk_insert(self, doctype, fields, values, ignore_duplicates=False, *, chunk_size=10_000):
|
||||
"""
|
||||
Insert multiple records at a time
|
||||
|
||||
|
|
@ -1236,22 +1231,20 @@ class Database(object):
|
|||
:param fields: list of fields
|
||||
:params values: list of list of values
|
||||
"""
|
||||
insert_list = []
|
||||
fields = ", ".join("`" + field + "`" for field in fields)
|
||||
values = list(values)
|
||||
table = frappe.qb.DocType(doctype)
|
||||
|
||||
for idx, value in enumerate(values):
|
||||
insert_list.append(tuple(value))
|
||||
if idx and (idx % 10000 == 0 or idx < len(values) - 1):
|
||||
self.sql(
|
||||
"""INSERT {ignore_duplicates} INTO `tab{doctype}` ({fields}) VALUES {values}""".format(
|
||||
ignore_duplicates="IGNORE" if ignore_duplicates else "",
|
||||
doctype=doctype,
|
||||
fields=fields,
|
||||
values=", ".join(["%s"] * len(insert_list)),
|
||||
),
|
||||
tuple(insert_list),
|
||||
)
|
||||
insert_list = []
|
||||
for start_index in range(0, len(values), chunk_size):
|
||||
query = frappe.qb.into(table)
|
||||
if ignore_duplicates:
|
||||
# Pypika does not have same api for ignoring duplicates
|
||||
if frappe.conf.db_type == "mariadb":
|
||||
query = query.ignore()
|
||||
elif frappe.conf.db_type == "postgres":
|
||||
query = query.on_conflict().do_nothing()
|
||||
|
||||
values_to_insert = values[start_index : start_index + chunk_size]
|
||||
query.columns(fields).insert(*values_to_insert).run()
|
||||
|
||||
|
||||
def enqueue_jobs_after_commit():
|
||||
|
|
|
|||
|
|
@ -338,7 +338,7 @@ def get_desktop_page(page):
|
|||
"onboardings": workspace.onboardings,
|
||||
}
|
||||
except DoesNotExistError:
|
||||
frappe.log_error(frappe.get_traceback())
|
||||
frappe.log_error("Workspace Missing")
|
||||
return {}
|
||||
|
||||
|
||||
|
|
@ -472,7 +472,7 @@ def save_new_widget(doc, page, blocks, new_widgets):
|
|||
""".format(
|
||||
page, json_config, e
|
||||
)
|
||||
frappe.log_error(log, _("Could not save customization"))
|
||||
doc.log_error("Could not save customization", log)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -1,267 +1,124 @@
|
|||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:kanban_board_name",
|
||||
"beta": 0,
|
||||
"creation": "2016-10-19 12:26:04.809812",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"kanban_board_name",
|
||||
"reference_doctype",
|
||||
"field_name",
|
||||
"column_break_4",
|
||||
"private",
|
||||
"show_labels",
|
||||
"section_break_3",
|
||||
"columns",
|
||||
"filters",
|
||||
"fields"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "kanban_board_name",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Kanban Board Name",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "reference_doctype",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Reference Document Type",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "DocType",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "field_name",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Field Name",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "section_break_3",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "columns",
|
||||
"fieldtype": "Table",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Columns",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Kanban Board Column",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"options": "Kanban Board Column"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "filters",
|
||||
"fieldtype": "Text",
|
||||
"hidden": 1,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"fieldtype": "Code",
|
||||
"label": "Filters",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"options": "JSON",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "0",
|
||||
"fieldname": "private",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Private",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "fields",
|
||||
"fieldtype": "Code",
|
||||
"label": "Fields",
|
||||
"options": "JSON",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "show_labels",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Labels",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2019-09-05 14:22:27.664645",
|
||||
"links": [],
|
||||
"modified": "2022-04-13 12:10:20.284367",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Kanban Board",
|
||||
"name_case": "",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"apply_user_permissions": 0,
|
||||
"cancel": 0,
|
||||
"read": 1,
|
||||
"role": "All"
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"if_owner": 1,
|
||||
"read": 1,
|
||||
"role": "All",
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "All",
|
||||
"set_user_permissions": 0,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 0,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"read_only": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 0,
|
||||
"track_seen": 0
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -24,7 +24,7 @@ class KanbanBoard(Document):
|
|||
def validate_column_name(self):
|
||||
for column in self.columns:
|
||||
if not column.column_name:
|
||||
frappe.msgprint(frappe._("Column Name cannot be empty"), raise_exception=True)
|
||||
frappe.msgprint(_("Column Name cannot be empty"), raise_exception=True)
|
||||
|
||||
|
||||
def get_permission_query_conditions(user):
|
||||
|
|
@ -92,7 +92,6 @@ def update_order(board_name, order):
|
|||
|
||||
updated_cards = []
|
||||
for col_name, cards in order_dict.items():
|
||||
order_list = []
|
||||
for card in cards:
|
||||
column = frappe.get_value(doctype, {"name": card}, fieldname)
|
||||
if column != col_name:
|
||||
|
|
@ -246,3 +245,22 @@ def set_indicator(board_name, column_name, indicator):
|
|||
|
||||
board.save()
|
||||
return board
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def save_settings(board_name: str, settings: str) -> Document:
|
||||
settings = json.loads(settings)
|
||||
doc = frappe.get_doc("Kanban Board", board_name)
|
||||
|
||||
fields = settings["fields"]
|
||||
if not isinstance(fields, str):
|
||||
fields = json.dumps(fields)
|
||||
|
||||
doc.fields = fields
|
||||
doc.show_labels = settings["show_labels"]
|
||||
doc.save()
|
||||
|
||||
resp = doc.as_dict()
|
||||
resp["fields"] = frappe.parse_json(resp["fields"])
|
||||
|
||||
return resp
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ class NotificationLog(Document):
|
|||
try:
|
||||
send_notification_email(self)
|
||||
except frappe.OutgoingEmailError:
|
||||
frappe.log_error(message=frappe.get_traceback(), title=_("Failed to send notification email"))
|
||||
self.log_error(_("Failed to send notification email"))
|
||||
|
||||
|
||||
def get_permission_query_conditions(for_user):
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"actions": [
|
||||
{
|
||||
"action": "app/console-log",
|
||||
"action": "/app/console-log",
|
||||
"action_type": "Route",
|
||||
"label": "Logs"
|
||||
},
|
||||
|
|
@ -86,7 +86,7 @@
|
|||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2022-04-09 16:35:32.345542",
|
||||
"modified": "2022-04-15 14:15:58.398590",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "System Console",
|
||||
|
|
@ -106,4 +106,4 @@
|
|||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,7 +80,14 @@ class Workspace(Document):
|
|||
|
||||
# remove duplicate before adding
|
||||
for idx, link in enumerate(self.links):
|
||||
if link.label == card.get("label") and link.type == "Card Break":
|
||||
if link.get("label") == card.get("label") and link.get("type") == "Card Break":
|
||||
# count and set number of links for the card if link_count is 0
|
||||
if link.link_count == 0:
|
||||
for count, card_link in enumerate(self.links[idx + 1 :]):
|
||||
if card_link.get("type") == "Card Break":
|
||||
break
|
||||
link.link_count = count + 1
|
||||
|
||||
del self.links[idx : idx + link.link_count + 1]
|
||||
|
||||
self.append(
|
||||
|
|
@ -199,21 +206,31 @@ def update_page(name, title, icon, parent, public):
|
|||
doc.sequence_id = frappe.db.count("Workspace", {"public": public}, cache=True)
|
||||
doc.public = public
|
||||
doc.for_user = "" if public else doc.for_user or frappe.session.user
|
||||
doc.label = "{0}-{1}".format(title, doc.for_user) if doc.for_user else title
|
||||
doc.label = new_name = "{0}-{1}".format(title, doc.for_user) if doc.for_user else title
|
||||
doc.save(ignore_permissions=True)
|
||||
|
||||
if name != doc.label:
|
||||
rename_doc("Workspace", name, doc.label, force=True, ignore_permissions=True)
|
||||
if name != new_name:
|
||||
rename_doc("Workspace", name, new_name, force=True, ignore_permissions=True)
|
||||
|
||||
# update new name and public in child pages
|
||||
if child_docs:
|
||||
for child in child_docs:
|
||||
child_doc = frappe.get_doc("Workspace", child.name)
|
||||
child_doc.parent_page = doc.title
|
||||
child_doc.public = doc.public
|
||||
if child_doc.public != public:
|
||||
child_doc.public = public
|
||||
child_doc.for_user = "" if public else child_doc.for_user or frappe.session.user
|
||||
child_doc.label = new_child_name = (
|
||||
"{0}-{1}".format(child_doc.title, child_doc.for_user)
|
||||
if child_doc.for_user
|
||||
else child_doc.title
|
||||
)
|
||||
child_doc.save(ignore_permissions=True)
|
||||
|
||||
return {"name": doc.title, "public": doc.public, "label": doc.label}
|
||||
if child.name != new_child_name:
|
||||
rename_doc("Workspace", child.name, new_child_name, force=True, ignore_permissions=True)
|
||||
|
||||
return {"name": title, "public": public, "label": new_name}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
|
|||
|
|
@ -314,7 +314,7 @@ def get_prepared_report_result(report, filters, dn="", user=None):
|
|||
|
||||
latest_report_data = {"columns": columns, "result": data}
|
||||
except Exception:
|
||||
frappe.log_error(frappe.get_traceback())
|
||||
doc.log_error("Prepared report failed")
|
||||
frappe.delete_doc("Prepared Report", doc.name)
|
||||
frappe.db.commit()
|
||||
doc = None
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from frappe.model.document import Document
|
|||
from frappe.model.naming import append_number_if_name_exists
|
||||
from frappe.utils import (
|
||||
add_to_date,
|
||||
cint,
|
||||
format_time,
|
||||
get_link_to_form,
|
||||
get_url_to_report,
|
||||
|
|
@ -51,14 +52,18 @@ class AutoEmailReport(Document):
|
|||
self.email_to = "\n".join(valid)
|
||||
|
||||
def validate_report_count(self):
|
||||
"""check that there are only 3 enabled reports per user"""
|
||||
count = frappe.db.sql(
|
||||
"select count(*) from `tabAuto Email Report` where user=%s and enabled=1", self.user
|
||||
)[0][0]
|
||||
max_reports_per_user = frappe.local.conf.max_reports_per_user or 3
|
||||
count = frappe.db.count("Auto Email Report", {"user": self.user, "enabled": 1})
|
||||
|
||||
max_reports_per_user = (
|
||||
cint(frappe.local.conf.max_reports_per_user) # kept for backward compatibilty
|
||||
or cint(frappe.db.get_single_value("System Settings", "max_auto_email_report_per_user"))
|
||||
or 20
|
||||
)
|
||||
|
||||
if count > max_reports_per_user + (-1 if self.flags.in_insert else 0):
|
||||
frappe.throw(_("Only {0} emailed reports are allowed per user").format(max_reports_per_user))
|
||||
msg = _("Only {0} emailed reports are allowed per user.").format(max_reports_per_user)
|
||||
msg += " " + _("To allow more reports update limit in System Settings.")
|
||||
frappe.throw(msg, title=_("Report limit reached"))
|
||||
|
||||
def validate_report_format(self):
|
||||
"""check if user has select correct report format"""
|
||||
|
|
@ -255,7 +260,9 @@ def send_daily():
|
|||
try:
|
||||
auto_email_report.send()
|
||||
except Exception as e:
|
||||
frappe.log_error(e, _("Failed to send {0} Auto Email Report").format(auto_email_report.name))
|
||||
auto_email_report.log_error(
|
||||
"Failed to send {0} Auto Email Report".format(auto_email_report.name)
|
||||
)
|
||||
|
||||
|
||||
def send_monthly():
|
||||
|
|
|
|||
|
|
@ -473,7 +473,7 @@ class EmailAccount(Document):
|
|||
frappe.db.rollback()
|
||||
except Exception:
|
||||
frappe.db.rollback()
|
||||
frappe.log_error(title="EmailAccount.receive")
|
||||
self.log_error(title="EmailAccount.receive")
|
||||
if self.use_imap:
|
||||
self.handle_bad_emails(mail.uid, mail.raw_message, frappe.get_traceback())
|
||||
exceptions.append(frappe.get_traceback())
|
||||
|
|
@ -521,7 +521,7 @@ class EmailAccount(Document):
|
|||
# close connection to mailserver
|
||||
email_server.logout()
|
||||
except Exception:
|
||||
frappe.log_error(title=_("Error while connecting to email account {0}").format(self.name))
|
||||
self.log_error(title=_("Error while connecting to email account {0}").format(self.name))
|
||||
return []
|
||||
return mails
|
||||
|
||||
|
|
@ -667,7 +667,7 @@ class EmailAccount(Document):
|
|||
try:
|
||||
email_server = self.get_incoming_server(in_receive=True)
|
||||
except Exception:
|
||||
frappe.log_error(title=_("Error while connecting to email account {0}").format(self.name))
|
||||
self.log_error("Email Connection Error")
|
||||
|
||||
if not email_server:
|
||||
return
|
||||
|
|
@ -679,7 +679,7 @@ class EmailAccount(Document):
|
|||
message = safe_encode(message)
|
||||
email_server.imap.append("Sent", "\\Seen", imaplib.Time2Internaldate(time.time()), message)
|
||||
except Exception:
|
||||
frappe.log_error(title="EmailAccount.append_email_to_sent_folder")
|
||||
self.log_error("Unable to add to Sent folder")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
|
|||
|
|
@ -198,10 +198,7 @@ class SendMailContext:
|
|||
traceback_string = "".join(traceback.format_tb(exc_tb))
|
||||
traceback_string += f"\n Queue Name: {self.queue_doc.name}"
|
||||
|
||||
if self.is_background_task:
|
||||
frappe.log_error(title="frappe.email.queue.flush", message=traceback_string)
|
||||
else:
|
||||
frappe.log_error(message=traceback_string)
|
||||
self.queue_doc.log_error("Email sending failed", traceback_string)
|
||||
|
||||
@property
|
||||
def smtp_session(self):
|
||||
|
|
@ -625,11 +622,11 @@ class QueueBuilder:
|
|||
mail_to_string = cstr(mail.as_string())
|
||||
except frappe.InvalidEmailAddressError:
|
||||
# bad Email Address - don't add to queue
|
||||
frappe.log_error(
|
||||
"Invalid Email ID Sender: {0}, Recipients: {1}, \nTraceback: {2} ".format(
|
||||
self.log_error(
|
||||
title="Invalid email address",
|
||||
message="Invalid email address Sender: {0}, Recipients: {1}, \nTraceback: {2} ".format(
|
||||
self.sender, ", ".join(self.final_recipients()), traceback.format_exc()
|
||||
),
|
||||
"Email Not Sent",
|
||||
)
|
||||
return
|
||||
|
||||
|
|
|
|||
|
|
@ -329,19 +329,17 @@ def send_scheduled_email():
|
|||
pluck="name",
|
||||
)
|
||||
|
||||
for newsletter in scheduled_newsletter:
|
||||
for newsletter_name in scheduled_newsletter:
|
||||
try:
|
||||
frappe.get_doc("Newsletter", newsletter).queue_all()
|
||||
newsletter = frappe.get_doc("Newsletter", newsletter_name)
|
||||
newsletter.queue_all()
|
||||
|
||||
except Exception:
|
||||
frappe.db.rollback()
|
||||
|
||||
# wasn't able to send emails :(
|
||||
frappe.db.set_value("Newsletter", newsletter, "email_sent", 0)
|
||||
message = (
|
||||
f"Newsletter {newsletter} failed to send" "\n\n" f"Traceback: {frappe.get_traceback()}"
|
||||
)
|
||||
frappe.log_error(title="Send Newsletter", message=message)
|
||||
frappe.db.set_value("Newsletter", newsletter_name, "email_sent", 0)
|
||||
newsletter.log_error("Failed to send newsletter")
|
||||
|
||||
if not frappe.flags.in_test:
|
||||
frappe.db.commit()
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ def get_context(context):
|
|||
self.create_system_notification(doc, context)
|
||||
|
||||
except:
|
||||
frappe.log_error(title="Failed to send notification", message=frappe.get_traceback())
|
||||
self.log_error("Failed to send Notification")
|
||||
|
||||
if self.set_property_after_alert:
|
||||
allow_update = True
|
||||
|
|
@ -168,7 +168,7 @@ def get_context(context):
|
|||
doc.save(ignore_permissions=True)
|
||||
doc.flags.in_notification_update = False
|
||||
except Exception:
|
||||
frappe.log_error(title="Document update failed", message=frappe.get_traceback())
|
||||
self.log_error("Document update failed")
|
||||
|
||||
def create_system_notification(self, doc, context):
|
||||
subject = self.subject
|
||||
|
|
@ -433,7 +433,7 @@ def evaluate_alert(doc, alert, event):
|
|||
if event == "Value Change" and not doc.is_new():
|
||||
if not frappe.db.has_column(doc.doctype, alert.value_changed):
|
||||
alert.db_set("enabled", 0)
|
||||
frappe.log_error("Notification {0} has been disabled due to missing field".format(alert.name))
|
||||
alert.log_error("Notification {0} has been disabled due to missing field".format(alert.name))
|
||||
return
|
||||
|
||||
doc_before_save = doc.get_doc_before_save()
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ def flush(from_test=False):
|
|||
is_background_task = not from_test
|
||||
func(email_queue_name=row.name, is_background_task=is_background_task)
|
||||
except Exception:
|
||||
frappe.log_error()
|
||||
frappe.get_doc("Email Queue", row.name).log_error()
|
||||
|
||||
|
||||
def get_queue():
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ class EmailServer:
|
|||
|
||||
except _socket.error:
|
||||
# log performs rollback and logs error in Error Log
|
||||
frappe.log_error("receive.connect_pop")
|
||||
self.log_error("POP: Unable to connect")
|
||||
|
||||
# Invalid mail server -- due to refusing connection
|
||||
frappe.msgprint(_("Invalid Mail Server. Please rectify and try again."))
|
||||
|
|
@ -306,7 +306,7 @@ class EmailServer:
|
|||
|
||||
else:
|
||||
# log performs rollback and logs error in Error Log
|
||||
frappe.log_error("receive.get_messages", self.make_error_msg(msg_num, incoming_mail))
|
||||
self.log_error("Unable to fetch email", self.make_error_msg(msg_num, incoming_mail))
|
||||
self.errors = True
|
||||
frappe.db.rollback()
|
||||
|
||||
|
|
|
|||
|
|
@ -213,5 +213,5 @@ def has_consumer_access(consumer, update_log):
|
|||
else:
|
||||
return frappe.safe_eval(condition, frappe._dict(doc=doc))
|
||||
except Exception as e:
|
||||
frappe.log_error(title="has_consumer_access error", message=e)
|
||||
consumer.log_error("has_consumer_access error")
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -226,7 +226,6 @@ scheduler_events = {
|
|||
"frappe.sessions.clear_expired_sessions",
|
||||
"frappe.email.doctype.notification.notification.trigger_daily_alerts",
|
||||
"frappe.utils.scheduler.restrict_scheduler_events_if_dormant",
|
||||
"frappe.email.doctype.auto_email_report.auto_email_report.send_daily",
|
||||
"frappe.website.doctype.personal_data_deletion_request.personal_data_deletion_request.remove_unverified_record",
|
||||
"frappe.desk.form.document_follow.send_daily_updates",
|
||||
"frappe.social.doctype.energy_point_settings.energy_point_settings.allocate_review_points",
|
||||
|
|
@ -241,6 +240,7 @@ scheduler_events = {
|
|||
"frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_daily",
|
||||
"frappe.utils.change_log.check_for_update",
|
||||
"frappe.integrations.doctype.s3_backup_settings.s3_backup_settings.take_backups_daily",
|
||||
"frappe.email.doctype.auto_email_report.auto_email_report.send_daily",
|
||||
"frappe.integrations.doctype.google_drive.google_drive.daily_backup",
|
||||
],
|
||||
"weekly_long": [
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ def enqueue_webhook(doc, webhook):
|
|||
if i != 2:
|
||||
continue
|
||||
else:
|
||||
raise e
|
||||
webhook.log_error("Webhook failed")
|
||||
|
||||
|
||||
def log_request(url, headers, data, res):
|
||||
|
|
|
|||
|
|
@ -159,13 +159,13 @@ class SiteMigration:
|
|||
"""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()
|
||||
|
||||
if not self.required_services_running():
|
||||
raise SystemExit(1)
|
||||
|
||||
self.setUp()
|
||||
try:
|
||||
self.pre_schema_updates()
|
||||
|
|
|
|||
|
|
@ -40,6 +40,9 @@ data_fieldtypes = (
|
|||
"JSON",
|
||||
)
|
||||
|
||||
float_like_fields = {"Float", "Currency", "Percent"}
|
||||
datetime_fields = {"Datetime", "Date", "Time"}
|
||||
|
||||
attachment_fieldtypes = (
|
||||
"Attach",
|
||||
"Attach Image",
|
||||
|
|
|
|||
|
|
@ -2,10 +2,18 @@
|
|||
# License: MIT. See LICENSE
|
||||
import datetime
|
||||
import json
|
||||
from typing import Dict, List
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model import child_table_fields, default_fields, display_fieldtypes, table_fields
|
||||
from frappe.model import (
|
||||
child_table_fields,
|
||||
datetime_fields,
|
||||
default_fields,
|
||||
display_fieldtypes,
|
||||
float_like_fields,
|
||||
table_fields,
|
||||
)
|
||||
from frappe.model.docstatus import DocStatus
|
||||
from frappe.model.naming import set_new_name
|
||||
from frappe.model.utils.link_count import notify_link_count
|
||||
|
|
@ -252,10 +260,11 @@ class BaseDocument(object):
|
|||
|
||||
def get_valid_dict(
|
||||
self, sanitize=True, convert_dates_to_str=False, ignore_nulls=False, ignore_virtual=False
|
||||
):
|
||||
) -> Dict:
|
||||
d = frappe._dict()
|
||||
for fieldname in self.meta.get_valid_columns():
|
||||
d[fieldname] = self.get(fieldname)
|
||||
# column is valid, we can use getattr
|
||||
d[fieldname] = getattr(self, fieldname, None)
|
||||
|
||||
# if no need for sanitization and value is None, continue
|
||||
if not sanitize and d[fieldname] is None:
|
||||
|
|
@ -263,25 +272,24 @@ class BaseDocument(object):
|
|||
|
||||
df = self.meta.get_field(fieldname)
|
||||
|
||||
if df and df.get("is_virtual"):
|
||||
if ignore_virtual:
|
||||
del d[fieldname]
|
||||
continue
|
||||
if df:
|
||||
if getattr(df, "is_virtual", False):
|
||||
if ignore_virtual:
|
||||
del d[fieldname]
|
||||
continue
|
||||
|
||||
from frappe.utils.safe_exec import get_safe_globals
|
||||
if d[fieldname] is None and (options := getattr(df, "options", None)):
|
||||
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"),
|
||||
code=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 isinstance(d[fieldname], list) and df.fieldtype not in table_fields:
|
||||
frappe.throw(_("Value for {0} cannot be a list").format(_(df.label)))
|
||||
|
||||
if df.fieldtype == "Check":
|
||||
d[fieldname] = 1 if cint(d[fieldname]) else 0
|
||||
|
||||
|
|
@ -291,25 +299,20 @@ class BaseDocument(object):
|
|||
elif df.fieldtype == "JSON" and isinstance(d[fieldname], dict):
|
||||
d[fieldname] = json.dumps(d[fieldname], sort_keys=True, indent=4, separators=(",", ": "))
|
||||
|
||||
elif df.fieldtype in ("Currency", "Float", "Percent") and not isinstance(d[fieldname], float):
|
||||
elif df.fieldtype in float_like_fields and not isinstance(d[fieldname], float):
|
||||
d[fieldname] = flt(d[fieldname])
|
||||
|
||||
elif df.fieldtype in ("Datetime", "Date", "Time") and d[fieldname] == "":
|
||||
elif (df.fieldtype in datetime_fields and d[fieldname] == "") or (
|
||||
getattr(df, "unique", False) and cstr(d[fieldname]).strip() == ""
|
||||
):
|
||||
d[fieldname] = None
|
||||
|
||||
elif df.get("unique") and cstr(d[fieldname]).strip() == "":
|
||||
# unique empty field should be set to None
|
||||
d[fieldname] = None
|
||||
|
||||
if isinstance(d[fieldname], list) and df.fieldtype not in table_fields:
|
||||
frappe.throw(_("Value for {0} cannot be a list").format(_(df.label)))
|
||||
|
||||
if convert_dates_to_str and isinstance(
|
||||
d[fieldname], (datetime.datetime, datetime.date, datetime.time, datetime.timedelta)
|
||||
):
|
||||
d[fieldname] = str(d[fieldname])
|
||||
|
||||
if d[fieldname] is None and ignore_nulls:
|
||||
if ignore_nulls and d[fieldname] is None:
|
||||
del d[fieldname]
|
||||
|
||||
return d
|
||||
|
|
@ -329,7 +332,7 @@ class BaseDocument(object):
|
|||
if key not in self.__dict__:
|
||||
self.__dict__[key] = None
|
||||
|
||||
def get_valid_columns(self):
|
||||
def get_valid_columns(self) -> List[str]:
|
||||
if self.doctype not in frappe.local.valid_columns:
|
||||
if self.doctype in DOCTYPES_FOR_DOCTYPE:
|
||||
from frappe.model.meta import get_table_columns
|
||||
|
|
@ -342,7 +345,7 @@ class BaseDocument(object):
|
|||
|
||||
return frappe.local.valid_columns[self.doctype]
|
||||
|
||||
def is_new(self):
|
||||
def is_new(self) -> bool:
|
||||
return self.get("__islocal")
|
||||
|
||||
@property
|
||||
|
|
@ -359,8 +362,8 @@ class BaseDocument(object):
|
|||
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)
|
||||
) -> Dict:
|
||||
doc = self.get_valid_dict(convert_dates_to_str=convert_dates_to_str, ignore_nulls=no_nulls)
|
||||
doc["doctype"] = self.doctype
|
||||
|
||||
for df in self.meta.get_table_fields():
|
||||
|
|
@ -375,20 +378,15 @@ class BaseDocument(object):
|
|||
for d in children
|
||||
]
|
||||
|
||||
if no_nulls:
|
||||
for k in list(doc):
|
||||
if doc[k] is None:
|
||||
del doc[k]
|
||||
|
||||
if no_default_fields:
|
||||
for k in list(doc):
|
||||
if k in default_fields:
|
||||
del doc[k]
|
||||
for key in default_fields:
|
||||
if key in doc:
|
||||
del doc[key]
|
||||
|
||||
if no_child_table_fields:
|
||||
for k in list(doc):
|
||||
if k in child_table_fields:
|
||||
del doc[k]
|
||||
for key in child_table_fields:
|
||||
if key in doc:
|
||||
del doc[key]
|
||||
|
||||
for key in (
|
||||
"_user_tags",
|
||||
|
|
@ -398,8 +396,8 @@ class BaseDocument(object):
|
|||
"__run_link_triggers",
|
||||
"__unsaved",
|
||||
):
|
||||
if self.get(key):
|
||||
doc[key] = self.get(key)
|
||||
if value := getattr(self, key, None):
|
||||
doc[key] = value
|
||||
|
||||
return doc
|
||||
|
||||
|
|
|
|||
|
|
@ -1362,6 +1362,12 @@ class Document(BaseDocument):
|
|||
).insert(ignore_permissions=True)
|
||||
frappe.local.flags.commit = True
|
||||
|
||||
def log_error(self, title=None, message=None):
|
||||
"""Helper function to create an Error Log"""
|
||||
return frappe.log_error(
|
||||
message=message, title=title, reference_doctype=self.doctype, reference_name=self.name
|
||||
)
|
||||
|
||||
def get_signature(self):
|
||||
"""Returns signature (hash) for private URL."""
|
||||
return hashlib.sha224(get_datetime_str(self.creation).encode()).hexdigest()
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ from typing import TYPE_CHECKING, Optional, Union
|
|||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.database.sequence import get_next_val, set_next_val
|
||||
from frappe.model import log_types
|
||||
from frappe.query_builder import DocType
|
||||
from frappe.utils import cint, cstr, now_datetime
|
||||
|
|
@ -36,6 +35,8 @@ def set_new_name(doc):
|
|||
doc.name = None
|
||||
|
||||
if is_autoincremented(doc.doctype, meta):
|
||||
from frappe.database.sequence import get_next_val
|
||||
|
||||
doc.name = get_next_val(doc.doctype)
|
||||
return
|
||||
|
||||
|
|
@ -322,11 +323,14 @@ def get_default_naming_series(doctype):
|
|||
|
||||
|
||||
def validate_name(doctype: str, name: Union[int, str], case: Optional[str] = None):
|
||||
|
||||
if not name:
|
||||
frappe.throw(_("No Name Specified for {0}").format(doctype))
|
||||
|
||||
if isinstance(name, int):
|
||||
if is_autoincremented(doctype):
|
||||
from frappe.database.sequence import set_next_val
|
||||
|
||||
# this will set the sequence val to be the provided name and set it to be used
|
||||
# so that the sequence will start from the next val of the setted val(name)
|
||||
set_next_val(doctype, name, is_val_used=True)
|
||||
|
|
|
|||
|
|
@ -254,8 +254,9 @@ def bulk_workflow_approval(docnames, doctype, action):
|
|||
|
||||
frappe.db.rollback()
|
||||
frappe.log_error(
|
||||
frappe.get_traceback(),
|
||||
"Workflow {0} threw an error for {1} {2}".format(action, doctype, docname),
|
||||
title="Workflow {0} threw an error for {1} {2}".format(action, doctype, docname),
|
||||
reference_doctype="Workflow",
|
||||
reference_name=action,
|
||||
)
|
||||
finally:
|
||||
if not message_dict:
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@ class ParallelTestRunner:
|
|||
|
||||
class ParallelTestResult(unittest.TextTestResult):
|
||||
def startTest(self, test):
|
||||
self.tb_locals = True
|
||||
self._started_at = time.time()
|
||||
super(unittest.TextTestResult, self).startTest(test)
|
||||
test_class = unittest.util.strclass(test.__class__)
|
||||
|
|
|
|||
|
|
@ -106,7 +106,6 @@ frappe.patches.v12_0.set_default_incoming_email_port
|
|||
frappe.patches.v12_0.update_global_search
|
||||
frappe.patches.v12_0.setup_tags
|
||||
frappe.patches.v12_0.update_auto_repeat_status_and_not_submittable
|
||||
frappe.patches.v12_0.copy_to_parent_for_tags
|
||||
frappe.patches.v12_0.create_notification_settings_for_user
|
||||
frappe.patches.v11_0.make_all_prepared_report_attachments_private #2019-11-26
|
||||
frappe.patches.v12_0.setup_email_linking
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
|
||||
frappe.db.sql("UPDATE `tabTag Link` SET parenttype=document_type")
|
||||
frappe.db.sql("UPDATE `tabTag Link` SET parent=document_name")
|
||||
|
|
@ -375,7 +375,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
|
|||
}
|
||||
|
||||
set_open_count() {
|
||||
if (!this.data.transactions || !this.data.fieldname) {
|
||||
if (!this.data || (!this.data.transactions || !this.data.fieldname)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -289,19 +289,23 @@ export default class GridRow {
|
|||
var me = this;
|
||||
if(this.doc && !this.grid.df.in_place_edit) {
|
||||
// remove row
|
||||
if(!this.open_form_button) {
|
||||
this.open_form_button = $(`
|
||||
<div class="btn-open-row">
|
||||
<a>${frappe.utils.icon('edit', 'xs')}</a>
|
||||
<div class="hidden-xs edit-grid-row">${ __("Edit") }</div>
|
||||
</div>
|
||||
`)
|
||||
.appendTo($('<div class="col col-xs-1"></div>').appendTo(this.row))
|
||||
.on('click', function() {
|
||||
me.toggle_view(); return false;
|
||||
});
|
||||
if (!this.open_form_button) {
|
||||
this.open_form_button = $('<div class="col col-xs-1"></div>').appendTo(this.row);
|
||||
|
||||
if(this.is_too_small()) {
|
||||
if (!this.configure_columns) {
|
||||
this.open_form_button = $(`
|
||||
<div class="btn-open-row">
|
||||
<a>${frappe.utils.icon('edit', 'xs')}</a>
|
||||
<div class="hidden-xs edit-grid-row">${ __("Edit") }</div>
|
||||
</div>
|
||||
`)
|
||||
.appendTo(this.open_form_button)
|
||||
.on('click', function() {
|
||||
me.toggle_view(); return false;
|
||||
});
|
||||
}
|
||||
|
||||
if (this.is_too_small()) {
|
||||
// narrow
|
||||
this.open_form_button.css({'margin-right': '-2px'});
|
||||
}
|
||||
|
|
@ -310,7 +314,9 @@ export default class GridRow {
|
|||
}
|
||||
|
||||
add_column_configure_button() {
|
||||
if (this.configure_columns) {
|
||||
if (this.grid.df.in_place_edit && !this.frm) return;
|
||||
|
||||
if (this.configure_columns && this.frm) {
|
||||
this.configure_columns_button = $(`
|
||||
<div class="col grid-static-col col-xs-1 d-flex justify-content-center" style="cursor: pointer;">
|
||||
<a>${frappe.utils.icon('setting-gear', 'sm', '', 'filter: opacity(0.5)')}</a>
|
||||
|
|
@ -320,6 +326,10 @@ export default class GridRow {
|
|||
.on('click', () => {
|
||||
this.configure_dialog_for_columns_selector();
|
||||
});
|
||||
} else if (this.configure_columns && !this.frm) {
|
||||
this.configure_columns_button = $(`
|
||||
<div class="col grid-static-col col-xs-1"></div>
|
||||
`).appendTo(this.row);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -595,6 +605,8 @@ export default class GridRow {
|
|||
// to get update df for the row
|
||||
let df = this.docfields.find(field => field.fieldname === col[0].fieldname);
|
||||
|
||||
this.set_dependant_property(df);
|
||||
|
||||
let colsize = col[1];
|
||||
let txt = this.doc ?
|
||||
frappe.format(this.doc[df.fieldname], df, null, this.doc) :
|
||||
|
|
@ -633,6 +645,56 @@ export default class GridRow {
|
|||
}
|
||||
}
|
||||
|
||||
set_dependant_property(df) {
|
||||
if (!df.reqd && df.mandatory_depends_on &&
|
||||
this.evaluate_depends_on_value(df.mandatory_depends_on)) {
|
||||
df.reqd = 1;
|
||||
}
|
||||
|
||||
if (!df.read_only && df.read_only_depends_on &&
|
||||
this.evaluate_depends_on_value(df.read_only_depends_on)) {
|
||||
df.read_only = 1;
|
||||
}
|
||||
}
|
||||
|
||||
evaluate_depends_on_value(expression) {
|
||||
let out = null;
|
||||
let doc = this.doc;
|
||||
|
||||
if (!doc) return;
|
||||
|
||||
let parent = this.frm ? this.frm.doc : this.doc || null;
|
||||
|
||||
if (typeof (expression) === 'boolean') {
|
||||
out = expression;
|
||||
|
||||
} else if (typeof (expression) === 'function') {
|
||||
out = expression(doc);
|
||||
|
||||
} else if (expression.substr(0, 5)=='eval:') {
|
||||
try {
|
||||
out = frappe.utils.eval(expression.substr(5), { doc, parent });
|
||||
if (parent && parent.istable && expression.includes('is_submittable')) {
|
||||
out = true;
|
||||
}
|
||||
} catch (e) {
|
||||
frappe.throw(__('Invalid "depends_on" expression'));
|
||||
}
|
||||
|
||||
} else if (expression.substr(0, 3)=='fn:' && this.frm) {
|
||||
out = this.frm.script_manager.trigger(expression.substr(3), this.doctype, this.docname);
|
||||
} else {
|
||||
var value = doc[expression];
|
||||
if ($.isArray(value)) {
|
||||
out = !!value.length;
|
||||
} else {
|
||||
out = !!value;
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
show_search_row() {
|
||||
// show or remove search columns based on grid rows
|
||||
this.show_search = this.frm && this.frm.doc &&
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ frappe.ui.form.Layout = class Layout {
|
|||
// remove previous color
|
||||
this.message.removeClass(this.message_color);
|
||||
}
|
||||
this.message_color = (color && ['yellow', 'blue', 'red'].includes(color)) ? color : 'blue';
|
||||
this.message_color = (color && ['yellow', 'blue', 'red', 'green', 'orange'].includes(color)) ? color : 'blue';
|
||||
if (html) {
|
||||
if (html.substr(0, 1)!=='<') {
|
||||
// wrap in a block
|
||||
|
|
@ -439,7 +439,7 @@ frappe.ui.form.Layout = class Layout {
|
|||
}
|
||||
|
||||
handle_tab(doctype, fieldname, shift) {
|
||||
let grid_row = null,
|
||||
let grid_row = null,
|
||||
prev = null,
|
||||
fields = this.fields_list,
|
||||
focused = false;
|
||||
|
|
|
|||
|
|
@ -130,7 +130,8 @@ frappe.ui.form.save = function (frm, action, callback, btn) {
|
|||
folded = frm.layout.folded;
|
||||
}
|
||||
|
||||
if (df.reqd && !frappe.model.has_value(doc.doctype, doc.name, df.fieldname)) {
|
||||
if (is_docfield_mandatory(doc, df) &&
|
||||
!frappe.model.has_value(doc.doctype, doc.name, df.fieldname)) {
|
||||
has_errors = true;
|
||||
error_fields[error_fields.length] = __(df.label);
|
||||
// scroll to field
|
||||
|
|
@ -173,6 +174,42 @@ frappe.ui.form.save = function (frm, action, callback, btn) {
|
|||
return !has_errors;
|
||||
};
|
||||
|
||||
let is_docfield_mandatory = function(doc, df) {
|
||||
if (df.reqd) return true;
|
||||
if (!df.mandatory_depends_on || !doc) return;
|
||||
|
||||
let out = null;
|
||||
let expression = df.mandatory_depends_on;
|
||||
let parent = frappe.get_meta(df.parent);
|
||||
|
||||
if (typeof (expression) === 'boolean') {
|
||||
out = expression;
|
||||
|
||||
} else if (typeof (expression) === 'function') {
|
||||
out = expression(doc);
|
||||
|
||||
} else if (expression.substr(0, 5) == 'eval:') {
|
||||
try {
|
||||
out = frappe.utils.eval(expression.substr(5), { doc, parent });
|
||||
if (parent && parent.istable && expression.includes('is_submittable')) {
|
||||
out = true;
|
||||
}
|
||||
} catch (e) {
|
||||
frappe.throw(__('Invalid "mandatory_depends_on" expression'));
|
||||
}
|
||||
|
||||
} else {
|
||||
var value = doc[expression];
|
||||
if ($.isArray(value)) {
|
||||
out = !!value.length;
|
||||
} else {
|
||||
out = !!value;
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
const scroll_to = (fieldname) => {
|
||||
frm.scroll_to_field(fieldname);
|
||||
frm.scroll_set = true;
|
||||
|
|
|
|||
|
|
@ -323,7 +323,7 @@ frappe.ui.form.Toolbar = class Toolbar {
|
|||
}
|
||||
|
||||
// New
|
||||
if(p[CREATE] && !this.frm.meta.issingle) {
|
||||
if (p[CREATE] && !this.frm.meta.issingle && !this.frm.meta.in_create) {
|
||||
this.page.add_menu_item(__("New {0}", [__(me.frm.doctype)]), function() {
|
||||
frappe.new_doc(me.frm.doctype, true);
|
||||
}, true, {
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ frappe.views.BaseList = class BaseList {
|
|||
// df is passed
|
||||
const df = fieldname;
|
||||
fieldname = df.fieldname;
|
||||
doctype = df.parent;
|
||||
doctype = df.parent || doctype;
|
||||
}
|
||||
|
||||
if (!this.fields) this.fields = [];
|
||||
|
|
|
|||
|
|
@ -1580,15 +1580,22 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
}
|
||||
|
||||
if (frappe.user.has_role("System Manager")) {
|
||||
items.push({
|
||||
label: __("List Settings", null, "Button in list view menu"),
|
||||
action: () => this.show_list_settings(),
|
||||
standard: true,
|
||||
});
|
||||
if (this.get_view_settings) {
|
||||
items.push(this.get_view_settings());
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
get_view_settings() {
|
||||
return {
|
||||
label: __("List Settings", null, "Button in list view menu"),
|
||||
action: () => this.show_list_settings(),
|
||||
standard: true,
|
||||
};
|
||||
}
|
||||
|
||||
show_list_settings() {
|
||||
frappe.model.with_doctype(this.doctype, () => {
|
||||
new ListSettings({
|
||||
|
|
|
|||
|
|
@ -22,15 +22,17 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout {
|
|||
super.make();
|
||||
this.refresh();
|
||||
// set default
|
||||
$.each(this.fields_list, (_, field) => {
|
||||
if (!is_null(field.df.default)) {
|
||||
let def_value = field.df.default;
|
||||
$.each(this.fields_list, function(i, field) {
|
||||
if (field.df["default"]) {
|
||||
let def_value = field.df["default"];
|
||||
|
||||
if (def_value === "Today" && field.df.fieldtype === "Date") {
|
||||
if (def_value == 'Today' && field.df["fieldtype"] == 'Date') {
|
||||
def_value = frappe.datetime.get_today();
|
||||
}
|
||||
|
||||
this.set_value(field.df.fieldname, def_value);
|
||||
field.set_input(def_value);
|
||||
// if default and has depends_on, render its fields.
|
||||
me.refresh_dependency();
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -127,7 +129,6 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout {
|
|||
if (f) {
|
||||
f.set_value(val).then(() => {
|
||||
f.set_input(val);
|
||||
f.refresh();
|
||||
this.refresh_dependency();
|
||||
resolve();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -630,8 +630,6 @@ frappe.provide("frappe.views");
|
|||
if(!card) return;
|
||||
make_dom();
|
||||
render_card_meta();
|
||||
add_task_link();
|
||||
// edit_card_title();
|
||||
}
|
||||
|
||||
function make_dom() {
|
||||
|
|
@ -640,12 +638,35 @@ frappe.provide("frappe.views");
|
|||
title: frappe.utils.html2text(card.title),
|
||||
disable_click: card._disable_click ? 'disable-click' : '',
|
||||
creation: card.creation,
|
||||
doc_content: get_doc_content(card),
|
||||
image_url: cur_list.get_image_url(card),
|
||||
form_link: frappe.utils.get_form_link(card.doctype, card.name)
|
||||
};
|
||||
|
||||
self.$card = $(frappe.render_template('kanban_card', opts))
|
||||
.appendTo(wrapper);
|
||||
}
|
||||
|
||||
function get_doc_content(card) {
|
||||
let fields = [];
|
||||
for (let field_name of cur_list.board.fields) {
|
||||
let field = (
|
||||
frappe.meta.get_docfield(card.doctype, field_name, card.name)
|
||||
|| frappe.model.get_std_field(field_name)
|
||||
);
|
||||
let label = cur_list.board.show_labels ? `<span>${__(field.label)}: </span>` : '';
|
||||
let value = frappe.format(card.doc[field_name], field);
|
||||
fields.push(`
|
||||
<div class="text-muted text-truncate">
|
||||
${label}
|
||||
<span>${value}</span>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
return fields.join("");
|
||||
}
|
||||
|
||||
function get_tags_html(card) {
|
||||
return card.tags
|
||||
? `<div class="kanban-tags">
|
||||
|
|
@ -688,12 +709,6 @@ frappe.provide("frappe.views");
|
|||
.find('.kanban-assignments').append($assignees_group);
|
||||
}
|
||||
|
||||
function add_task_link() {
|
||||
let task_link = frappe.utils.get_form_link(card.doctype, card.name);
|
||||
self.$card.find('.kanban-card-redirect')
|
||||
.attr('href', task_link);
|
||||
}
|
||||
|
||||
function get_assignees_group() {
|
||||
return frappe.avatar_group(card.assigned_list, 3, {
|
||||
css_class: 'avatar avatar-small',
|
||||
|
|
@ -744,7 +759,7 @@ frappe.provide("frappe.views");
|
|||
assigned_list: card.assigned_list || assigned_list,
|
||||
comment_count: card.comment_count || comment_count,
|
||||
color: card.color || null,
|
||||
doc: doc
|
||||
doc: doc || card
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,23 +1,24 @@
|
|||
<div class="kanban-card-wrapper {{ disable_click }}" data-name="{{encodeURIComponent(name)}}">
|
||||
<a class="kanban-card-redirect" href="#">
|
||||
<div class="kanban-card content">
|
||||
{% if(image_url) { %}
|
||||
<div class="kanban-image">
|
||||
<img src="{{image_url}}" alt="{{title}}">
|
||||
</div>
|
||||
{% } %}
|
||||
<div class="kanban-card-body">
|
||||
<div class="kanban-title-area">
|
||||
<div class="kanban-card content">
|
||||
{% if(image_url) { %}
|
||||
<div class="kanban-image">
|
||||
<img src="{{image_url}}" alt="{{title}}">
|
||||
</div>
|
||||
{% } %}
|
||||
<div class="kanban-card-body">
|
||||
<div class="kanban-title-area">
|
||||
<a href="{{ form_link }}">
|
||||
<div class="kanban-card-title ellipsis" title="{{title}}">
|
||||
{{ title }}
|
||||
</div>
|
||||
<div class="kanban-card-creation">
|
||||
{{ creation }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="kanban-card-meta">
|
||||
</a>
|
||||
<br>
|
||||
<div class="kanban-card-doc text-muted">
|
||||
{{ doc_content }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="kanban-card-meta">
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
264
frappe/public/js/frappe/views/kanban/kanban_settings.js
Normal file
264
frappe/public/js/frappe/views/kanban/kanban_settings.js
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
export default class KanbanSettings {
|
||||
constructor({ kanbanview, doctype, meta, settings }) {
|
||||
if (!doctype) {
|
||||
frappe.throw(__("DocType required"));
|
||||
}
|
||||
|
||||
this.kanbanview = kanbanview;
|
||||
this.doctype = doctype;
|
||||
this.meta = meta;
|
||||
this.settings = settings;
|
||||
this.dialog = null;
|
||||
this.fields = this.settings && this.settings.fields;
|
||||
|
||||
frappe.model.with_doctype("List View Settings", () => {
|
||||
this.make();
|
||||
this.get_fields();
|
||||
this.setup_fields();
|
||||
this.setup_remove_fields();
|
||||
this.add_new_fields();
|
||||
this.show_dialog();
|
||||
});
|
||||
}
|
||||
|
||||
make() {
|
||||
this.dialog = new frappe.ui.Dialog({
|
||||
title: __("{0} Settings", [__(this.doctype)]),
|
||||
fields: [
|
||||
{
|
||||
fieldname: "show_labels",
|
||||
label: __("Show Labels"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "fields_html",
|
||||
fieldtype: "HTML"
|
||||
},
|
||||
{
|
||||
fieldname: "fields",
|
||||
fieldtype: "Code",
|
||||
hidden: 1
|
||||
}
|
||||
]
|
||||
});
|
||||
this.dialog.set_values(this.settings);
|
||||
this.dialog.set_primary_action(__("Save"), () => {
|
||||
frappe.show_alert({
|
||||
message: __("Saving"),
|
||||
indicator: "green"
|
||||
});
|
||||
|
||||
frappe.call({
|
||||
method:
|
||||
"frappe.desk.doctype.kanban_board.kanban_board.save_settings",
|
||||
args: {
|
||||
board_name: this.settings.name,
|
||||
settings: this.dialog.get_values()
|
||||
},
|
||||
callback: r => {
|
||||
this.kanbanview.board = r.message;
|
||||
this.kanbanview.render();
|
||||
this.dialog.hide();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.setup_fields();
|
||||
this.add_new_fields();
|
||||
this.setup_remove_fields();
|
||||
}
|
||||
|
||||
show_dialog() {
|
||||
if (!this.settings.fields) {
|
||||
this.update_fields();
|
||||
}
|
||||
|
||||
this.dialog.show();
|
||||
}
|
||||
|
||||
setup_fields() {
|
||||
const fields_html = this.dialog.get_field("fields_html");
|
||||
const wrapper = fields_html.$wrapper[0];
|
||||
let fields = "";
|
||||
|
||||
for (let fieldname of this.fields) {
|
||||
let field = this.get_docfield(fieldname);
|
||||
|
||||
fields += `
|
||||
<div class="control-input flex align-center form-control fields_order sortable"
|
||||
style="display: block; margin-bottom: 5px;"
|
||||
data-fieldname="${field.fieldname}"
|
||||
data-label="${field.label}"
|
||||
data-type="${field.type}">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-1">
|
||||
${frappe.utils.icon("drag", "xs", "", "", "sortable-handle")}
|
||||
</div>
|
||||
<div class="col-md-10" style="padding-left:0px;">
|
||||
${__(field.label)}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<a class="text-muted remove-field" data-fieldname="${field.fieldname}">
|
||||
${frappe.utils.icon("delete", "xs")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
fields_html.html(`
|
||||
<div class="form-group">
|
||||
<div class="clearfix">
|
||||
<label class="control-label" style="padding-right: 0px;">Fields</label>
|
||||
</div>
|
||||
<div class="control-input-wrapper">
|
||||
${fields}
|
||||
</div>
|
||||
<p class="help-box small text-muted">
|
||||
<a class="add-new-fields text-muted">
|
||||
+ Add / Remove Fields
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
`);
|
||||
|
||||
new Sortable(
|
||||
wrapper.getElementsByClassName("control-input-wrapper")[0],
|
||||
{
|
||||
handle: ".sortable-handle",
|
||||
draggable: ".sortable",
|
||||
onUpdate: params => {
|
||||
this.fields.splice(
|
||||
params.newIndex,
|
||||
0,
|
||||
this.fields.splice(params.oldIndex, 1)[0]
|
||||
);
|
||||
this.dialog.set_value(
|
||||
"fields",
|
||||
JSON.stringify(this.fields)
|
||||
);
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
add_new_fields() {
|
||||
let add_new_fields = this.get_dialog_fields_wrapper().getElementsByClassName(
|
||||
"add-new-fields"
|
||||
)[0];
|
||||
add_new_fields.onclick = () => this.show_column_selector();
|
||||
}
|
||||
|
||||
setup_remove_fields() {
|
||||
let remove_fields = this.get_dialog_fields_wrapper().getElementsByClassName(
|
||||
"remove-field"
|
||||
);
|
||||
|
||||
for (let idx = 0; idx < remove_fields.length; idx++) {
|
||||
remove_fields.item(idx).onclick = () =>
|
||||
this.remove_fields(
|
||||
remove_fields.item(idx).getAttribute("data-fieldname")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
get_dialog_fields_wrapper() {
|
||||
return this.dialog.get_field("fields_html").$wrapper[0];
|
||||
}
|
||||
|
||||
remove_fields(fieldname) {
|
||||
this.fields = this.fields.filter(field => field !== fieldname);
|
||||
this.dialog.set_value("fields", JSON.stringify(this.fields));
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
update_fields() {
|
||||
const wrapper = this.dialog.get_field("fields_html").$wrapper[0];
|
||||
let fields_order = wrapper.getElementsByClassName("fields_order");
|
||||
this.fields = [];
|
||||
|
||||
for (let idx = 0; idx < fields_order.length; idx++) {
|
||||
this.fields.push(
|
||||
fields_order.item(idx).getAttribute("data-fieldname")
|
||||
);
|
||||
}
|
||||
|
||||
this.dialog.set_value("fields", JSON.stringify(this.fields));
|
||||
}
|
||||
|
||||
show_column_selector() {
|
||||
let dialog = new frappe.ui.Dialog({
|
||||
title: __("{0} Fields", [__(this.doctype)]),
|
||||
fields: [
|
||||
{
|
||||
label: __("Select Fields"),
|
||||
fieldtype: "MultiCheck",
|
||||
fieldname: "fields",
|
||||
options: this.get_multiselect_fields(),
|
||||
columns: 2
|
||||
}
|
||||
]
|
||||
});
|
||||
dialog.set_primary_action(__("Save"), () => {
|
||||
this.fields = dialog.get_values().fields || [];
|
||||
this.dialog.set_value("fields", JSON.stringify(this.fields));
|
||||
this.refresh();
|
||||
dialog.hide();
|
||||
});
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
get_fields() {
|
||||
this.fields = this.settings.fields;
|
||||
this.fields.uniqBy(f => f.fieldname);
|
||||
}
|
||||
|
||||
get_docfield(field_name) {
|
||||
return (
|
||||
frappe.meta.get_docfield(this.doctype, field_name) ||
|
||||
frappe.model.get_std_field(field_name)
|
||||
);
|
||||
}
|
||||
|
||||
get_multiselect_fields() {
|
||||
const ignore_fields = [
|
||||
"idx",
|
||||
"lft",
|
||||
"rgt",
|
||||
"old_parent",
|
||||
"_user_tags",
|
||||
"_liked_by",
|
||||
"_comments",
|
||||
"_assign",
|
||||
this.meta.title_field || "name"
|
||||
];
|
||||
|
||||
const ignore_fieldtypes = [
|
||||
"Attach Image",
|
||||
"Text Editor",
|
||||
"HTML Editor",
|
||||
"Code",
|
||||
"Color",
|
||||
...frappe.model.no_value_type
|
||||
];
|
||||
|
||||
return frappe.model.std_fields
|
||||
.concat(this.kanbanview.get_fields_in_list_view())
|
||||
.filter(
|
||||
field =>
|
||||
!ignore_fields.includes(field.fieldname) &&
|
||||
!ignore_fieldtypes.includes(field.fieldtype)
|
||||
)
|
||||
.map(field => {
|
||||
return {
|
||||
label: __(field.label),
|
||||
value: field.fieldname,
|
||||
checked: this.fields.includes(field.fieldname)
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import KanbanSettings from "./kanban_settings";
|
||||
|
||||
frappe.provide('frappe.views');
|
||||
|
||||
frappe.views.KanbanView = class KanbanView extends frappe.views.ListView {
|
||||
|
|
@ -57,6 +59,7 @@ frappe.views.KanbanView = class KanbanView extends frappe.views.ListView {
|
|||
.then(board => {
|
||||
this.board = board;
|
||||
this.board.filters_array = JSON.parse(this.board.filters || '[]');
|
||||
this.board.fields = JSON.parse(this.board.fields || '[]');
|
||||
this.filters = this.board.filters_array;
|
||||
});
|
||||
}
|
||||
|
|
@ -187,6 +190,25 @@ frappe.views.KanbanView = class KanbanView extends frappe.views.ListView {
|
|||
};
|
||||
}
|
||||
|
||||
get_view_settings() {
|
||||
return {
|
||||
label: __("Kanban Settings", null, "Button in kanban view menu"),
|
||||
action: () => this.show_kanban_settings(),
|
||||
standard: true,
|
||||
};
|
||||
}
|
||||
|
||||
show_kanban_settings() {
|
||||
frappe.model.with_doctype(this.doctype, () => {
|
||||
new KanbanSettings({
|
||||
kanbanview: this,
|
||||
doctype: this.doctype,
|
||||
settings: this.board,
|
||||
meta: frappe.get_meta(this.doctype)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
get required_libs() {
|
||||
return [
|
||||
'assets/frappe/js/lib/fluxify.min.js',
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export default class Block {
|
|||
|
||||
make(block, block_name, widget_type = block) {
|
||||
let block_data = this.config.page_data[block+'s'].items.find(obj => {
|
||||
return frappe.utils.unescape_html(obj.label) == frappe.utils.unescape_html(block_name);
|
||||
return frappe.utils.unescape_html(obj.label) == frappe.utils.unescape_html(__(block_name));
|
||||
});
|
||||
if (!block_data) return false;
|
||||
this.wrapper.innerHTML = '';
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export default class Card extends Block {
|
|||
this.new('card', 'links');
|
||||
|
||||
if (this.data && this.data.card_name) {
|
||||
let has_data = this.make('card', __(this.data.card_name), 'links');
|
||||
let has_data = this.make('card', this.data.card_name, 'links');
|
||||
if (!has_data) return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export default class Chart extends Block {
|
|||
this.new('chart');
|
||||
|
||||
if (this.data && this.data.chart_name) {
|
||||
let has_data = this.make('chart', __(this.data.chart_name));
|
||||
let has_data = this.make('chart', this.data.chart_name);
|
||||
if (!has_data) return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ export default class Onboarding extends Block {
|
|||
|
||||
make(block, block_name) {
|
||||
let block_data = this.config.page_data['onboardings'].items.find(obj => {
|
||||
return obj.label == block_name;
|
||||
return obj.label == __(block_name);
|
||||
});
|
||||
if (!block_data) return false;
|
||||
this.wrapper.innerHTML = '';
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export default class Shortcut extends Block {
|
|||
this.new('shortcut');
|
||||
|
||||
if (this.data && this.data.shortcut_name) {
|
||||
let has_data = this.make('shortcut', __(this.data.shortcut_name));
|
||||
let has_data = this.make('shortcut', this.data.shortcut_name);
|
||||
if (!has_data) return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -228,30 +228,35 @@ class CardDialog extends WidgetDialog {
|
|||
}
|
||||
|
||||
process_data(data) {
|
||||
data.links.map((item, idx) => {
|
||||
let message = '';
|
||||
let row = idx+1;
|
||||
let message = '';
|
||||
|
||||
if (!item.link_type) {
|
||||
message = "Following fields have missing values: <br><br><ul>";
|
||||
message += `<li>Link Type in Row ${row}</li>`;
|
||||
}
|
||||
if (!data.links) {
|
||||
message = "You must add atleast one link.";
|
||||
} else {
|
||||
data.links.map((item, idx) => {
|
||||
let row = idx+1;
|
||||
|
||||
if (!item.link_to) {
|
||||
message += `<li>Link To in Row ${row}</li>`;
|
||||
}
|
||||
if (!item.link_type) {
|
||||
message = "Following fields have missing values: <br><br><ul>";
|
||||
message += `<li>Link Type in Row ${row}</li>`;
|
||||
}
|
||||
|
||||
if (message) {
|
||||
message += "</ul>";
|
||||
frappe.throw({
|
||||
message: __(message),
|
||||
title: __("Missing Values Required"),
|
||||
indicator: 'orange'
|
||||
});
|
||||
}
|
||||
if (!item.link_to) {
|
||||
message += `<li>Link To in Row ${row}</li>`;
|
||||
}
|
||||
|
||||
item.label = item.label ? item.label : item.link_to;
|
||||
});
|
||||
item.label = item.label ? item.label : item.link_to;
|
||||
});
|
||||
}
|
||||
|
||||
if (message) {
|
||||
message += "</ul>";
|
||||
frappe.throw({
|
||||
message: __(message),
|
||||
title: __("Missing Values Required"),
|
||||
indicator: 'orange'
|
||||
});
|
||||
}
|
||||
|
||||
data.label = data.label ? data.label : data.chart_name;
|
||||
return data;
|
||||
|
|
|
|||
|
|
@ -343,11 +343,10 @@ textarea.form-control {
|
|||
.duration-picker {
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--shadow-sm);
|
||||
background: var(--popover-bg);
|
||||
|
||||
width: max-content;
|
||||
&:after,
|
||||
&:before {
|
||||
border: solid transparent;
|
||||
|
|
@ -466,4 +465,4 @@ button.data-pill {
|
|||
top: 0;
|
||||
right: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
font-family: inherit;
|
||||
}
|
||||
|
||||
/*rtl:begin:ignore*/
|
||||
.ql-editor {
|
||||
font-family: var(--font-stack);
|
||||
color: var(--text-color);
|
||||
|
|
@ -22,7 +23,15 @@
|
|||
a[href] {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.ql-direction-rtl {
|
||||
direction: rtl;
|
||||
+ .table {
|
||||
direction: ltr;
|
||||
}
|
||||
}
|
||||
}
|
||||
/*rtl:end:ignore*/
|
||||
|
||||
|
||||
.ql-toolbar.ql-snow {
|
||||
border-top-left-radius: var(--border-radius);
|
||||
|
|
@ -70,6 +79,7 @@
|
|||
min-height: 0;
|
||||
max-height: none;
|
||||
overflow: hidden;
|
||||
resize: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -139,6 +139,7 @@
|
|||
}
|
||||
|
||||
.kanban-cards {
|
||||
height: 100%;
|
||||
max-height: calc(100vh - 250px);
|
||||
margin: -5px;
|
||||
padding: 5px;
|
||||
|
|
@ -149,39 +150,120 @@
|
|||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.kanban-card {
|
||||
@include flex(flex, space-between, null, column);
|
||||
margin-top: var(--margin-sm);
|
||||
min-height: 100px;
|
||||
@include card(
|
||||
$padding: 0,
|
||||
$background-color: var(--kanban-card-bg)
|
||||
);
|
||||
.kanban-card-body {
|
||||
padding: var(--padding-sm);
|
||||
.kanban-card-wrapper {
|
||||
position: relative;
|
||||
display: block;
|
||||
|
||||
&:last-child .kanban-card {
|
||||
margin-bottom: var(--margin-xl);
|
||||
}
|
||||
.kanban-card {
|
||||
@include flex(flex, space-between, null, column);
|
||||
margin-top: var(--margin-sm);
|
||||
min-height: 100px;
|
||||
@include card(
|
||||
$padding: 0,
|
||||
$background-color: var(--kanban-card-bg)
|
||||
);
|
||||
|
||||
.kanban-image {
|
||||
height: 125px;
|
||||
|
||||
img {
|
||||
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
||||
object-position: top;
|
||||
object-fit: cover;
|
||||
margin: 0 auto;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
color: transparent;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@include broken-img(
|
||||
$height: 125px,
|
||||
$top: -4px,
|
||||
)
|
||||
}
|
||||
|
||||
.kanban-card-body {
|
||||
cursor: grab;
|
||||
padding: var(--padding-sm);
|
||||
|
||||
.kanban-title-area {
|
||||
margin-bottom: 12px;
|
||||
max-width: 90%;
|
||||
font-size: var(--text-md);
|
||||
font-weight: 500;
|
||||
|
||||
.kanban-card-doc {
|
||||
.text-muted div {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
.kanban-card-creation {
|
||||
font-size: var(--text-md);
|
||||
color: var(--text-muted);
|
||||
margin-top: var(--margin-xs);
|
||||
}
|
||||
}
|
||||
|
||||
.kanban-card-meta {
|
||||
|
||||
.list-comment-count {
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.like-action:not(.liked) {
|
||||
.icon use {
|
||||
stroke: var(--text-muted);
|
||||
}
|
||||
}
|
||||
|
||||
.kanban-tags {
|
||||
font-size: var(--text-sm);
|
||||
margin-bottom: 8px;
|
||||
|
||||
.tag-pill {
|
||||
border-radius: 100px;
|
||||
height: 22px;
|
||||
width: auto;
|
||||
padding: 2px 8px;
|
||||
margin-bottom: var(--margin-xs);
|
||||
margin-right: var(--margin-xs);
|
||||
}
|
||||
}
|
||||
|
||||
.kanban-assignments {
|
||||
display: flex;
|
||||
float: right;
|
||||
|
||||
.avatar {
|
||||
cursor: default;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.avatar-action {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
|
||||
.icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.kanban-card-wrapper {
|
||||
position: relative;
|
||||
|
||||
.kanban-card-redirect {
|
||||
display: block;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child .kanban-card {
|
||||
margin-bottom: var(--margin-xl);
|
||||
}
|
||||
}
|
||||
|
||||
.kanban-card:hover,
|
||||
.new-card-area,
|
||||
.edit-card-area {
|
||||
|
|
@ -189,7 +271,6 @@
|
|||
}
|
||||
|
||||
.kanban-card-wrapper:hover {
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
|
||||
.kanban-card-edit {
|
||||
|
|
@ -197,43 +278,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.kanban-title-area {
|
||||
margin-bottom: 12px;
|
||||
|
||||
.kanban-card-title {
|
||||
max-width: 90%;
|
||||
font-size: var(--text-md);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.kanban-card-creation {
|
||||
font-size: var(--text-md);
|
||||
color: var(--text-muted);
|
||||
margin-top: var(--margin-xs);
|
||||
}
|
||||
}
|
||||
|
||||
.kanban-image {
|
||||
height: 125px;
|
||||
|
||||
img {
|
||||
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
||||
object-position: top;
|
||||
object-fit: cover;
|
||||
margin: 0 auto;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
color: transparent;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@include broken-img(
|
||||
$height: 125px,
|
||||
$top: -4px,
|
||||
)
|
||||
}
|
||||
|
||||
.kanban-card-edit {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
|
|
@ -291,54 +335,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.kanban-card-meta {
|
||||
|
||||
.list-comment-count {
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.like-action:not(.liked) {
|
||||
.icon use {
|
||||
stroke: var(--text-muted);
|
||||
}
|
||||
}
|
||||
|
||||
.kanban-tags {
|
||||
font-size: var(--text-sm);
|
||||
margin-bottom: 8px;
|
||||
|
||||
.tag-pill {
|
||||
border-radius: 100px;
|
||||
height: 22px;
|
||||
width: auto;
|
||||
padding: 2px 8px;
|
||||
margin-bottom: var(--margin-xs);
|
||||
margin-right: var(--margin-xs);
|
||||
}
|
||||
}
|
||||
|
||||
.kanban-assignments {
|
||||
display: flex;
|
||||
float: right;
|
||||
|
||||
.avatar {
|
||||
cursor: default;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.avatar-action {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
|
||||
.icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.kanban-empty-state {
|
||||
width: 100%;
|
||||
line-height: 400px;
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@
|
|||
.my-account-container {
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
.account-info {
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ class EnergyPointRule(Document):
|
|||
self.apply_only_once,
|
||||
)
|
||||
except Exception as e:
|
||||
frappe.log_error(frappe.get_traceback(), "apply_energy_point")
|
||||
self.log_error("Energy points failed")
|
||||
|
||||
def rule_condition_satisfied(self, doc):
|
||||
if self.for_doc_event == "New":
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import frappe
|
|||
import frappe.utils.scheduler
|
||||
from frappe.model.naming import revert_series_if_last
|
||||
from frappe.modules import get_module_name, load_doctype_module
|
||||
from frappe.utils import cint
|
||||
|
||||
unittest_runner = unittest.TextTestRunner
|
||||
SLOW_TEST_THRESHOLD = 2
|
||||
|
|
@ -177,10 +178,13 @@ def run_all_tests(
|
|||
_add_test(app, path, filename, verbose, test_suite, ui_tests)
|
||||
|
||||
if junit_xml_output:
|
||||
runner = unittest_runner(verbosity=1 + (verbose and 1 or 0), failfast=failfast)
|
||||
runner = unittest_runner(verbosity=1 + cint(verbose), failfast=failfast)
|
||||
else:
|
||||
runner = unittest_runner(
|
||||
resultclass=TimeLoggingTestResult, verbosity=1 + (verbose and 1 or 0), failfast=failfast
|
||||
resultclass=TimeLoggingTestResult,
|
||||
verbosity=1 + cint(verbose),
|
||||
failfast=failfast,
|
||||
tb_locals=verbose,
|
||||
)
|
||||
|
||||
if profile:
|
||||
|
|
@ -279,10 +283,13 @@ def _run_unittest(
|
|||
test_suite.addTest(module_test_cases)
|
||||
|
||||
if junit_xml_output:
|
||||
runner = unittest_runner(verbosity=1 + (verbose and 1 or 0), failfast=failfast)
|
||||
runner = unittest_runner(verbosity=1 + cint(verbose), failfast=failfast)
|
||||
else:
|
||||
runner = unittest_runner(
|
||||
resultclass=TimeLoggingTestResult, verbosity=1 + (verbose and 1 or 0), failfast=failfast
|
||||
resultclass=TimeLoggingTestResult,
|
||||
verbosity=1 + cint(verbose),
|
||||
failfast=failfast,
|
||||
tb_locals=verbose,
|
||||
)
|
||||
|
||||
if profile:
|
||||
|
|
|
|||
|
|
@ -141,3 +141,40 @@ class TestClient(unittest.TestCase):
|
|||
self.assertEqual(get("ToDo", filters=filters_json).description, "test")
|
||||
|
||||
todo.delete()
|
||||
|
||||
def test_client_insert(self):
|
||||
from frappe.client import insert
|
||||
|
||||
def get_random_title():
|
||||
return "test-{0}".format(frappe.generate_hash())
|
||||
|
||||
# test insert dict
|
||||
doc = {"doctype": "Note", "title": get_random_title(), "content": "test"}
|
||||
note1 = insert(doc)
|
||||
self.assertTrue(note1)
|
||||
|
||||
# test insert json
|
||||
doc["title"] = get_random_title()
|
||||
json_doc = frappe.as_json(doc)
|
||||
note2 = insert(json_doc)
|
||||
self.assertTrue(note2)
|
||||
|
||||
# test insert child doc without parent fields
|
||||
child_doc = {"doctype": "Note Seen By", "user": "Administrator"}
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
insert(child_doc)
|
||||
|
||||
# test insert child doc with parent fields
|
||||
child_doc = {
|
||||
"doctype": "Note Seen By",
|
||||
"user": "Administrator",
|
||||
"parenttype": "Note",
|
||||
"parent": note1.name,
|
||||
"parentfield": "seen_by",
|
||||
}
|
||||
note3 = insert(child_doc)
|
||||
self.assertTrue(note3)
|
||||
|
||||
# cleanup
|
||||
frappe.delete_doc("Note", note1.name)
|
||||
frappe.delete_doc("Note", note2.name)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import datetime
|
||||
import inspect
|
||||
import unittest
|
||||
from math import ceil
|
||||
from random import choice
|
||||
from unittest.mock import patch
|
||||
|
||||
|
|
@ -445,6 +446,33 @@ class TestDB(unittest.TestCase):
|
|||
|
||||
self.assertEqual(frappe.db.exists(dt, [["name", "=", dn]]), dn)
|
||||
|
||||
def test_bulk_insert(self):
|
||||
current_count = frappe.db.count("ToDo")
|
||||
test_body = f"test_bulk_insert - {random_string(10)}"
|
||||
chunk_size = 10
|
||||
|
||||
for number_of_values in (1, 2, 5, 27):
|
||||
current_transaction_writes = frappe.db.transaction_writes
|
||||
|
||||
frappe.db.bulk_insert(
|
||||
"ToDo",
|
||||
["name", "description"],
|
||||
[[f"ToDo Test Bulk Insert {i}", test_body] for i in range(number_of_values)],
|
||||
ignore_duplicates=True,
|
||||
chunk_size=chunk_size,
|
||||
)
|
||||
|
||||
# check that all records were inserted
|
||||
self.assertEqual(number_of_values, frappe.db.count("ToDo") - current_count)
|
||||
|
||||
# check if inserts were done in chunks
|
||||
expected_number_of_writes = ceil(number_of_values / chunk_size)
|
||||
self.assertEqual(
|
||||
expected_number_of_writes, frappe.db.transaction_writes - current_transaction_writes
|
||||
)
|
||||
|
||||
frappe.db.delete("ToDo", {"description": test_body})
|
||||
|
||||
|
||||
@run_only_if(db_type_is.MARIADB)
|
||||
class TestDDLCommandsMaria(unittest.TestCase):
|
||||
|
|
|
|||
|
|
@ -137,12 +137,10 @@ def create_contact_records():
|
|||
|
||||
@frappe.whitelist()
|
||||
def create_multiple_todo_records():
|
||||
values = []
|
||||
if frappe.db.get_all("ToDo", {"description": "Multiple ToDo 1"}):
|
||||
return
|
||||
|
||||
for index in range(1, 1002):
|
||||
values.append(("100{}".format(index), "Multiple ToDo {}".format(index)))
|
||||
values = [("100{}".format(i), "Multiple ToDo {}".format(i)) for i in range(1, 1002)]
|
||||
|
||||
frappe.db.bulk_insert("ToDo", fields=["name", "description"], values=set(values))
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ from typing import Generator, Iterable
|
|||
from urllib.parse import quote, urlparse
|
||||
|
||||
from redis.exceptions import ConnectionError
|
||||
from traceback_with_variables import iter_exc_lines
|
||||
from werkzeug.test import Client
|
||||
|
||||
import frappe
|
||||
|
|
@ -255,7 +256,7 @@ def get_gravatar(email):
|
|||
return gravatar_url
|
||||
|
||||
|
||||
def get_traceback() -> str:
|
||||
def get_traceback(with_context=False) -> str:
|
||||
"""
|
||||
Returns the traceback of the Exception
|
||||
"""
|
||||
|
|
@ -264,14 +265,19 @@ def get_traceback() -> str:
|
|||
if not any([exc_type, exc_value, exc_tb]):
|
||||
return ""
|
||||
|
||||
trace_list = traceback.format_exception(exc_type, exc_value, exc_tb)
|
||||
bench_path = get_bench_path() + "/"
|
||||
if with_context:
|
||||
trace_list = iter_exc_lines()
|
||||
tb = "\n".join(trace_list)
|
||||
else:
|
||||
trace_list = traceback.format_exception(exc_type, exc_value, exc_tb)
|
||||
tb = "".join(cstr(t) for t in trace_list)
|
||||
|
||||
return "".join(cstr(t) for t in trace_list).replace(bench_path, "")
|
||||
bench_path = get_bench_path() + "/"
|
||||
return tb.replace(bench_path, "")
|
||||
|
||||
|
||||
def log(event, details):
|
||||
frappe.logger().info(details)
|
||||
frappe.logger(event).info(details)
|
||||
|
||||
|
||||
def dict_to_str(args, sep="&"):
|
||||
|
|
|
|||
|
|
@ -92,7 +92,12 @@ def download_multi_pdf(doctype, name, format=None, no_letterhead=False, options=
|
|||
pdf_options=options,
|
||||
)
|
||||
except Exception:
|
||||
frappe.log_error("Permission Error on doc {} of doctype {}".format(doc_name, doctype_name))
|
||||
frappe.log_error(
|
||||
title="Error in Multi PDF download",
|
||||
message="Permission Error on doc {} of doctype {}".format(doc_name, doctype_name),
|
||||
reference_doctype=doctype_name,
|
||||
reference_name=doc_name,
|
||||
)
|
||||
frappe.local.response.filename = "{}.pdf".format(name)
|
||||
|
||||
frappe.local.response.filecontent = read_multi_pdf(output)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ import frappe.utils.data
|
|||
from frappe import _
|
||||
from frappe.frappeclient import FrappeClient
|
||||
from frappe.handler import execute_cmd
|
||||
from frappe.model.delete_doc import delete_doc
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.model.rename_doc import rename_doc
|
||||
from frappe.modules import scrub
|
||||
from frappe.utils.background_jobs import enqueue, get_jobs
|
||||
from frappe.website.utils import get_next_link, get_shade, get_toc
|
||||
|
|
@ -110,12 +113,16 @@ def get_safe_globals():
|
|||
errprint=frappe.errprint,
|
||||
qb=frappe.qb,
|
||||
get_meta=frappe.get_meta,
|
||||
new_doc=frappe.new_doc,
|
||||
get_doc=frappe.get_doc,
|
||||
get_mapped_doc=get_mapped_doc,
|
||||
get_last_doc=frappe.get_last_doc,
|
||||
get_cached_doc=frappe.get_cached_doc,
|
||||
get_list=frappe.get_list,
|
||||
get_all=frappe.get_all,
|
||||
get_system_settings=frappe.get_system_settings,
|
||||
rename_doc=frappe.rename_doc,
|
||||
rename_doc=rename_doc,
|
||||
delete_doc=delete_doc,
|
||||
utils=datautils,
|
||||
get_url=frappe.utils.get_url,
|
||||
render_template=frappe.render_template,
|
||||
|
|
|
|||
|
|
@ -70,6 +70,8 @@ data-web-form="{{ name }}" data-web-form-doctype="{{ doc_type }}" data-login-req
|
|||
<div class="comments" style="margin-top: 3rem;">
|
||||
{% include 'templates/includes/comments/comments.html' %}
|
||||
</div>
|
||||
{%- else -%}
|
||||
<div style="height: 3rem"></div>
|
||||
{%- endif %} {# comments #}
|
||||
|
||||
{% endblock page_content %}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ def get_response(path=None, http_status_code=200):
|
|||
except frappe.PermissionError as e:
|
||||
response = NotPermittedPage(endpoint, http_status_code, exception=e).render()
|
||||
except Exception as e:
|
||||
frappe.log_error(f"{path} failed")
|
||||
response = ErrorPage(exception=e).render()
|
||||
|
||||
return response
|
||||
|
|
|
|||
|
|
@ -63,10 +63,11 @@ semantic-version~=2.8.5
|
|||
sqlparse~=0.4.1
|
||||
stripe~=2.56.0
|
||||
terminaltables~=3.1.0
|
||||
traceback-with-variables~=2.0.4
|
||||
urllib3~=1.26.4
|
||||
Werkzeug~=2.0.3
|
||||
Whoosh~=2.7.4
|
||||
wrapt~=1.12.1
|
||||
wrapt~=1.14.0
|
||||
xlrd~=2.0.1
|
||||
zxcvbn-python~=4.4.24
|
||||
tenacity~=8.0.1
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue