Merge branch 'develop' of https://github.com/frappe/frappe into fix-document-signature
This commit is contained in:
commit
0fab4de3b8
230 changed files with 3708 additions and 1609 deletions
76
.github/helper/flake8.conf
vendored
Normal file
76
.github/helper/flake8.conf
vendored
Normal 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
|
||||
32
.github/helper/roulette.py
vendored
32
.github/helper/roulette.py
vendored
|
|
@ -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"')
|
||||
|
|
|
|||
|
|
@ -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 }}"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
77
cypress/integration/control_color.js
Normal file
77
cypress/integration/control_color.js
Normal 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');
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
59
cypress/integration/custom_buttons.js
Normal file
59
cypress/integration/custom_buttons.js
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
140
cypress/integration/workspace_blocks.js
Normal file
140
cypress/integration/workspace_blocks.js
Normal 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');
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
|
|
|
|||
|
|
@ -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 "",
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@
|
|||
# License: MIT. See LICENSE
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
# test_records = frappe.get_test_records('Custom DocPerm')
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@
|
|||
# License: MIT. See LICENSE
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
# test_records = frappe.get_test_records('Custom Role')
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@
|
|||
# License: MIT. See LICENSE
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
# test_records = frappe.get_test_records('Deleted Document')
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"""
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@
|
|||
# License: MIT. See LICENSE
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
|
||||
class TestDomain(unittest.TestCase):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@
|
|||
# License: MIT. See LICENSE
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
# test_records = frappe.get_test_records('Error Snapshot')
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
# Copyright (c) 2017, Frappe Technologies and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@
|
|||
# License: MIT. See LICENSE
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
# test_records = frappe.get_test_records('Language')
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@
|
|||
# License: MIT. See LICENSE
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
# test_records = frappe.get_test_records('Module Def')
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@
|
|||
# License: MIT. See LICENSE
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
# test_records = frappe.get_test_records('Patch Log')
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@
|
|||
# License: MIT. See LICENSE
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
# test_records = frappe.get_test_records('Payment Gateway')
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@
|
|||
# License: MIT. See LICENSE
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
|
||||
class TestSMSSettings(unittest.TestCase):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@
|
|||
# License: MIT. See LICENSE
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
|
||||
class TestSystemSettings(unittest.TestCase):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
# Copyright (c) 2015, Frappe Technologies and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
# Copyright (c) 2018, Frappe Technologies and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
import frappe
|
||||
|
||||
|
||||
def get_context(context):
|
||||
# do your magic here
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@
|
|||
# License: MIT. See LICENSE
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
# test_records = frappe.get_test_records('Client Script')
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@
|
|||
# License: MIT. See LICENSE
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
# test_records = frappe.get_test_records('Property Setter')
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
) ;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ from frappe.utils import (
|
|||
cstr,
|
||||
date_diff,
|
||||
format_datetime,
|
||||
get_datetime,
|
||||
get_datetime_str,
|
||||
getdate,
|
||||
now_datetime,
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@
|
|||
# License: MIT. See LICENSE
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
# test_records = frappe.get_test_records('Kanban Board')
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
# Copyright (c) 2015, Frappe Technologies and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
# Copyright (c) 2015, Frappe Technologies and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
}
|
||||
0
frappe/desk/doctype/workspace_quick_list/__init__.py
Normal file
0
frappe/desk/doctype/workspace_quick_list/__init__.py
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.model import no_value_fields, table_fields
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Reference in a new issue