Merge branch 'develop' of https://github.com/frappe/frappe into fix-document-signature

This commit is contained in:
Suraj Shetty 2022-05-25 14:18:10 +05:30
commit 0fab4de3b8
230 changed files with 3708 additions and 1609 deletions

76
.github/helper/flake8.conf vendored Normal file
View file

@ -0,0 +1,76 @@
[flake8]
ignore =
B001,
B007,
B009,
B010,
B950,
E101,
E111,
E114,
E116,
E117,
E121,
E122,
E123,
E124,
E125,
E126,
E127,
E128,
E131,
E201,
E202,
E203,
E211,
E221,
E222,
E223,
E224,
E225,
E226,
E228,
E231,
E241,
E242,
E251,
E261,
E262,
E265,
E266,
E271,
E272,
E273,
E274,
E301,
E302,
E303,
E305,
E306,
E402,
E501,
E502,
E701,
E702,
E703,
E741,
F401,
F403,
F405,
W191,
W291,
W292,
W293,
W391,
W503,
W504,
E711,
E129,
F841,
E713,
E712,
E722,
max-line-length = 200
exclude=.github/helper/semgrep_rules,test_*.py

View file

@ -7,17 +7,27 @@ import sys
import urllib.request
def get_files_list(pr_number, repo="frappe/frappe"):
req = urllib.request.Request(f"https://api.github.com/repos/{repo}/pulls/{pr_number}/files")
def fetch_pr_data(pr_number, repo, endpoint):
api_url = f"https://api.github.com/repos/{repo}/pulls/{pr_number}"
if endpoint:
api_url += f"/{endpoint}"
req = urllib.request.Request(api_url)
res = urllib.request.urlopen(req)
dump = json.loads(res.read().decode('utf8'))
return [change["filename"] for change in dump]
return json.loads(res.read().decode('utf8'))
def get_files_list(pr_number, repo="frappe/frappe"):
return [change["filename"] for change in fetch_pr_data(pr_number, repo, "files")]
def get_output(command, shell=True):
print(command)
command = shlex.split(command)
return subprocess.check_output(command, shell=shell, encoding="utf8").strip()
def has_skip_ci_label(pr_number, repo="frappe/frappe"):
return any([label["name"] for label in fetch_pr_data(pr_number, repo, "")["labels"] if label["name"] == "Skip CI"])
def is_py(file):
return file.endswith("py")
@ -59,6 +69,10 @@ if __name__ == "__main__":
if ci_files_changed:
print("CI related files were updated, running all build processes.")
elif has_skip_ci_label(pr_number, repo):
print("Found `Skip CI` label on pr, stopping build process.")
sys.exit(0)
elif only_docs_changed:
print("Only docs were updated, stopping build process.")
sys.exit(0)
@ -67,12 +81,8 @@ if __name__ == "__main__":
print("Only Frontend code was updated; Stopping Python build process.")
sys.exit(0)
elif build_type == "ui":
if only_py_changed:
print("Only Python code was updated, stopping Cypress build process.")
sys.exit(0)
elif updated_py_file_count > 0:
# both frontend and backend code were updated
os.system('echo "::set-output name=build-server::strawberry"')
elif build_type == "ui" and only_py_changed:
print("Only Python code was updated, stopping Cypress build process.")
sys.exit(0)
os.system('echo "::set-output name=build::strawberry"')

View file

@ -6,6 +6,7 @@ pull_request_rules:
- author!=surajshetty3416
- author!=gavindsouza
- author!=deepeshgarg007
- author!=ankush
- or:
- base=version-13
- base=version-12
@ -13,7 +14,7 @@ pull_request_rules:
close:
comment:
message: |
@{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on an appropriate hotfix branch.
@{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on an appropriate hotfix branch.
https://github.com/frappe/erpnext/wiki/Pull-Request-Checklist#which-branch
- name: Automatic merge on CI success and review
@ -53,7 +54,7 @@ pull_request_rules:
{{ title }} (#{{ number }})
{{ body }}
- name: backport to develop
conditions:
- label="backport develop"
@ -92,4 +93,4 @@ pull_request_rules:
branches:
- version-12-hotfix
assignees:
- "{{ author }}"
- "{{ author }}"

View file

@ -26,7 +26,15 @@ repos:
rev: 5.9.1
hooks:
- id: isort
exclude: ".*setup.py$"
- repo: https://gitlab.com/pycqa/flake8
rev: 3.9.2
hooks:
- id: flake8
additional_dependencies: [
'flake8-bugbear',
]
args: ['--config', '.github/helper/flake8.conf']
ci:
autoupdate_schedule: weekly

View file

@ -0,0 +1,77 @@
context('Control Color', () => {
before(() => {
cy.login();
cy.visit('/app/website');
});
function get_dialog_with_color() {
return cy.dialog({
title: 'Color',
fields: [{
label: 'Color',
fieldname: 'color',
fieldtype: 'Color'
}]
});
}
it('Verifying if the color control is selecting correct', () => {
get_dialog_with_color().as('dialog');
cy.findByPlaceholderText('Choose a color').click();
///Selecting a color from the color palette
cy.get('[style="background-color: rgb(79, 157, 217);"]').click();
//Checking if the css attribute is correct
cy.get('.color-map').should('have.css', 'color', 'rgb(79, 157, 217)');
cy.get('.hue-map').should('have.css', 'color', 'rgb(0, 145, 255)');
//Checking if the correct color is being selected
cy.get('@dialog').then(dialog => {
let value = dialog.get_value('color');
expect(value).to.equal('#4F9DD9');
});
//Selecting a color
cy.get('[style="background-color: rgb(203, 41, 41);"]').click();
//Checking if the correct css is being selected
cy.get('.color-map').should('have.css', 'color', 'rgb(203, 41, 41)');
cy.get('.hue-map').should('have.css', 'color', 'rgb(255, 0, 0)');
//Checking if the correct color is being selected
cy.get('@dialog').then(dialog => {
let value = dialog.get_value('color');
expect(value).to.equal('#CB2929');
});
//Selecting color from the palette
cy.get('.color-map > .color-selector').click(65, 87, {force: true});
cy.get('.color-map').should('have.css', 'color', 'rgb(56, 0, 0)');
//Checking if the expected color is selected and getting displayed
cy.get('@dialog').then(dialog => {
let value = dialog.get_value('color');
expect(value).to.equal('#380000');
});
//Selecting the color from the hue map
cy.get('.hue-map > .hue-selector').click(35, -1, {force: true});
cy.get('.color-map').should('have.css', 'color', 'rgb(56, 45, 0)');
cy.get('.hue-map').should('have.css', 'color', 'rgb(255, 204, 0)');
cy.get('.color-map > .color-selector').click(55, 12, {force: true});
cy.get('.color-map').should('have.css', 'color', 'rgb(46, 37, 0)');
//Checking if the correct color is being displayed
cy.get('@dialog').then(dialog => {
let value = dialog.get_value('color');
expect(value).to.equal('#2e2500');
});
//Clearing the field and checking if the field contains the placeholder "Choose a color"
cy.get('.input-with-feedback').click({force: true});
cy.get_field('color', 'Color').type('{selectall}').clear();
cy.get_field('color', 'Color').invoke('attr', 'placeholder').should('contain', 'Choose a color');
});
});

View file

@ -59,7 +59,7 @@ context('Data Control', () => {
//Checking for the error message
cy.get('.modal-title').should('have.text', 'Message');
cy.get('.msgprint').should('have.text', '@@### is not a valid Name');
cy.get('.modal').type('{esc}');
cy.hide_dialog();
cy.get_field('name1', 'Data').clear({force: true});
cy.fill_field('name1', 'Komal{}/!', 'Data');
@ -67,10 +67,10 @@ context('Data Control', () => {
cy.findByRole('button', {name: 'Save'}).click();
cy.get('.modal-title').should('have.text', 'Message');
cy.get('.msgprint').should('have.text', 'Komal{}/! is not a valid Name');
cy.hide_dialog();
});
it('Verifying data control by inputting different patterns for "Email" field', () => {
cy.get('.modal-actions > .btn-modal-close').trigger("click");
cy.get_field('name1', 'Data').clear({force: true});
cy.fill_field('name1', 'Komal', 'Data');
cy.get_field('email', 'Data').clear({force: true});
@ -79,17 +79,17 @@ context('Data Control', () => {
cy.findByRole('button', {name: 'Save'}).click();
cy.get('.modal-title').should('have.text', 'Message');
cy.get('.msgprint').should('have.text', 'komal is not a valid Email Address');
cy.get('.modal-actions > .btn-modal-close').trigger("click");
cy.hide_dialog();
cy.get_field('email', 'Data').clear({force: true});
cy.fill_field('email', 'komal@test', 'Data');
cy.get('.frappe-control[data-fieldname="email"]').should('have.class', 'has-error');
cy.findByRole('button', {name: 'Save'}).click();
cy.get('.modal-title').should('have.text', 'Message');
cy.get('.msgprint').should('have.text', 'komal@test is not a valid Email Address');
cy.hide_dialog();
});
it('Verifying data control by inputting different patterns for "Phone" field', () => {
cy.get('.modal-actions > .btn-modal-close').trigger("click");
cy.get_field('email', 'Data').clear({force: true});
cy.fill_field('email', 'komal@test.com', 'Data');
cy.get_field('phone', 'Data').clear({force: true});
@ -98,7 +98,7 @@ context('Data Control', () => {
cy.findByRole('button', {name: 'Save'}).click({force: true});
cy.get('.modal-title').should('have.text', 'Message');
cy.get('.msgprint').should('have.text', 'komal is not a valid Phone Number');
cy.get('.modal-actions > .btn-modal-close').trigger("click");
cy.hide_dialog();
});
it('Inputting correct data and saving the doc', () => {
@ -124,6 +124,5 @@ context('Data Control', () => {
cy.get('.actions-btn-group > .btn').contains('Actions').click();
cy.get('.actions-btn-group > .dropdown-menu [data-label="Delete"]').click();
cy.click_modal_primary_button('Yes');
cy.get('.btn-modal-close').click();
});
});

View file

@ -20,7 +20,21 @@ context('Control Link', () => {
'label': 'Select ToDo',
'fieldname': 'link',
'fieldtype': 'Link',
'options': 'ToDo'
'options': 'ToDo',
}
]
});
}
function get_dialog_with_user_link() {
return cy.dialog({
title: 'Link',
fields: [
{
'label': 'Select User',
'fieldname': 'link',
'fieldtype': 'Link',
'options': 'User',
}
]
});
@ -29,6 +43,24 @@ context('Control Link', () => {
it('should set the valid value', () => {
get_dialog_with_link().as('dialog');
cy.insert_doc("Property Setter", {
"doctype": "Property Setter",
"doc_type": "User",
"property": "translate_link_fields",
"property_type": "Check",
"doctype_or_field": "DocType",
"value": "0"
}, true);
cy.insert_doc("Property Setter", {
"doctype": "Property Setter",
"doc_type": "ToDo",
"property": "show_title_field_in_link",
"property_type": "Check",
"doctype_or_field": "DocType",
"value": "0"
}, true);
cy.intercept('POST', '/api/method/frappe.desk.search.search_link').as('search_link');
cy.get('.frappe-control[data-fieldname=link] input').focus().as('input');
@ -88,7 +120,8 @@ context('Control Link', () => {
cy.get('@input').type(todos[0]).blur();
cy.wait('@validate_link');
cy.get('@input').focus();
cy.findByTitle('Open Link')
cy.wait(500); // wait for arrow to show
cy.get('.frappe-control[data-fieldname=link] .btn-open')
.should('be.visible')
.click();
cy.location('pathname').should('eq', `/app/todo/${todos[0]}`);
@ -96,7 +129,15 @@ context('Control Link', () => {
});
it('show title field in link', () => {
get_dialog_with_link().as('dialog');
cy.insert_doc("Property Setter", {
"doctype": "Property Setter",
"doc_type": "User",
"property": "translate_link_fields",
"property_type": "Check",
"doctype_or_field": "DocType",
"value": "0"
}, true);
cy.insert_doc("Property Setter", {
"doctype": "Property Setter",
@ -107,6 +148,10 @@ context('Control Link', () => {
"value": "1"
}, true);
cy.clear_cache();
cy.wait(500);
get_dialog_with_link().as('dialog');
cy.window().its('frappe').then(frappe => {
if (!frappe.boot) {
frappe.boot = {
@ -134,8 +179,6 @@ context('Control Link', () => {
expect(value).to.eq(todos[0]);
expect(label).to.eq('this is a test todo for link');
cy.remove_doc("Property Setter", "ToDo-main-show_title_field_in_link");
});
});
});
@ -143,6 +186,7 @@ context('Control Link', () => {
it('should update dependant fields (via fetch_from)', () => {
cy.get('@todos').then(todos => {
cy.visit(`/app/todo/${todos[0]}`);
cy.intercept('POST', '/api/method/frappe.desk.search.search_link').as('search_link');
cy.intercept('POST', '/api/method/frappe.client.validate_link').as('validate_link');
cy.get('.frappe-control[data-fieldname=assigned_by] input').focus().as('input');
@ -167,7 +211,9 @@ context('Control Link', () => {
.should("eq", null);
// set valid value again
cy.get('@input').clear().type('Administrator', {delay: 100}).blur();
cy.get('@input').clear().focus();
cy.wait('@search_link');
cy.get('@input').type('Administrator', {delay: 100}).blur();
cy.wait('@validate_link');
cy.window()
@ -214,4 +260,130 @@ context('Control Link', () => {
"contain", ""
);
});
it('show translated text for link with show_title_field_in_link enabled', () => {
cy.insert_doc("Property Setter", {
"doctype": "Property Setter",
"doc_type": "ToDo",
"property": "translate_link_fields",
"property_type": "Check",
"doctype_or_field": "DocType",
"value": "1"
}, true);
cy.insert_doc("Property Setter", {
"doctype": "Property Setter",
"doc_type": "ToDo",
"property": "show_title_field_in_link",
"property_type": "Check",
"doctype_or_field": "DocType",
"value": "1"
}, true);
cy.window().its('frappe').then(frappe => {
cy.insert_doc("Translation", {
doctype: "Translation",
language: frappe.boot.lang,
source_text: "this is a test todo for link",
translated_text: "this is a translated test todo for link",
});
});
cy.clear_cache();
cy.wait(500);
cy.window().its('frappe').then(frappe => {
if (!frappe.boot) {
frappe.boot = {
link_title_doctypes: ['ToDo'],
translatable_doctypes: ['ToDo']
};
} else {
frappe.boot.link_title_doctypes = ['ToDo'];
frappe.boot.translatable_doctypes = ['ToDo'];
}
});
get_dialog_with_link().as('dialog');
cy.intercept('POST', '/api/method/frappe.desk.search.search_link').as('search_link');
cy.get('.frappe-control[data-fieldname=link] input').focus().as('input');
cy.wait('@search_link');
cy.get('@input').type('todo for link', { delay: 100 });
cy.wait('@search_link');
cy.get('.frappe-control[data-fieldname=link] ul').should('be.visible');
cy.get('.frappe-control[data-fieldname=link] input').type('{enter}', { delay: 100 });
cy.get('.frappe-control[data-fieldname=link] input').blur();
cy.get('@dialog').then(dialog => {
cy.get('@todos').then(todos => {
let field = dialog.get_field('link');
let value = field.get_value();
let label = field.get_label_value();
expect(value).to.eq(todos[0]);
expect(label).to.eq('this is a translated test todo for link');
});
});
});
it('show translated text for link with show_title_field_in_link disabled', () => {
cy.insert_doc("Property Setter", {
"doctype": "Property Setter",
"doc_type": "User",
"property": "translate_link_fields",
"property_type": "Check",
"doctype_or_field": "DocType",
"value": "1"
}, true);
cy.insert_doc("Property Setter", {
"doctype": "Property Setter",
"doc_type": "ToDo",
"property": "show_title_field_in_link",
"property_type": "Check",
"doctype_or_field": "DocType",
"value": "0"
}, true);
cy.window().its('frappe').then(frappe => {
cy.insert_doc("Translation", {
doctype: "Translation",
language: frappe.boot.lang,
source_text: "test@erpnext.com",
translated_text: "translatedtest@erpnext.com",
});
});
cy.clear_cache();
cy.wait(500);
cy.window().its('frappe').then(frappe => {
if (!frappe.boot) {
frappe.boot = {
translatable_doctypes: ['User']
};
} else {
frappe.boot.translatable_doctypes = ['User'];
}
});
get_dialog_with_user_link().as('dialog');
cy.intercept('POST', '/api/method/frappe.desk.search.search_link').as('search_link');
cy.get('.frappe-control[data-fieldname=link] input').focus().as('input');
cy.wait('@search_link');
cy.get('@input').type('test@erpnext.com', { delay: 100 });
cy.wait('@search_link');
cy.get('.frappe-control[data-fieldname=link] ul').should('be.visible');
cy.get('.frappe-control[data-fieldname=link] input').type('{enter}', { delay: 100 });
cy.get('.frappe-control[data-fieldname=link] input').blur();
cy.get('@dialog').then(dialog => {
let field = dialog.get_field('link');
let value = field.get_value();
let label = field.get_label_value();
expect(value).to.eq('test@erpnext.com');
expect(label).to.eq('translatedtest@erpnext.com');
});
});
});

View file

@ -0,0 +1,59 @@
const test_button_names = [
"Metallica",
"Pink Floyd",
"Porcupine Tree (the GOAT)",
"AC / DC",
`Electronic Dance "music"`,
];
const add_button = (label, group = "TestGroup") => {
cy.window()
.its("cur_frm")
.then((frm) => {
frm.add_custom_button(label, () => {}, group);
});
};
const check_button_count = (label, group = "TestGroup") => {
// Verify main buttons
cy.findByRole("button", { name: group }).click();
cy.get(`[data-label="${encodeURIComponent(label)}"]`)
.should("have.length", 1)
.should("be.visible");
// Verify dropdown buttons in mobile view
cy.viewport(420, 900);
const dropdown_btn_label = `${group} > ${label}`;
cy.get(".menu-btn-group > .btn").click();
cy.get(`[data-label="${encodeURIComponent(dropdown_btn_label)}"]`)
.should("have.length", 1)
.should("be.visible");
//reset viewport
cy.viewport(
Cypress.config("viewportWidth"),
Cypress.config("viewportHeight")
);
};
describe(
"Custom group button behaviour on desk",
{ scrollBehavior: false }, // speeds up the test
() => {
before(() => {
cy.login();
cy.visit(`/app/note/new`);
});
test_button_names.forEach((button_name) => {
it(`Custom button works with name '${button_name}'`, () => {
add_button(button_name);
check_button_count(button_name);
// duplicate button shouldn't be added
add_button(button_name);
check_button_count(button_name);
});
});
}
);

View file

@ -15,10 +15,9 @@ context('Folder Navigation', () => {
cy.get('.filter-action-buttons > div > .btn-primary').findByText('Apply Filters').click();
//Adding folder (Test Folder)
cy.get('.menu-btn-group > .btn').click();
cy.get('.menu-btn-group [data-label="New Folder"]').click();
cy.get('form > [data-fieldname="value"]').type('Test Folder');
cy.findByRole('button', {name: 'Create'}).click();
cy.click_menu_button("New Folder");
cy.fill_field('value', 'Test Folder');
cy.click_modal_primary_button('Create');
});
it('Navigating the nested folders, checking if the URL formed is correct, checking if the added content in the child folder is correct', () => {
@ -30,10 +29,9 @@ context('Folder Navigation', () => {
cy.visit('/app/file/view/home/Attachments');
//Adding folder inside the attachments folder
cy.get('.menu-btn-group > .btn').click();
cy.get('.menu-btn-group [data-label="New Folder"]').click();
cy.get('form > [data-fieldname="value"]').type('Test Folder');
cy.findByRole('button', {name: 'Create'}).click();
cy.click_menu_button("New Folder");
cy.fill_field('value', 'Test Folder');
cy.click_modal_primary_button('Create');
//Navigating inside the added folder in the Attachments folder
cy.get('[title="Test Folder"] > span').click();
@ -46,34 +44,36 @@ context('Folder Navigation', () => {
cy.findByRole('button', {name: 'Add File'}).eq(0).click({force: true});
cy.get('.file-uploader').findByText('Link').click();
cy.get('.input-group > .form-control').type('https://wallpaperplay.com/walls/full/8/2/b/72402.jpg');
cy.findByRole('button', {name: 'Upload'}).click();
cy.click_modal_primary_button('Upload');
//To check if the added file is present in the Test Folder
cy.get('span.level-item > span').should('contain', 'Test Folder');
cy.get('.list-row-container').eq(0).should('contain.text', '72402.jpg');
cy.get('.list-row-checkbox').eq(0).click();
cy.intercept({
method: 'POST',
url: 'api/method/frappe.desk.reportview.delete_items'
}).as('file_deleted');
//Deleting the added file from the Test folder
cy.findByRole('button', {name: 'Actions'}).click();
cy.get('.actions-btn-group [data-label="Delete"]').click();
cy.wait(700);
cy.findByRole('button', {name: 'Yes'}).click();
cy.wait(700);
cy.click_action_button("Delete");
cy.click_modal_primary_button('Yes');
cy.wait('@file_deleted');
//Deleting the Test Folder
cy.visit('/app/file/view/home/Attachments');
cy.get('.list-row-checkbox').eq(0).click();
cy.findByRole('button', {name: 'Actions'}).click();
cy.get('.actions-btn-group [data-label="Delete"]').click();
cy.findByRole('button', {name: 'Yes'}).click();
cy.click_action_button("Delete");
cy.click_modal_primary_button('Yes');
cy.wait('@file_deleted');
});
it('Deleting Test Folder from the home', () => {
//Deleting the Test Folder added in the home directory
cy.visit('/app/file/view/home');
cy.get('.level-left > .list-subject > .file-select >.list-row-checkbox').eq(0).click({force: true, delay: 500});
cy.findByRole('button', {name: 'Actions'}).click();
cy.get('.actions-btn-group [data-label="Delete"]').click();
cy.findByRole('button', {name: 'Yes'}).click();
cy.click_action_button("Delete");
cy.click_modal_primary_button('Yes');
});
});
});

View file

@ -1,7 +1,7 @@
context('Form Tour', () => {
context.skip('Form Tour', () => {
before(() => {
cy.login();
cy.visit('/app/form-tour');
cy.visit('/app');
return cy.window().its('frappe').then(frappe => {
return frappe.call("frappe.tests.ui_test_helpers.create_form_tour");
});

View file

@ -26,7 +26,7 @@ context('Kanban Board', () => {
cy.click_listview_primary_button('Add ToDo');
cy.fill_field('description', 'Test Kanban ToDo', 'Text Editor');
cy.fill_field('description', 'Test Kanban ToDo', 'Text Editor').wait(300);
cy.get('.modal-footer .btn-primary').last().click();
cy.wait('@save-todo');

View file

@ -48,4 +48,4 @@ context('Table MultiSelect', () => {
cy.get('@existing_value').find('.btn-link-to-form').click();
cy.location('pathname').should('contain', '/user/test@erpnext.com');
});
});
});

View file

@ -78,12 +78,5 @@ context('Timeline', () => {
cy.get('.page-actions').findByRole('button', {name: 'Actions'}).click();
cy.get('.page-actions .actions-btn-group [data-label="Delete"]').click();
cy.click_modal_primary_button('Yes');
//Deleting the custom doctype
cy.visit('/app/doctype');
cy.select_listview_row_checkbox(0);
cy.get('.page-actions').findByRole('button', {name: 'Actions'}).click();
cy.get('.page-actions .actions-btn-group [data-label="Delete"]').click();
cy.click_modal_primary_button('Yes');
});
});

View file

@ -0,0 +1,140 @@
context('Workspace Blocks', () => {
before(() => {
cy.login();
cy.visit('/app');
return cy.window().its('frappe').then(frappe => {
return frappe.xcall("frappe.tests.ui_test_helpers.setup_workflow");
});
});
it('Create Test Page', () => {
cy.intercept({
method: 'POST',
url: 'api/method/frappe.desk.doctype.workspace.workspace.new_page'
}).as('new_page');
cy.visit('/app/website');
cy.get('.codex-editor__redactor .ce-block');
cy.get('.custom-actions button[data-label="Create%20Workspace"]').click();
cy.fill_field('title', 'Test Block Page', 'Data');
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 private section
cy.get('.sidebar-item-container[item-name="Test Block 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 Block Page"]').should('have.attr', 'item-public', '0');
cy.wait('@new_page');
});
it('Quick List Block', () => {
cy.create_records([
{
doctype: 'ToDo',
description: 'Quick List ToDo 1',
status: 'Open'
},
{
doctype: 'ToDo',
description: 'Quick List ToDo 2',
status: 'Open'
},
{
doctype: 'ToDo',
description: 'Quick List ToDo 3',
status: 'Open'
},
{
doctype: 'ToDo',
description: 'Quick List ToDo 4',
status: 'Open'
}
]);
cy.intercept({
method: 'GET',
url: 'api/method/frappe.desk.form.load.getdoctype'
}).as('get_doctype');
cy.get('.codex-editor__redactor .ce-block');
cy.get('.standard-actions .btn-secondary[data-label=Edit]').click();
// test quick list creation
cy.get('.ce-block').first().click({force: true}).type('{enter}');
cy.get('.block-list-container .block-list-item').contains('Quick List').click();
cy.get_open_dialog().find('.modal-header').click();
cy.fill_field('document_type', 'ToDo', 'Link').blur();
cy.fill_field('label', 'ToDo', 'Data').blur();
cy.wait('@get_doctype');
cy.get_open_dialog().find('.filter-edit-area').should('contain', 'No filters selected');
cy.get_open_dialog().find('.filter-area .add-filter').click();
cy.get_open_dialog().find('.fieldname-select-area input').type('Workflow State{enter}').blur();
cy.get_open_dialog().find('.filter-field .input-with-feedback').type('Pending');
cy.get_open_dialog().find('.modal-header').click();
cy.get_open_dialog().find('.btn-primary').click();
cy.get('.standard-actions .btn-primary[data-label="Save"]').click();
cy.get('.codex-editor__redactor .ce-block');
cy.get('.ce-block .quick-list-widget-box').first().as('todo-quick-list');
cy.get('@todo-quick-list').find('.quick-list-item .status').should('contain', 'Pending');
// test quick-list-item
cy.get('@todo-quick-list').find('.quick-list-item .title')
.first()
.invoke('attr', 'title')
.then(title => {
cy.get('@todo-quick-list').find('.quick-list-item').contains(title).click();
cy.get_field('description', 'Text Editor').should('contain', title);
cy.click_action_button('Approve');
});
cy.go('back');
// test filter-list
cy.get('@todo-quick-list').realHover().find('.widget-control .filter-list').click();
cy.get_open_dialog().find('.filter-field .input-with-feedback').clear().type('Approved');
cy.get_open_dialog().find('.modal-header').click();
cy.get_open_dialog().find('.btn-primary').click();
cy.get('@todo-quick-list').find('.quick-list-item .status').should('contain', 'Approved');
// test refresh-list
cy.intercept({
method: 'POST',
url: 'api/method/frappe.desk.reportview.get'
}).as('refresh-list');
cy.get('@todo-quick-list').realHover().find('.widget-control .refresh-list').click();
cy.wait('@refresh-list');
// test add-new
cy.get('@todo-quick-list').realHover().find('.widget-control .add-new').click();
cy.url().should('include', `/todo/new-todo-1`);
cy.go('back');
// test see-all
cy.get('@todo-quick-list').find('.widget-footer .see-all').click();
cy.open_list_filter();
cy.get('.filter-field input[data-fieldname="workflow_state"]')
.invoke('val')
.should('eq', 'Pending');
cy.go('back');
});
});

View file

@ -1,6 +1,7 @@
import 'cypress-file-upload';
import '@testing-library/cypress/add-commands';
import '@4tw/cypress-drag-drop';
import "cypress-real-events/support";
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
@ -312,12 +313,22 @@ Cypress.Commands.add('insert_doc', (doctype, args, ignore_duplicate) => {
});
});
Cypress.Commands.add('add_filter', () => {
Cypress.Commands.add('open_list_filter', () => {
cy.get('.filter-section .filter-button').click();
cy.wait(300);
cy.get('.filter-popover').should('exist');
});
Cypress.Commands.add('click_action_button', (name) => {
cy.findByRole('button', {name: 'Actions'}).click();
cy.get(`.actions-btn-group [data-label="${encodeURIComponent(name)}"]`).click();
});
Cypress.Commands.add('click_menu_button', (name) => {
cy.get('.standard-actions .menu-btn-group > .btn').click();
cy.get(`.menu-btn-group [data-label="${encodeURIComponent(name)}"]`).click();
});
Cypress.Commands.add('clear_filters', () => {
let has_filter = false;
cy.intercept({
@ -341,7 +352,8 @@ Cypress.Commands.add('clear_filters', () => {
});
Cypress.Commands.add('click_modal_primary_button', (btn_name) => {
cy.get('.modal-footer > .standard-actions > .btn-primary').contains(btn_name).trigger('click', {force: true});
cy.wait(400);
cy.get('.modal-footer > .standard-actions > .btn-primary').contains(btn_name).click({force: true});
});
Cypress.Commands.add('click_sidebar_button', (btn_name) => {

View file

@ -10,28 +10,19 @@ be used to build database driven apps.
Read the documentation: https://frappeframework.com/docs
"""
import os
import warnings
STANDARD_USERS = ("Guest", "Administrator")
_dev_server = os.environ.get("DEV_SERVER", False)
if _dev_server:
warnings.simplefilter("always", DeprecationWarning)
warnings.simplefilter("always", PendingDeprecationWarning)
import importlib
import inspect
import json
import os
import sys
from typing import TYPE_CHECKING, Dict, List, Union
import warnings
from typing import TYPE_CHECKING, Dict, List, Optional, Union
import click
from werkzeug.local import Local, release_local
from frappe.query_builder import get_query_builder, patch_query_aggregation, patch_query_execute
from frappe.utils.data import cstr
from frappe.utils.data import cstr, sbool
# Local application imports
from .exceptions import *
@ -45,11 +36,17 @@ from .utils.jinja import (
from .utils.lazy_loader import lazy_import
__version__ = "14.0.0-dev"
__title__ = "Frappe Framework"
local = Local()
controllers = {}
local = Local()
STANDARD_USERS = ("Guest", "Administrator")
_dev_server = int(sbool(os.environ.get("DEV_SERVER", False)))
if _dev_server:
warnings.simplefilter("always", DeprecationWarning)
warnings.simplefilter("always", PendingDeprecationWarning)
class _dict(dict):
@ -435,7 +432,7 @@ def msgprint(
if as_table and type(msg) in (list, tuple):
out.as_table = 1
if as_list and type(msg) in (list, tuple) and len(msg) > 1:
if as_list and type(msg) in (list, tuple):
out.as_list = 1
if flags.print_messages and out.message:
@ -973,7 +970,7 @@ def get_precision(doctype, fieldname, currency=None, doc=None):
return get_field_precision(get_meta(doctype).get_field(fieldname), doc, currency)
def generate_hash(txt=None, length=None):
def generate_hash(txt: Optional[str] = None, length: Optional[int] = None) -> str:
"""Generates random hash for given text + current timestamp + random string."""
import hashlib
import time

View file

@ -100,6 +100,7 @@ def get_bootinfo():
bootinfo.desk_settings = get_desk_settings()
bootinfo.app_logo_url = get_app_logo()
bootinfo.link_title_doctypes = get_link_title_doctypes()
bootinfo.translatable_doctypes = get_translatable_doctypes()
return bootinfo
@ -408,3 +409,11 @@ def set_time_zone(bootinfo):
"user": bootinfo.get("user_info", {}).get(frappe.session.user, {}).get("time_zone", None)
or get_time_zone(),
}
def get_translatable_doctypes():
dts = frappe.get_all("DocType", {"translate_link_fields": 1}, pluck="name")
custom_dts = frappe.get_all(
"Property Setter", {"property": "translate_link_fields", "value": "1"}, pluck="doc_type"
)
return dts + custom_dts

View file

@ -2,6 +2,7 @@
# License: MIT. See LICENSE
import json
import os
from typing import TYPE_CHECKING
import frappe
import frappe.model
@ -11,6 +12,9 @@ from frappe.desk.reportview import validate_args
from frappe.model.db_query import check_parent_permission
from frappe.utils import get_safe_filters
if TYPE_CHECKING:
from frappe.model.document import Document
"""
Handle RESTful requests that are mapped to the `/api/resource` route.
@ -189,18 +193,7 @@ def insert(doc=None):
if isinstance(doc, str):
doc = json.loads(doc)
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)
parent.save()
return parent.as_dict()
else:
doc = frappe.get_doc(doc).insert()
return doc.as_dict()
return insert_doc(doc).as_dict()
@frappe.whitelist(methods=["POST", "PUT"])
@ -211,21 +204,12 @@ def insert_many(docs=None):
if isinstance(docs, str):
docs = json.loads(docs)
out = []
if len(docs) > 200:
frappe.throw(_("Only 200 inserts allowed in one request"))
out = set()
for doc in docs:
if doc.get("parenttype"):
# inserting a child record
parent = frappe.get_doc(doc.parenttype, doc.parent)
parent.append(doc.parentfield, doc)
parent.save()
out.append(parent.name)
else:
doc = frappe.get_doc(doc).insert()
out.append(doc.name)
out.add(insert_doc(doc).name)
return out
@ -496,3 +480,23 @@ def validate_link(doctype: str, docname: str, fields=None):
)
return values
def insert_doc(doc) -> "Document":
"""Inserts document and returns parent document object with appended child document
if `doc` is child document else returns the inserted document object
:param doc: doc to insert (dict)"""
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)
parent.save()
return parent
return frappe.get_doc(doc).insert()

View file

@ -54,7 +54,6 @@ def new_site(
db_root_password=None,
admin_password=None,
verbose=False,
install_apps=None,
source_sql=None,
force=None,
no_mariadb_socket=False,
@ -398,8 +397,9 @@ def _reinstall(
@click.command("install-app")
@click.argument("apps", nargs=-1)
@click.option("--force", is_flag=True, default=False)
@pass_context
def install_app(context, apps):
def install_app(context, apps, force=False):
"Install a new app to site, supports multiple apps"
from frappe.installer import install_app as _install_app
@ -414,7 +414,7 @@ def install_app(context, apps):
for app in apps:
try:
_install_app(app, verbose=context.verbose)
_install_app(app, verbose=context.verbose, force=force)
except frappe.IncompatibleApp as err:
err_msg = ":\n{}".format(err) if str(err) else ""
print("App {} is Incompatible with Site {}{}".format(app, site, err_msg))
@ -825,7 +825,7 @@ def _drop_site(
try:
if not no_backup:
click.secho(f"Taking backup of {site}", fg="green")
odb = scheduled_backup(ignore_files=False, force=True, verbose=True)
odb = scheduled_backup(ignore_files=False, ignore_conf=True, force=True, verbose=True)
odb.print_summary()
except Exception as err:
if force:
@ -923,7 +923,6 @@ def set_user_password(site, user, password, logout_all_sessions=False):
update_password(user=user, pwd=password, logout_all_sessions=logout_all_sessions)
frappe.db.commit()
password = None
finally:
frappe.destroy()

View file

@ -856,6 +856,8 @@ def run_ui_tests(
node_bin = subprocess.getoutput("npm bin")
cypress_path = f"{node_bin}/cypress"
plugin_path = f"{node_bin}/../cypress-file-upload"
drag_drop_plugin_path = f"{node_bin}/../@4tw/cypress-drag-drop"
real_events_plugin_path = f"{node_bin}/../cypress-real-events"
testing_library_path = f"{node_bin}/../@testing-library"
coverage_plugin_path = f"{node_bin}/../@cypress/code-coverage"
@ -863,6 +865,8 @@ def run_ui_tests(
if not (
os.path.exists(cypress_path)
and os.path.exists(plugin_path)
and os.path.exists(drag_drop_plugin_path)
and os.path.exists(real_events_plugin_path)
and os.path.exists(testing_library_path)
and os.path.exists(coverage_plugin_path)
and cint(subprocess.getoutput("npm view cypress version")[:1]) >= 6
@ -870,7 +874,7 @@ def run_ui_tests(
# install cypress
click.secho("Installing Cypress...", fg="yellow")
frappe.commands.popen(
"yarn add cypress@^6 cypress-file-upload@^5 @4tw/cypress-drag-drop@^2 @testing-library/cypress@^8 @cypress/code-coverage@^3 --no-lockfile"
"yarn add cypress@^6 cypress-file-upload@^5 @4tw/cypress-drag-drop@^2 cypress-real-events @testing-library/cypress@^8 @cypress/code-coverage@^3 --no-lockfile"
)
# run for headless mode

View file

@ -17,7 +17,7 @@ def load_address_and_contact(doc, key=None):
["Dynamic Link", "link_name", "=", doc.name],
["Dynamic Link", "parenttype", "=", "Address"],
]
address_list = frappe.get_list("Address", filters=filters, fields=["*"])
address_list = frappe.get_list("Address", filters=filters, fields=["*"], order_by="creation asc")
address_list = [a.update({"display": get_address_display(a)}) for a in address_list]

View file

@ -268,7 +268,6 @@ def address_query(doctype, txt, searchfield, start, page_len, filters):
`tabAddress`.idx desc, `tabAddress`.name
limit %(start)s, %(page_len)s """.format(
mcond=get_match_cond(doctype),
key=searchfield,
search_condition=search_condition,
condition=condition or "",
),

View file

@ -2,7 +2,6 @@
# License: MIT. See LICENSE
import frappe
from frappe import _
from frappe.core.utils import set_timeline_doc
from frappe.model.document import Document
from frappe.query_builder import DocType, Interval

View file

@ -2,7 +2,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document

View file

@ -20,7 +20,6 @@ from frappe.core.doctype.communication.mixins import CommunicationEmailMixin
from frappe.core.utils import get_parent_doc
from frappe.model.document import Document
from frappe.utils import (
cstr,
parse_addr,
split_emails,
strip_html,
@ -152,8 +151,6 @@ class Communication(Document, CommunicationEmailMixin):
if not email_body:
return
email_body = email_body[0]
user_email_signature = (
frappe.db.get_value(
"User",

View file

@ -3,8 +3,6 @@
# License: MIT. See LICENSE
import unittest
import frappe
# test_records = frappe.get_test_records('Custom DocPerm')

View file

@ -3,8 +3,6 @@
# License: MIT. See LICENSE
import unittest
import frappe
# test_records = frappe.get_test_records('Custom Role')

View file

@ -11,7 +11,6 @@ import frappe
from frappe import _
from frappe.core.doctype.version.version import get_diff
from frappe.model import no_value_fields
from frappe.model import table_fields as table_fieldtypes
from frappe.utils import cint, cstr, duration_to_seconds, flt, update_progress_bar
from frappe.utils.csvutils import get_csv_content_from_google_sheets, read_csv_content
from frappe.utils.xlsxutils import (
@ -574,7 +573,7 @@ class ImportFile:
######
def read_file(self, file_path):
extn = file_path.split(".")[1]
extn = os.path.splitext(file_path)[1][1:]
file_content = None
with io.open(file_path, mode="rb") as f:

View file

@ -3,8 +3,6 @@
# License: MIT. See LICENSE
import unittest
import frappe
# test_records = frappe.get_test_records('Deleted Document')

View file

@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document

View file

@ -47,6 +47,7 @@
"view_settings",
"title_field",
"show_title_field_in_link",
"translate_link_fields",
"search_fields",
"default_print_format",
"sort_field",
@ -591,6 +592,12 @@
"fieldname": "show_title_field_in_link",
"fieldtype": "Check",
"label": "Show Title in Link Fields"
},
{
"default": "0",
"fieldname": "translate_link_fields",
"fieldtype": "Check",
"label": "Translate Link Fields"
}
],
"icon": "fa fa-bolt",
@ -673,7 +680,7 @@
"link_fieldname": "reference_doctype"
}
],
"modified": "2022-02-15 21:47:16.467217",
"modified": "2022-02-28 21:56:52.116915",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType",
@ -708,5 +715,6 @@
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
"track_changes": 1,
"translate_link_fields": 1
}

View file

@ -32,7 +32,7 @@ from frappe.model.meta import Meta
from frappe.modules import get_doc_path, make_boilerplate
from frappe.modules.import_file import get_file_path
from frappe.query_builder.functions import Concat
from frappe.utils import cint, now
from frappe.utils import cint
from frappe.website.utils import clear_cache
@ -100,6 +100,7 @@ class DocType(Document):
self.set_default_in_list_view()
self.set_default_translatable()
validate_series(self)
self.set("can_change_name_type", validate_autoincrement_autoname(self))
self.validate_document_type()
validate_fields(self)
@ -124,12 +125,6 @@ class DocType(Document):
if self.default_print_format and not self.custom:
frappe.throw(_("Standard DocType cannot have default print format, use Customize Form"))
if check_if_can_change_name_type(self):
change_name_column_type(
self.name,
"bigint" if self.autoname == "autoincrement" else f"varchar({frappe.db.VARCHAR_LEN})",
)
def validate_field_name_conflicts(self):
"""Check if field names dont conflict with controller properties and methods"""
core_doctypes = [
@ -374,6 +369,10 @@ class DocType(Document):
def on_update(self):
"""Update database schema, make controller templates if `custom` is not set and clear cache."""
if self.get("can_change_name_type"):
self.setup_autoincrement_and_sequence()
try:
frappe.db.updatedb(self.name, Meta(self))
except Exception as e:
@ -413,6 +412,17 @@ class DocType(Document):
clear_linked_doctype_cache()
def setup_autoincrement_and_sequence(self):
"""Changes name type and makes sequence on change (if required)"""
name_type = f"varchar({frappe.db.VARCHAR_LEN})"
if self.autoname == "autoincrement":
name_type = "bigint"
frappe.db.create_sequence(self.name, check_not_exists=True, cache=frappe.db.SEQUENCE_CACHE)
change_name_column_type(self.name, name_type)
def sync_global_search(self):
"""If global search settings are changed, rebuild search properties for this table"""
global_search_fields_before_update = [
@ -903,26 +913,25 @@ def validate_series(dt, autoname=None, name=None):
frappe.throw(_("Series {0} already used in {1}").format(prefix, used_in[0][0]))
def check_if_can_change_name_type(dt: DocType, raise_err: bool = True) -> bool:
def get_autoname_before_save(doctype: str, to_be_customized_dt: str) -> str:
if doctype == "Customize Form":
property_value = frappe.db.get_value(
"Property Setter", {"doc_type": to_be_customized_dt, "property": "autoname"}, "value"
)
def validate_autoincrement_autoname(dt: DocType) -> bool:
"""Checks if can doctype can change to/from autoincrement autoname"""
def get_autoname_before_save(dt: DocType) -> str:
if dt.name == "Customize Form":
property_value = frappe.db.get_value(
"Property Setter", {"doc_type": dt.doc_type, "property": "autoname"}, "value"
)
# initially no property setter is set,
# hence getting autoname value from the doctype itself
if not property_value:
return frappe.db.get_value("DocType", to_be_customized_dt, "autoname") or ""
return frappe.db.get_value("DocType", dt.doc_type, "autoname") or ""
return property_value
return getattr(dt.get_doc_before_save(), "autoname", "")
doctype_name = dt.doc_type if dt.doctype == "Customize Form" else dt.name
if not dt.is_new():
autoname_before_save = get_autoname_before_save(dt.doctype, doctype_name)
autoname_before_save = get_autoname_before_save(dt)
is_autoname_autoincrement = dt.autoname == "autoincrement"
if (
@ -930,23 +939,35 @@ def check_if_can_change_name_type(dt: DocType, raise_err: bool = True) -> bool:
and autoname_before_save != "autoincrement"
or (not is_autoname_autoincrement and autoname_before_save == "autoincrement")
):
if not frappe.get_all(doctype_name, limit=1):
if frappe.get_meta(dt.name).issingle:
if dt.name == "Customize Form":
frappe.throw(_("Cannot change to/from autoincrement autoname in Customize Form"))
return False
if not frappe.get_all(dt.name, limit=1):
# allow changing the column type if there is no data
return True
if raise_err:
frappe.throw(
_("Can only change to/from Autoincrement naming rule when there is no data in the doctype")
)
frappe.throw(
_("Can only change to/from Autoincrement naming rule when there is no data in the doctype")
)
return False
def change_name_column_type(doctype_name: str, type: str) -> None:
return frappe.db.change_column_type(
doctype_name, "name", type, True if frappe.db.db_type == "mariadb" else False
"""Changes name column type"""
args = (
(doctype_name, "name", type, False, True)
if (frappe.db.db_type == "postgres")
else (doctype_name, "name", type, True)
)
frappe.db.change_column_type(*args)
def validate_links_table_fieldnames(meta):
"""Validate fieldnames in Links table"""

View file

@ -38,6 +38,52 @@ class TestDocType(unittest.TestCase):
doc = new_doctype(name).insert()
doc.delete()
def test_making_sequence_on_change(self):
frappe.delete_doc_if_exists("DocType", self._testMethodName)
dt = new_doctype(self._testMethodName).insert(ignore_permissions=True)
autoname = dt.autoname
# change autoname
dt.autoname = "autoincrement"
dt.save()
# check if name type has been changed
self.assertEqual(
frappe.db.sql(
f"""select data_type FROM information_schema.columns
where column_name = 'name' and table_name = 'tab{self._testMethodName}'"""
)[0][0],
"bigint",
)
if frappe.db.db_type == "mariadb":
table_name = "information_schema.tables"
conditions = f"table_type = 'sequence' and table_name = '{self._testMethodName}_id_seq'"
else:
table_name = "information_schema.sequences"
conditions = f"sequence_name = '{self._testMethodName}_id_seq'"
# check if sequence table is created
self.assertTrue(
frappe.db.sql(
f"""select * from {table_name}
where {conditions}"""
)
)
# change the autoname/naming rule back to original
dt.autoname = autoname
dt.save()
# check if name type has changed
self.assertEqual(
frappe.db.sql(
f"""select data_type FROM information_schema.columns
where column_name = 'name' and table_name = 'tab{self._testMethodName}'"""
)[0][0],
"varchar" if frappe.db.db_type == "mariadb" else "character varying",
)
def test_doctype_unique_constraint_dropped(self):
if frappe.db.exists("DocType", "With_Unique"):
frappe.delete_doc("DocType", "With_Unique")

View file

@ -3,8 +3,6 @@
# License: MIT. See LICENSE
import unittest
import frappe
class TestDomain(unittest.TestCase):
pass

View file

@ -39,18 +39,20 @@
"in_standard_filter": 1,
"label": "Reference DocType",
"options": "DocType",
"read_only": 1,
"search_index": 1
},
{
"fieldname": "reference_name",
"fieldtype": "Data",
"label": "Reference Name"
"label": "Reference Name",
"read_only": 1
}
],
"icon": "fa fa-warning-sign",
"idx": 1,
"links": [],
"modified": "2022-04-18 17:25:47.406873",
"modified": "2022-05-19 05:32:16.026684",
"modified_by": "Administrator",
"module": "Core",
"name": "Error Log",

View file

@ -3,8 +3,6 @@
# License: MIT. See LICENSE
import unittest
import frappe
# test_records = frappe.get_test_records('Error Snapshot')

View file

@ -41,7 +41,6 @@ from frappe.utils.file_manager import safe_b64decode
from frappe.utils.image import optimize_image, strip_exif_data
if TYPE_CHECKING:
from PIL.ImageFile import ImageFile
from requests.models import Response
@ -608,7 +607,7 @@ def on_doctype_update():
def make_home_folder():
home = frappe.get_doc(
{"doctype": "File", "is_folder": 1, "is_home_folder": 1, "file_name": _("Home")}
).insert()
).insert(ignore_if_duplicate=True)
frappe.get_doc(
{
@ -618,7 +617,7 @@ def make_home_folder():
"is_attachments_folder": 1,
"file_name": _("Attachments"),
}
).insert()
).insert(ignore_if_duplicate=True)
@frappe.whitelist()

View file

@ -2,7 +2,6 @@
# Copyright (c) 2017, Frappe Technologies and contributors
# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document

View file

@ -3,8 +3,6 @@
# License: MIT. See LICENSE
import unittest
import frappe
# test_records = frappe.get_test_records('Language')

View file

@ -10,10 +10,15 @@ from frappe.query_builder.functions import Now
class LogSettings(Document):
def clear_logs(self):
def clear_logs(self, commit=False):
self.clear_email_queue()
if commit:
# Since since deleting many logs can take significant amount of time, commit is required to relase locks.
# Error log table doesn't require commit - myisam
# activity logs are deleted last so background job finishes and commits.
frappe.db.commit()
self.clear_error_logs()
self.clear_activity_logs()
self.clear_email_queue()
def clear_error_logs(self):
table = DocType("Error Log")
@ -34,7 +39,7 @@ class LogSettings(Document):
def run_log_clean_up():
doc = frappe.get_doc("Log Settings")
doc.clear_logs()
doc.clear_logs(commit=True)
@frappe.whitelist()

View file

@ -3,8 +3,6 @@
# License: MIT. See LICENSE
import unittest
import frappe
# test_records = frappe.get_test_records('Module Def')

View file

@ -3,7 +3,6 @@
# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document

View file

@ -3,8 +3,6 @@
# License: MIT. See LICENSE
import unittest
import frappe
# test_records = frappe.get_test_records('Patch Log')

View file

@ -2,7 +2,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document

View file

@ -3,8 +3,6 @@
# License: MIT. See LICENSE
import unittest
import frappe
# test_records = frappe.get_test_records('Payment Gateway')

View file

@ -1,8 +1,15 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// MIT License. See license.txt
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
// MIT License. See LICENSE
frappe.ui.form.on('Role', {
refresh: function(frm) {
if (frm.doc.name === "All") {
frm.dashboard.add_comment(
__("Role 'All' will be given to all System Users."),
"yellow"
);
}
frm.set_df_property('is_custom', 'read_only', frappe.session.user !== 'Administrator');
frm.add_custom_button("Role Permissions Manager", function() {

View file

@ -2,7 +2,6 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# License: MIT. See LICENSE
import ast
from types import FunctionType, MethodType, ModuleType
from typing import Dict, List
@ -17,6 +16,7 @@ class ServerScript(Document):
frappe.only_for("Script Manager", True)
self.sync_scheduled_jobs()
self.clear_scheduled_events()
self.check_if_compilable_in_restricted_context()
def on_update(self):
frappe.cache().delete_value("server_script_map")
@ -60,6 +60,15 @@ class ServerScript(Document):
for scheduled_job in self.scheduled_jobs:
frappe.delete_doc("Scheduled Job Type", scheduled_job.name)
def check_if_compilable_in_restricted_context(self):
"""Check compilation errors and send them back as warnings."""
from RestrictedPython import compile_restricted
try:
compile_restricted(self.script)
except Exception as e:
frappe.msgprint(str(e), title=_("Compilation warning"))
def execute_method(self) -> Dict:
"""Specific to API endpoint Server Scripts

View file

@ -1,7 +1,6 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document

View file

@ -3,8 +3,6 @@
# License: MIT. See LICENSE
import unittest
import frappe
class TestSMSSettings(unittest.TestCase):
pass

View file

@ -11,7 +11,6 @@
"language",
"column_break_3",
"time_zone",
"is_first_startup",
"enable_onboarding",
"setup_complete",
"date_and_number_format",
@ -46,6 +45,7 @@
"password_settings",
"logout_on_password_reset",
"force_user_to_reset_password",
"reset_password_link_expiry_duration",
"password_reset_limit",
"column_break_31",
"enable_password_policy",
@ -73,7 +73,8 @@
"column_break_64",
"max_auto_email_report_per_user",
"system_updates_section",
"disable_system_update_notification"
"disable_system_update_notification",
"disable_change_log_notification"
],
"fields": [
{
@ -105,14 +106,6 @@
"read_only": 1,
"reqd": 1
},
{
"default": "0",
"fieldname": "is_first_startup",
"fieldtype": "Check",
"hidden": 1,
"label": "Is First Startup",
"read_only": 1
},
{
"default": "0",
"fieldname": "setup_complete",
@ -512,12 +505,25 @@
"fieldname": "max_auto_email_report_per_user",
"fieldtype": "Int",
"label": "Max auto email report per user"
},
{
"default": "0",
"fieldname": "disable_change_log_notification",
"fieldtype": "Check",
"label": "Disable Change Log Notification"
},
{
"default": "1200",
"fieldname": "reset_password_link_expiry_duration",
"fieldtype": "Duration",
"label": "Reset Password Link Expiry Duration",
"non_negative": 1
}
],
"icon": "fa fa-cog",
"issingle": 1,
"links": [],
"modified": "2022-04-22 09:11:35.218721",
"modified": "2022-05-19 00:00:18.095269",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",

View file

@ -3,8 +3,6 @@
# License: MIT. See LICENSE
import unittest
import frappe
class TestSystemSettings(unittest.TestCase):
pass

View file

@ -4,7 +4,6 @@
import hashlib
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.query_builder import DocType
from frappe.utils import cint, now_datetime

View file

@ -1,6 +1,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import json
import time
import unittest
from unittest.mock import patch
@ -256,7 +257,8 @@ class TestUser(unittest.TestCase):
<span class="mention" data-id="Team" data-value="Team" data-is-group="true" data-denotation-char="@">
<span><span class="ql-mention-denotation-char">@</span>Team</span>
</span> and
<span class="mention" data-id="Unknown Team" data-value="Unknown Team" data-is-group="true" data-denotation-char="@">
<span class="mention" data-id="Unknown Team" data-value="Unknown Team" data-is-group="true"
data-denotation-char="@">
<span><span class="ql-mention-denotation-char">@</span>Unknown Team</span>
</span><!-- this should be ignored-->
please check
@ -365,7 +367,7 @@ class TestUser(unittest.TestCase):
self.assertEqual(update_password(new_password, key=test_user.reset_password_key), "/app")
self.assertEqual(
update_password(new_password, key="wrong_key"),
"The Link specified has either been used before or Invalid",
"The reset password link has either been used before or is invalid",
)
# password verification should fail with old password
@ -374,7 +376,6 @@ class TestUser(unittest.TestCase):
# reset password
update_password(old_password, old_password=new_password)
self.assertRaisesRegex(
frappe.exceptions.ValidationError, "Invalid key type", update_password, "test", 1, ["like", "%"]
)
@ -434,6 +435,21 @@ class TestUser(unittest.TestCase):
[m.get("module_name") for m in get_modules_from_all_apps()],
)
def test_reset_password_link_expiry(self):
new_password = "new_password"
# set the reset password expiry to 1 second
frappe.db.set_value(
"System Settings", "System Settings", "reset_password_link_expiry_duration", 1
)
frappe.set_user("testpassword@example.com")
test_user = frappe.get_doc("User", "testpassword@example.com")
test_user.reset_password()
time.sleep(1) # sleep for 1 sec to expire the reset link
self.assertEqual(
update_password(new_password, key=test_user.reset_password_key),
"The reset password link has been expired",
)
def delete_contact(user):
frappe.db.delete("Contact", {"email_id": user})

View file

@ -43,6 +43,7 @@
"new_password",
"logout_all_sessions",
"reset_password_key",
"last_reset_password_key_generated_on",
"last_password_reset_date",
"redirect_url",
"document_follow_notifications_section",
@ -613,6 +614,14 @@
"label": "Module Profile",
"options": "Module Profile"
},
{
"description": "Stores the datetime when the last reset password key was generated.",
"fieldname": "last_reset_password_key_generated_on",
"fieldtype": "Datetime",
"hidden": 1,
"label": "Last Reset Password Key Generated On",
"read_only": 1
},
{
"fieldname": "column_break_75",
"fieldtype": "Column Break"

View file

@ -1,5 +1,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
from datetime import timedelta
from bs4 import BeautifulSoup
import frappe
@ -276,6 +278,7 @@ class User(Document):
key = random_string(32)
self.db_set("reset_password_key", key)
self.db_set("last_reset_password_key_generated_on", now_datetime())
url = "/update-password?key=" + key
if password_expired:
@ -780,16 +783,27 @@ def _get_user_for_update_password(key, old_password):
# verify old password
result = frappe._dict()
if key:
result.user = frappe.db.get_value("User", {"reset_password_key": key})
if not result.user:
result.message = _("The Link specified has either been used before or Invalid")
user = frappe.db.get_value(
"User", {"reset_password_key": key}, ["name", "last_reset_password_key_generated_on"]
)
result.user, last_reset_password_key_generated_on = user or (None, None)
if result.user:
reset_password_link_expiry = cint(
frappe.db.get_single_value("System Settings", "reset_password_link_expiry_duration")
)
if (
reset_password_link_expiry
and now_datetime()
> last_reset_password_key_generated_on + timedelta(seconds=reset_password_link_expiry)
):
result.message = _("The reset password link has been expired")
else:
result.message = _("The reset password link has either been used before or is invalid")
elif old_password:
# verify old password
frappe.local.login_manager.check_password(frappe.session.user, old_password)
user = frappe.session.user
result.user = user
return result

View file

@ -2,7 +2,6 @@
# Copyright (c) 2015, Frappe Technologies and contributors
# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document

View file

@ -8,7 +8,6 @@ from frappe import _
from frappe.core.utils import find
from frappe.desk.form.linked_with import get_linked_doctypes
from frappe.model.document import Document
from frappe.permissions import get_valid_perms, update_permission_property
from frappe.utils import cstr

View file

@ -2,7 +2,6 @@
# Copyright (c) 2018, Frappe Technologies and contributors
# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document

View file

@ -2,8 +2,6 @@
# License: MIT. See LICENSE
import frappe
from frappe.query_builder import DocType, Interval
from frappe.query_builder.functions import Now
def get_notification_config():

View file

@ -1,13 +1,9 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import json
from typing import TYPE_CHECKING, Dict, List
from rq import Worker
import frappe
from frappe import _
from frappe.utils import convert_utc_to_user_timezone
from frappe.utils.background_jobs import get_queues, get_workers
from frappe.utils.scheduler import is_scheduler_inactive

View file

@ -3,7 +3,6 @@
import frappe
import frappe.utils.user
from frappe import _, throw
from frappe.model import data_fieldtypes
from frappe.permissions import check_admin_or_system_manager, rights

View file

@ -1,6 +1,3 @@
import frappe
def get_context(context):
# do your magic here
pass

View file

@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import frappe
from frappe import _
from frappe.model.document import Document

View file

@ -3,8 +3,6 @@
# License: MIT. See LICENSE
import unittest
import frappe
# test_records = frappe.get_test_records('Client Script')

View file

@ -16,7 +16,6 @@ frappe.ui.form.on("Customize Form", {
onload: function(frm) {
frm.set_query("doc_type", function() {
return {
translate_values: false,
filters: [
["DocType", "issingle", "=", 0],
["DocType", "custom", "=", 0],

View file

@ -29,6 +29,7 @@
"view_settings_section",
"title_field",
"show_title_field_in_link",
"translate_link_fields",
"image_field",
"default_print_format",
"column_break_29",
@ -287,7 +288,7 @@
"label": "Naming"
},
{
"description": "Naming Options:\n<ol><li><b>field:[fieldname]</b> - By Field</li>\n<li><b>autoincrement</b> - Uses Databases' Auto Increment feature</li><li><b>naming_series:</b> - By Naming Series (field called naming_series must be present</li><li><b>Prompt</b> - Prompt user for a name</li><li><b>[series]</b> - Series by prefix (separated by a dot); for example PRE.#####</li>\n<li><b>format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####}</b> - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.</li></ol>",
"description": "Naming Options:\n<ol><li><b>field:[fieldname]</b> - By Field</li><li><b>naming_series:</b> - By Naming Series (field called naming_series must be present)</li><li><b>Prompt</b> - Prompt user for a name</li><li><b>[series]</b> - Series by prefix (separated by a dot); for example PRE.#####</li>\n<li><b>format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####}</b> - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.</li></ol>",
"fieldname": "autoname",
"fieldtype": "Data",
"label": "Auto Name"
@ -311,6 +312,12 @@
"fieldname": "show_title_field_in_link",
"fieldtype": "Check",
"label": "Show Title in Link Fields"
},
{
"default": "0",
"fieldname": "translate_link_fields",
"fieldtype": "Check",
"label": "Translate Link Fields"
}
],
"hide_toolbar": 1,
@ -319,7 +326,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2022-04-21 15:36:16.772277",
"modified": "2022-05-13 15:36:16.772277",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form",

View file

@ -11,9 +11,8 @@ import frappe
import frappe.translate
from frappe import _
from frappe.core.doctype.doctype.doctype import (
change_name_column_type,
check_email_append_to,
check_if_can_change_name_type,
validate_autoincrement_autoname,
validate_fields_for_doctype,
validate_series,
)
@ -163,7 +162,7 @@ class CustomizeForm(Document):
return
validate_series(self, self.autoname, self.doc_type)
can_change_name_type = check_if_can_change_name_type(self)
validate_autoincrement_autoname(self)
self.flags.update_db = False
self.flags.rebuild_doctype_for_global_search = False
self.set_property_setters()
@ -172,12 +171,6 @@ class CustomizeForm(Document):
validate_fields_for_doctype(self.doc_type)
check_email_append_to(self)
if can_change_name_type:
change_name_column_type(
self.doc_type,
"bigint" if self.autoname == "autoincrement" else f"varchar({frappe.db.VARCHAR_LEN})",
)
if self.flags.update_db:
frappe.db.updatedb(self.doc_type)
@ -531,7 +524,10 @@ class CustomizeForm(Document):
"""allow type change, if both old_type and new_type are in same field group.
field groups are defined in ALLOWED_FIELDTYPE_CHANGE variables.
"""
in_field_group = lambda group: (old_type in group) and (new_type in group)
def in_field_group(group):
return (old_type in group) and (new_type in group)
return any(map(in_field_group, ALLOWED_FIELDTYPE_CHANGE))
@ -584,6 +580,7 @@ doctype_properties = {
"naming_rule": "Data",
"autoname": "Data",
"show_title_field_in_link": "Check",
"translate_link_fields": "Check",
}
docfield_properties = {

View file

@ -396,3 +396,10 @@ class TestCustomizeForm(unittest.TestCase):
d.label = ""
d.run_method("save_customization")
self.assertEqual(d.label, "")
def test_change_to_autoincrement_autoname(self):
d = self.get_customize_form("Event")
d.autoname = "autoincrement"
with self.assertRaises(frappe.ValidationError):
d.run_method("save_customization")

View file

@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document

View file

@ -3,8 +3,6 @@
# License: MIT. See LICENSE
import unittest
import frappe
# test_records = frappe.get_test_records('Property Setter')

View file

@ -1019,21 +1019,17 @@ class Database(object):
return self.get_value(dt, dn, ignore=True, cache=cache)
def count(self, dt, filters=None, debug=False, cache=False):
def count(self, dt, filters=None, debug=False, cache=False, distinct: bool = True):
"""Returns `COUNT(*)` for given DocType and filters."""
if cache and not filters:
cache_count = frappe.cache().get_value("doctype:count:{}".format(dt))
if cache_count is not None:
return cache_count
query = self.query.get_sql(table=dt, filters=filters, fields=Count("*"))
if filters:
count = self.sql(query, debug=debug)[0][0]
return count
else:
count = self.sql(query, debug=debug)[0][0]
if cache:
frappe.cache().set_value("doctype:count:{}".format(dt), count, expires_in_sec=86400)
return count
query = self.query.get_sql(table=dt, filters=filters, fields=Count("*"), distinct=distinct)
count = self.sql(query, debug=debug)[0][0]
if not filters and cache:
frappe.cache().set_value("doctype:count:{}".format(dt), count, expires_in_sec=86400)
return count
@staticmethod
def format_date(date):

View file

@ -19,6 +19,15 @@ class MariaDBDatabase(Database):
DataError = pymysql.err.DataError
REGEX_CHARACTER = "regexp"
# NOTE: using a very small cache - as during backup, if the sequence was used in anyform,
# it drops the cache and uses the next non cached value in setval query and
# puts that in the backup file, which will start the counter
# from that value when inserting any new record in the doctype.
# By default the cache is 1000 which will mess up the sequence when
# using the system after a restore.
# issue link: https://jira.mariadb.org/browse/MDEV-21786
SEQUENCE_CACHE = 50
def setup_type_map(self):
self.db_type = "mariadb"
self.type_map = {

View file

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

View file

@ -40,14 +40,7 @@ class MariaDBTable(DBTable):
not self.meta.issingle and self.meta.autoname == "autoincrement"
) or self.doctype in log_types:
# NOTE: using a very small cache - as during backup, if the sequence was used in anyform,
# it drops the cache and uses the next non cached value in setval func and
# puts that in the backup file, which will start the counter
# from that value when inserting any new record in the doctype.
# By default the cache is 1000 which will mess up the sequence when
# using the system after a restore.
# issue link: https://jira.mariadb.org/browse/MDEV-21786
frappe.db.create_sequence(self.doctype, check_not_exists=True, cache=50)
frappe.db.create_sequence(self.doctype, check_not_exists=True, cache=frappe.db.SEQUENCE_CACHE)
# NOTE: not used nextval func as default as the ability to restore
# database with sequences has bugs in mariadb and gives a scary error.

View file

@ -31,6 +31,12 @@ class PostgresDatabase(Database):
InterfaceError = psycopg2.InterfaceError
REGEX_CHARACTER = "~"
# NOTE; The sequence cache for postgres is per connection.
# Since we're opening and closing connections for every transaction this results in skipping the cache
# to the next non-cached value hence not using cache in postgres.
# ref: https://stackoverflow.com/questions/21356375/postgres-9-0-4-sequence-skipping-numbers
SEQUENCE_CACHE = 0
def setup_type_map(self):
self.db_type = "postgres"
self.type_map = {
@ -209,18 +215,19 @@ class PostgresDatabase(Database):
)
def change_column_type(
self, doctype: str, column: str, type: str, nullable: bool = False
self, doctype: str, column: str, type: str, nullable: bool = False, use_cast: bool = False
) -> Union[List, Tuple]:
table_name = get_table_name(doctype)
null_constraint = "SET NOT NULL" if not nullable else "DROP NOT NULL"
using_cast = f'using "{column}"::{type}' if use_cast else ""
# postgres allows ddl in transactions but since we've currently made
# things same as mariadb (raising exception on ddl commands if the transaction has any writes),
# hence using sql_ddl here for committing and then moving forward.
return self.sql_ddl(
f"""ALTER TABLE "{table_name}"
ALTER COLUMN "{column}" TYPE {type},
ALTER COLUMN "{column}" {null_constraint}"""
ALTER COLUMN "{column}" TYPE {type} {using_cast},
ALTER COLUMN "{column}" {null_constraint}"""
)
def create_auth_table(self):

View file

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

View file

@ -34,11 +34,7 @@ class PostgresTable(DBTable):
not self.meta.issingle and self.meta.autoname == "autoincrement"
) or self.doctype in log_types:
# The sequence cache is per connection.
# Since we're opening and closing connections for every transaction this results in skipping the cache
# to the next non-cached value hence not using cache in postgres.
# ref: https://stackoverflow.com/questions/21356375/postgres-9-0-4-sequence-skipping-numbers
frappe.db.create_sequence(self.doctype, check_not_exists=True)
frappe.db.create_sequence(self.doctype, check_not_exists=True, cache=frappe.db.SEQUENCE_CACHE)
name_column = "name bigint primary key"
# TODO: set docstatus length

View file

@ -114,7 +114,7 @@ def drop_user_and_database(db_name, root_login, root_password):
)
root_conn.commit()
root_conn.sql(
f"SELECT pg_terminate_backend (pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = %s",
"SELECT pg_terminate_backend (pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = %s",
(db_name,),
)
root_conn.sql(f"DROP DATABASE IF EXISTS {db_name}")

View file

@ -1,13 +1,16 @@
import operator
import re
from typing import Any, Dict, List, Tuple, Union
from functools import cached_property
from typing import Any, Callable, Dict, List, Tuple, Union
import frappe
from frappe import _
from frappe.query_builder import Criterion, Field, Order
from frappe.boot import get_additional_filters_from_hooks
from frappe.model.db_query import get_timespan_date_range
from frappe.query_builder import Criterion, Field, Order, Table
def like(key: str, value: str) -> frappe.qb:
def like(key: Field, value: str) -> frappe.qb:
"""Wrapper method for `LIKE`
Args:
@ -17,10 +20,10 @@ def like(key: str, value: str) -> frappe.qb:
Returns:
frappe.qb: `frappe.qb object with `LIKE`
"""
return Field(key).like(value)
return key.like(value)
def func_in(key: str, value: Union[List, Tuple]) -> frappe.qb:
def func_in(key: Field, value: Union[List, Tuple]) -> frappe.qb:
"""Wrapper method for `IN`
Args:
@ -30,10 +33,10 @@ def func_in(key: str, value: Union[List, Tuple]) -> frappe.qb:
Returns:
frappe.qb: `frappe.qb object with `IN`
"""
return Field(key).isin(value)
return key.isin(value)
def not_like(key: str, value: str) -> frappe.qb:
def not_like(key: Field, value: str) -> frappe.qb:
"""Wrapper method for `NOT LIKE`
Args:
@ -43,10 +46,10 @@ def not_like(key: str, value: str) -> frappe.qb:
Returns:
frappe.qb: `frappe.qb object with `NOT LIKE`
"""
return Field(key).not_like(value)
return key.not_like(value)
def func_not_in(key: str, value: Union[List, Tuple]):
def func_not_in(key: Field, value: Union[List, Tuple]):
"""Wrapper method for `NOT IN`
Args:
@ -56,10 +59,10 @@ def func_not_in(key: str, value: Union[List, Tuple]):
Returns:
frappe.qb: `frappe.qb object with `NOT IN`
"""
return Field(key).notin(value)
return key.notin(value)
def func_regex(key: str, value: str) -> frappe.qb:
def func_regex(key: Field, value: str) -> frappe.qb:
"""Wrapper method for `REGEX`
Args:
@ -69,10 +72,10 @@ def func_regex(key: str, value: str) -> frappe.qb:
Returns:
frappe.qb: `frappe.qb object with `REGEX`
"""
return Field(key).regex(value)
return key.regex(value)
def func_between(key: str, value: Union[List, Tuple]) -> frappe.qb:
def func_between(key: Field, value: Union[List, Tuple]) -> frappe.qb:
"""Wrapper method for `BETWEEN`
Args:
@ -82,7 +85,26 @@ def func_between(key: str, value: Union[List, Tuple]) -> frappe.qb:
Returns:
frappe.qb: `frappe.qb object with `BETWEEN`
"""
return Field(key)[slice(*value)]
return key[slice(*value)]
def func_is(key, value):
"Wrapper for IS"
return Field(key).isnotnull() if value.lower() == "set" else Field(key).isnull()
def func_timespan(key: Field, value: str) -> frappe.qb:
"""Wrapper method for `TIMESPAN`
Args:
key (str): field
value (str): criterion
Returns:
frappe.qb: `frappe.qb object with `TIMESPAN`
"""
return func_between(key, get_timespan_date_range(value))
def make_function(key: Any, value: Union[int, str]):
@ -95,7 +117,7 @@ def make_function(key: Any, value: Union[int, str]):
Returns:
frappe.qb: frappe.qb object
"""
return OPERATOR_MAP[value[0]](key, value[1])
return OPERATOR_MAP[value[0].casefold()](key, value[1])
def change_orderby(order: str):
@ -118,7 +140,8 @@ def change_orderby(order: str):
return order[0], Order.desc
OPERATOR_MAP = {
# default operators
OPERATOR_MAP: Dict[str, Callable] = {
"+": operator.add,
"=": operator.eq,
"-": operator.sub,
@ -135,11 +158,38 @@ OPERATOR_MAP = {
"not like": not_like,
"regex": func_regex,
"between": func_between,
"is": func_is,
"timespan": func_timespan,
# TODO: Add support for nested set
# TODO: Add support for custom operators (WIP) - via filters_config hooks
}
class Query:
def get_condition(self, table: str, **kwargs) -> frappe.qb:
tables: dict = {}
@cached_property
def OPERATOR_MAP(self):
# default operators
all_operators = OPERATOR_MAP.copy()
# update with site-specific custom operators
additional_filters_config = get_additional_filters_from_hooks()
if additional_filters_config:
from frappe.utils.commands import warn
warn("'filters_config' hook is not completely implemented yet in frappe.db.query engine")
for _operator, function in additional_filters_config.items():
if callable(function):
all_operators.update({_operator.casefold(): function})
elif isinstance(function, dict):
all_operators[_operator.casefold()] = frappe.get_attr(function.get("get_field"))()["operator"]
return all_operators
def get_condition(self, table: Union[str, Table], **kwargs) -> frappe.qb:
"""Get initial table object
Args:
@ -148,11 +198,20 @@ class Query:
Returns:
frappe.qb: DocType with initial condition
"""
table_object = self.get_table(table)
if kwargs.get("update"):
return frappe.qb.update(table)
return frappe.qb.update(table_object)
if kwargs.get("into"):
return frappe.qb.into(table)
return frappe.qb.from_(table)
return frappe.qb.into(table_object)
return frappe.qb.from_(table_object)
def get_table(self, table_name: Union[str, Table]) -> Table:
if isinstance(table_name, Table):
return table_name
table_name = table_name.strip('"').strip("'")
if table_name not in self.tables:
self.tables[table_name] = frappe.qb.DocType(table_name)
return self.tables[table_name]
def criterion_query(self, table: str, criterion: Criterion, **kwargs) -> frappe.qb:
"""Generate filters from Criterion objects
@ -210,15 +269,20 @@ class Query:
if isinstance(filters, list):
for f in filters:
if not isinstance(f, (list, tuple)):
_operator = OPERATOR_MAP[filters[1]]
_operator = self.OPERATOR_MAP[filters[1].casefold()]
if not isinstance(filters[0], str):
conditions = make_function(filters[0], filters[2])
break
conditions = conditions.where(_operator(Field(filters[0]), filters[2]))
break
else:
_operator = OPERATOR_MAP[f[1]]
conditions = conditions.where(_operator(Field(f[0]), f[2]))
_operator = self.OPERATOR_MAP[f[-2].casefold()]
if len(f) == 4:
table_object = self.get_table(f[0])
_field = table_object[f[1]]
else:
_field = Field(f[0])
conditions = conditions.where(_operator(_field, f[-1]))
return self.add_conditions(conditions, **kwargs)
@ -241,18 +305,14 @@ class Query:
for key in filters:
value = filters.get(key)
_operator = OPERATOR_MAP["="]
_operator = self.OPERATOR_MAP["="]
if not isinstance(key, str):
conditions = conditions.where(make_function(key, value))
continue
if isinstance(value, (list, tuple)):
if isinstance(value[1], (list, tuple)) or value[0] in list(OPERATOR_MAP.keys())[-4:]:
_operator = OPERATOR_MAP[value[0]]
conditions = conditions.where(_operator(key, value[1]))
else:
_operator = OPERATOR_MAP[value[0]]
conditions = conditions.where(_operator(Field(key), value[1]))
_operator = self.OPERATOR_MAP[value[0].casefold()]
conditions = conditions.where(_operator(Field(key), value[1]))
else:
if value is not None:
conditions = conditions.where(_operator(Field(key), value))
@ -293,10 +353,19 @@ class Query:
self,
table: str,
fields: Union[List, Tuple],
filters: Union[Dict[str, Union[str, int]], str, int] = None,
filters: Union[Dict[str, Union[str, int]], str, int, List[Union[List, str, int]]] = None,
**kwargs,
):
# Clean up state before each query
self.tables = {}
criterion = self.build_conditions(table, filters, **kwargs)
if len(self.tables) > 1:
primary_table = self.tables[table]
del self.tables[table]
for table_object in self.tables.values():
criterion = criterion.left_join(table_object).on(table_object.parent == primary_table.name)
if isinstance(fields, (list, tuple)):
query = criterion.select(*kwargs.get("field_objects", fields))

View file

@ -5,7 +5,7 @@ def create_sequence(
doctype_name: str,
*,
slug: str = "_id_seq",
temporary=False,
temporary: bool = False,
check_not_exists: bool = False,
cycle: bool = False,
cache: int = 0,
@ -51,7 +51,7 @@ def create_sequence(
else:
query += " cycle"
db.sql(query)
db.sql_ddl(query)
return sequence_name

View file

@ -166,6 +166,8 @@ class Workspace:
self.onboardings = {"items": self.get_onboardings()}
self.quick_lists = {"items": self.get_quick_lists()}
def _doctype_contains_a_record(self, name):
exists = self.table_counts.get(name, False)
@ -284,6 +286,21 @@ class Workspace:
return items
@handle_not_exist
def get_quick_lists(self):
items = []
quick_lists = self.doc.quick_lists
for item in quick_lists:
new_item = item.as_dict().copy()
# Translate label
new_item["label"] = _(item.label) if item.label else _(item.document_type)
items.append(new_item)
return items
@handle_not_exist
def get_onboardings(self):
if self.onboarding_list:
@ -336,6 +353,7 @@ def get_desktop_page(page):
"shortcuts": workspace.shortcuts,
"cards": workspace.cards,
"onboardings": workspace.onboardings,
"quick_lists": workspace.quick_lists,
}
except DoesNotExistError:
frappe.log_error("Workspace Missing")
@ -452,6 +470,8 @@ def save_new_widget(doc, page, blocks, new_widgets):
doc.charts.extend(new_widget(widgets.chart, "Workspace Chart", "charts"))
if widgets.shortcut:
doc.shortcuts.extend(new_widget(widgets.shortcut, "Workspace Shortcut", "shortcuts"))
if widgets.quick_list:
doc.quick_lists.extend(new_widget(widgets.quick_list, "Workspace Quick List", "quick_lists"))
if widgets.card:
doc.build_links_table_from_card(widgets.card)
@ -481,12 +501,12 @@ def save_new_widget(doc, page, blocks, new_widgets):
def clean_up(original_page, blocks):
page_widgets = {}
for wid in ["shortcut", "card", "chart"]:
for wid in ["shortcut", "card", "chart", "quick_list"]:
# get list of widget's name from blocks
page_widgets[wid] = [x["data"][wid + "_name"] for x in loads(blocks) if x["type"] == wid]
# shortcut & chart cleanup
for wid in ["shortcut", "chart"]:
# shortcut, chart & quick_list cleanup
for wid in ["shortcut", "chart", "quick_list"]:
updated_widgets = []
original_page.get(wid + "s").reverse()

View file

@ -5,7 +5,6 @@
import os
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.modules import get_module_path, scrub
from frappe.modules.export_file import export_to_files

View file

@ -277,7 +277,7 @@
"icon": "fa fa-calendar",
"idx": 1,
"links": [],
"modified": "2021-11-18 05:06:24.881742",
"modified": "2022-05-12 05:43:27.935510",
"modified_by": "Administrator",
"module": "Desk",
"name": "Event",
@ -312,6 +312,7 @@
"sender_field": "sender",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"subject_field": "subject",
"title_field": "subject",
"track_changes": 1,

View file

@ -18,7 +18,6 @@ from frappe.utils import (
cstr,
date_diff,
format_datetime,
get_datetime,
get_datetime_str,
getdate,
now_datetime,

View file

@ -3,8 +3,6 @@
# License: MIT. See LICENSE
import unittest
import frappe
# test_records = frappe.get_test_records('Kanban Board')

View file

@ -2,7 +2,6 @@
# Copyright (c) 2015, Frappe Technologies and contributors
# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document

View file

@ -2,9 +2,6 @@
# Copyright (c) 2018, Frappe Technologies and contributors
# License: MIT. See LICENSE
import json
import frappe
from frappe.model.document import Document

View file

@ -2,7 +2,6 @@
# Copyright (c) 2015, Frappe Technologies and contributors
# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document

View file

@ -7,7 +7,6 @@ from frappe import _
from frappe.desk.doctype.notification_settings.notification_settings import (
is_email_notifications_enabled_for_type,
is_notifications_enabled,
set_seen_value,
)
from frappe.model.document import Document

View file

@ -26,6 +26,8 @@
"shortcuts",
"tab_break_18",
"links",
"quick_lists_tab",
"quick_lists",
"roles_tab",
"roles"
],
@ -155,11 +157,22 @@
"fieldname": "roles_tab",
"fieldtype": "Tab Break",
"label": "Roles"
},
{
"fieldname": "quick_lists_tab",
"fieldtype": "Tab Break",
"label": "Quick Lists"
},
{
"fieldname": "quick_lists",
"fieldtype": "Table",
"label": "Quick Lists",
"options": "Workspace Quick List"
}
],
"in_create": 1,
"links": [],
"modified": "2022-01-27 12:06:13.111743",
"modified": "2022-05-12 13:00:03.925605",
"modified_by": "Administrator",
"module": "Desk",
"name": "Workspace",
@ -189,5 +202,6 @@
}
],
"sort_field": "modified",
"sort_order": "DESC"
"sort_order": "DESC",
"states": []
}

View file

@ -0,0 +1,60 @@
{
"actions": [],
"creation": "2022-05-12 12:58:41.824496",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"document_type",
"column_break_1",
"label",
"section_break_4",
"quick_list_filter"
],
"fields": [
{
"fieldname": "document_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "DocType",
"options": "DocType",
"reqd": 1
},
{
"fieldname": "column_break_1",
"fieldtype": "Column Break"
},
{
"fieldname": "label",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Label",
"reqd": 1
},
{
"fieldname": "section_break_4",
"fieldtype": "Section Break"
},
{
"fieldname": "quick_list_filter",
"fieldtype": "Code",
"in_list_view": 1,
"label": "Quick List Filter",
"options": "JSON"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2022-05-12 13:48:40.617623",
"modified_by": "Administrator",
"module": "Desk",
"name": "Workspace Quick List",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View file

@ -0,0 +1,9 @@
# Copyright (c) 2022, Frappe Technologies and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class WorkspaceQuickList(Document):
pass

View file

@ -2,7 +2,7 @@
# License: MIT. See LICENSE
import json
from typing import Dict, List, Union
from typing import List, Union
from urllib.parse import quote
import frappe

View file

@ -1,5 +1,3 @@
import json
import frappe
from frappe.model import no_value_fields, table_fields

View file

@ -249,9 +249,9 @@ def get_open_count(doctype, name, items=None):
if frappe.flags.in_migrate or frappe.flags.in_install:
return {"count": []}
frappe.has_permission(doc=frappe.get_doc(doctype, name), throw=True)
meta = frappe.get_meta(doctype)
doc = frappe.get_doc(doctype, name)
doc.check_permission()
meta = doc.meta
links = meta.get_dashboard_data()
# compile all items in a list
@ -266,7 +266,6 @@ def get_open_count(doctype, name, items=None):
out = []
for d in items:
if d in links.get("internal_links", {}):
# internal link
continue
filters = get_filters_for(d)

View file

@ -2,7 +2,6 @@
# License: MIT. See LICENSE
import frappe
from frappe import _
from frappe.desk.doctype.global_search_settings.global_search_settings import (
update_global_search_doctypes,
)

View file

@ -269,7 +269,6 @@ def add_all_roles_to(name):
def disable_future_access():
frappe.db.set_default("desktop:home_page", "workspace")
frappe.db.set_value("System Settings", "System Settings", "setup_complete", 1)
frappe.db.set_value("System Settings", "System Settings", "is_first_startup", 1)
# Enable onboarding after install
frappe.db.set_value("System Settings", "System Settings", "enable_onboarding", 1)
@ -334,11 +333,6 @@ def load_user_details():
}
@frappe.whitelist()
def reset_is_first_startup():
frappe.db.set_value("System Settings", "System Settings", "is_first_startup", 0)
def prettify_args(args):
# remove attachments
for key, val in args.items():

View file

@ -348,7 +348,7 @@ def get_names_for_mentions(search_term):
def get_users_for_mentions():
return frappe.get_all(
return frappe.get_list(
"User",
fields=["name as id", "full_name as value"],
filters={
@ -361,7 +361,7 @@ def get_users_for_mentions():
def get_user_groups():
return frappe.get_all(
return frappe.get_list(
"User Group", fields=["name as id", "name as value"], update={"is_group": True}
)

View file

@ -2,7 +2,6 @@
# Copyright (c) 2015, Frappe Technologies and contributors
# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document

Some files were not shown because too many files have changed in this diff Show more