Merge branch 'develop' of https://github.com/frappe/frappe into fix-document-signature
This commit is contained in:
commit
e0e12d43f1
237 changed files with 6002 additions and 3956 deletions
2
.github/workflows/ui-tests.yml
vendored
2
.github/workflows/ui-tests.yml
vendored
|
|
@ -137,7 +137,7 @@ jobs:
|
|||
|
||||
- name: UI Tests
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests frappe --with-coverage --headless --parallel --ci-build-id $GITHUB_RUN_ID
|
||||
run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests frappe --with-coverage --headless --parallel --ci-build-id $GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT
|
||||
env:
|
||||
CYPRESS_RECORD_KEY: 4a48f41c-11b3-425b-aa88-c58048fa69eb
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ context('Control Barcode', () => {
|
|||
get_dialog_with_barcode().as('dialog');
|
||||
|
||||
cy.get('.frappe-control[data-fieldname=barcode]').findByRole('textbox')
|
||||
.focus()
|
||||
.type('123456789')
|
||||
.blur();
|
||||
cy.get('.frappe-control[data-fieldname=barcode] svg[data-barcode-value="123456789"]')
|
||||
|
|
@ -38,7 +37,6 @@ context('Control Barcode', () => {
|
|||
get_dialog_with_barcode().as('dialog');
|
||||
|
||||
cy.get('.frappe-control[data-fieldname=barcode]').findByRole('textbox')
|
||||
.focus()
|
||||
.type('123456789')
|
||||
.blur();
|
||||
cy.get('.frappe-control[data-fieldname=barcode]').findByRole('textbox')
|
||||
|
|
|
|||
|
|
@ -19,18 +19,18 @@ context('Control Icon', () => {
|
|||
get_dialog_with_icon().as('dialog');
|
||||
cy.get('.frappe-control[data-fieldname=icon]').findByRole('textbox').click();
|
||||
|
||||
cy.get('.icon-picker .icon-wrapper[id=active]').first().click();
|
||||
cy.get('.frappe-control[data-fieldname=icon]').findByRole('textbox').should('have.value', 'active');
|
||||
cy.get('.icon-picker .icon-wrapper[id=heart-active]').first().click();
|
||||
cy.get('.frappe-control[data-fieldname=icon]').findByRole('textbox').should('have.value', 'heart-active');
|
||||
cy.get('@dialog').then(dialog => {
|
||||
let value = dialog.get_value('icon');
|
||||
expect(value).to.equal('active');
|
||||
expect(value).to.equal('heart-active');
|
||||
});
|
||||
|
||||
cy.get('.icon-picker .icon-wrapper[id=resting]').first().click();
|
||||
cy.get('.frappe-control[data-fieldname=icon]').findByRole('textbox').should('have.value', 'resting');
|
||||
cy.get('.icon-picker .icon-wrapper[id=heart]').first().click();
|
||||
cy.get('.frappe-control[data-fieldname=icon]').findByRole('textbox').should('have.value', 'heart');
|
||||
cy.get('@dialog').then(dialog => {
|
||||
let value = dialog.get_value('icon');
|
||||
expect(value).to.equal('resting');
|
||||
expect(value).to.equal('heart');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,23 @@ context('Control Link', () => {
|
|||
cy.get('.frappe-control[data-fieldname=link] input').should('have.value', '');
|
||||
});
|
||||
|
||||
it("should be possible set empty value explicitly", () => {
|
||||
get_dialog_with_link().as("dialog");
|
||||
|
||||
cy.intercept("POST", "/api/method/frappe.client.validate_link").as("validate_link");
|
||||
|
||||
cy.get(".frappe-control[data-fieldname=link] input")
|
||||
.type(" ", { delay: 100 })
|
||||
.blur();
|
||||
cy.wait("@validate_link");
|
||||
cy.get(".frappe-control[data-fieldname=link] input").should("have.value", "");
|
||||
cy.window()
|
||||
.its("cur_dialog")
|
||||
.then((dialog) => {
|
||||
expect(dialog.get_value("link")).to.equal('');
|
||||
});
|
||||
});
|
||||
|
||||
it('should route to form on arrow click', () => {
|
||||
get_dialog_with_link().as('dialog');
|
||||
|
||||
|
|
@ -78,7 +95,7 @@ context('Control Link', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should fetch valid value', () => {
|
||||
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.client.validate_link').as('validate_link');
|
||||
|
|
@ -89,7 +106,67 @@ context('Control Link', () => {
|
|||
cy.get('.frappe-control[data-fieldname=assigned_by_full_name] .control-value').should(
|
||||
'contain', 'Administrator'
|
||||
);
|
||||
|
||||
cy.window()
|
||||
.its("cur_frm.doc.assigned_by")
|
||||
.should("eq", "Administrator");
|
||||
|
||||
// invalid input
|
||||
cy.get('@input').clear().type('invalid input', {delay: 100}).blur();
|
||||
cy.get('.frappe-control[data-fieldname=assigned_by_full_name] .control-value').should(
|
||||
'contain', ''
|
||||
);
|
||||
|
||||
cy.window()
|
||||
.its("cur_frm.doc.assigned_by")
|
||||
.should("eq", null);
|
||||
|
||||
// set valid value again
|
||||
cy.get('@input').clear().type('Administrator', {delay: 100}).blur();
|
||||
cy.wait('@validate_link');
|
||||
|
||||
cy.window()
|
||||
.its("cur_frm.doc.assigned_by")
|
||||
.should("eq", "Administrator");
|
||||
|
||||
// clear input
|
||||
cy.get('@input').clear().blur();
|
||||
cy.get('.frappe-control[data-fieldname=assigned_by_full_name] .control-value').should(
|
||||
'contain', ''
|
||||
);
|
||||
|
||||
cy.window()
|
||||
.its("cur_frm.doc.assigned_by")
|
||||
.should("eq", "");
|
||||
});
|
||||
});
|
||||
|
||||
it("should set default values", () => {
|
||||
cy.insert_doc("Property Setter", {
|
||||
"doctype_or_field": "DocField",
|
||||
"doc_type": "ToDo",
|
||||
"field_name": "assigned_by",
|
||||
"property": "default",
|
||||
"property_type": "Text",
|
||||
"value": "Administrator"
|
||||
}, true);
|
||||
cy.reload();
|
||||
cy.new_form("ToDo");
|
||||
cy.fill_field("description", "new", "Text Editor");
|
||||
cy.intercept("POST", "/api/method/frappe.desk.form.save.savedocs").as("save_form");
|
||||
cy.findByRole("button", {name: "Save"}).click();
|
||||
cy.wait("@save_form");
|
||||
cy.get(".frappe-control[data-fieldname=assigned_by_full_name] .control-value").should(
|
||||
"contain", "Administrator"
|
||||
);
|
||||
// if user clears default value explicitly, system should not reset default again
|
||||
cy.get_field("assigned_by").clear().blur();
|
||||
cy.intercept("POST", "/api/method/frappe.desk.form.save.savedocs").as("save_form");
|
||||
cy.findByRole("button", {name: "Save"}).click();
|
||||
cy.wait("@save_form");
|
||||
cy.get_field("assigned_by").should("have.value", "");
|
||||
cy.get(".frappe-control[data-fieldname=assigned_by_full_name] .control-value").should(
|
||||
"contain", ""
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ context('Control Date, Time and DateTime', () => {
|
|||
input_value: '12-02-2019 11:00' // admin timezone (Asia/Kolkata)
|
||||
}
|
||||
];
|
||||
|
||||
datetime_formats.forEach(d => {
|
||||
it(`test datetime format ${d.date_format} ${d.time_format}`, () => {
|
||||
cy.set_value('System Settings', 'System Settings', {
|
||||
|
|
|
|||
|
|
@ -77,11 +77,11 @@ context('MultiSelectDialog', () => {
|
|||
|
||||
it('tests more button', () => {
|
||||
cy.get_open_dialog()
|
||||
.get(`.frappe-control[data-fieldname="more_btn"]`)
|
||||
.get(`.frappe-control[data-fieldname="more_child_btn"]`)
|
||||
.should('exist')
|
||||
.as('more-btn');
|
||||
|
||||
cy.get_open_dialog().get('.list-item-container').should(($rows) => {
|
||||
cy.get_open_dialog().get('.datatable .dt-scrollable .dt-row').should(($rows) => {
|
||||
expect($rows).to.have.length(20);
|
||||
});
|
||||
|
||||
|
|
@ -89,7 +89,7 @@ context('MultiSelectDialog', () => {
|
|||
cy.get('@more-btn').find('button').click({force: true});
|
||||
cy.wait('@get-more-records');
|
||||
|
||||
cy.get_open_dialog().get('.list-item-container').should(($rows) => {
|
||||
cy.get_open_dialog().get('.datatable .dt-scrollable .dt-row').should(($rows) => {
|
||||
if ($rows.length <= 20) {
|
||||
throw new Error("More button doesn't work");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@ context('Report View', () => {
|
|||
cy.visit('/app/website');
|
||||
cy.insert_doc('DocType', custom_submittable_doctype, true);
|
||||
cy.clear_cache();
|
||||
});
|
||||
it('Field with enabled allow_on_submit should be editable.', () => {
|
||||
cy.insert_doc(doctype_name, {
|
||||
'title': 'Doc 1',
|
||||
'description': 'Random Text',
|
||||
|
|
@ -16,6 +14,8 @@ context('Report View', () => {
|
|||
// submit document
|
||||
'docstatus': 1
|
||||
}, true).as('doc');
|
||||
});
|
||||
it('Field with enabled allow_on_submit should be editable.', () => {
|
||||
cy.intercept('POST', 'api/method/frappe.client.set_value').as('value-update');
|
||||
cy.visit(`/app/List/${doctype_name}/Report`);
|
||||
// check status column added from docstatus
|
||||
|
|
|
|||
|
|
@ -14,12 +14,12 @@ context('Timeline Email', () => {
|
|||
cy.wait(700);
|
||||
});
|
||||
|
||||
it('Adding email and verifying timeline content for email attachment, deleting attachment and ToDo', () => {
|
||||
it('Adding email and verifying timeline content for email attachment', () => {
|
||||
cy.visit('/app/todo');
|
||||
cy.get('.list-row > .level-left > .list-subject').eq(0).click();
|
||||
|
||||
//Creating a new email
|
||||
cy.get('.timeline-actions > .btn').click();
|
||||
cy.get('.timeline-actions > .timeline-item > .action-buttons > .action-btn').click();
|
||||
cy.fill_field('recipients', 'test@example.com', 'MultiSelect');
|
||||
cy.get('.modal.show > .modal-dialog > .modal-content > .modal-body > :nth-child(1) > .form-layout > .form-page > :nth-child(3) > .section-body > .form-column > form > [data-fieldtype="Text Editor"] > .form-group > .control-input-wrapper > .control-input > .ql-container > .ql-editor').type('Test Mail');
|
||||
|
||||
|
|
@ -43,7 +43,9 @@ context('Timeline Email', () => {
|
|||
cy.get('#page-Communication > .page-head > .container > .row > .col > .standard-actions > .menu-btn-group > .btn').click();
|
||||
cy.get('#page-Communication > .page-head > .container > .row > .col > .standard-actions > .menu-btn-group > .dropdown-menu > li > .grey-link').eq(9).click();
|
||||
cy.get('.modal.show > .modal-dialog > .modal-content > .modal-footer > .standard-actions > .btn-primary').click();
|
||||
});
|
||||
|
||||
it('Deleting attachment and ToDo', () => {
|
||||
cy.visit('/app/todo');
|
||||
cy.get('.list-row > .level-left > .list-subject > .level-item.ellipsis > .ellipsis').eq(0).click();
|
||||
|
||||
|
|
@ -57,11 +59,11 @@ context('Timeline Email', () => {
|
|||
cy.wait(500);
|
||||
|
||||
//To check if the discard button functionality in email is working correctly
|
||||
cy.get('.timeline-actions > .btn').click();
|
||||
cy.get('.timeline-actions > .timeline-item > .action-buttons > .action-btn').click();
|
||||
cy.fill_field('recipients', 'test@example.com', 'MultiSelect');
|
||||
cy.get('.modal-footer > .standard-actions > .btn-secondary').contains('Discard').click();
|
||||
cy.wait(500);
|
||||
cy.get('.timeline-actions > .btn').click();
|
||||
cy.get('.timeline-actions > .timeline-item > .action-buttons > .action-btn').click();
|
||||
cy.wait(500);
|
||||
cy.get_field('recipients', 'MultiSelect').should('have.text', '');
|
||||
cy.get('.modal-header:visible > .modal-actions > .btn-modal-close > .icon').click();
|
||||
|
|
|
|||
|
|
@ -33,44 +33,39 @@ context('Workspace 2.0', () => {
|
|||
});
|
||||
|
||||
it('Add New Block', () => {
|
||||
cy.get('.codex-editor__redactor .ce-block');
|
||||
cy.get('.custom-actions .inner-group-button[data-label="Add%20Block"]').click();
|
||||
cy.get('.custom-actions .inner-group-button .dropdown-menu .block-menu-item-label').contains('Heading').click();
|
||||
cy.get('.ce-block').click().type('{enter}');
|
||||
cy.get('.block-list-container .block-list-item').contains('Heading').click();
|
||||
cy.get(":focus").type('Header');
|
||||
cy.get(".ce-block:last").find('.ce-header').should('exist');
|
||||
|
||||
cy.get('.custom-actions .inner-group-button[data-label="Add%20Block"]').click();
|
||||
cy.get('.custom-actions .inner-group-button .dropdown-menu .block-menu-item-label').contains('Text').click();
|
||||
cy.get('.ce-block:last').click().type('{enter}');
|
||||
cy.get('.block-list-container .block-list-item').contains('Text').click();
|
||||
cy.get(":focus").type('Paragraph text');
|
||||
cy.get(".ce-block:last").find('.ce-paragraph').should('exist');
|
||||
});
|
||||
|
||||
it('Delete A Block', () => {
|
||||
cy.get(".ce-block:last").find('.delete-paragraph').click();
|
||||
cy.get(":focus").click();
|
||||
cy.get('.paragraph-control .setting-btn').click();
|
||||
cy.get('.paragraph-control .dropdown-item').contains('Delete').click();
|
||||
cy.get(".ce-block:last").find('.ce-paragraph').should('not.exist');
|
||||
});
|
||||
|
||||
it('Shrink and Expand A Block', () => {
|
||||
cy.get(".ce-block:last").find('.tune-btn').click();
|
||||
cy.get('.ce-settings--opened .ce-shrink-button').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-11');
|
||||
cy.get('.ce-settings--opened .ce-shrink-button').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-10');
|
||||
cy.get('.ce-settings--opened .ce-shrink-button').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-9');
|
||||
cy.get('.ce-settings--opened .ce-expand-button').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-10');
|
||||
cy.get('.ce-settings--opened .ce-expand-button').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-11');
|
||||
cy.get('.ce-settings--opened .ce-expand-button').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-12');
|
||||
});
|
||||
|
||||
it('Change Header Text Size', () => {
|
||||
cy.get('.ce-settings--opened .cdx-settings-button[data-level="3"]').click();
|
||||
cy.get(".ce-block:last").find('.widget-head h3').should('exist');
|
||||
cy.get('.ce-settings--opened .cdx-settings-button[data-level="4"]').click();
|
||||
cy.get(".ce-block:last").find('.widget-head h4').should('exist');
|
||||
cy.get(":focus").click();
|
||||
cy.get('.ce-block:last .setting-btn').click();
|
||||
cy.get('.ce-block:last .dropdown-item').contains('Shrink').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-xs-11');
|
||||
cy.get('.ce-block:last .dropdown-item').contains('Shrink').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-xs-10');
|
||||
cy.get('.ce-block:last .dropdown-item').contains('Shrink').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-xs-9');
|
||||
cy.get('.ce-block:last .dropdown-item').contains('Expand').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-xs-10');
|
||||
cy.get('.ce-block:last .dropdown-item').contains('Expand').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-xs-11');
|
||||
cy.get('.ce-block:last .dropdown-item').contains('Expand').click();
|
||||
cy.get(".ce-block:last").should('have.class', 'col-xs-12');
|
||||
|
||||
cy.get('.standard-actions .btn-primary[data-label="Save Customizations"]').click();
|
||||
});
|
||||
|
|
@ -79,7 +74,10 @@ context('Workspace 2.0', () => {
|
|||
cy.get('.codex-editor__redactor .ce-block');
|
||||
cy.get('.standard-actions .btn-secondary[data-label=Edit]').click();
|
||||
|
||||
cy.get('.sidebar-item-container[item-name="Test Private Page"]').find('.sidebar-item-control .delete-page').click();
|
||||
cy.get('.sidebar-item-container[item-name="Test Private Page"]')
|
||||
.find('.sidebar-item-control .setting-btn').click();
|
||||
cy.get('.sidebar-item-container[item-name="Test Private Page"]')
|
||||
.find('.dropdown-item[title="Delete Workspace"]').click({force: true});
|
||||
cy.wait(300);
|
||||
cy.get('.modal-footer > .standard-actions > .btn-modal-primary:visible').first().click();
|
||||
cy.get('.standard-actions .btn-primary[data-label="Save Customizations"]').click();
|
||||
|
|
|
|||
|
|
@ -110,34 +110,6 @@ Cypress.Commands.add('get_doc', (doctype, name) => {
|
|||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('insert_doc', (doctype, args, ignore_duplicate) => {
|
||||
return cy
|
||||
.window()
|
||||
.its('frappe.csrf_token')
|
||||
.then(csrf_token => {
|
||||
return cy
|
||||
.request({
|
||||
method: 'POST',
|
||||
url: `/api/resource/${doctype}`,
|
||||
body: args,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'X-Frappe-CSRF-Token': csrf_token
|
||||
},
|
||||
failOnStatusCode: !ignore_duplicate
|
||||
})
|
||||
.then(res => {
|
||||
let status_codes = [200];
|
||||
if (ignore_duplicate) {
|
||||
status_codes.push(409);
|
||||
}
|
||||
expect(res.status).to.be.oneOf(status_codes);
|
||||
return res.body;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('remove_doc', (doctype, name) => {
|
||||
return cy
|
||||
.window()
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ Read the documentation: https://frappeframework.com/docs
|
|||
"""
|
||||
import os, warnings
|
||||
|
||||
STANDARD_USERS = ('Guest', 'Administrator')
|
||||
|
||||
_dev_server = os.environ.get('DEV_SERVER', False)
|
||||
|
||||
if _dev_server:
|
||||
|
|
@ -100,7 +102,7 @@ def as_unicode(text, encoding='utf-8'):
|
|||
'''Convert to unicode if required'''
|
||||
if isinstance(text, str):
|
||||
return text
|
||||
elif text==None:
|
||||
elif text is None:
|
||||
return ''
|
||||
elif isinstance(text, bytes):
|
||||
return str(text, encoding)
|
||||
|
|
@ -121,6 +123,7 @@ def set_user_lang(user, user_language=None):
|
|||
local.lang = get_user_lang(user)
|
||||
|
||||
# local-globals
|
||||
|
||||
db = local("db")
|
||||
qb = local("qb")
|
||||
conf = local("conf")
|
||||
|
|
@ -140,6 +143,8 @@ lang = local("lang")
|
|||
# This if block is never executed when running the code. It is only used for
|
||||
# telling static code analyzer where to find dynamically defined attributes.
|
||||
if typing.TYPE_CHECKING:
|
||||
from frappe.utils.redis_wrapper import RedisWrapper
|
||||
|
||||
from frappe.database.mariadb.database import MariaDBDatabase
|
||||
from frappe.database.postgres.database import PostgresDatabase
|
||||
from frappe.query_builder.builder import MariaDB, Postgres
|
||||
|
|
@ -147,6 +152,7 @@ if typing.TYPE_CHECKING:
|
|||
db: typing.Union[MariaDBDatabase, PostgresDatabase]
|
||||
qb: typing.Union[MariaDB, Postgres]
|
||||
|
||||
|
||||
# end: static analysis hack
|
||||
|
||||
def init(site, sites_path=None, new_site=False):
|
||||
|
|
@ -291,7 +297,7 @@ def get_conf(site=None):
|
|||
|
||||
class init_site:
|
||||
def __init__(self, site=None):
|
||||
'''If site==None, initialize it for empty site ('') to load common_site_config.json'''
|
||||
'''If site is None, initialize it for empty site ('') to load common_site_config.json'''
|
||||
self.site = site or ''
|
||||
|
||||
def __enter__(self):
|
||||
|
|
@ -308,9 +314,8 @@ def destroy():
|
|||
|
||||
release_local(local)
|
||||
|
||||
# memcache
|
||||
redis_server = None
|
||||
def cache():
|
||||
def cache() -> "RedisWrapper":
|
||||
"""Returns redis connection."""
|
||||
global redis_server
|
||||
if not redis_server:
|
||||
|
|
@ -443,7 +448,7 @@ def throw(msg, exc=ValidationError, title=None, is_minimizable=None, wide=None,
|
|||
msgprint(msg, raise_exception=exc, title=title, indicator='red', is_minimizable=is_minimizable, wide=wide, as_list=as_list)
|
||||
|
||||
def emit_js(js, user=False, **kwargs):
|
||||
if user == False:
|
||||
if user is False:
|
||||
user = session.user
|
||||
publish_realtime('eval_js', js, user=user, **kwargs)
|
||||
|
||||
|
|
@ -1658,7 +1663,7 @@ def local_cache(namespace, key, generator, regenerate_if_none=False):
|
|||
if key not in local.cache[namespace]:
|
||||
local.cache[namespace][key] = generator()
|
||||
|
||||
elif local.cache[namespace][key]==None and regenerate_if_none:
|
||||
elif local.cache[namespace][key] is None and regenerate_if_none:
|
||||
# if key exists but the previous result was None
|
||||
local.cache[namespace][key] = generator()
|
||||
|
||||
|
|
|
|||
|
|
@ -192,12 +192,7 @@ def make_form_dict(request):
|
|||
if not isinstance(args, dict):
|
||||
frappe.throw(_("Invalid request arguments"))
|
||||
|
||||
try:
|
||||
frappe.local.form_dict = frappe._dict({
|
||||
k: v[0] if isinstance(v, (list, tuple)) else v for k, v in args.items()
|
||||
})
|
||||
except IndexError:
|
||||
frappe.local.form_dict = frappe._dict(args)
|
||||
frappe.local.form_dict = frappe._dict(args)
|
||||
|
||||
if "_" in frappe.local.form_dict:
|
||||
# _ is passed by $.ajax so that the request is not cached by the browser. So, remove _ from form_dict
|
||||
|
|
|
|||
|
|
@ -111,7 +111,8 @@ class LoginManager:
|
|||
self.user_type = None
|
||||
|
||||
if frappe.local.form_dict.get('cmd')=='login' or frappe.local.request.path=="/api/method/login":
|
||||
if self.login()==False: return
|
||||
if self.login() is False:
|
||||
return
|
||||
self.resume = False
|
||||
|
||||
# run login triggers
|
||||
|
|
@ -250,8 +251,7 @@ class LoginManager:
|
|||
if not self.user:
|
||||
return
|
||||
|
||||
from frappe.core.doctype.user.user import STANDARD_USERS
|
||||
if self.user in STANDARD_USERS:
|
||||
if self.user in frappe.STANDARD_USERS:
|
||||
return False
|
||||
|
||||
reset_pwd_after_days = cint(frappe.db.get_single_value("System Settings",
|
||||
|
|
|
|||
|
|
@ -272,7 +272,7 @@ def apply(doc=None, method=None, doctype=None, name=None):
|
|||
for todo in todos_to_close:
|
||||
_todo = frappe.get_doc("ToDo", todo)
|
||||
_todo.status = "Closed"
|
||||
_todo.save()
|
||||
_todo.save(ignore_permissions=True)
|
||||
break
|
||||
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"charts": [],
|
||||
"content": "[{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"ToDo\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Note\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"File\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Assignment Rule\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Auto Repeat\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Tools\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Email\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Automation\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Event Streaming\", \"col\": 4}}]",
|
||||
"content": "[{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"ToDo\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Note\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"File\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Assignment Rule\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Auto Repeat\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Email\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Automation\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Event Streaming\",\"col\":4}}]",
|
||||
"creation": "2020-03-02 14:53:24.980279",
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace",
|
||||
|
|
@ -208,7 +208,7 @@
|
|||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2021-08-05 12:16:02.839181",
|
||||
"modified": "2022-01-13 17:48:48.456763",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Automation",
|
||||
"name": "Tools",
|
||||
|
|
@ -217,7 +217,7 @@
|
|||
"public": 1,
|
||||
"restrict_to_domain": "",
|
||||
"roles": [],
|
||||
"sequence_id": 26,
|
||||
"sequence_id": 26.0,
|
||||
"shortcuts": [
|
||||
{
|
||||
"label": "ToDo",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ bootstrap client session
|
|||
import frappe
|
||||
import frappe.defaults
|
||||
import frappe.desk.desk_page
|
||||
from frappe.desk.doctype.route_history.route_history import frequently_visited_links
|
||||
from frappe.desk.form.load import get_meta_bundle
|
||||
from frappe.utils.change_log import get_versions
|
||||
from frappe.translate import get_lang_dict
|
||||
|
|
@ -15,9 +16,8 @@ from frappe.social.doctype.energy_point_settings.energy_point_settings import is
|
|||
from frappe.website.doctype.web_page_view.web_page_view import is_tracking_enabled
|
||||
from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_points
|
||||
from frappe.model.base_document import get_controller
|
||||
from frappe.social.doctype.post.post import frequently_visited_links
|
||||
from frappe.core.doctype.navbar_settings.navbar_settings import get_navbar_settings, get_app_logo
|
||||
from frappe.utils import get_time_zone
|
||||
from frappe.utils import get_time_zone, add_user_info
|
||||
|
||||
def get_bootinfo():
|
||||
"""build and return boot info"""
|
||||
|
|
@ -107,8 +107,8 @@ def load_conf_settings(bootinfo):
|
|||
if key in conf: bootinfo[key] = conf.get(key)
|
||||
|
||||
def load_desktop_data(bootinfo):
|
||||
from frappe.desk.desktop import get_wspace_sidebar_items
|
||||
bootinfo.allowed_workspaces = get_wspace_sidebar_items().get('pages')
|
||||
from frappe.desk.desktop import get_workspace_sidebar_items
|
||||
bootinfo.allowed_workspaces = get_workspace_sidebar_items().get('pages')
|
||||
bootinfo.module_page_map = get_controller("Workspace").get_module_page_map()
|
||||
bootinfo.dashboards = frappe.get_all("Dashboard")
|
||||
|
||||
|
|
@ -222,17 +222,14 @@ def load_translations(bootinfo):
|
|||
bootinfo["__messages"] = messages
|
||||
|
||||
def get_user_info():
|
||||
user_info = frappe.db.get_all('User', fields=['`name`', 'full_name as fullname', 'user_image as image', 'gender',
|
||||
'email', 'username', 'bio', 'location', 'interest', 'banner_image', 'allowed_in_mentions', 'user_type', 'time_zone'],
|
||||
filters=dict(enabled=1))
|
||||
# get info for current user
|
||||
user_info = frappe._dict()
|
||||
add_user_info(frappe.session.user, user_info)
|
||||
|
||||
user_info_map = {d.name: d for d in user_info}
|
||||
if frappe.session.user == 'Administrator' and user_info.Administrator.email:
|
||||
user_info[user_info.Administrator.email] = user_info.Administrator
|
||||
|
||||
admin_data = user_info_map.get('Administrator')
|
||||
if admin_data:
|
||||
user_info_map[admin_data.email] = admin_data
|
||||
|
||||
return user_info_map
|
||||
return user_info
|
||||
|
||||
def get_user(bootinfo):
|
||||
"""get user info"""
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ def build_table_count_cache():
|
|||
data = (
|
||||
frappe.qb.from_(information_schema.tables).select(table_name, table_rows)
|
||||
).run(as_dict=True)
|
||||
counts = {d.get('name').lstrip('tab'): d.get('count', None) for d in data}
|
||||
counts = {d.get('name').replace('tab', '', 1): d.get('count', None) for d in data}
|
||||
_cache.set_value("information_schema:counts", counts)
|
||||
|
||||
return counts
|
||||
|
|
|
|||
|
|
@ -99,7 +99,6 @@ def get_value(doctype, fieldname, filters=None, as_dict=True, debug=False, paren
|
|||
if not filters:
|
||||
filters = None
|
||||
|
||||
|
||||
if frappe.get_meta(doctype).issingle:
|
||||
value = frappe.db.get_values_from_single(fields, filters, doctype, as_dict=as_dict, debug=debug)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -952,7 +952,7 @@ def trim_database(context, dry_run, format, no_backup):
|
|||
doctype_tables = frappe.get_all("DocType", pluck="name")
|
||||
|
||||
for x in database_tables:
|
||||
doctype = x.lstrip("tab")
|
||||
doctype = x.replace("tab", "", 1)
|
||||
if not (doctype in doctype_tables or x.startswith("__") or x in STANDARD_TABLES):
|
||||
TABLES_TO_DROP.append(x)
|
||||
|
||||
|
|
@ -966,7 +966,7 @@ def trim_database(context, dry_run, format, no_backup):
|
|||
|
||||
odb = scheduled_backup(
|
||||
ignore_conf=False,
|
||||
include_doctypes=",".join(x.lstrip("tab") for x in TABLES_TO_DROP),
|
||||
include_doctypes=",".join(x.replace("tab", "", 1) for x in TABLES_TO_DROP),
|
||||
ignore_files=True,
|
||||
force=True,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -623,6 +623,7 @@ def transform_database(context, table, engine, row_format, failfast):
|
|||
@click.command('run-tests')
|
||||
@click.option('--app', help="For App")
|
||||
@click.option('--doctype', help="For DocType")
|
||||
@click.option('--case', help="Select particular TestCase")
|
||||
@click.option('--doctype-list-path', help="Path to .txt file for list of doctypes. Example erpnext/tests/server/agriculture.txt")
|
||||
@click.option('--test', multiple=True, help="Specific test")
|
||||
@click.option('--ui-tests', is_flag=True, default=False, help="Run UI Tests")
|
||||
|
|
@ -636,7 +637,7 @@ def transform_database(context, table, engine, row_format, failfast):
|
|||
@pass_context
|
||||
def run_tests(context, app=None, module=None, doctype=None, test=(), profile=False,
|
||||
coverage=False, junit_xml_output=False, ui_tests = False, doctype_list_path=None,
|
||||
skip_test_records=False, skip_before_tests=False, failfast=False):
|
||||
skip_test_records=False, skip_before_tests=False, failfast=False, case=None):
|
||||
|
||||
with CodeCoverage(coverage, app):
|
||||
import frappe.test_runner
|
||||
|
|
@ -658,7 +659,7 @@ def run_tests(context, app=None, module=None, doctype=None, test=(), profile=Fal
|
|||
|
||||
ret = frappe.test_runner.main(app, module, doctype, context.verbose, tests=tests,
|
||||
force=context.force, profile=profile, junit_xml_output=junit_xml_output,
|
||||
ui_tests=ui_tests, doctype_list_path=doctype_list_path, failfast=failfast)
|
||||
ui_tests=ui_tests, doctype_list_path=doctype_list_path, failfast=failfast, case=case)
|
||||
|
||||
if len(ret.failures) == 0 and len(ret.errors) == 0:
|
||||
ret = 0
|
||||
|
|
|
|||
|
|
@ -1,29 +1,32 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe
|
||||
import json
|
||||
from email.utils import formataddr
|
||||
from frappe.core.utils import get_parent_doc
|
||||
from frappe.utils import (get_url, get_formatted_email, cint, list_to_str,
|
||||
validate_email_address, split_emails, parse_addr, get_datetime)
|
||||
from frappe.email.email_body import get_message_id
|
||||
from typing import TYPE_CHECKING, Dict
|
||||
|
||||
import frappe
|
||||
import frappe.email.smtp
|
||||
import time
|
||||
from frappe import _
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
from frappe.email.email_body import get_message_id
|
||||
from frappe.utils import (cint, get_datetime, get_formatted_email,
|
||||
list_to_str, split_emails, validate_email_address)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.core.doctype.communication.communication import Communication
|
||||
|
||||
|
||||
OUTGOING_EMAIL_ACCOUNT_MISSING = _("""
|
||||
Unable to send mail because of a missing email account.
|
||||
Please setup default Email Account from Setup > Email > Email Account
|
||||
""")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make(doctype=None, name=None, content=None, subject=None, sent_or_received = "Sent",
|
||||
sender=None, sender_full_name=None, recipients=None, communication_medium="Email", send_email=False,
|
||||
print_html=None, print_format=None, attachments='[]', send_me_a_copy=False, cc=None, bcc=None,
|
||||
flags=None, read_receipt=None, print_letterhead=True, email_template=None, communication_type=None,
|
||||
ignore_permissions=False):
|
||||
ignore_permissions=False) -> Dict[str, str]:
|
||||
"""Make a new communication.
|
||||
|
||||
:param doctype: Reference DocType.
|
||||
|
|
@ -56,7 +59,7 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received =
|
|||
cc = list_to_str(cc) if isinstance(cc, list) else cc
|
||||
bcc = list_to_str(bcc) if isinstance(bcc, list) else bcc
|
||||
|
||||
comm = frappe.get_doc({
|
||||
comm: "Communication" = frappe.get_doc({
|
||||
"doctype":"Communication",
|
||||
"subject": subject,
|
||||
"content": content,
|
||||
|
|
@ -73,16 +76,13 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received =
|
|||
"message_id":get_message_id().strip(" <>"),
|
||||
"read_receipt":read_receipt,
|
||||
"has_attachment": 1 if attachments else 0,
|
||||
"communication_type": communication_type
|
||||
"communication_type": communication_type,
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
comm.save(ignore_permissions=True)
|
||||
|
||||
if isinstance(attachments, str):
|
||||
attachments = json.loads(attachments)
|
||||
|
||||
# if not committed, delayed task doesn't find the communication
|
||||
if attachments:
|
||||
if isinstance(attachments, str):
|
||||
attachments = json.loads(attachments)
|
||||
add_attachments(comm.name, attachments)
|
||||
|
||||
if cint(send_email):
|
||||
|
|
@ -93,12 +93,13 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received =
|
|||
send_me_a_copy=send_me_a_copy, print_letterhead=print_letterhead)
|
||||
|
||||
emails_not_sent_to = comm.exclude_emails_list(include_sender=send_me_a_copy)
|
||||
|
||||
return {
|
||||
"name": comm.name,
|
||||
"emails_not_sent_to": ", ".join(emails_not_sent_to or [])
|
||||
"emails_not_sent_to": ", ".join(emails_not_sent_to)
|
||||
}
|
||||
|
||||
def validate_email(doc):
|
||||
def validate_email(doc: "Communication") -> None:
|
||||
"""Validate Email Addresses of Recipients and CC"""
|
||||
if not (doc.communication_type=="Communication" and doc.communication_medium == "Email") or doc.flags.in_receive:
|
||||
return
|
||||
|
|
@ -114,8 +115,6 @@ def validate_email(doc):
|
|||
for email in split_emails(doc.bcc):
|
||||
validate_email_address(email, throw=True)
|
||||
|
||||
# validate sender
|
||||
|
||||
def set_incoming_outgoing_accounts(doc):
|
||||
from frappe.email.doctype.email_account.email_account import EmailAccount
|
||||
incoming_email_account = EmailAccount.find_incoming(
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from typing import List
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.core.utils import get_parent_doc
|
||||
|
|
@ -194,14 +195,18 @@ class CommunicationEmailMixin:
|
|||
return _("Leave this conversation")
|
||||
return ''
|
||||
|
||||
def exclude_emails_list(self, is_inbound_mail_communcation=False, include_sender=False):
|
||||
def exclude_emails_list(self, is_inbound_mail_communcation=False, include_sender=False) -> List:
|
||||
"""List of mail id's excluded while sending mail.
|
||||
"""
|
||||
all_ids = self.get_all_email_addresses(exclude_displayname=True)
|
||||
final_ids = self.mail_recipients(is_inbound_mail_communcation = is_inbound_mail_communcation) + \
|
||||
self.mail_bcc(is_inbound_mail_communcation = is_inbound_mail_communcation) + \
|
||||
self.mail_cc(is_inbound_mail_communcation = is_inbound_mail_communcation, include_sender=include_sender)
|
||||
return set(all_ids) - set(final_ids)
|
||||
|
||||
final_ids = (
|
||||
self.mail_recipients(is_inbound_mail_communcation=is_inbound_mail_communcation)
|
||||
+ self.mail_bcc(is_inbound_mail_communcation=is_inbound_mail_communcation)
|
||||
+ self.mail_cc(is_inbound_mail_communcation=is_inbound_mail_communcation, include_sender=include_sender)
|
||||
)
|
||||
|
||||
return list(set(all_ids) - set(final_ids))
|
||||
|
||||
def get_assignees(self):
|
||||
"""Get owners of the reference document.
|
||||
|
|
|
|||
|
|
@ -314,7 +314,7 @@ class DataExporter:
|
|||
.where(child_doctype_table.parentfield == c["parentfield"])
|
||||
.orderby(child_doctype_table.idx)
|
||||
)
|
||||
for ci, child in enumerate(data_row.run()):
|
||||
for ci, child in enumerate(data_row.run(as_dict=True)):
|
||||
self.add_data_row(rows, c['doctype'], c['parentfield'], child, ci)
|
||||
|
||||
for row in rows:
|
||||
|
|
|
|||
105
frappe/core/doctype/data_export/test_data_exporter.py
Normal file
105
frappe/core/doctype/data_export/test_data_exporter.py
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import unittest
|
||||
import frappe
|
||||
from frappe.core.doctype.data_export.exporter import DataExporter
|
||||
|
||||
class TestDataExporter(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.doctype_name = 'Test DocType for Export Tool'
|
||||
self.doc_name = 'Test Data for Export Tool'
|
||||
self.create_doctype_if_not_exists(doctype_name=self.doctype_name)
|
||||
self.create_test_data()
|
||||
|
||||
def create_doctype_if_not_exists(self, doctype_name, force=False):
|
||||
"""
|
||||
Helper Function for setting up doctypes
|
||||
"""
|
||||
if force:
|
||||
frappe.delete_doc_if_exists('DocType', doctype_name)
|
||||
frappe.delete_doc_if_exists('DocType', 'Child 1 of ' + doctype_name)
|
||||
|
||||
if frappe.db.exists('DocType', doctype_name):
|
||||
return
|
||||
|
||||
# Child Table 1
|
||||
table_1_name = 'Child 1 of ' + doctype_name
|
||||
frappe.get_doc({
|
||||
'doctype': 'DocType',
|
||||
'name': table_1_name,
|
||||
'module': 'Custom',
|
||||
'custom': 1,
|
||||
'istable': 1,
|
||||
'fields': [
|
||||
{'label': 'Child Title', 'fieldname': 'child_title', 'reqd': 1, 'fieldtype': 'Data'},
|
||||
{'label': 'Child Number', 'fieldname': 'child_number', 'fieldtype': 'Int'},
|
||||
]
|
||||
}).insert()
|
||||
|
||||
# Main Table
|
||||
frappe.get_doc({
|
||||
'doctype': 'DocType',
|
||||
'name': doctype_name,
|
||||
'module': 'Custom',
|
||||
'custom': 1,
|
||||
'autoname': 'field:title',
|
||||
'fields': [
|
||||
{'label': 'Title', 'fieldname': 'title', 'reqd': 1, 'fieldtype': 'Data'},
|
||||
{'label': 'Number', 'fieldname': 'number', 'fieldtype': 'Int'},
|
||||
{'label': 'Table Field 1', 'fieldname': 'table_field_1', 'fieldtype': 'Table', 'options': table_1_name},
|
||||
],
|
||||
'permissions': [
|
||||
{'role': 'System Manager'}
|
||||
]
|
||||
}).insert()
|
||||
|
||||
def create_test_data(self, force=False):
|
||||
"""
|
||||
Helper Function creating test data
|
||||
"""
|
||||
if force:
|
||||
frappe.delete_doc(self.doctype_name, self.doc_name)
|
||||
|
||||
if not frappe.db.exists(self.doctype_name, self.doc_name):
|
||||
self.doc = frappe.get_doc(
|
||||
doctype=self.doctype_name,
|
||||
title=self.doc_name,
|
||||
number="100",
|
||||
table_field_1=[
|
||||
{"child_title": "Child Title 1", "child_number": "50"},
|
||||
{"child_title": "Child Title 2", "child_number": "51"},
|
||||
]
|
||||
).insert()
|
||||
else:
|
||||
self.doc = frappe.get_doc(self.doctype_name, self.doc_name)
|
||||
|
||||
def test_export_content(self):
|
||||
exp = DataExporter(doctype=self.doctype_name, file_type='CSV')
|
||||
exp.build_response()
|
||||
|
||||
self.assertEqual(frappe.response['type'],'csv')
|
||||
self.assertEqual(frappe.response['doctype'], self.doctype_name)
|
||||
self.assertTrue(frappe.response['result'])
|
||||
self.assertIn('Child Title 1\",50',frappe.response['result'])
|
||||
self.assertIn('Child Title 2\",51',frappe.response['result'])
|
||||
|
||||
def test_export_type(self):
|
||||
for type in ['csv', 'Excel']:
|
||||
with self.subTest(type=type):
|
||||
exp = DataExporter(doctype=self.doctype_name, file_type=type)
|
||||
exp.build_response()
|
||||
|
||||
self.assertEqual(frappe.response['doctype'], self.doctype_name)
|
||||
self.assertTrue(frappe.response['result'])
|
||||
|
||||
if type == 'csv':
|
||||
self.assertEqual(frappe.response['type'],'csv')
|
||||
elif type == 'Excel':
|
||||
self.assertEqual(frappe.response['type'],'binary')
|
||||
self.assertEqual(frappe.response['filename'], self.doctype_name+'.xlsx') # 'Test DocType for Export Tool.xlsx')
|
||||
self.assertTrue(frappe.response['filecontent'])
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
|
|
@ -44,6 +44,7 @@ frappe.ui.form.on('Data Import', {
|
|||
}
|
||||
frm.dashboard.show_progress(__('Import Progress'), percent, message);
|
||||
frm.page.set_indicator(__('In Progress'), 'orange');
|
||||
frm.trigger('update_primary_action');
|
||||
|
||||
// hide progress when complete
|
||||
if (data.current === data.total) {
|
||||
|
|
@ -80,7 +81,10 @@ frappe.ui.form.on('Data Import', {
|
|||
frm.trigger('show_import_log');
|
||||
frm.trigger('show_import_warnings');
|
||||
frm.trigger('toggle_submit_after_import');
|
||||
frm.trigger('show_import_status');
|
||||
|
||||
if (frm.doc.status != 'Pending')
|
||||
frm.trigger('show_import_status');
|
||||
|
||||
frm.trigger('show_report_error_button');
|
||||
|
||||
if (frm.doc.status === 'Partial Success') {
|
||||
|
|
@ -128,40 +132,49 @@ frappe.ui.form.on('Data Import', {
|
|||
},
|
||||
|
||||
show_import_status(frm) {
|
||||
let import_log = JSON.parse(frm.doc.import_log || '[]');
|
||||
let successful_records = import_log.filter(log => log.success);
|
||||
let failed_records = import_log.filter(log => !log.success);
|
||||
if (successful_records.length === 0) return;
|
||||
frappe.call({
|
||||
'method': 'frappe.core.doctype.data_import.data_import.get_import_status',
|
||||
'args': {
|
||||
'data_import_name': frm.doc.name
|
||||
},
|
||||
'callback': function(r) {
|
||||
let successful_records = cint(r.message.success);
|
||||
let failed_records = cint(r.message.failed);
|
||||
let total_records = cint(r.message.total_records);
|
||||
|
||||
let message;
|
||||
if (failed_records.length === 0) {
|
||||
let message_args = [successful_records.length];
|
||||
if (frm.doc.import_type === 'Insert New Records') {
|
||||
message =
|
||||
successful_records.length > 1
|
||||
? __('Successfully imported {0} records.', message_args)
|
||||
: __('Successfully imported {0} record.', message_args);
|
||||
} else {
|
||||
message =
|
||||
successful_records.length > 1
|
||||
? __('Successfully updated {0} records.', message_args)
|
||||
: __('Successfully updated {0} record.', message_args);
|
||||
if (!total_records) return;
|
||||
|
||||
let message;
|
||||
if (failed_records === 0) {
|
||||
let message_args = [successful_records];
|
||||
if (frm.doc.import_type === 'Insert New Records') {
|
||||
message =
|
||||
successful_records > 1
|
||||
? __('Successfully imported {0} records.', message_args)
|
||||
: __('Successfully imported {0} record.', message_args);
|
||||
} else {
|
||||
message =
|
||||
successful_records > 1
|
||||
? __('Successfully updated {0} records.', message_args)
|
||||
: __('Successfully updated {0} record.', message_args);
|
||||
}
|
||||
} else {
|
||||
let message_args = [successful_records, total_records];
|
||||
if (frm.doc.import_type === 'Insert New Records') {
|
||||
message =
|
||||
successful_records > 1
|
||||
? __('Successfully imported {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.', message_args)
|
||||
: __('Successfully imported {0} record out of {1}. Click on Export Errored Rows, fix the errors and import again.', message_args);
|
||||
} else {
|
||||
message =
|
||||
successful_records > 1
|
||||
? __('Successfully updated {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.', message_args)
|
||||
: __('Successfully updated {0} record out of {1}. Click on Export Errored Rows, fix the errors and import again.', message_args);
|
||||
}
|
||||
}
|
||||
frm.dashboard.set_headline(message);
|
||||
}
|
||||
} else {
|
||||
let message_args = [successful_records.length, import_log.length];
|
||||
if (frm.doc.import_type === 'Insert New Records') {
|
||||
message =
|
||||
successful_records.length > 1
|
||||
? __('Successfully imported {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.', message_args)
|
||||
: __('Successfully imported {0} record out of {1}. Click on Export Errored Rows, fix the errors and import again.', message_args);
|
||||
} else {
|
||||
message =
|
||||
successful_records.length > 1
|
||||
? __('Successfully updated {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.', message_args)
|
||||
: __('Successfully updated {0} record out of {1}. Click on Export Errored Rows, fix the errors and import again.', message_args);
|
||||
}
|
||||
}
|
||||
frm.dashboard.set_headline(message);
|
||||
});
|
||||
},
|
||||
|
||||
show_report_error_button(frm) {
|
||||
|
|
@ -275,7 +288,7 @@ frappe.ui.form.on('Data Import', {
|
|||
},
|
||||
|
||||
show_import_preview(frm, preview_data) {
|
||||
let import_log = JSON.parse(frm.doc.import_log || '[]');
|
||||
let import_log = preview_data.import_log;
|
||||
|
||||
if (
|
||||
frm.import_preview &&
|
||||
|
|
@ -316,6 +329,15 @@ frappe.ui.form.on('Data Import', {
|
|||
);
|
||||
},
|
||||
|
||||
export_import_log(frm) {
|
||||
open_url_post(
|
||||
'/api/method/frappe.core.doctype.data_import.data_import.download_import_log',
|
||||
{
|
||||
data_import_name: frm.doc.name
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
show_import_warnings(frm, preview_data) {
|
||||
let columns = preview_data.columns;
|
||||
let warnings = JSON.parse(frm.doc.template_warnings || '[]');
|
||||
|
|
@ -391,92 +413,131 @@ frappe.ui.form.on('Data Import', {
|
|||
frm.trigger('show_import_log');
|
||||
},
|
||||
|
||||
show_import_log(frm) {
|
||||
let import_log = JSON.parse(frm.doc.import_log || '[]');
|
||||
let logs = import_log;
|
||||
frm.toggle_display('import_log', false);
|
||||
frm.toggle_display('import_log_section', logs.length > 0);
|
||||
render_import_log(frm) {
|
||||
frappe.call({
|
||||
'method': 'frappe.client.get_list',
|
||||
'args': {
|
||||
'doctype': 'Data Import Log',
|
||||
'filters': {
|
||||
'data_import': frm.doc.name
|
||||
},
|
||||
'fields': ['success', 'docname', 'messages', 'exception', 'row_indexes'],
|
||||
'limit_page_length': 5000,
|
||||
'order_by': 'log_index'
|
||||
},
|
||||
callback: function(r) {
|
||||
let logs = r.message;
|
||||
|
||||
if (logs.length === 0) {
|
||||
frm.get_field('import_log_preview').$wrapper.empty();
|
||||
if (logs.length === 0) return;
|
||||
|
||||
frm.toggle_display('import_log_section', true);
|
||||
|
||||
let rows = logs
|
||||
.map(log => {
|
||||
let html = '';
|
||||
if (log.success) {
|
||||
if (frm.doc.import_type === 'Insert New Records') {
|
||||
html = __('Successfully imported {0}', [
|
||||
`<span class="underline">${frappe.utils.get_form_link(
|
||||
frm.doc.reference_doctype,
|
||||
log.docname,
|
||||
true
|
||||
)}<span>`
|
||||
]);
|
||||
} else {
|
||||
html = __('Successfully updated {0}', [
|
||||
`<span class="underline">${frappe.utils.get_form_link(
|
||||
frm.doc.reference_doctype,
|
||||
log.docname,
|
||||
true
|
||||
)}<span>`
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
let messages = (JSON.parse(log.messages || '[]'))
|
||||
.map(JSON.parse)
|
||||
.map(m => {
|
||||
let title = m.title ? `<strong>${m.title}</strong>` : '';
|
||||
let message = m.message ? `<div>${m.message}</div>` : '';
|
||||
return title + message;
|
||||
})
|
||||
.join('');
|
||||
let id = frappe.dom.get_unique_id();
|
||||
html = `${messages}
|
||||
<button class="btn btn-default btn-xs" type="button" data-toggle="collapse" data-target="#${id}" aria-expanded="false" aria-controls="${id}" style="margin-top: 15px;">
|
||||
${__('Show Traceback')}
|
||||
</button>
|
||||
<div class="collapse" id="${id}" style="margin-top: 15px;">
|
||||
<div class="well">
|
||||
<pre>${log.exception}</pre>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
let indicator_color = log.success ? 'green' : 'red';
|
||||
let title = log.success ? __('Success') : __('Failure');
|
||||
|
||||
if (frm.doc.show_failed_logs && log.success) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return `<tr>
|
||||
<td>${JSON.parse(log.row_indexes).join(', ')}</td>
|
||||
<td>
|
||||
<div class="indicator ${indicator_color}">${title}</div>
|
||||
</td>
|
||||
<td>
|
||||
${html}
|
||||
</td>
|
||||
</tr>`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
if (!rows && frm.doc.show_failed_logs) {
|
||||
rows = `<tr><td class="text-center text-muted" colspan=3>
|
||||
${__('No failed logs')}
|
||||
</td></tr>`;
|
||||
}
|
||||
|
||||
frm.get_field('import_log_preview').$wrapper.html(`
|
||||
<table class="table table-bordered">
|
||||
<tr class="text-muted">
|
||||
<th width="10%">${__('Row Number')}</th>
|
||||
<th width="10%">${__('Status')}</th>
|
||||
<th width="80%">${__('Message')}</th>
|
||||
</tr>
|
||||
${rows}
|
||||
</table>
|
||||
`);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
show_import_log(frm) {
|
||||
frm.toggle_display('import_log_section', false);
|
||||
|
||||
if (frm.import_in_progress) {
|
||||
return;
|
||||
}
|
||||
|
||||
let rows = logs
|
||||
.map(log => {
|
||||
let html = '';
|
||||
if (log.success) {
|
||||
if (frm.doc.import_type === 'Insert New Records') {
|
||||
html = __('Successfully imported {0}', [
|
||||
`<span class="underline">${frappe.utils.get_form_link(
|
||||
frm.doc.reference_doctype,
|
||||
log.docname,
|
||||
true
|
||||
)}<span>`
|
||||
]);
|
||||
} else {
|
||||
html = __('Successfully updated {0}', [
|
||||
`<span class="underline">${frappe.utils.get_form_link(
|
||||
frm.doc.reference_doctype,
|
||||
log.docname,
|
||||
true
|
||||
)}<span>`
|
||||
]);
|
||||
}
|
||||
frappe.call({
|
||||
'method': 'frappe.client.get_count',
|
||||
'args': {
|
||||
'doctype': 'Data Import Log',
|
||||
'filters': {
|
||||
'data_import': frm.doc.name
|
||||
}
|
||||
},
|
||||
'callback': function(r) {
|
||||
let count = r.message;
|
||||
if (count < 5000) {
|
||||
frm.trigger('render_import_log');
|
||||
} else {
|
||||
let messages = log.messages
|
||||
.map(JSON.parse)
|
||||
.map(m => {
|
||||
let title = m.title ? `<strong>${m.title}</strong>` : '';
|
||||
let message = m.message ? `<div>${m.message}</div>` : '';
|
||||
return title + message;
|
||||
})
|
||||
.join('');
|
||||
let id = frappe.dom.get_unique_id();
|
||||
html = `${messages}
|
||||
<button class="btn btn-default btn-xs" type="button" data-toggle="collapse" data-target="#${id}" aria-expanded="false" aria-controls="${id}" style="margin-top: 15px;">
|
||||
${__('Show Traceback')}
|
||||
</button>
|
||||
<div class="collapse" id="${id}" style="margin-top: 15px;">
|
||||
<div class="well">
|
||||
<pre>${log.exception}</pre>
|
||||
</div>
|
||||
</div>`;
|
||||
frm.toggle_display('import_log_section', false);
|
||||
frm.add_custom_button(__('Export Import Log'), () =>
|
||||
frm.trigger('export_import_log')
|
||||
);
|
||||
}
|
||||
let indicator_color = log.success ? 'green' : 'red';
|
||||
let title = log.success ? __('Success') : __('Failure');
|
||||
|
||||
if (frm.doc.show_failed_logs && log.success) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return `<tr>
|
||||
<td>${log.row_indexes.join(', ')}</td>
|
||||
<td>
|
||||
<div class="indicator ${indicator_color}">${title}</div>
|
||||
</td>
|
||||
<td>
|
||||
${html}
|
||||
</td>
|
||||
</tr>`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
if (!rows && frm.doc.show_failed_logs) {
|
||||
rows = `<tr><td class="text-center text-muted" colspan=3>
|
||||
${__('No failed logs')}
|
||||
</td></tr>`;
|
||||
}
|
||||
|
||||
frm.get_field('import_log_preview').$wrapper.html(`
|
||||
<table class="table table-bordered">
|
||||
<tr class="text-muted">
|
||||
<th width="10%">${__('Row Number')}</th>
|
||||
<th width="10%">${__('Status')}</th>
|
||||
<th width="80%">${__('Message')}</th>
|
||||
</tr>
|
||||
${rows}
|
||||
</table>
|
||||
`);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,194 +1,197 @@
|
|||
{
|
||||
"actions": [],
|
||||
"autoname": "format:{reference_doctype} Import on {creation}",
|
||||
"beta": 1,
|
||||
"creation": "2019-08-04 14:16:08.318714",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"reference_doctype",
|
||||
"import_type",
|
||||
"download_template",
|
||||
"import_file",
|
||||
"html_5",
|
||||
"google_sheets_url",
|
||||
"refresh_google_sheet",
|
||||
"column_break_5",
|
||||
"status",
|
||||
"submit_after_import",
|
||||
"mute_emails",
|
||||
"template_options",
|
||||
"import_warnings_section",
|
||||
"template_warnings",
|
||||
"import_warnings",
|
||||
"section_import_preview",
|
||||
"import_preview",
|
||||
"import_log_section",
|
||||
"import_log",
|
||||
"show_failed_logs",
|
||||
"import_log_preview"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "reference_doctype",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Document Type",
|
||||
"options": "DocType",
|
||||
"reqd": 1,
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "import_type",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Import Type",
|
||||
"options": "\nInsert New Records\nUpdate Existing Records",
|
||||
"reqd": 1,
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "import_file",
|
||||
"fieldtype": "Attach",
|
||||
"in_list_view": 1,
|
||||
"label": "Import File",
|
||||
"read_only_depends_on": "eval: ['Success', 'Partial Success'].includes(doc.status)"
|
||||
},
|
||||
{
|
||||
"fieldname": "import_preview",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Import Preview"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_import_preview",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Preview"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_5",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "template_options",
|
||||
"fieldtype": "Code",
|
||||
"hidden": 1,
|
||||
"label": "Template Options",
|
||||
"options": "JSON",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "import_log",
|
||||
"fieldtype": "Code",
|
||||
"label": "Import Log",
|
||||
"options": "JSON"
|
||||
},
|
||||
{
|
||||
"fieldname": "import_log_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Import Log"
|
||||
},
|
||||
{
|
||||
"fieldname": "import_log_preview",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Import Log Preview"
|
||||
},
|
||||
{
|
||||
"default": "Pending",
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 1,
|
||||
"label": "Status",
|
||||
"options": "Pending\nSuccess\nPartial Success\nError",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "template_warnings",
|
||||
"fieldtype": "Code",
|
||||
"hidden": 1,
|
||||
"label": "Template Warnings",
|
||||
"options": "JSON"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "submit_after_import",
|
||||
"fieldtype": "Check",
|
||||
"label": "Submit After Import",
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "import_warnings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Import File Errors and Warnings"
|
||||
},
|
||||
{
|
||||
"fieldname": "import_warnings",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Import Warnings"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "download_template",
|
||||
"fieldtype": "Button",
|
||||
"label": "Download Template"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "mute_emails",
|
||||
"fieldtype": "Check",
|
||||
"label": "Don't Send Emails",
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "show_failed_logs",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Failed Logs"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal && !doc.import_file",
|
||||
"fieldname": "html_5",
|
||||
"fieldtype": "HTML",
|
||||
"options": "<h5 class=\"text-muted uppercase\">Or</h5>"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal && !doc.import_file\n",
|
||||
"description": "Must be a publicly accessible Google Sheets URL",
|
||||
"fieldname": "google_sheets_url",
|
||||
"fieldtype": "Data",
|
||||
"label": "Import from Google Sheets",
|
||||
"read_only_depends_on": "eval: ['Success', 'Partial Success'].includes(doc.status)"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.google_sheets_url && !doc.__unsaved && ['Success', 'Partial Success'].includes(doc.status)",
|
||||
"fieldname": "refresh_google_sheet",
|
||||
"fieldtype": "Button",
|
||||
"label": "Refresh Google Sheet"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-11 01:50:42.074623",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Data Import",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
"actions": [],
|
||||
"autoname": "format:{reference_doctype} Import on {creation}",
|
||||
"beta": 1,
|
||||
"creation": "2019-08-04 14:16:08.318714",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"reference_doctype",
|
||||
"import_type",
|
||||
"download_template",
|
||||
"import_file",
|
||||
"payload_count",
|
||||
"html_5",
|
||||
"google_sheets_url",
|
||||
"refresh_google_sheet",
|
||||
"column_break_5",
|
||||
"status",
|
||||
"submit_after_import",
|
||||
"mute_emails",
|
||||
"template_options",
|
||||
"import_warnings_section",
|
||||
"template_warnings",
|
||||
"import_warnings",
|
||||
"section_import_preview",
|
||||
"import_preview",
|
||||
"import_log_section",
|
||||
"show_failed_logs",
|
||||
"import_log_preview"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "reference_doctype",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Document Type",
|
||||
"options": "DocType",
|
||||
"reqd": 1,
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "import_type",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Import Type",
|
||||
"options": "\nInsert New Records\nUpdate Existing Records",
|
||||
"reqd": 1,
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "import_file",
|
||||
"fieldtype": "Attach",
|
||||
"in_list_view": 1,
|
||||
"label": "Import File",
|
||||
"read_only_depends_on": "eval: ['Success', 'Partial Success'].includes(doc.status)"
|
||||
},
|
||||
{
|
||||
"fieldname": "import_preview",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Import Preview"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_import_preview",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Preview"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_5",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "template_options",
|
||||
"fieldtype": "Code",
|
||||
"hidden": 1,
|
||||
"label": "Template Options",
|
||||
"options": "JSON",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "import_log_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Import Log"
|
||||
},
|
||||
{
|
||||
"fieldname": "import_log_preview",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Import Log Preview"
|
||||
},
|
||||
{
|
||||
"default": "Pending",
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 1,
|
||||
"label": "Status",
|
||||
"options": "Pending\nSuccess\nPartial Success\nError",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "template_warnings",
|
||||
"fieldtype": "Code",
|
||||
"hidden": 1,
|
||||
"label": "Template Warnings",
|
||||
"options": "JSON"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "submit_after_import",
|
||||
"fieldtype": "Check",
|
||||
"label": "Submit After Import",
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "import_warnings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Import File Errors and Warnings"
|
||||
},
|
||||
{
|
||||
"fieldname": "import_warnings",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Import Warnings"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "download_template",
|
||||
"fieldtype": "Button",
|
||||
"label": "Download Template"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "mute_emails",
|
||||
"fieldtype": "Check",
|
||||
"label": "Don't Send Emails",
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "show_failed_logs",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Failed Logs"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal && !doc.import_file",
|
||||
"fieldname": "html_5",
|
||||
"fieldtype": "HTML",
|
||||
"options": "<h5 class=\"text-muted uppercase\">Or</h5>"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal && !doc.import_file\n",
|
||||
"description": "Must be a publicly accessible Google Sheets URL",
|
||||
"fieldname": "google_sheets_url",
|
||||
"fieldtype": "Data",
|
||||
"label": "Import from Google Sheets",
|
||||
"read_only_depends_on": "eval: ['Success', 'Partial Success'].includes(doc.status)"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.google_sheets_url && !doc.__unsaved && ['Success', 'Partial Success'].includes(doc.status)",
|
||||
"fieldname": "refresh_google_sheet",
|
||||
"fieldtype": "Button",
|
||||
"label": "Refresh Google Sheet"
|
||||
},
|
||||
{
|
||||
"fieldname": "payload_count",
|
||||
"fieldtype": "Int",
|
||||
"hidden": 1,
|
||||
"label": "Payload Count",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"links": [],
|
||||
"modified": "2022-02-01 20:08:37.624914",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Data Import",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -27,6 +27,7 @@ class DataImport(Document):
|
|||
|
||||
self.validate_import_file()
|
||||
self.validate_google_sheets_url()
|
||||
self.set_payload_count()
|
||||
|
||||
def validate_import_file(self):
|
||||
if self.import_file:
|
||||
|
|
@ -38,6 +39,12 @@ class DataImport(Document):
|
|||
return
|
||||
validate_google_sheets_url(self.google_sheets_url)
|
||||
|
||||
def set_payload_count(self):
|
||||
if self.import_file:
|
||||
i = self.get_importer()
|
||||
payloads = i.import_file.get_payloads_for_import()
|
||||
self.payload_count = len(payloads)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_preview_from_template(self, import_file=None, google_sheets_url=None):
|
||||
if import_file:
|
||||
|
|
@ -67,7 +74,7 @@ class DataImport(Document):
|
|||
enqueue(
|
||||
start_import,
|
||||
queue="default",
|
||||
timeout=6000,
|
||||
timeout=10000,
|
||||
event="data_import",
|
||||
job_name=self.name,
|
||||
data_import=self.name,
|
||||
|
|
@ -80,6 +87,9 @@ class DataImport(Document):
|
|||
def export_errored_rows(self):
|
||||
return self.get_importer().export_errored_rows()
|
||||
|
||||
def download_import_log(self):
|
||||
return self.get_importer().export_import_log()
|
||||
|
||||
def get_importer(self):
|
||||
return Importer(self.reference_doctype, data_import=self)
|
||||
|
||||
|
|
@ -90,7 +100,6 @@ def get_preview_from_template(data_import, import_file=None, google_sheets_url=N
|
|||
import_file, google_sheets_url
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def form_start_import(data_import):
|
||||
return frappe.get_doc("Data Import", data_import).start_import()
|
||||
|
|
@ -145,6 +154,30 @@ def download_errored_template(data_import_name):
|
|||
data_import = frappe.get_doc("Data Import", data_import_name)
|
||||
data_import.export_errored_rows()
|
||||
|
||||
@frappe.whitelist()
|
||||
def download_import_log(data_import_name):
|
||||
data_import = frappe.get_doc("Data Import", data_import_name)
|
||||
data_import.download_import_log()
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_import_status(data_import_name):
|
||||
import_status = {}
|
||||
|
||||
logs = frappe.get_all('Data Import Log', fields=['count(*) as count', 'success'],
|
||||
filters={'data_import': data_import_name},
|
||||
group_by='success')
|
||||
|
||||
total_payload_count = frappe.db.get_value('Data Import', data_import_name, 'payload_count')
|
||||
|
||||
for log in logs:
|
||||
if log.get('success'):
|
||||
import_status['success'] = log.get('count')
|
||||
else:
|
||||
import_status['failed'] = log.get('count')
|
||||
|
||||
import_status['total_records'] = total_payload_count
|
||||
|
||||
return import_status
|
||||
|
||||
def import_file(
|
||||
doctype, file_path, import_type, submit_after_import=False, console=False
|
||||
|
|
|
|||
|
|
@ -24,12 +24,14 @@ frappe.listview_settings['Data Import'] = {
|
|||
'Error': 'red'
|
||||
};
|
||||
let status = doc.status;
|
||||
|
||||
if (imports_in_progress.includes(doc.name)) {
|
||||
status = 'In Progress';
|
||||
}
|
||||
if (status == 'Pending') {
|
||||
status = 'Not Started';
|
||||
}
|
||||
|
||||
return [__(status), colors[status], 'status,=,' + doc.status];
|
||||
},
|
||||
formatters: {
|
||||
|
|
|
|||
|
|
@ -47,7 +47,13 @@ class Importer:
|
|||
)
|
||||
|
||||
def get_data_for_import_preview(self):
|
||||
return self.import_file.get_data_for_import_preview()
|
||||
out = self.import_file.get_data_for_import_preview()
|
||||
|
||||
out.import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success"],
|
||||
filters={"data_import": self.data_import.name},
|
||||
order_by="log_index", limit=10)
|
||||
|
||||
return out
|
||||
|
||||
def before_import(self):
|
||||
# set user lang for translations
|
||||
|
|
@ -58,7 +64,6 @@ class Importer:
|
|||
frappe.flags.in_import = True
|
||||
frappe.flags.mute_emails = self.data_import.mute_emails
|
||||
|
||||
self.data_import.db_set("status", "Pending")
|
||||
self.data_import.db_set("template_warnings", "")
|
||||
|
||||
def import_data(self):
|
||||
|
|
@ -79,20 +84,25 @@ class Importer:
|
|||
return
|
||||
|
||||
# setup import log
|
||||
if self.data_import.import_log:
|
||||
import_log = frappe.parse_json(self.data_import.import_log)
|
||||
else:
|
||||
import_log = []
|
||||
import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success", "log_index"],
|
||||
filters={"data_import": self.data_import.name},
|
||||
order_by="log_index") or []
|
||||
|
||||
# remove previous failures from import log
|
||||
import_log = [log for log in import_log if log.get("success")]
|
||||
log_index = 0
|
||||
|
||||
# Do not remove rows in case of retry after an error or pending data import
|
||||
if self.data_import.status == "Partial Success" and len(import_log) >= self.data_import.payload_count:
|
||||
# remove previous failures from import log only in case of retry after partial success
|
||||
import_log = [log for log in import_log if log.get("success")]
|
||||
|
||||
# get successfully imported rows
|
||||
imported_rows = []
|
||||
for log in import_log:
|
||||
log = frappe._dict(log)
|
||||
if log.success:
|
||||
imported_rows += log.row_indexes
|
||||
if log.success or len(import_log) < self.data_import.payload_count:
|
||||
imported_rows += json.loads(log.row_indexes)
|
||||
|
||||
log_index = log.log_index
|
||||
|
||||
# start import
|
||||
total_payload_count = len(payloads)
|
||||
|
|
@ -146,25 +156,41 @@ class Importer:
|
|||
},
|
||||
)
|
||||
|
||||
import_log.append(
|
||||
frappe._dict(success=True, docname=doc.name, row_indexes=row_indexes)
|
||||
)
|
||||
create_import_log(self.data_import.name, log_index, {
|
||||
'success': True,
|
||||
'docname': doc.name,
|
||||
'row_indexes': row_indexes
|
||||
})
|
||||
|
||||
log_index += 1
|
||||
|
||||
if not self.data_import.status == "Partial Success":
|
||||
self.data_import.db_set("status", "Partial Success")
|
||||
|
||||
# commit after every successful import
|
||||
frappe.db.commit()
|
||||
|
||||
except Exception:
|
||||
import_log.append(
|
||||
frappe._dict(
|
||||
success=False,
|
||||
exception=frappe.get_traceback(),
|
||||
messages=frappe.local.message_log,
|
||||
row_indexes=row_indexes,
|
||||
)
|
||||
)
|
||||
messages = frappe.local.message_log
|
||||
frappe.clear_messages()
|
||||
|
||||
# rollback if exception
|
||||
frappe.db.rollback()
|
||||
|
||||
create_import_log(self.data_import.name, log_index, {
|
||||
'success': False,
|
||||
'exception': frappe.get_traceback(),
|
||||
'messages': messages,
|
||||
'row_indexes': row_indexes
|
||||
})
|
||||
|
||||
log_index += 1
|
||||
|
||||
# Logs are db inserted directly so will have to be fetched again
|
||||
import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success", "log_index"],
|
||||
filters={"data_import": self.data_import.name},
|
||||
order_by="log_index") or []
|
||||
|
||||
# set status
|
||||
failures = [log for log in import_log if not log.get("success")]
|
||||
if len(failures) == total_payload_count:
|
||||
|
|
@ -178,7 +204,6 @@ class Importer:
|
|||
self.print_import_log(import_log)
|
||||
else:
|
||||
self.data_import.db_set("status", status)
|
||||
self.data_import.db_set("import_log", json.dumps(import_log))
|
||||
|
||||
self.after_import()
|
||||
|
||||
|
|
@ -248,11 +273,14 @@ class Importer:
|
|||
if not self.data_import:
|
||||
return
|
||||
|
||||
import_log = frappe.parse_json(self.data_import.import_log or "[]")
|
||||
import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success"],
|
||||
filters={"data_import": self.data_import.name},
|
||||
order_by="log_index") or []
|
||||
|
||||
failures = [log for log in import_log if not log.get("success")]
|
||||
row_indexes = []
|
||||
for f in failures:
|
||||
row_indexes.extend(f.get("row_indexes", []))
|
||||
row_indexes.extend(json.loads(f.get("row_indexes", [])))
|
||||
|
||||
# de duplicate
|
||||
row_indexes = list(set(row_indexes))
|
||||
|
|
@ -264,6 +292,30 @@ class Importer:
|
|||
|
||||
build_csv_response(rows, _(self.doctype))
|
||||
|
||||
def export_import_log(self):
|
||||
from frappe.utils.csvutils import build_csv_response
|
||||
|
||||
if not self.data_import:
|
||||
return
|
||||
|
||||
import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success", "messages", "exception", "docname"],
|
||||
filters={"data_import": self.data_import.name},
|
||||
order_by="log_index")
|
||||
|
||||
header_row = ["Row Numbers", "Status", "Message", "Exception"]
|
||||
|
||||
rows = [header_row]
|
||||
|
||||
for log in import_log:
|
||||
row_number = json.loads(log.get("row_indexes"))[0]
|
||||
status = "Success" if log.get('success') else "Failure"
|
||||
message = "Successfully Imported {0}".format(log.get('docname')) if log.get('success') else \
|
||||
log.get("messages")
|
||||
exception = frappe.utils.cstr(log.get("exception", ''))
|
||||
rows += [[row_number, status, message, exception]]
|
||||
|
||||
build_csv_response(rows, self.doctype)
|
||||
|
||||
def print_import_log(self, import_log):
|
||||
failed_records = [log for log in import_log if not log.success]
|
||||
successful_records = [log for log in import_log if log.success]
|
||||
|
|
@ -1172,3 +1224,17 @@ def df_as_json(df):
|
|||
|
||||
def get_select_options(df):
|
||||
return [d for d in (df.options or "").split("\n") if d]
|
||||
|
||||
def create_import_log(data_import, log_index, log_details):
|
||||
frappe.get_doc({
|
||||
'doctype': 'Data Import Log',
|
||||
'log_index': log_index,
|
||||
'success': log_details.get('success'),
|
||||
'data_import': data_import,
|
||||
'row_indexes': json.dumps(log_details.get('row_indexes')),
|
||||
'docname': log_details.get('docname'),
|
||||
'messages': json.dumps(log_details.get('messages', '[]')),
|
||||
'exception': log_details.get('exception')
|
||||
}).db_insert()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -60,15 +60,19 @@ class TestImporter(unittest.TestCase):
|
|||
frappe.local.message_log = []
|
||||
data_import.start_import()
|
||||
data_import.reload()
|
||||
import_log = frappe.parse_json(data_import.import_log)
|
||||
self.assertEqual(import_log[0]['row_indexes'], [2,3])
|
||||
expected_error = "Error: <strong>Child 1 of DocType for Import</strong> Row #1: Value missing for: Child Title"
|
||||
self.assertEqual(frappe.parse_json(import_log[0]['messages'][0])['message'], expected_error)
|
||||
expected_error = "Error: <strong>Child 1 of DocType for Import</strong> Row #2: Value missing for: Child Title"
|
||||
self.assertEqual(frappe.parse_json(import_log[0]['messages'][1])['message'], expected_error)
|
||||
|
||||
self.assertEqual(import_log[1]['row_indexes'], [4])
|
||||
self.assertEqual(frappe.parse_json(import_log[1]['messages'][0])['message'], "Title is required")
|
||||
import_log = frappe.db.get_all("Data Import Log", fields=["row_indexes", "success", "messages", "exception", "docname"],
|
||||
filters={"data_import": data_import.name},
|
||||
order_by="log_index")
|
||||
|
||||
self.assertEqual(frappe.parse_json(import_log[0]['row_indexes']), [2,3])
|
||||
expected_error = "Error: <strong>Child 1 of DocType for Import</strong> Row #1: Value missing for: Child Title"
|
||||
self.assertEqual(frappe.parse_json(frappe.parse_json(import_log[0]['messages'])[0])['message'], expected_error)
|
||||
expected_error = "Error: <strong>Child 1 of DocType for Import</strong> Row #2: Value missing for: Child Title"
|
||||
self.assertEqual(frappe.parse_json(frappe.parse_json(import_log[0]['messages'])[1])['message'], expected_error)
|
||||
|
||||
self.assertEqual(frappe.parse_json(import_log[1]['row_indexes']), [4])
|
||||
self.assertEqual(frappe.parse_json(frappe.parse_json(import_log[1]['messages'])[0])['message'], "Title is required")
|
||||
|
||||
def test_data_import_update(self):
|
||||
existing_doc = frappe.get_doc(
|
||||
|
|
|
|||
8
frappe/core/doctype/data_import_log/data_import_log.js
Normal file
8
frappe/core/doctype/data_import_log/data_import_log.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// Copyright (c) 2021, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Data Import Log', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
});
|
||||
84
frappe/core/doctype/data_import_log/data_import_log.json
Normal file
84
frappe/core/doctype/data_import_log/data_import_log.json
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2021-12-25 16:12:20.205889",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "MyISAM",
|
||||
"field_order": [
|
||||
"data_import",
|
||||
"row_indexes",
|
||||
"success",
|
||||
"docname",
|
||||
"messages",
|
||||
"exception",
|
||||
"log_index"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "data_import",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Data Import",
|
||||
"options": "Data Import"
|
||||
},
|
||||
{
|
||||
"fieldname": "docname",
|
||||
"fieldtype": "Data",
|
||||
"label": "Reference Name"
|
||||
},
|
||||
{
|
||||
"fieldname": "exception",
|
||||
"fieldtype": "Text",
|
||||
"label": "Exception"
|
||||
},
|
||||
{
|
||||
"fieldname": "row_indexes",
|
||||
"fieldtype": "Code",
|
||||
"label": "Row Indexes",
|
||||
"options": "JSON"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "success",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Success"
|
||||
},
|
||||
{
|
||||
"fieldname": "log_index",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Log Index"
|
||||
},
|
||||
{
|
||||
"fieldname": "messages",
|
||||
"fieldtype": "Code",
|
||||
"label": "Messages",
|
||||
"options": "JSON"
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-12-29 11:19:19.646076",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Data Import Log",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
||||
8
frappe/core/doctype/data_import_log/data_import_log.py
Normal file
8
frappe/core/doctype/data_import_log/data_import_log.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Copyright (c) 2021, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class DataImportLog(Document):
|
||||
pass
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# Copyright (c) 2021, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestDataImportLog(unittest.TestCase):
|
||||
pass
|
||||
|
|
@ -699,6 +699,13 @@ class DocType(Document):
|
|||
if not name:
|
||||
name = self.name
|
||||
|
||||
# a Doctype name is the tablename created in database
|
||||
# `tab<Doctype Name>` the length of tablename is limited to 64 characters
|
||||
max_length = frappe.db.MAX_COLUMN_LENGTH - 3
|
||||
if len(name) > max_length:
|
||||
# length(tab + <Doctype Name>) should be equal to 64 characters hence doctype should be 61 characters
|
||||
frappe.throw(_("Doctype name is limited to {0} characters ({1})").format(max_length, name), frappe.NameError)
|
||||
|
||||
flags = {"flags": re.ASCII}
|
||||
|
||||
# a DocType name should not start or end with an empty space
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ class TestDocType(unittest.TestCase):
|
|||
self.assertRaises(frappe.NameError, new_doctype("_Some DocType").insert)
|
||||
self.assertRaises(frappe.NameError, new_doctype("8Some DocType").insert)
|
||||
self.assertRaises(frappe.NameError, new_doctype("Some (DocType)").insert)
|
||||
self.assertRaises(frappe.NameError, new_doctype("Some Doctype with a name whose length is more than 61 characters").insert)
|
||||
for name in ("Some DocType", "Some_DocType"):
|
||||
if frappe.db.exists("DocType", name):
|
||||
frappe.delete_doc("DocType", name)
|
||||
|
|
@ -353,7 +354,6 @@ class TestDocType(unittest.TestCase):
|
|||
dump_docs = json.dumps(docs.get('docs'))
|
||||
cancel_all_linked_docs(dump_docs)
|
||||
data_link_doc.cancel()
|
||||
data_doc.name = '{}-CANC-0'.format(data_doc.name)
|
||||
data_doc.load_from_db()
|
||||
self.assertEqual(data_link_doc.docstatus, 2)
|
||||
self.assertEqual(data_doc.docstatus, 2)
|
||||
|
|
@ -377,7 +377,7 @@ class TestDocType(unittest.TestCase):
|
|||
for data in link_doc.get('permissions'):
|
||||
data.submit = 1
|
||||
data.cancel = 1
|
||||
link_doc.insert(ignore_if_duplicate=True)
|
||||
link_doc.insert()
|
||||
|
||||
#create first parent doctype
|
||||
test_doc_1 = new_doctype('Test Doctype 1')
|
||||
|
|
@ -392,7 +392,7 @@ class TestDocType(unittest.TestCase):
|
|||
for data in test_doc_1.get('permissions'):
|
||||
data.submit = 1
|
||||
data.cancel = 1
|
||||
test_doc_1.insert(ignore_if_duplicate=True)
|
||||
test_doc_1.insert()
|
||||
|
||||
#crete second parent doctype
|
||||
doc = new_doctype('Test Doctype 2')
|
||||
|
|
@ -407,7 +407,7 @@ class TestDocType(unittest.TestCase):
|
|||
for data in link_doc.get('permissions'):
|
||||
data.submit = 1
|
||||
data.cancel = 1
|
||||
doc.insert(ignore_if_duplicate=True)
|
||||
doc.insert()
|
||||
|
||||
# create doctype data
|
||||
data_link_doc_1 = frappe.new_doc('Test Linked Doctype 1')
|
||||
|
|
@ -438,7 +438,6 @@ class TestDocType(unittest.TestCase):
|
|||
# checking that doc for Test Doctype 2 is not canceled
|
||||
self.assertRaises(frappe.LinkExistsError, data_link_doc_1.cancel)
|
||||
|
||||
data_doc_2.name = '{}-CANC-0'.format(data_doc_2.name)
|
||||
data_doc.load_from_db()
|
||||
data_doc_2.load_from_db()
|
||||
self.assertEqual(data_link_doc_1.docstatus, 2)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.data import evaluate_filters
|
||||
from frappe.model.naming import parse_naming_series
|
||||
from frappe import _
|
||||
|
||||
class DocumentNamingRule(Document):
|
||||
|
|
@ -27,7 +28,9 @@ class DocumentNamingRule(Document):
|
|||
return
|
||||
|
||||
counter = frappe.db.get_value(self.doctype, self.name, 'counter', for_update=True) or 0
|
||||
doc.name = self.prefix + ('%0'+str(self.prefix_digits)+'d') % (counter + 1)
|
||||
naming_series = parse_naming_series(self.prefix, doc=doc)
|
||||
|
||||
doc.name = naming_series + ('%0'+str(self.prefix_digits)+'d') % (counter + 1)
|
||||
frappe.db.set_value(self.doctype, self.name, 'counter', counter + 1)
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
"""
|
||||
|
|
@ -7,7 +7,6 @@ record of files
|
|||
naming for same name files: file.gif, file-1.gif, file-2.gif etc
|
||||
"""
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import imghdr
|
||||
import io
|
||||
|
|
@ -17,9 +16,10 @@ import os
|
|||
import re
|
||||
import shutil
|
||||
import zipfile
|
||||
from typing import TYPE_CHECKING, Tuple
|
||||
|
||||
import requests
|
||||
import requests.exceptions
|
||||
from requests.exceptions import HTTPError, SSLError
|
||||
from PIL import Image, ImageFile, ImageOps
|
||||
from io import BytesIO
|
||||
from urllib.parse import quote, unquote
|
||||
|
|
@ -31,6 +31,11 @@ from frappe.utils import call_hook_method, cint, cstr, encode, get_files_path, g
|
|||
from frappe.utils.image import strip_exif_data, optimize_image
|
||||
from frappe.utils.file_manager import safe_b64decode
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from PIL.ImageFile import ImageFile
|
||||
from requests.models import Response
|
||||
|
||||
|
||||
class MaxFileSizeReachedError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
|
@ -276,7 +281,7 @@ class File(Document):
|
|||
image, filename, extn = get_local_image(self.file_url)
|
||||
else:
|
||||
image, filename, extn = get_web_image(self.file_url)
|
||||
except (requests.exceptions.HTTPError, requests.exceptions.SSLError, IOError, TypeError):
|
||||
except (HTTPError, SSLError, IOError, TypeError):
|
||||
return
|
||||
|
||||
size = width, height
|
||||
|
|
@ -572,12 +577,10 @@ class File(Document):
|
|||
|
||||
@staticmethod
|
||||
def zip_files(files):
|
||||
from six import string_types
|
||||
|
||||
zip_file = io.BytesIO()
|
||||
zf = zipfile.ZipFile(zip_file, "w", zipfile.ZIP_DEFLATED)
|
||||
for _file in files:
|
||||
if isinstance(_file, string_types):
|
||||
if isinstance(_file, str):
|
||||
_file = frappe.get_doc("File", _file)
|
||||
if not isinstance(_file, File):
|
||||
continue
|
||||
|
|
@ -650,9 +653,17 @@ def setup_folder_path(filename, new_parent):
|
|||
from frappe.model.rename_doc import rename_doc
|
||||
rename_doc("File", file.name, file.get_name_based_on_parent_folder(), ignore_permissions=True)
|
||||
|
||||
def get_extension(filename, extn, content):
|
||||
def get_extension(filename, extn, content: bytes = None, response: "Response" = None) -> str:
|
||||
mimetype = None
|
||||
|
||||
if response:
|
||||
content_type = response.headers.get("Content-Type")
|
||||
|
||||
if content_type:
|
||||
_extn = mimetypes.guess_extension(content_type)
|
||||
if _extn:
|
||||
return _extn[1:]
|
||||
|
||||
if extn:
|
||||
# remove '?' char and parameters from extn if present
|
||||
if '?' in extn:
|
||||
|
|
@ -695,14 +706,14 @@ def get_local_image(file_url):
|
|||
|
||||
return image, filename, extn
|
||||
|
||||
def get_web_image(file_url):
|
||||
def get_web_image(file_url: str) -> Tuple["ImageFile", str, str]:
|
||||
# download
|
||||
file_url = frappe.utils.get_url(file_url)
|
||||
r = requests.get(file_url, stream=True)
|
||||
try:
|
||||
r.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if "404" in e.args[0]:
|
||||
except HTTPError:
|
||||
if r.status_code == 404:
|
||||
frappe.msgprint(_("File '{0}' not found").format(file_url))
|
||||
else:
|
||||
frappe.msgprint(_("Unable to read file format for {0}").format(file_url))
|
||||
|
|
@ -721,7 +732,10 @@ def get_web_image(file_url):
|
|||
filename = get_random_filename()
|
||||
extn = None
|
||||
|
||||
extn = get_extension(filename, extn, r.content)
|
||||
extn = get_extension(filename, extn, response=r)
|
||||
if extn == "bin":
|
||||
extn = get_extension(filename, extn, content=r.content) or "png"
|
||||
|
||||
filename = "/files/" + strip(unquote(filename))
|
||||
|
||||
return image, filename, extn
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import base64
|
||||
import json
|
||||
import frappe
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from frappe import _
|
||||
from frappe.core.doctype.file.file import get_attached_images, move_file, get_files_in_folder, unzip_file
|
||||
from frappe.core.doctype.file.file import File, get_attached_images, move_file, get_files_in_folder, unzip_file
|
||||
from frappe.utils import get_files_path
|
||||
# test_records = frappe.get_test_records('File')
|
||||
|
||||
test_content1 = 'Hello'
|
||||
test_content2 = 'Hello World'
|
||||
|
|
@ -24,8 +23,6 @@ def make_test_doc():
|
|||
|
||||
|
||||
class TestSimpleFile(unittest.TestCase):
|
||||
|
||||
|
||||
def setUp(self):
|
||||
self.attached_to_doctype, self.attached_to_docname = make_test_doc()
|
||||
self.test_content = test_content1
|
||||
|
|
@ -38,21 +35,13 @@ class TestSimpleFile(unittest.TestCase):
|
|||
_file.save()
|
||||
self.saved_file_url = _file.file_url
|
||||
|
||||
|
||||
def test_save(self):
|
||||
_file = frappe.get_doc("File", {"file_url": self.saved_file_url})
|
||||
content = _file.get_content()
|
||||
self.assertEqual(content, self.test_content)
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
# File gets deleted on rollback, so blank
|
||||
pass
|
||||
|
||||
|
||||
class TestBase64File(unittest.TestCase):
|
||||
|
||||
|
||||
def setUp(self):
|
||||
self.attached_to_doctype, self.attached_to_docname = make_test_doc()
|
||||
self.test_content = base64.b64encode(test_content1.encode('utf-8'))
|
||||
|
|
@ -66,18 +55,12 @@ class TestBase64File(unittest.TestCase):
|
|||
_file.save()
|
||||
self.saved_file_url = _file.file_url
|
||||
|
||||
|
||||
def test_saved_content(self):
|
||||
_file = frappe.get_doc("File", {"file_url": self.saved_file_url})
|
||||
content = _file.get_content()
|
||||
self.assertEqual(content, test_content1)
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
# File gets deleted on rollback, so blank
|
||||
pass
|
||||
|
||||
|
||||
class TestSameFileName(unittest.TestCase):
|
||||
def test_saved_content(self):
|
||||
self.attached_to_doctype, self.attached_to_docname = make_test_doc()
|
||||
|
|
@ -130,8 +113,6 @@ class TestSameFileName(unittest.TestCase):
|
|||
|
||||
|
||||
class TestSameContent(unittest.TestCase):
|
||||
|
||||
|
||||
def setUp(self):
|
||||
self.attached_to_doctype1, self.attached_to_docname1 = make_test_doc()
|
||||
self.attached_to_doctype2, self.attached_to_docname2 = make_test_doc()
|
||||
|
|
@ -186,10 +167,6 @@ class TestSameContent(unittest.TestCase):
|
|||
limit_property.delete()
|
||||
frappe.clear_cache(doctype='ToDo')
|
||||
|
||||
def tearDown(self):
|
||||
# File gets deleted on rollback, so blank
|
||||
pass
|
||||
|
||||
|
||||
class TestFile(unittest.TestCase):
|
||||
def setUp(self):
|
||||
|
|
@ -398,7 +375,7 @@ class TestFile(unittest.TestCase):
|
|||
|
||||
def test_make_thumbnail(self):
|
||||
# test web image
|
||||
test_file = frappe.get_doc({
|
||||
test_file: File = frappe.get_doc({
|
||||
"doctype": "File",
|
||||
"file_name": 'logo',
|
||||
"file_url": frappe.utils.get_url('/_test/assets/image.jpg'),
|
||||
|
|
@ -407,6 +384,16 @@ class TestFile(unittest.TestCase):
|
|||
test_file.make_thumbnail()
|
||||
self.assertEquals(test_file.thumbnail_url, '/files/image_small.jpg')
|
||||
|
||||
# test web image without extension
|
||||
test_file = frappe.get_doc({
|
||||
"doctype": "File",
|
||||
"file_name": 'logo',
|
||||
"file_url": frappe.utils.get_url('/_test/assets/image'),
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
test_file.make_thumbnail()
|
||||
self.assertTrue(test_file.thumbnail_url.endswith("_small.jpeg"))
|
||||
|
||||
# test local image
|
||||
test_file.db_set('thumbnail_url', None)
|
||||
test_file.reload()
|
||||
|
|
|
|||
|
|
@ -1,154 +1,55 @@
|
|||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"autoname": "field:gateway",
|
||||
"beta": 0,
|
||||
"creation": "2015-12-15 22:26:45.221162",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"actions": [],
|
||||
"autoname": "field:gateway",
|
||||
"creation": "2022-01-24 21:09:47.229371",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"gateway",
|
||||
"gateway_settings",
|
||||
"gateway_controller"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "gateway",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Gateway",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "gateway",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Gateway",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "gateway_settings",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Gateway Settings",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "DocType",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "gateway_settings",
|
||||
"fieldtype": "Link",
|
||||
"label": "Gateway Settings",
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "gateway_controller",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Gateway Controller",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "gateway_settings",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"fieldname": "gateway_controller",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"label": "Gateway Controller",
|
||||
"options": "gateway_settings"
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 1,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2018-02-05 14:24:33.526645",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Payment Gateway",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2022-01-24 21:17:03.864719",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Payment Gateway",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"apply_user_permissions": 0,
|
||||
"cancel": 0,
|
||||
"create": 0,
|
||||
"delete": 0,
|
||||
"email": 0,
|
||||
"export": 0,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 0,
|
||||
"read": 1,
|
||||
"report": 0,
|
||||
"role": "System Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 0,
|
||||
"submit": 0,
|
||||
"write": 0
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"read": 1,
|
||||
"role": "System Manager",
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 0,
|
||||
"track_seen": 0
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
|
|
@ -34,19 +34,7 @@ def run_server_script_for_doc_event(doc, event):
|
|||
if scripts:
|
||||
# run all scripts for this doctype + event
|
||||
for script_name in scripts:
|
||||
try:
|
||||
frappe.get_doc('Server Script', script_name).execute_doc(doc)
|
||||
except Exception as e:
|
||||
message = frappe._('Error executing Server Script {0}. Open Browser Console to see traceback.').format(
|
||||
frappe.utils.get_link_to_form('Server Script', script_name)
|
||||
)
|
||||
exception = type(e)
|
||||
if getattr(frappe, 'request', None):
|
||||
# all exceptions throw 500 which is internal server error
|
||||
# however server script error is a user error
|
||||
# so we should throw 417 which is expectation failed
|
||||
exception.http_status_code = 417
|
||||
frappe.throw(title=frappe._('Server Script Error'), msg=message, exc=exception)
|
||||
frappe.get_doc('Server Script', script_name).execute_doc(doc)
|
||||
|
||||
def get_server_script_map():
|
||||
# fetch cached server script methods
|
||||
|
|
|
|||
|
|
@ -31,4 +31,15 @@ class test(Document):
|
|||
def get_value(self, fields, filters, **kwargs):
|
||||
# return []
|
||||
with open("data_file.json", "r") as read_file:
|
||||
return [json.load(read_file)]
|
||||
return [json.load(read_file)]
|
||||
|
||||
def get_count(self, args):
|
||||
# return []
|
||||
with open("data_file.json", "r") as read_file:
|
||||
return [json.load(read_file)]
|
||||
|
||||
def get_stats(self, args):
|
||||
# return []
|
||||
with open("data_file.json", "r") as read_file:
|
||||
return [json.load(read_file)]
|
||||
|
||||
|
|
|
|||
|
|
@ -355,7 +355,10 @@ class TestUser(unittest.TestCase):
|
|||
test_user.reload()
|
||||
self.assertEqual(update_password(new_password, key=test_user.reset_password_key), "/")
|
||||
update_password(old_password, old_password=new_password)
|
||||
self.assertEqual(json.loads(frappe.message_log[0]), {"message": "Password reset instructions have been sent to your email"})
|
||||
self.assertEqual(
|
||||
json.loads(frappe.message_log[0]).get("message"),
|
||||
"Password reset instructions have been sent to your email"
|
||||
)
|
||||
sendmail.assert_called_once()
|
||||
self.assertEqual(sendmail.call_args[1]["recipients"], "test2@example.com")
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ from frappe.core.doctype.user_type.user_type import user_linked_with_permission_
|
|||
from frappe.query_builder import DocType
|
||||
|
||||
|
||||
STANDARD_USERS = ("Guest", "Administrator")
|
||||
STANDARD_USERS = frappe.STANDARD_USERS
|
||||
|
||||
class User(Document):
|
||||
__new_password = None
|
||||
|
|
@ -344,7 +344,7 @@ class User(Document):
|
|||
|
||||
frappe.sendmail(recipients=self.email, sender=sender, subject=subject,
|
||||
template=template, args=args, header=[subject, "green"],
|
||||
delayed=(not now) if now!=None else self.flags.delay_emails, retry=3)
|
||||
delayed=(not now) if now is not None else self.flags.delay_emails, retry=3)
|
||||
|
||||
def a_system_manager_should_exist(self):
|
||||
if not self.get_other_system_managers():
|
||||
|
|
@ -756,7 +756,7 @@ def verify_password(password):
|
|||
@frappe.whitelist(allow_guest=True)
|
||||
def sign_up(email, full_name, redirect_to):
|
||||
if is_signup_disabled():
|
||||
frappe.throw(_('Sign Up is disabled'), title='Not Allowed')
|
||||
frappe.throw(_("Sign Up is disabled"), title=_("Not Allowed"))
|
||||
|
||||
user = frappe.db.get("User", {"email": email})
|
||||
if user:
|
||||
|
|
@ -810,8 +810,10 @@ def reset_password(user):
|
|||
user.validate_reset_password()
|
||||
user.reset_password(send_email=True)
|
||||
|
||||
return frappe.msgprint(_("Password reset instructions have been sent to your email"))
|
||||
|
||||
return frappe.msgprint(
|
||||
msg=_("Password reset instructions have been sent to your email"),
|
||||
title=_("Password Email Sent")
|
||||
)
|
||||
except frappe.DoesNotExistError:
|
||||
frappe.local.response['http_status_code'] = 400
|
||||
frappe.clear_messages()
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
from frappe.core.doctype.user_permission.user_permission import add_user_permissions, remove_applicable
|
||||
from frappe.permissions import has_user_permission
|
||||
from frappe.core.doctype.doctype.test_doctype import new_doctype
|
||||
from frappe.website.doctype.blog_post.test_blog_post import make_test_blog
|
||||
|
||||
import frappe
|
||||
import unittest
|
||||
|
|
@ -31,6 +32,18 @@ class TestUserPermission(unittest.TestCase):
|
|||
param = get_params(user, 'User', perm_user.name, is_default=1)
|
||||
self.assertRaises(frappe.ValidationError, add_user_permissions, param)
|
||||
|
||||
def test_default_user_permission_corectness(self):
|
||||
user = create_user('test_default_corectness_permission_1@example.com')
|
||||
param = get_params(user, 'User', user.name, is_default=1, hide_descendants= 1)
|
||||
add_user_permissions(param)
|
||||
#create a duplicate entry with default
|
||||
perm_user = create_user('test_default_corectness2@example.com')
|
||||
test_blog = make_test_blog()
|
||||
param = get_params(perm_user, 'Blog Post', test_blog.name, is_default=1, hide_descendants= 1)
|
||||
add_user_permissions(param)
|
||||
frappe.db.delete('User Permission', filters={'for_value': test_blog.name})
|
||||
frappe.delete_doc('Blog Post', test_blog.name)
|
||||
|
||||
def test_default_user_permission(self):
|
||||
frappe.set_user('Administrator')
|
||||
user = create_user('test_user_perm1@example.com', 'Website Manager')
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@ class UserPermission(Document):
|
|||
}, or_filters={
|
||||
'applicable_for': cstr(self.applicable_for),
|
||||
'apply_to_all_doctypes': 1,
|
||||
'hide_descendants': cstr(self.hide_descendants)
|
||||
}, limit=1)
|
||||
if overlap_exists:
|
||||
ref_link = frappe.get_desk_link(self.doctype, overlap_exists[0].name)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,68 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021, Frappe Technologies and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
# import frappe
|
||||
import frappe
|
||||
import unittest
|
||||
|
||||
from frappe.installer import update_site_config
|
||||
|
||||
class TestUserType(unittest.TestCase):
|
||||
pass
|
||||
def setUp(self):
|
||||
create_role()
|
||||
|
||||
def test_add_select_perm_doctypes(self):
|
||||
user_type = create_user_type('Test User Type')
|
||||
|
||||
# select perms added for all link fields
|
||||
doc = frappe.get_meta('Contact')
|
||||
link_fields = doc.get_link_fields()
|
||||
select_doctypes = frappe.get_all('User Select Document Type', {'parent': user_type.name}, pluck='document_type')
|
||||
|
||||
for entry in link_fields:
|
||||
self.assertTrue(entry.options in select_doctypes)
|
||||
|
||||
# select perms added for all child table link fields
|
||||
link_fields = []
|
||||
for child_table in doc.get_table_fields():
|
||||
child_doc = frappe.get_meta(child_table.options)
|
||||
link_fields.extend(child_doc.get_link_fields())
|
||||
|
||||
for entry in link_fields:
|
||||
self.assertTrue(entry.options in select_doctypes)
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
|
||||
def create_user_type(user_type):
|
||||
if frappe.db.exists('User Type', user_type):
|
||||
frappe.delete_doc('User Type', user_type)
|
||||
|
||||
user_type_limit = {frappe.scrub(user_type): 1}
|
||||
update_site_config('user_type_doctype_limit', user_type_limit)
|
||||
|
||||
doc = frappe.get_doc({
|
||||
'doctype': 'User Type',
|
||||
'name': user_type,
|
||||
'role': '_Test User Type',
|
||||
'user_id_field': 'user',
|
||||
'apply_user_permission_on': 'User'
|
||||
})
|
||||
|
||||
doc.append('user_doctypes', {
|
||||
'document_type': 'Contact',
|
||||
'read': 1,
|
||||
'write': 1
|
||||
})
|
||||
|
||||
return doc.insert()
|
||||
|
||||
|
||||
def create_role():
|
||||
if not frappe.db.exists('Role', '_Test User Type'):
|
||||
frappe.get_doc({
|
||||
'doctype': 'Role',
|
||||
'role_name': '_Test User Type',
|
||||
'desk_access': 1,
|
||||
'is_custom': 1
|
||||
}).insert()
|
||||
|
|
@ -121,7 +121,7 @@ class UserType(Document):
|
|||
|
||||
for child_table in doc.get_table_fields():
|
||||
child_doc = frappe.get_meta(child_table.options)
|
||||
if not child_doc.istable:
|
||||
if child_doc:
|
||||
self.prepare_select_perm_doctypes(child_doc, user_doctypes, select_doctypes)
|
||||
|
||||
if select_doctypes:
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ class Dashboard {
|
|||
|
||||
show() {
|
||||
this.route = frappe.get_route();
|
||||
this.set_breadcrumbs();
|
||||
if (this.route.length > 1) {
|
||||
// from route
|
||||
this.show_dashboard(this.route.slice(-1)[0]);
|
||||
|
|
@ -75,6 +76,10 @@ class Dashboard {
|
|||
frappe.last_dashboard = current_dashboard_name;
|
||||
}
|
||||
|
||||
set_breadcrumbs() {
|
||||
frappe.breadcrumbs.add("Desk", "Dashboard");
|
||||
}
|
||||
|
||||
refresh() {
|
||||
frappe.run_serially([
|
||||
() => this.render_cards(),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"charts": [],
|
||||
"content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"DocType\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Workspace\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Report\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Elements\",\"level\":4,\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Modules\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Models\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Views\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Scripting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Packages\",\"col\":4}}]",
|
||||
"content": "[{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"DocType\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Workspace\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Report\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Elements</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Modules\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Models\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Views\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Scripting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Packages\",\"col\":4}}]",
|
||||
"creation": "2021-01-02 10:51:16.579957",
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace",
|
||||
|
|
@ -222,7 +222,7 @@
|
|||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2021-09-05 21:14:52.384816",
|
||||
"modified": "2022-01-13 17:26:02.736366",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Build",
|
||||
|
|
@ -231,7 +231,7 @@
|
|||
"public": 1,
|
||||
"restrict_to_domain": "",
|
||||
"roles": [],
|
||||
"sequence_id": 5,
|
||||
"sequence_id": 5.0,
|
||||
"shortcuts": [
|
||||
{
|
||||
"doc_view": "",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"charts": [],
|
||||
"content": "[{\"type\":\"header\",\"data\": {\"text\":\"Settings\",\"level\": 4,\"col\": 12}}, {\"type\":\"shortcut\",\"data\": {\"shortcut_name\":\"System Settings\",\"col\": 4}}, {\"type\":\"shortcut\",\"data\": {\"shortcut_name\":\"Print Settings\",\"col\": 4}}, {\"type\":\"shortcut\",\"data\": {\"shortcut_name\":\"Website Settings\",\"col\": 4}}, {\"type\":\"spacer\",\"data\": {\"col\": 12}}, {\"type\":\"header\",\"data\": {\"text\":\"Reports & Masters\",\"level\": 4,\"col\": 12}}, {\"type\":\"card\",\"data\": {\"card_name\":\"Data\",\"col\": 4}}, {\"type\":\"card\",\"data\": {\"card_name\":\"Email / Notifications\",\"col\": 4}}, {\"type\":\"card\",\"data\": {\"card_name\":\"Website\",\"col\": 4}}, {\"type\":\"card\",\"data\": {\"card_name\":\"Core\",\"col\": 4}}, {\"type\":\"card\",\"data\": {\"card_name\":\"Printing\",\"col\": 4}}, {\"type\":\"card\",\"data\": {\"card_name\":\"Workflow\",\"col\": 4}}]",
|
||||
"content": "[{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Settings</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"System Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Print Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Website Settings\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Data\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Email / Notifications\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Website\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Core\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Printing\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Workflow\",\"col\":4}}]",
|
||||
"creation": "2020-03-02 15:09:40.527211",
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace",
|
||||
|
|
@ -367,7 +367,7 @@
|
|||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2021-08-05 12:16:03.456174",
|
||||
"modified": "2022-01-13 17:49:59.586909",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Settings",
|
||||
|
|
@ -376,7 +376,7 @@
|
|||
"public": 1,
|
||||
"restrict_to_domain": "",
|
||||
"roles": [],
|
||||
"sequence_id": 29,
|
||||
"sequence_id": 29.0,
|
||||
"shortcuts": [
|
||||
{
|
||||
"icon": "setting",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"charts": [],
|
||||
"content": "[{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"User\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Role\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Permission Manager\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"User Profile\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"User Type\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Users\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Logs\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Permissions\", \"col\": 4}}]",
|
||||
"content": "[{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"User\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Role\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Permission Manager\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"User Profile\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"User Type\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Users\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Logs\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Permissions\",\"col\":4}}]",
|
||||
"creation": "2020-03-02 15:12:16.754449",
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace",
|
||||
|
|
@ -145,7 +145,7 @@
|
|||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2021-08-05 12:16:03.010205",
|
||||
"modified": "2022-01-13 17:49:08.912772",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Users",
|
||||
|
|
@ -154,7 +154,7 @@
|
|||
"public": 1,
|
||||
"restrict_to_domain": "",
|
||||
"roles": [],
|
||||
"sequence_id": 27,
|
||||
"sequence_id": 27.0,
|
||||
"shortcuts": [
|
||||
{
|
||||
"label": "User",
|
||||
|
|
|
|||
|
|
@ -107,20 +107,26 @@ class CustomizeForm(Document):
|
|||
def set_name_translation(self):
|
||||
'''Create, update custom translation for this doctype'''
|
||||
current = self.get_name_translation()
|
||||
if current:
|
||||
if self.label and current.translated_text != self.label:
|
||||
frappe.db.set_value('Translation', current.name, 'translated_text', self.label)
|
||||
frappe.translate.clear_cache()
|
||||
else:
|
||||
if not self.label:
|
||||
if current:
|
||||
# clear translation
|
||||
frappe.delete_doc('Translation', current.name)
|
||||
return
|
||||
|
||||
else:
|
||||
if self.label:
|
||||
frappe.get_doc(dict(doctype='Translation',
|
||||
source_text=self.doc_type,
|
||||
translated_text=self.label,
|
||||
language_code=frappe.local.lang or 'en')).insert()
|
||||
if not current:
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": 'Translation',
|
||||
"source_text": self.doc_type,
|
||||
"translated_text": self.label,
|
||||
"language_code": frappe.local.lang or 'en'
|
||||
}
|
||||
).insert()
|
||||
return
|
||||
|
||||
if self.label != current.translated_text:
|
||||
frappe.db.set_value('Translation', current.name, 'translated_text', self.label)
|
||||
frappe.translate.clear_cache()
|
||||
|
||||
def clear_existing_doc(self):
|
||||
doc_type = self.doc_type
|
||||
|
|
@ -377,7 +383,7 @@ class CustomizeForm(Document):
|
|||
|
||||
def make_property_setter(self, prop, value, property_type, fieldname=None,
|
||||
apply_on=None, row_name = None):
|
||||
delete_property_setter(self.doc_type, prop, fieldname)
|
||||
delete_property_setter(self.doc_type, prop, fieldname, row_name)
|
||||
|
||||
property_value = self.get_existing_property_value(prop, fieldname)
|
||||
|
||||
|
|
|
|||
|
|
@ -304,3 +304,25 @@ class TestCustomizeForm(unittest.TestCase):
|
|||
|
||||
action = [d for d in event.actions if d.label=='Test Action']
|
||||
self.assertEqual(len(action), 0)
|
||||
|
||||
def test_custom_label(self):
|
||||
d = self.get_customize_form("Event")
|
||||
|
||||
# add label
|
||||
d.label = "Test Rename"
|
||||
d.run_method("save_customization")
|
||||
self.assertEqual(d.label, "Test Rename")
|
||||
|
||||
# change label
|
||||
d.label = "Test Rename 2"
|
||||
d.run_method("save_customization")
|
||||
self.assertEqual(d.label, "Test Rename 2")
|
||||
|
||||
# saving again to make sure existing label persists
|
||||
d.run_method("save_customization")
|
||||
self.assertEqual(d.label, "Test Rename 2")
|
||||
|
||||
# clear label
|
||||
d.label = ""
|
||||
d.run_method("save_customization")
|
||||
self.assertEqual(d.label, "")
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class PropertySetter(Document):
|
|||
def validate(self):
|
||||
self.validate_fieldtype_change()
|
||||
if self.is_new():
|
||||
delete_property_setter(self.doc_type, self.property, self.field_name)
|
||||
delete_property_setter(self.doc_type, self.property, self.field_name, self.row_name)
|
||||
|
||||
# clear cache
|
||||
frappe.clear_cache(doctype = self.doc_type)
|
||||
|
|
@ -91,11 +91,13 @@ def make_property_setter(doctype, fieldname, property, value, property_type, for
|
|||
property_setter.insert()
|
||||
return property_setter
|
||||
|
||||
def delete_property_setter(doc_type, property, field_name=None):
|
||||
def delete_property_setter(doc_type, property, field_name=None, row_name=None):
|
||||
"""delete other property setters on this, if this is new"""
|
||||
filters = dict(doc_type = doc_type, property=property)
|
||||
filters = dict(doc_type=doc_type, property=property)
|
||||
if field_name:
|
||||
filters['field_name'] = field_name
|
||||
if row_name:
|
||||
filters["row_name"] = row_name
|
||||
|
||||
frappe.db.delete('Property Setter', filters)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"charts": [],
|
||||
"content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Customization\",\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customize Form\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Custom Role\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Client Script\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Server Script\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Dashboards\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Form Customization\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other\",\"col\":4}}]",
|
||||
"content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Customization\",\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customize Form\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Custom Role\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Client Script\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Server Script\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Dashboards\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Form Customization\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other\",\"col\":4}}]",
|
||||
"creation": "2020-03-02 15:15:03.839594",
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace",
|
||||
|
|
@ -123,7 +123,7 @@
|
|||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2021-11-24 16:20:03.500885",
|
||||
"modified": "2022-01-13 17:28:08.345794",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Customization",
|
||||
|
|
@ -132,7 +132,7 @@
|
|||
"public": 1,
|
||||
"restrict_to_domain": "",
|
||||
"roles": [],
|
||||
"sequence_id": 8,
|
||||
"sequence_id": 8.0,
|
||||
"shortcuts": [
|
||||
{
|
||||
"label": "Customize Form",
|
||||
|
|
|
|||
|
|
@ -10,19 +10,20 @@ import re
|
|||
import string
|
||||
from contextlib import contextmanager
|
||||
from time import time
|
||||
from typing import Dict, List, Union, Tuple
|
||||
from typing import Dict, List, Tuple, Union
|
||||
|
||||
from pypika.terms import Criterion, NullValue, PseudoColumn
|
||||
|
||||
import frappe
|
||||
import frappe.defaults
|
||||
import frappe.model.meta
|
||||
from frappe import _
|
||||
from frappe.utils import now, getdate, cast, get_datetime
|
||||
from frappe.model.utils.link_count import flush_local_link_count
|
||||
from frappe.query_builder.functions import Count
|
||||
from frappe.query_builder.functions import Min, Max, Avg, Sum
|
||||
from frappe.query_builder.utils import Column
|
||||
from frappe.query_builder.utils import DocType
|
||||
from frappe.utils import cast, get_datetime, getdate, now, sbool
|
||||
|
||||
from .query import Query
|
||||
from pypika.terms import Criterion, PseudoColumn
|
||||
|
||||
|
||||
class Database(object):
|
||||
|
|
@ -278,7 +279,9 @@ class Database(object):
|
|||
if self.auto_commit_on_many_writes:
|
||||
self.commit()
|
||||
else:
|
||||
frappe.throw(_("Too many writes in one request. Please send smaller requests"), frappe.ValidationError)
|
||||
msg = "<br><br>" + _("Too many changes to database in single action.") + "<br>"
|
||||
msg += _("The changes have been reverted.") + "<br>"
|
||||
raise frappe.TooManyWritesError(msg)
|
||||
|
||||
def check_implicit_commit(self, query):
|
||||
if self.transaction_writes and \
|
||||
|
|
@ -555,7 +558,21 @@ class Database(object):
|
|||
def get_list(*args, **kwargs):
|
||||
return frappe.get_list(*args, **kwargs)
|
||||
|
||||
def get_single_value(self, doctype, fieldname, cache=False):
|
||||
def set_single_value(self, doctype, fieldname, value, *args, **kwargs):
|
||||
"""Set field value of Single DocType.
|
||||
|
||||
:param doctype: DocType of the single object
|
||||
:param fieldname: `fieldname` of the property
|
||||
:param value: `value` of the property
|
||||
|
||||
Example:
|
||||
|
||||
# Update the `deny_multiple_sessions` field in System Settings DocType.
|
||||
company = frappe.db.set_single_value("System Settings", "deny_multiple_sessions", True)
|
||||
"""
|
||||
return self.set_value(doctype, doctype, fieldname, value, *args, **kwargs)
|
||||
|
||||
def get_single_value(self, doctype, fieldname, cache=True):
|
||||
"""Get property of Single DocType. Cache locally by default
|
||||
|
||||
:param doctype: DocType of the single object whose value is requested
|
||||
|
|
@ -570,7 +587,7 @@ class Database(object):
|
|||
if not doctype in self.value_cache:
|
||||
self.value_cache[doctype] = {}
|
||||
|
||||
if fieldname in self.value_cache[doctype]:
|
||||
if cache and fieldname in self.value_cache[doctype]:
|
||||
return self.value_cache[doctype][fieldname]
|
||||
|
||||
val = self.query.get_sql(
|
||||
|
|
@ -677,53 +694,55 @@ class Database(object):
|
|||
:param debug: Print the query in the developer / js console.
|
||||
:param for_update: Will add a row-level lock to the value that is being set so that it can be released on commit.
|
||||
"""
|
||||
if not modified:
|
||||
modified = now()
|
||||
if not modified_by:
|
||||
modified_by = frappe.session.user
|
||||
is_single_doctype = not (dn and dt != dn)
|
||||
to_update = field if isinstance(field, dict) else {field: val}
|
||||
|
||||
to_update = {}
|
||||
if update_modified:
|
||||
to_update = {"modified": modified, "modified_by": modified_by}
|
||||
modified = modified or now()
|
||||
modified_by = modified_by or frappe.session.user
|
||||
to_update.update({"modified": modified, "modified_by": modified_by})
|
||||
|
||||
if is_single_doctype:
|
||||
frappe.db.delete(
|
||||
"Singles",
|
||||
filters={"field": ("in", tuple(to_update)), "doctype": dt}, debug=debug
|
||||
)
|
||||
|
||||
singles_data = ((dt, key, sbool(value)) for key, value in to_update.items())
|
||||
query = (
|
||||
frappe.qb.into("Singles")
|
||||
.columns("doctype", "field", "value")
|
||||
.insert(*singles_data)
|
||||
).run(debug=debug)
|
||||
frappe.clear_document_cache(dt, dt)
|
||||
|
||||
if isinstance(field, dict):
|
||||
to_update.update(field)
|
||||
else:
|
||||
to_update.update({field: val})
|
||||
table = DocType(dt)
|
||||
|
||||
if dn and dt!=dn:
|
||||
# with table
|
||||
set_values = []
|
||||
for key in to_update:
|
||||
set_values.append('`{0}`=%({0})s'.format(key))
|
||||
if for_update:
|
||||
docnames = tuple(
|
||||
self.get_values(dt, dn, "name", debug=debug, for_update=for_update, pluck=True)
|
||||
) or (NullValue(),)
|
||||
query = frappe.qb.update(table).where(table.name.isin(docnames))
|
||||
|
||||
for name in self.get_values(dt, dn, 'name', for_update=for_update, debug=debug):
|
||||
values = dict(name=name[0])
|
||||
values.update(to_update)
|
||||
for docname in docnames:
|
||||
frappe.clear_document_cache(dt, docname)
|
||||
|
||||
self.sql("""update `tab{0}`
|
||||
set {1} where name=%(name)s""".format(dt, ', '.join(set_values)),
|
||||
values, debug=debug)
|
||||
else:
|
||||
query = self.query.build_conditions(table=dt, filters=dn, update=True)
|
||||
# TODO: Fix this; doesn't work rn - gavin@frappe.io
|
||||
# frappe.cache().hdel_keys(dt, "document_cache")
|
||||
# Workaround: clear all document caches
|
||||
frappe.cache().delete_value('document_cache')
|
||||
|
||||
frappe.clear_document_cache(dt, values['name'])
|
||||
else:
|
||||
# for singles
|
||||
keys = list(to_update)
|
||||
self.sql('''
|
||||
delete from `tabSingles`
|
||||
where field in ({0}) and
|
||||
doctype=%s'''.format(', '.join(['%s']*len(keys))),
|
||||
list(keys) + [dt], debug=debug)
|
||||
for key, value in to_update.items():
|
||||
self.sql('''insert into `tabSingles` (doctype, field, value) values (%s, %s, %s)''',
|
||||
(dt, key, value), debug=debug)
|
||||
for column, value in to_update.items():
|
||||
query = query.set(column, value)
|
||||
|
||||
frappe.clear_document_cache(dt, dn)
|
||||
query.run(debug=debug)
|
||||
|
||||
if dt in self.value_cache:
|
||||
del self.value_cache[dt]
|
||||
|
||||
|
||||
@staticmethod
|
||||
def set(doc, field, val):
|
||||
"""Set value in document. **Avoid**"""
|
||||
|
|
|
|||
|
|
@ -245,9 +245,16 @@ class MariaDBDatabase(Database):
|
|||
column_name as 'name',
|
||||
column_type as 'type',
|
||||
column_default as 'default',
|
||||
column_key = 'MUL' as 'index',
|
||||
COALESCE(
|
||||
(select 1
|
||||
from information_schema.statistics
|
||||
where table_name="{table_name}"
|
||||
and column_name=columns.column_name
|
||||
and NON_UNIQUE=1
|
||||
limit 1
|
||||
), 0) as 'index',
|
||||
column_key = 'UNI' as 'unique'
|
||||
from information_schema.columns
|
||||
from information_schema.columns as columns
|
||||
where table_name = '{table_name}' '''.format(table_name=table_name), as_dict=1)
|
||||
|
||||
def has_index(self, table_name, index_name):
|
||||
|
|
|
|||
|
|
@ -58,18 +58,34 @@ class MariaDBTable(DBTable):
|
|||
modify_column_query.append("MODIFY `{}` {}".format(col.fieldname, col.get_definition()))
|
||||
|
||||
for col in self.add_index:
|
||||
# if index key not exists
|
||||
if not frappe.db.sql("SHOW INDEX FROM `%s` WHERE key_name = %s" %
|
||||
(self.table_name, '%s'), col.fieldname):
|
||||
add_index_query.append("ADD INDEX `{}`(`{}`)".format(col.fieldname, col.fieldname))
|
||||
# if index key does not exists
|
||||
if not frappe.db.has_index(self.table_name, col.fieldname + '_index'):
|
||||
add_index_query.append("ADD INDEX `{}_index`(`{}`)".format(col.fieldname, col.fieldname))
|
||||
|
||||
for col in self.drop_index:
|
||||
for col in self.drop_index + self.drop_unique:
|
||||
if col.fieldname != 'name': # primary key
|
||||
current_column = self.current_columns.get(col.fieldname.lower())
|
||||
unique_constraint_changed = current_column.unique != col.unique
|
||||
if unique_constraint_changed and not col.unique:
|
||||
# nosemgrep
|
||||
unique_index_record = frappe.db.sql("""
|
||||
SHOW INDEX FROM `{0}`
|
||||
WHERE Key_name=%s
|
||||
AND Non_unique=0
|
||||
""".format(self.table_name), (col.fieldname), as_dict=1)
|
||||
if unique_index_record:
|
||||
drop_index_query.append("DROP INDEX `{}`".format(unique_index_record[0].Key_name))
|
||||
index_constraint_changed = current_column.index != col.set_index
|
||||
# if index key exists
|
||||
if frappe.db.sql("""SHOW INDEX FROM `{0}`
|
||||
WHERE key_name=%s
|
||||
AND Non_unique=%s""".format(self.table_name), (col.fieldname, col.unique)):
|
||||
drop_index_query.append("drop index `{}`".format(col.fieldname))
|
||||
if index_constraint_changed and not col.set_index:
|
||||
# nosemgrep
|
||||
index_record = frappe.db.sql("""
|
||||
SHOW INDEX FROM `{0}`
|
||||
WHERE Key_name=%s
|
||||
AND Non_unique=1
|
||||
""".format(self.table_name), (col.fieldname + '_index'), as_dict=1)
|
||||
if index_record:
|
||||
drop_index_query.append("DROP INDEX `{}`".format(index_record[0].Key_name))
|
||||
|
||||
try:
|
||||
for query_parts in [add_column_query, modify_column_query, add_index_query, drop_index_query]:
|
||||
|
|
|
|||
|
|
@ -77,11 +77,11 @@ class PostgresDatabase(Database):
|
|||
"""Escape quotes and percent in given string."""
|
||||
if isinstance(s, bytes):
|
||||
s = s.decode('utf-8')
|
||||
|
||||
|
||||
# MariaDB's driver treats None as an empty string
|
||||
# So Postgres should do the same
|
||||
|
||||
if s is None:
|
||||
if s is None:
|
||||
s = ''
|
||||
|
||||
if percent:
|
||||
|
|
@ -170,11 +170,11 @@ class PostgresDatabase(Database):
|
|||
|
||||
@staticmethod
|
||||
def is_primary_key_violation(e):
|
||||
return e.pgcode == '23505' and '_pkey' in cstr(e.args[0])
|
||||
return getattr(e, "pgcode", None) == '23505' and '_pkey' in cstr(e.args[0])
|
||||
|
||||
@staticmethod
|
||||
def is_unique_key_violation(e):
|
||||
return e.pgcode == '23505' and '_key' in cstr(e.args[0])
|
||||
return getattr(e, "pgcode", None) == '23505' and '_key' in cstr(e.args[0])
|
||||
|
||||
@staticmethod
|
||||
def is_duplicate_fieldname(e):
|
||||
|
|
@ -308,18 +308,20 @@ class PostgresDatabase(Database):
|
|||
WHEN 'timestamp without time zone' THEN 'timestamp'
|
||||
ELSE a.data_type
|
||||
END AS type,
|
||||
COUNT(b.indexdef) AS Index,
|
||||
BOOL_OR(b.index) AS index,
|
||||
SPLIT_PART(COALESCE(a.column_default, NULL), '::', 1) AS default,
|
||||
BOOL_OR(b.unique) AS unique
|
||||
FROM information_schema.columns a
|
||||
LEFT JOIN
|
||||
(SELECT indexdef, tablename, indexdef LIKE '%UNIQUE INDEX%' AS unique
|
||||
(SELECT indexdef, tablename,
|
||||
indexdef LIKE '%UNIQUE INDEX%' AS unique,
|
||||
indexdef NOT LIKE '%UNIQUE INDEX%' AS index
|
||||
FROM pg_indexes
|
||||
WHERE tablename='{table_name}') b
|
||||
ON SUBSTRING(b.indexdef, '\(.*\)') LIKE CONCAT('%', a.column_name, '%')
|
||||
ON SUBSTRING(b.indexdef, '(.*)') LIKE CONCAT('%', a.column_name, '%')
|
||||
WHERE a.table_name = '{table_name}'
|
||||
GROUP BY a.column_name, a.data_type, a.column_default, a.character_maximum_length;'''
|
||||
.format(table_name=table_name), as_dict=1)
|
||||
GROUP BY a.column_name, a.data_type, a.column_default, a.character_maximum_length;
|
||||
'''.format(table_name=table_name), as_dict=1)
|
||||
|
||||
def get_database_list(self, target):
|
||||
return [d[0] for d in self.sql("SELECT datname FROM pg_database;")]
|
||||
|
|
|
|||
|
|
@ -11,8 +11,6 @@ class PostgresTable(DBTable):
|
|||
column_defs = self.get_column_definitions()
|
||||
if column_defs: add_text += ',\n'.join(column_defs)
|
||||
|
||||
# index
|
||||
# index_defs = self.get_index_definitions()
|
||||
# TODO: set docstatus length
|
||||
# create table
|
||||
frappe.db.sql("""create table `%s` (
|
||||
|
|
@ -28,8 +26,25 @@ class PostgresTable(DBTable):
|
|||
idx bigint not null default '0',
|
||||
%s)""".format(varchar_len=frappe.db.VARCHAR_LEN) % (self.table_name, add_text))
|
||||
|
||||
self.create_indexes()
|
||||
frappe.db.commit()
|
||||
|
||||
def create_indexes(self):
|
||||
create_index_query = ""
|
||||
for key, col in self.columns.items():
|
||||
if (col.set_index
|
||||
and col.fieldtype in frappe.db.type_map
|
||||
and frappe.db.type_map.get(col.fieldtype)[0]
|
||||
not in ('text', 'longtext')):
|
||||
create_index_query += 'CREATE INDEX IF NOT EXISTS "{index_name}" ON `{table_name}`(`{field}`);'.format(
|
||||
index_name=col.fieldname,
|
||||
table_name=self.table_name,
|
||||
field=col.fieldname
|
||||
)
|
||||
if create_index_query:
|
||||
# nosemgrep
|
||||
frappe.db.sql(create_index_query)
|
||||
|
||||
def alter(self):
|
||||
for col in self.columns.values():
|
||||
col.build_for_alter_table(self.current_columns.get(col.fieldname.lower()))
|
||||
|
|
@ -52,8 +67,8 @@ class PostgresTable(DBTable):
|
|||
query.append("ALTER COLUMN `{0}` TYPE {1} {2}".format(
|
||||
col.fieldname,
|
||||
get_definition(col.fieldtype, precision=col.precision, length=col.length),
|
||||
using_clause)
|
||||
)
|
||||
using_clause
|
||||
))
|
||||
|
||||
for col in self.set_default:
|
||||
if col.fieldname=="name":
|
||||
|
|
@ -73,37 +88,54 @@ class PostgresTable(DBTable):
|
|||
|
||||
query.append("ALTER COLUMN `{}` SET DEFAULT {}".format(col.fieldname, col_default))
|
||||
|
||||
create_index_query = ""
|
||||
create_contraint_query = ""
|
||||
for col in self.add_index:
|
||||
# if index key not exists
|
||||
create_index_query += 'CREATE INDEX IF NOT EXISTS "{index_name}" ON `{table_name}`(`{field}`);'.format(
|
||||
create_contraint_query += 'CREATE INDEX IF NOT EXISTS "{index_name}" ON `{table_name}`(`{field}`);'.format(
|
||||
index_name=col.fieldname,
|
||||
table_name=self.table_name,
|
||||
field=col.fieldname)
|
||||
|
||||
drop_index_query = ""
|
||||
for col in self.add_unique:
|
||||
# if index key not exists
|
||||
create_contraint_query += 'CREATE UNIQUE INDEX IF NOT EXISTS "unique_{index_name}" ON `{table_name}`(`{field}`);'.format(
|
||||
index_name=col.fieldname,
|
||||
table_name=self.table_name,
|
||||
field=col.fieldname
|
||||
)
|
||||
|
||||
drop_contraint_query = ""
|
||||
for col in self.drop_index:
|
||||
# primary key
|
||||
if col.fieldname != 'name':
|
||||
# if index key exists
|
||||
if not frappe.db.has_index(self.table_name, col.fieldname):
|
||||
drop_index_query += 'DROP INDEX IF EXISTS "{}" ;'.format(col.fieldname)
|
||||
drop_contraint_query += 'DROP INDEX IF EXISTS "{}" ;'.format(col.fieldname)
|
||||
|
||||
if query:
|
||||
try:
|
||||
for col in self.drop_unique:
|
||||
# primary key
|
||||
if col.fieldname != 'name':
|
||||
# if index key exists
|
||||
drop_contraint_query += 'DROP INDEX IF EXISTS "unique_{}" ;'.format(col.fieldname)
|
||||
try:
|
||||
if query:
|
||||
final_alter_query = "ALTER TABLE `{}` {}".format(self.table_name, ", ".join(query))
|
||||
if final_alter_query: frappe.db.sql(final_alter_query)
|
||||
if create_index_query: frappe.db.sql(create_index_query)
|
||||
if drop_index_query: frappe.db.sql(drop_index_query)
|
||||
except Exception as e:
|
||||
# sanitize
|
||||
if frappe.db.is_duplicate_fieldname(e):
|
||||
frappe.throw(str(e))
|
||||
elif frappe.db.is_duplicate_entry(e):
|
||||
fieldname = str(e).split("'")[-2]
|
||||
frappe.throw(_("""{0} field cannot be set as unique in {1},
|
||||
as there are non-unique existing values""".format(
|
||||
fieldname, self.table_name)))
|
||||
raise e
|
||||
else:
|
||||
raise e
|
||||
# nosemgrep
|
||||
frappe.db.sql(final_alter_query)
|
||||
if create_contraint_query:
|
||||
# nosemgrep
|
||||
frappe.db.sql(create_contraint_query)
|
||||
if drop_contraint_query:
|
||||
# nosemgrep
|
||||
frappe.db.sql(drop_contraint_query)
|
||||
except Exception as e:
|
||||
# sanitize
|
||||
if frappe.db.is_duplicate_fieldname(e):
|
||||
frappe.throw(str(e))
|
||||
elif frappe.db.is_duplicate_entry(e):
|
||||
fieldname = str(e).split("'")[-2]
|
||||
frappe.throw(
|
||||
_("{0} field cannot be set as unique in {1}, as there are non-unique existing values")
|
||||
.format(fieldname, self.table_name)
|
||||
)
|
||||
else:
|
||||
raise e
|
||||
|
|
|
|||
|
|
@ -308,7 +308,7 @@ class Permission:
|
|||
doctype = [doctype]
|
||||
|
||||
for dt in doctype:
|
||||
dt = re.sub("tab", "", dt)
|
||||
dt = re.sub("^tab", "", dt)
|
||||
if not frappe.has_permission(
|
||||
dt,
|
||||
"select",
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ class DBTable:
|
|||
self.change_name = []
|
||||
self.add_unique = []
|
||||
self.add_index = []
|
||||
self.drop_unique = []
|
||||
self.drop_index = []
|
||||
self.set_default = []
|
||||
|
||||
|
|
@ -219,8 +220,10 @@ class DbColumn:
|
|||
self.table.change_type.append(self)
|
||||
|
||||
# unique
|
||||
if((self.unique and not current_def['unique']) and column_type not in ('text', 'longtext')):
|
||||
if ((self.unique and not current_def['unique']) and column_type not in ('text', 'longtext')):
|
||||
self.table.add_unique.append(self)
|
||||
elif (current_def['unique'] and not self.unique) and column_type not in ('text', 'longtext'):
|
||||
self.table.drop_unique.append(self)
|
||||
|
||||
# default
|
||||
if (self.default_changed(current_def)
|
||||
|
|
@ -230,9 +233,7 @@ class DbColumn:
|
|||
self.table.set_default.append(self)
|
||||
|
||||
# index should be applied or dropped irrespective of type change
|
||||
if ((current_def['index'] and not self.set_index and not self.unique)
|
||||
or (current_def['unique'] and not self.unique)):
|
||||
# to drop unique you have to drop index
|
||||
if (current_def['index'] and not self.set_index) and column_type not in ('text', 'longtext'):
|
||||
self.table.drop_index.append(self)
|
||||
|
||||
elif (not current_def['index'] and self.set_index) and not (column_type in ('text', 'longtext')):
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ def set_default(key, value, parent, parenttype="__default"):
|
|||
"defkey": key,
|
||||
"parent": parent
|
||||
})
|
||||
if value != None:
|
||||
if value is not None:
|
||||
add_default(key, value, parent)
|
||||
else:
|
||||
_clear_cache(parent)
|
||||
|
|
@ -187,7 +187,7 @@ def get_defaults_for(parent="__default"):
|
|||
"""get all defaults"""
|
||||
defaults = frappe.cache().hget("defaults", parent)
|
||||
|
||||
if defaults==None:
|
||||
if defaults is None:
|
||||
# sort descending because first default must get precedence
|
||||
table = DocType("DefaultValue")
|
||||
res = frappe.qb.from_(table).where(
|
||||
|
|
|
|||
|
|
@ -56,31 +56,6 @@ class Workspace:
|
|||
self.restricted_doctypes = frappe.cache().get_value("domain_restricted_doctypes") or build_domain_restriced_doctype_cache()
|
||||
self.restricted_pages = frappe.cache().get_value("domain_restricted_pages") or build_domain_restriced_page_cache()
|
||||
|
||||
def is_page_allowed(self):
|
||||
cards = self.doc.get_link_groups() + get_custom_reports_and_doctypes(self.doc.module)
|
||||
shortcuts = self.doc.shortcuts
|
||||
|
||||
for section in cards:
|
||||
links = loads(section.get('links')) if isinstance(section.get('links'), str) else section.get('links')
|
||||
for item in links:
|
||||
if self.is_item_allowed(item.get('link_to'), item.get('link_type')):
|
||||
return True
|
||||
|
||||
def _in_active_domains(item):
|
||||
if not item.restrict_to_domain:
|
||||
return True
|
||||
else:
|
||||
return item.restrict_to_domain in frappe.get_active_domains()
|
||||
|
||||
for item in shortcuts:
|
||||
if self.is_item_allowed(item.link_to, item.type) and _in_active_domains(item):
|
||||
return True
|
||||
|
||||
if not shortcuts and not self.doc.links:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def is_permitted(self):
|
||||
"""Returns true if Has Role is not set or the user is allowed."""
|
||||
from frappe.utils import has_common
|
||||
|
|
@ -346,20 +321,20 @@ def get_desktop_page(page):
|
|||
dict: dictionary of cards, charts and shortcuts to be displayed on website
|
||||
"""
|
||||
try:
|
||||
wspace = Workspace(loads(page))
|
||||
wspace.build_workspace()
|
||||
workspace = Workspace(loads(page))
|
||||
workspace.build_workspace()
|
||||
return {
|
||||
'charts': wspace.charts,
|
||||
'shortcuts': wspace.shortcuts,
|
||||
'cards': wspace.cards,
|
||||
'onboardings': wspace.onboardings
|
||||
'charts': workspace.charts,
|
||||
'shortcuts': workspace.shortcuts,
|
||||
'cards': workspace.cards,
|
||||
'onboardings': workspace.onboardings
|
||||
}
|
||||
except DoesNotExistError:
|
||||
frappe.log_error(frappe.get_traceback())
|
||||
return {}
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_wspace_sidebar_items():
|
||||
def get_workspace_sidebar_items():
|
||||
"""Get list of sidebar items for desk"""
|
||||
has_access = "Workspace Manager" in frappe.get_roles()
|
||||
|
||||
|
|
@ -385,8 +360,8 @@ def get_wspace_sidebar_items():
|
|||
# Filter Page based on Permission
|
||||
for page in all_pages:
|
||||
try:
|
||||
wspace = Workspace(page, True)
|
||||
if wspace.is_permitted() and wspace.is_page_allowed() or has_access:
|
||||
workspace = Workspace(page, True)
|
||||
if has_access or workspace.is_permitted():
|
||||
if page.public:
|
||||
pages.append(page)
|
||||
elif page.for_user == frappe.session.user:
|
||||
|
|
@ -453,25 +428,24 @@ def get_custom_report_list(module):
|
|||
return out
|
||||
|
||||
def save_new_widget(doc, page, blocks, new_widgets):
|
||||
if loads(new_widgets):
|
||||
widgets = _dict(loads(new_widgets))
|
||||
|
||||
widgets = _dict(loads(new_widgets))
|
||||
|
||||
if widgets.chart:
|
||||
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.card:
|
||||
doc.build_links_table_from_card(widgets.card)
|
||||
if widgets.chart:
|
||||
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.card:
|
||||
doc.build_links_table_from_card(widgets.card)
|
||||
|
||||
# remove duplicate and unwanted widgets
|
||||
if widgets:
|
||||
clean_up(doc, blocks)
|
||||
clean_up(doc, blocks)
|
||||
|
||||
try:
|
||||
doc.save(ignore_permissions=True)
|
||||
except (ValidationError, TypeError) as e:
|
||||
# Create a json string to log
|
||||
json_config = dumps(widgets, sort_keys=True, indent=4)
|
||||
json_config = widgets and dumps(widgets, sort_keys=True, indent=4)
|
||||
|
||||
# Error log body
|
||||
log = \
|
||||
|
|
|
|||
|
|
@ -42,13 +42,13 @@ def submit_cancel_or_update_docs(doctype, docnames, action='submit', data=None):
|
|||
doc = frappe.get_doc(doctype, d)
|
||||
try:
|
||||
message = ''
|
||||
if action == 'submit' and doc.docstatus==0:
|
||||
if action == 'submit' and doc.docstatus.is_draft():
|
||||
doc.submit()
|
||||
message = _('Submiting {0}').format(doctype)
|
||||
elif action == 'cancel' and doc.docstatus==1:
|
||||
elif action == 'cancel' and doc.docstatus.is_submitted():
|
||||
doc.cancel()
|
||||
message = _('Cancelling {0}').format(doctype)
|
||||
elif action == 'update' and doc.docstatus < 2:
|
||||
elif action == 'update' and not doc.docstatus.is_cancelled():
|
||||
doc.update(data)
|
||||
doc.save()
|
||||
message = _('Updating {0}').format(doctype)
|
||||
|
|
|
|||
|
|
@ -52,3 +52,9 @@ def deferred_insert(routes):
|
|||
]
|
||||
|
||||
_deferred_insert("Route History", json.dumps(routes))
|
||||
|
||||
@frappe.whitelist()
|
||||
def frequently_visited_links():
|
||||
return frappe.get_all('Route History', fields=['route', 'count(name) as count'], filters={
|
||||
'user': frappe.session.user
|
||||
}, group_by="route", order_by="count desc", limit=5)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:label",
|
||||
"beta": 1,
|
||||
"creation": "2020-01-23 13:45:59.470592",
|
||||
|
|
@ -141,7 +142,7 @@
|
|||
},
|
||||
{
|
||||
"fieldname": "sequence_id",
|
||||
"fieldtype": "Int",
|
||||
"fieldtype": "Float",
|
||||
"label": "Sequence Id"
|
||||
},
|
||||
{
|
||||
|
|
@ -158,7 +159,7 @@
|
|||
],
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-16 12:01:06.450622",
|
||||
"modified": "2021-12-15 19:33:00.805265",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Workspace",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import frappe
|
|||
from frappe import _
|
||||
from frappe.modules.export_file import export_to_files
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.rename_doc import rename_doc
|
||||
from frappe.desk.desktop import save_new_widget
|
||||
from frappe.desk.utils import validate_route_conflict
|
||||
|
||||
|
|
@ -121,77 +122,157 @@ def get_report_type(report):
|
|||
report_type = frappe.get_value("Report", report, "report_type")
|
||||
return report_type in ["Query Report", "Script Report", "Custom Report"]
|
||||
|
||||
@frappe.whitelist()
|
||||
def new_page(new_page):
|
||||
if not loads(new_page):
|
||||
return
|
||||
|
||||
page = loads(new_page)
|
||||
|
||||
if page.get("public") and not is_workspace_manager():
|
||||
return
|
||||
|
||||
doc = frappe.new_doc('Workspace')
|
||||
doc.title = page.get('title')
|
||||
doc.icon = page.get('icon')
|
||||
doc.content = page.get('content')
|
||||
doc.parent_page = page.get('parent_page')
|
||||
doc.label = page.get('label')
|
||||
doc.for_user = page.get('for_user')
|
||||
doc.public = page.get('public')
|
||||
doc.sequence_id = last_sequence_id(doc) + 1
|
||||
doc.save(ignore_permissions=True)
|
||||
|
||||
return doc
|
||||
|
||||
@frappe.whitelist()
|
||||
def save_page(title, icon, parent, public, sb_public_items, sb_private_items, deleted_pages, new_widgets, blocks, save):
|
||||
save = frappe.parse_json(save)
|
||||
def save_page(title, public, new_widgets, blocks):
|
||||
public = frappe.parse_json(public)
|
||||
if save:
|
||||
doc = frappe.new_doc('Workspace')
|
||||
doc.title = title
|
||||
doc.icon = icon
|
||||
doc.content = blocks
|
||||
doc.parent_page = parent
|
||||
|
||||
if public:
|
||||
doc.label = title
|
||||
doc.public = 1
|
||||
else:
|
||||
doc.label = title + "-" + frappe.session.user
|
||||
doc.for_user = frappe.session.user
|
||||
doc.save(ignore_permissions=True)
|
||||
else:
|
||||
if public:
|
||||
filters = {
|
||||
'public': public,
|
||||
'label': title
|
||||
}
|
||||
else:
|
||||
filters = {
|
||||
'for_user': frappe.session.user,
|
||||
'label': title + "-" + frappe.session.user
|
||||
}
|
||||
pages = frappe.get_list("Workspace", filters=filters)
|
||||
if pages:
|
||||
doc = frappe.get_doc("Workspace", pages[0])
|
||||
filters = {
|
||||
'public': public,
|
||||
'label': title
|
||||
}
|
||||
|
||||
doc.content = blocks
|
||||
doc.save(ignore_permissions=True)
|
||||
if not public:
|
||||
filters = {
|
||||
'for_user': frappe.session.user,
|
||||
'label': title + "-" + frappe.session.user
|
||||
}
|
||||
pages = frappe.get_list("Workspace", filters=filters)
|
||||
if pages:
|
||||
doc = frappe.get_doc("Workspace", pages[0])
|
||||
|
||||
if loads(new_widgets):
|
||||
save_new_widget(doc, title, blocks, new_widgets)
|
||||
doc.content = blocks
|
||||
doc.save(ignore_permissions=True)
|
||||
|
||||
if loads(sb_public_items) or loads(sb_private_items):
|
||||
sort_pages(loads(sb_public_items), loads(sb_private_items))
|
||||
|
||||
if loads(deleted_pages):
|
||||
return delete_pages(loads(deleted_pages))
|
||||
save_new_widget(doc, title, blocks, new_widgets)
|
||||
|
||||
return {"name": title, "public": public, "label": doc.label}
|
||||
|
||||
def delete_pages(deleted_pages):
|
||||
for page in deleted_pages:
|
||||
if page.get("public") and not is_workspace_manager():
|
||||
return {"name": page.get("title"), "public": 1, "label": page.get("label")}
|
||||
@frappe.whitelist()
|
||||
def update_page(name, title, icon, parent, public):
|
||||
public = frappe.parse_json(public)
|
||||
|
||||
if frappe.db.exists("Workspace", page.get("name")):
|
||||
frappe.get_doc("Workspace", page.get("name")).delete(ignore_permissions=True)
|
||||
doc = frappe.get_doc("Workspace", name)
|
||||
|
||||
return {"name": "Home", "public": 1, "label": "Home"}
|
||||
filters = {
|
||||
'parent_page': doc.title,
|
||||
'public': doc.public
|
||||
}
|
||||
child_docs = frappe.get_list("Workspace", filters=filters)
|
||||
|
||||
if doc:
|
||||
doc.title = title
|
||||
doc.icon = icon
|
||||
doc.parent_page = parent
|
||||
if doc.public != public:
|
||||
doc.sequence_id = frappe.db.count('Workspace', {'public':public}, cache=True)
|
||||
doc.public = public
|
||||
doc.for_user = '' if public else doc.for_user or frappe.session.user
|
||||
doc.label = '{0}-{1}'.format(title, doc.for_user) if doc.for_user else title
|
||||
doc.save(ignore_permissions=True)
|
||||
|
||||
if name != doc.label:
|
||||
rename_doc("Workspace", name, doc.label, force=True, ignore_permissions=True)
|
||||
|
||||
# update new name and public in child pages
|
||||
if child_docs:
|
||||
for child in child_docs:
|
||||
child_doc = frappe.get_doc("Workspace", child.name)
|
||||
child_doc.parent_page = doc.title
|
||||
child_doc.public = doc.public
|
||||
child_doc.save(ignore_permissions=True)
|
||||
|
||||
return {"name": doc.title, "public": doc.public, "label": doc.label}
|
||||
|
||||
@frappe.whitelist()
|
||||
def duplicate_page(page_name, new_page):
|
||||
if not loads(new_page):
|
||||
return
|
||||
|
||||
new_page = loads(new_page)
|
||||
|
||||
if new_page.get("is_public") and not is_workspace_manager():
|
||||
return
|
||||
|
||||
old_doc = frappe.get_doc("Workspace", page_name)
|
||||
doc = frappe.copy_doc(old_doc)
|
||||
doc.title = new_page.get('title')
|
||||
doc.icon = new_page.get('icon')
|
||||
doc.parent_page = new_page.get('parent') or ''
|
||||
doc.public = new_page.get('is_public')
|
||||
doc.for_user = ''
|
||||
doc.label = doc.title
|
||||
if not doc.public:
|
||||
doc.for_user = doc.for_user or frappe.session.user
|
||||
doc.label = '{0}-{1}'.format(doc.title, doc.for_user)
|
||||
doc.name = doc.label
|
||||
if old_doc.public == doc.public:
|
||||
doc.sequence_id += 0.1
|
||||
else:
|
||||
doc.sequence_id = last_sequence_id(doc) + 1
|
||||
doc.insert(ignore_permissions=True)
|
||||
|
||||
return doc
|
||||
|
||||
@frappe.whitelist()
|
||||
def delete_page(page):
|
||||
if not loads(page):
|
||||
return
|
||||
|
||||
page = loads(page)
|
||||
|
||||
if page.get("public") and not is_workspace_manager():
|
||||
return
|
||||
|
||||
if frappe.db.exists("Workspace", page.get("name")):
|
||||
frappe.get_doc("Workspace", page.get("name")).delete(ignore_permissions=True)
|
||||
|
||||
return {"name": page.get("name"), "public": page.get("public"), "title": page.get("title")}
|
||||
|
||||
@frappe.whitelist()
|
||||
def sort_pages(sb_public_items, sb_private_items):
|
||||
wspace_public_pages = get_page_list(['name', 'title'], {'public': 1})
|
||||
wspace_private_pages = get_page_list(['name', 'title'], {'for_user': frappe.session.user})
|
||||
if not loads(sb_public_items) and not loads(sb_private_items):
|
||||
return
|
||||
|
||||
sb_public_items = loads(sb_public_items)
|
||||
sb_private_items = loads(sb_private_items)
|
||||
|
||||
workspace_public_pages = get_page_list(['name', 'title'], {'public': 1})
|
||||
workspace_private_pages = get_page_list(['name', 'title'], {'for_user': frappe.session.user})
|
||||
|
||||
if sb_private_items:
|
||||
sort_page(wspace_private_pages, sb_private_items)
|
||||
return sort_page(workspace_private_pages, sb_private_items)
|
||||
|
||||
if sb_public_items and is_workspace_manager():
|
||||
sort_page(wspace_public_pages, sb_public_items)
|
||||
return sort_page(workspace_public_pages, sb_public_items)
|
||||
|
||||
def sort_page(wspace_pages, pages):
|
||||
return False
|
||||
|
||||
def sort_page(workspace_pages, pages):
|
||||
for seq, d in enumerate(pages):
|
||||
for page in wspace_pages:
|
||||
for page in workspace_pages:
|
||||
if page.title == d.get('title'):
|
||||
doc = frappe.get_doc('Workspace', page.name)
|
||||
doc.sequence_id = seq + 1
|
||||
|
|
@ -199,6 +280,27 @@ def sort_page(wspace_pages, pages):
|
|||
doc.save(ignore_permissions=True)
|
||||
break
|
||||
|
||||
return True
|
||||
|
||||
def last_sequence_id(doc):
|
||||
doc_exists = frappe.db.exists({
|
||||
'doctype': 'Workspace',
|
||||
'public': doc.public,
|
||||
'for_user': doc.for_user
|
||||
})
|
||||
|
||||
if not doc_exists:
|
||||
return 0
|
||||
|
||||
return frappe.db.get_list('Workspace',
|
||||
fields=['sequence_id'],
|
||||
filters={
|
||||
'public': doc.public,
|
||||
'for_user': doc.for_user
|
||||
},
|
||||
order_by="sequence_id desc"
|
||||
)[0].sequence_id
|
||||
|
||||
def get_page_list(fields, filters):
|
||||
return frappe.get_list("Workspace", fields=fields, filters=filters, order_by='sequence_id asc')
|
||||
|
||||
|
|
|
|||
|
|
@ -91,33 +91,82 @@ def get_docinfo(doc=None, doctype=None, name=None):
|
|||
raise frappe.PermissionError
|
||||
|
||||
all_communications = _get_communications(doc.doctype, doc.name)
|
||||
automated_messages = filter(lambda x: x['communication_type'] == 'Automated Message', all_communications)
|
||||
communications_except_auto_messages = filter(lambda x: x['communication_type'] != 'Automated Message', all_communications)
|
||||
automated_messages = [msg for msg in all_communications if msg['communication_type'] == 'Automated Message']
|
||||
communications_except_auto_messages = [msg for msg in all_communications if msg['communication_type'] != 'Automated Message']
|
||||
|
||||
frappe.response["docinfo"] = {
|
||||
docinfo = frappe._dict(user_info = {})
|
||||
|
||||
add_comments(doc, docinfo)
|
||||
|
||||
docinfo.update({
|
||||
"attachments": get_attachments(doc.doctype, doc.name),
|
||||
"attachment_logs": get_comments(doc.doctype, doc.name, 'attachment'),
|
||||
"communications": communications_except_auto_messages,
|
||||
"automated_messages": automated_messages,
|
||||
'comments': get_comments(doc.doctype, doc.name),
|
||||
'total_comments': len(json.loads(doc.get('_comments') or '[]')),
|
||||
'versions': get_versions(doc),
|
||||
"assignments": get_assignments(doc.doctype, doc.name),
|
||||
"assignment_logs": get_comments(doc.doctype, doc.name, 'assignment'),
|
||||
"permissions": get_doc_permissions(doc),
|
||||
"shared": frappe.share.get_users(doc.doctype, doc.name),
|
||||
"info_logs": get_comments(doc.doctype, doc.name, comment_type=['Info', 'Edit', 'Label']),
|
||||
"share_logs": get_comments(doc.doctype, doc.name, 'share'),
|
||||
"like_logs": get_comments(doc.doctype, doc.name, 'Like'),
|
||||
"workflow_logs": get_comments(doc.doctype, doc.name, comment_type="Workflow"),
|
||||
"views": get_view_logs(doc.doctype, doc.name),
|
||||
"energy_point_logs": get_point_logs(doc.doctype, doc.name),
|
||||
"additional_timeline_content": get_additional_timeline_content(doc.doctype, doc.name),
|
||||
"milestones": get_milestones(doc.doctype, doc.name),
|
||||
"is_document_followed": is_document_followed(doc.doctype, doc.name, frappe.session.user),
|
||||
"tags": get_tags(doc.doctype, doc.name),
|
||||
"document_email": get_document_email(doc.doctype, doc.name)
|
||||
}
|
||||
"document_email": get_document_email(doc.doctype, doc.name),
|
||||
})
|
||||
|
||||
update_user_info(docinfo)
|
||||
|
||||
frappe.response["docinfo"] = docinfo
|
||||
return docinfo
|
||||
|
||||
def add_comments(doc, docinfo):
|
||||
# divide comments into separate lists
|
||||
docinfo.comments = []
|
||||
docinfo.shared = []
|
||||
docinfo.assignment_logs = []
|
||||
docinfo.attachment_logs = []
|
||||
docinfo.info_logs = []
|
||||
docinfo.like_logs = []
|
||||
docinfo.workflow_logs = []
|
||||
|
||||
comments = frappe.get_all("Comment",
|
||||
fields=["name", "creation", "content", "owner", "comment_type"],
|
||||
filters={
|
||||
"reference_doctype": doc.doctype,
|
||||
"reference_name": doc.name
|
||||
}
|
||||
)
|
||||
|
||||
for c in comments:
|
||||
if c.comment_type == "Comment":
|
||||
c.content = frappe.utils.markdown(c.content)
|
||||
docinfo.comments.append(c)
|
||||
|
||||
elif c.comment_type in ('Shared', 'Unshared'):
|
||||
docinfo.shared.append(c)
|
||||
|
||||
elif c.comment_type in ('Assignment Completed', 'Assigned'):
|
||||
docinfo.assignment_logs.append(c)
|
||||
|
||||
elif c.comment_type in ('Attachment', 'Attachment Removed'):
|
||||
docinfo.attachment_logs.append(c)
|
||||
|
||||
elif c.comment_type in ('Info', 'Edit', 'Label'):
|
||||
docinfo.info_logs.append(c)
|
||||
|
||||
elif c.comment_type == "Like":
|
||||
docinfo.like_logs.append(c)
|
||||
|
||||
elif c.comment_type == "Workflow":
|
||||
docinfo.workflow_logs.append(c)
|
||||
|
||||
frappe.utils.add_user_info(c.owner, docinfo.user_info)
|
||||
|
||||
|
||||
return comments
|
||||
|
||||
|
||||
def get_milestones(doctype, name):
|
||||
return frappe.db.get_all('Milestone', fields = ['creation', 'owner', 'track_field', 'value'],
|
||||
|
|
@ -252,7 +301,7 @@ def get_communication_data(doctype, name, start=0, limit=20, after=None, fields=
|
|||
return communications
|
||||
|
||||
def get_assignments(dt, dn):
|
||||
cl = frappe.get_all("ToDo",
|
||||
return frappe.get_all("ToDo",
|
||||
fields=['name', 'allocated_to as owner', 'description', 'status'],
|
||||
filters={
|
||||
'reference_type': dt,
|
||||
|
|
@ -260,8 +309,6 @@ def get_assignments(dt, dn):
|
|||
'status': ('!=', 'Cancelled'),
|
||||
})
|
||||
|
||||
return cl
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_badge_info(doctypes, filters):
|
||||
filters = json.loads(filters)
|
||||
|
|
@ -319,3 +366,24 @@ def get_additional_timeline_content(doctype, docname):
|
|||
contents.extend(frappe.get_attr(method)(doctype, docname) or [])
|
||||
|
||||
return contents
|
||||
|
||||
def update_user_info(docinfo):
|
||||
for d in docinfo.communications:
|
||||
frappe.utils.add_user_info(d.sender, docinfo.user_info)
|
||||
|
||||
for d in docinfo.shared:
|
||||
frappe.utils.add_user_info(d.user, docinfo.user_info)
|
||||
|
||||
for d in docinfo.assignments:
|
||||
frappe.utils.add_user_info(d.owner, docinfo.user_info)
|
||||
|
||||
for d in docinfo.views:
|
||||
frappe.utils.add_user_info(d.owner, docinfo.user_info)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_user_info_for_viewers(users):
|
||||
user_info = {}
|
||||
for user in json.loads(users):
|
||||
frappe.utils.add_user_info(user, user_info)
|
||||
|
||||
return user_info
|
||||
|
|
|
|||
|
|
@ -524,7 +524,7 @@ def get_last_modified(doctype):
|
|||
raise
|
||||
|
||||
# hack: save as -1 so that it is cached
|
||||
if last_modified==None:
|
||||
if last_modified is None:
|
||||
last_modified = -1
|
||||
|
||||
return last_modified
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ def get_energy_points_percentage_chart_data(user, field):
|
|||
as_list = True)
|
||||
|
||||
return {
|
||||
"labels": [r[0] for r in result if r[0] != None],
|
||||
"labels": [r[0] for r in result if r[0] is not None],
|
||||
"datasets": [{
|
||||
"values": [r[1] for r in result]
|
||||
}]
|
||||
|
|
|
|||
|
|
@ -12,14 +12,14 @@ from io import StringIO
|
|||
from frappe.core.doctype.access_log.access_log import make_access_log
|
||||
from frappe.utils import cstr, format_duration
|
||||
from frappe.model.base_document import get_controller
|
||||
|
||||
from frappe.utils import add_user_info
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.read_only()
|
||||
def get():
|
||||
args = get_form_params()
|
||||
# If virtual doctype get data from controller het_list method
|
||||
if frappe.db.get_value("DocType", filters={"name": args.doctype}, fieldname="is_virtual"):
|
||||
if is_virtual_doctype(args.doctype):
|
||||
controller = get_controller(args.doctype)
|
||||
data = compress(controller(args.doctype).get_list(args))
|
||||
else:
|
||||
|
|
@ -29,17 +29,31 @@ def get():
|
|||
@frappe.whitelist()
|
||||
@frappe.read_only()
|
||||
def get_list():
|
||||
# uncompressed (refactored from frappe.model.db_query.get_list)
|
||||
return execute(**get_form_params())
|
||||
args = get_form_params()
|
||||
|
||||
if is_virtual_doctype(args.doctype):
|
||||
controller = get_controller(args.doctype)
|
||||
data = controller(args.doctype).get_list(args)
|
||||
else:
|
||||
# uncompressed (refactored from frappe.model.db_query.get_list)
|
||||
data = execute(**args)
|
||||
|
||||
return data
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.read_only()
|
||||
def get_count():
|
||||
args = get_form_params()
|
||||
|
||||
distinct = 'distinct ' if args.distinct=='true' else ''
|
||||
args.fields = [f"count({distinct}`tab{args.doctype}`.name) as total_count"]
|
||||
return execute(**args)[0].get('total_count')
|
||||
if is_virtual_doctype(args.doctype):
|
||||
controller = get_controller(args.doctype)
|
||||
data = controller(args.doctype).get_count(args)
|
||||
else:
|
||||
distinct = 'distinct ' if args.distinct=='true' else ''
|
||||
args.fields = [f"count({distinct}`tab{args.doctype}`.name) as total_count"]
|
||||
data = execute(**args)[0].get('total_count')
|
||||
|
||||
return data
|
||||
|
||||
def execute(doctype, *args, **kwargs):
|
||||
return DatabaseQuery(doctype).execute(*args, **kwargs)
|
||||
|
|
@ -219,6 +233,8 @@ def compress(data, args=None):
|
|||
"""separate keys and values"""
|
||||
from frappe.desk.query_report import add_total_row
|
||||
|
||||
user_info = {}
|
||||
|
||||
if not data: return data
|
||||
if args is None:
|
||||
args = {}
|
||||
|
|
@ -230,13 +246,19 @@ def compress(data, args=None):
|
|||
new_row.append(row.get(key))
|
||||
values.append(new_row)
|
||||
|
||||
# add user info for assignments (avatar)
|
||||
if row._assign:
|
||||
for user in json.loads(row._assign):
|
||||
add_user_info(user, user_info)
|
||||
|
||||
if args.get("add_total_row"):
|
||||
meta = frappe.get_meta(args.doctype)
|
||||
values = add_total_row(values, keys, meta)
|
||||
|
||||
return {
|
||||
"keys": keys,
|
||||
"values": values
|
||||
"values": values,
|
||||
"user_info": user_info
|
||||
}
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
@ -297,7 +319,7 @@ def export_query():
|
|||
if add_totals_row:
|
||||
ret = append_totals_row(ret)
|
||||
|
||||
data = [['Sr'] + get_labels(db_query.fields, doctype)]
|
||||
data = [[_('Sr')] + get_labels(db_query.fields, doctype)]
|
||||
for i, row in enumerate(ret):
|
||||
data.append([i+1] + list(row))
|
||||
|
||||
|
|
@ -356,7 +378,8 @@ def get_labels(fields, doctype):
|
|||
for key in fields:
|
||||
key = key.split(" as ")[0]
|
||||
|
||||
if key.startswith(('count(', 'sum(', 'avg(')): continue
|
||||
if key.startswith(('count(', 'sum(', 'avg(')):
|
||||
continue
|
||||
|
||||
if "." in key:
|
||||
parenttype, fieldname = key.split(".")[0][4:-1], key.split(".")[1].strip("`")
|
||||
|
|
@ -364,10 +387,16 @@ def get_labels(fields, doctype):
|
|||
parenttype = doctype
|
||||
fieldname = fieldname.strip("`")
|
||||
|
||||
df = frappe.get_meta(parenttype).get_field(fieldname)
|
||||
label = df.label if df else fieldname.title()
|
||||
if label in labels:
|
||||
label = doctype + ": " + label
|
||||
if parenttype == doctype and fieldname == "name":
|
||||
label = _("ID", context="Label of name column in report")
|
||||
else:
|
||||
df = frappe.get_meta(parenttype).get_field(fieldname)
|
||||
label = _(df.label if df else fieldname.title())
|
||||
if parenttype != doctype:
|
||||
# If the column is from a child table, append the child doctype.
|
||||
# For example, "Item Code (Sales Invoice Item)".
|
||||
label += f" ({ _(parenttype) })"
|
||||
|
||||
labels.append(label)
|
||||
|
||||
return labels
|
||||
|
|
@ -430,7 +459,14 @@ def get_sidebar_stats(stats, doctype, filters=None):
|
|||
if filters is None:
|
||||
filters = []
|
||||
|
||||
return {"stats": get_stats(stats, doctype, filters)}
|
||||
if is_virtual_doctype(doctype):
|
||||
controller = get_controller(doctype)
|
||||
args = {"stats": stats, "filters": filters}
|
||||
data = controller(doctype).get_stats(args)
|
||||
else:
|
||||
data = get_stats(stats, doctype, filters)
|
||||
|
||||
return {"stats": data}
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.read_only()
|
||||
|
|
@ -552,7 +588,7 @@ def get_match_cond(doctype, as_condition=True):
|
|||
return ((' and ' + cond) if cond else "").replace("%", "%%")
|
||||
|
||||
def build_match_conditions(doctype, user=None, as_condition=True):
|
||||
match_conditions = DatabaseQuery(doctype, user=user).build_match_conditions(as_condition=as_condition)
|
||||
match_conditions = DatabaseQuery(doctype, user=user).build_match_conditions(as_condition=as_condition)
|
||||
if as_condition:
|
||||
return match_conditions.replace("%", "%%")
|
||||
else:
|
||||
|
|
@ -590,3 +626,7 @@ def get_filters_cond(doctype, filters, conditions, ignore_permissions=None, with
|
|||
else:
|
||||
cond = ''
|
||||
return cond
|
||||
|
||||
def is_virtual_doctype(doctype):
|
||||
return frappe.db.get_value("DocType", doctype, "is_virtual")
|
||||
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0,
|
|||
else:
|
||||
filters.append([doctype, f[0], "=", f[1]])
|
||||
|
||||
if filters==None:
|
||||
if filters is None:
|
||||
filters = []
|
||||
or_filters = []
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import email.utils
|
||||
import functools
|
||||
import imaplib
|
||||
|
|
@ -7,6 +8,7 @@ import socket
|
|||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from poplib import error_proto
|
||||
from typing import List
|
||||
|
||||
import frappe
|
||||
from frappe import _, are_emails_muted, safe_encode
|
||||
|
|
@ -82,9 +84,6 @@ class EmailAccount(Document):
|
|||
if frappe.local.flags.in_patch or frappe.local.flags.in_test:
|
||||
return
|
||||
|
||||
#if self.enable_incoming and not self.append_to:
|
||||
# frappe.throw(_("Append To is mandatory for incoming mails"))
|
||||
|
||||
if (not self.awaiting_password and not frappe.local.flags.in_install
|
||||
and not frappe.local.flags.in_patch):
|
||||
if self.password or self.smtp_server in ('127.0.0.1', 'localhost'):
|
||||
|
|
@ -442,7 +441,7 @@ class EmailAccount(Document):
|
|||
frappe.db.rollback()
|
||||
except Exception:
|
||||
frappe.db.rollback()
|
||||
frappe.log_error('email_account.receive')
|
||||
frappe.log_error(title="EmailAccount.receive")
|
||||
if self.use_imap:
|
||||
self.handle_bad_emails(mail.uid, mail.raw_message, frappe.get_traceback())
|
||||
exceptions.append(frappe.get_traceback())
|
||||
|
|
@ -458,7 +457,7 @@ class EmailAccount(Document):
|
|||
if exceptions:
|
||||
raise Exception(frappe.as_json(exceptions))
|
||||
|
||||
def get_inbound_mails(self, test_mails=None):
|
||||
def get_inbound_mails(self, test_mails=None) -> List[InboundMail]:
|
||||
"""retrive and return inbound mails.
|
||||
|
||||
"""
|
||||
|
|
@ -625,7 +624,6 @@ class EmailAccount(Document):
|
|||
if frappe.db.exists("Email Account", {"enable_automatic_linking": 1, "name": ('!=', self.name)}):
|
||||
frappe.throw(_("Automatic Linking can be activated only for one Email Account."))
|
||||
|
||||
|
||||
def append_email_to_sent_folder(self, message):
|
||||
email_server = None
|
||||
try:
|
||||
|
|
@ -643,7 +641,8 @@ class EmailAccount(Document):
|
|||
message = safe_encode(message)
|
||||
email_server.imap.append("Sent", "\\Seen", imaplib.Time2Internaldate(time.time()), message)
|
||||
except Exception:
|
||||
frappe.log_error()
|
||||
frappe.log_error(title="EmailAccount.append_email_to_sent_folder")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_append_to(doctype=None, txt=None, searchfield=None, start=None, page_len=None, filters=None):
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import quopri
|
|||
from email.parser import Parser
|
||||
from email.policy import SMTPUTF8
|
||||
from html2text import html2text
|
||||
from six.moves import html_parser as HTMLParser
|
||||
|
||||
import frappe
|
||||
from frappe import _, safe_encode, task
|
||||
|
|
@ -20,6 +19,7 @@ from frappe.email.queue import get_unsubcribed_url, get_unsubscribe_message
|
|||
from frappe.email.email_body import add_attachment, get_formatted_html, get_email
|
||||
from frappe.utils import cint, split_emails, add_days, nowdate, cstr, get_hook_method
|
||||
from frappe.email.doctype.email_account.email_account import EmailAccount
|
||||
from frappe.query_builder.utils import DocType
|
||||
|
||||
|
||||
MAX_RETRY_COUNT = 3
|
||||
|
|
@ -444,7 +444,7 @@ class QueueBuilder:
|
|||
|
||||
try:
|
||||
text_content = html2text(self._message)
|
||||
except HTMLParser.HTMLParseError:
|
||||
except Exception:
|
||||
text_content = "See html attachment"
|
||||
return text_content + unsubscribe_text_message
|
||||
|
||||
|
|
@ -477,18 +477,24 @@ class QueueBuilder:
|
|||
|
||||
all_ids = list(set(self.recipients + self.cc))
|
||||
|
||||
EmailUnsubscribe = frappe.qb.DocType("Email Unsubscribe")
|
||||
EmailUnsubscribe = DocType("Email Unsubscribe")
|
||||
|
||||
unsubscribed = (
|
||||
frappe.qb.from_(EmailUnsubscribe).select(
|
||||
EmailUnsubscribe.email
|
||||
).where(
|
||||
EmailUnsubscribe.email.isin(all_ids)
|
||||
& (
|
||||
(
|
||||
(EmailUnsubscribe.reference_doctype == self.reference_doctype)
|
||||
& (EmailUnsubscribe.reference_name == self.reference_name)
|
||||
) | (
|
||||
EmailUnsubscribe.global_unsubscribe == 1
|
||||
)
|
||||
)
|
||||
).distinct()
|
||||
).run(pluck=True)
|
||||
|
||||
unsubscribed = (frappe.qb.from_(EmailUnsubscribe)
|
||||
.select(EmailUnsubscribe.email)
|
||||
.where(EmailUnsubscribe.email.isin(all_ids) &
|
||||
(
|
||||
(
|
||||
(EmailUnsubscribe.reference_doctype == self.reference_doctype) & (EmailUnsubscribe.reference_name == self.reference_name)
|
||||
) | EmailUnsubscribe.global_unsubscribe == 1
|
||||
)
|
||||
).distinct()
|
||||
).run(pluck=True)
|
||||
self._unsubscribed_user_emails = unsubscribed or []
|
||||
return self._unsubscribed_user_emails
|
||||
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ def get_context(context):
|
|||
|
||||
if self.set_property_after_alert:
|
||||
allow_update = True
|
||||
if doc.docstatus == 1 and not doc.meta.get_field(self.set_property_after_alert).allow_on_submit:
|
||||
if doc.docstatus.is_submitted() and not doc.meta.get_field(self.set_property_after_alert).allow_on_submit:
|
||||
allow_update = False
|
||||
try:
|
||||
if allow_update and not doc.flags.in_notification_update:
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ class DocumentAlreadyRestored(ValidationError): pass
|
|||
class AttachmentLimitReached(ValidationError): pass
|
||||
class QueryTimeoutError(Exception): pass
|
||||
class QueryDeadlockError(Exception): pass
|
||||
class TooManyWritesError(Exception): pass
|
||||
# OAuth exceptions
|
||||
class InvalidAuthorizationHeader(CSRFTokenError): pass
|
||||
class InvalidAuthorizationPrefix(CSRFTokenError): pass
|
||||
|
|
|
|||
19
frappe/installer.py
Executable file → Normal file
19
frappe/installer.py
Executable file → Normal file
|
|
@ -154,7 +154,7 @@ def install_app(name, verbose=False, set_as_patched=True):
|
|||
|
||||
for before_install in app_hooks.before_install or []:
|
||||
out = frappe.get_attr(before_install)()
|
||||
if out==False:
|
||||
if out is False:
|
||||
return
|
||||
|
||||
if name != "frappe":
|
||||
|
|
@ -346,14 +346,15 @@ def post_install(rebuild_website=False):
|
|||
|
||||
|
||||
def set_all_patches_as_completed(app):
|
||||
patch_path = os.path.join(frappe.get_pymodule_path(app), "patches.txt")
|
||||
if os.path.exists(patch_path):
|
||||
for patch in frappe.get_file_items(patch_path):
|
||||
frappe.get_doc({
|
||||
"doctype": "Patch Log",
|
||||
"patch": patch
|
||||
}).insert(ignore_permissions=True)
|
||||
frappe.db.commit()
|
||||
from frappe.modules.patch_handler import get_patches_from_app
|
||||
|
||||
patches = get_patches_from_app(app)
|
||||
for patch in patches:
|
||||
frappe.get_doc({
|
||||
"doctype": "Patch Log",
|
||||
"patch": patch
|
||||
}).insert(ignore_permissions=True)
|
||||
frappe.db.commit()
|
||||
|
||||
|
||||
def init_singles():
|
||||
|
|
|
|||
|
|
@ -65,10 +65,7 @@ def get_latest_backup_file(with_files=False):
|
|||
return database, config
|
||||
|
||||
|
||||
def get_file_size(file_path, unit):
|
||||
if not unit:
|
||||
unit = "MB"
|
||||
|
||||
def get_file_size(file_path, unit='MB'):
|
||||
file_size = os.path.getsize(file_path)
|
||||
|
||||
memory_size_unit_mapper = {"KB": 1, "MB": 2, "GB": 3, "TB": 4}
|
||||
|
|
@ -99,7 +96,7 @@ def get_chunk_site(file_size):
|
|||
def validate_file_size():
|
||||
frappe.flags.create_new_backup = True
|
||||
latest_file, site_config = get_latest_backup_file()
|
||||
file_size = get_file_size(latest_file, unit="GB")
|
||||
file_size = get_file_size(latest_file, unit="GB") if latest_file else 0
|
||||
|
||||
if file_size > 1:
|
||||
frappe.flags.create_new_backup = False
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"charts": [],
|
||||
"content": "[{\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Backup\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Google Services\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Authentication\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Payments\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}]",
|
||||
"content": "[{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Backup\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Google Services\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Authentication\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Payments\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
|
||||
"creation": "2020-03-02 15:16:18.714190",
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace",
|
||||
|
|
@ -260,7 +260,7 @@
|
|||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2021-08-05 12:16:00.355268",
|
||||
"modified": "2022-01-13 17:39:01.292154",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Integrations",
|
||||
"name": "Integrations",
|
||||
|
|
@ -269,7 +269,7 @@
|
|||
"public": 1,
|
||||
"restrict_to_domain": "",
|
||||
"roles": [],
|
||||
"sequence_id": 15,
|
||||
"sequence_id": 15.0,
|
||||
"shortcuts": [],
|
||||
"title": "Integrations"
|
||||
}
|
||||
|
|
@ -19,6 +19,8 @@ from frappe.modules.utils import sync_customizations
|
|||
from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
|
||||
from frappe.search.website_search import build_index_for_all_routes
|
||||
from frappe.database.schema import add_column
|
||||
from frappe.modules.patch_handler import PatchType
|
||||
|
||||
|
||||
|
||||
def migrate(verbose=True, skip_failing=False, skip_search_index=False):
|
||||
|
|
@ -59,16 +61,13 @@ Otherwise, check the server logs and ensure that all the required services are r
|
|||
|
||||
clear_global_cache()
|
||||
|
||||
#run before_migrate hooks
|
||||
for app in frappe.get_installed_apps():
|
||||
for fn in frappe.get_hooks('before_migrate', app_name=app):
|
||||
frappe.get_attr(fn)()
|
||||
|
||||
# run patches
|
||||
frappe.modules.patch_handler.run_all(skip_failing)
|
||||
|
||||
# sync
|
||||
frappe.modules.patch_handler.run_all(skip_failing=skip_failing, patch_type=PatchType.pre_model_sync)
|
||||
frappe.model.sync.sync_all()
|
||||
frappe.modules.patch_handler.run_all(skip_failing=skip_failing, patch_type=PatchType.post_model_sync)
|
||||
frappe.translate.clear_cache()
|
||||
sync_jobs()
|
||||
sync_fixtures()
|
||||
|
|
@ -78,18 +77,16 @@ Otherwise, check the server logs and ensure that all the required services are r
|
|||
|
||||
frappe.get_doc('Portal Settings', 'Portal Settings').sync_menu()
|
||||
|
||||
# syncs statics
|
||||
# syncs static files
|
||||
clear_website_cache()
|
||||
|
||||
# updating installed applications data
|
||||
frappe.get_single('Installed Applications').update_versions()
|
||||
|
||||
#run after_migrate hooks
|
||||
for app in frappe.get_installed_apps():
|
||||
for fn in frappe.get_hooks('after_migrate', app_name=app):
|
||||
frappe.get_attr(fn)()
|
||||
|
||||
# build web_routes index
|
||||
if not skip_search_index:
|
||||
# Run this last as it updates the current session
|
||||
print('Building search index for {}'.format(frappe.local.site))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe
|
||||
import datetime
|
||||
from frappe import _
|
||||
|
|
@ -11,6 +12,7 @@ from frappe.model import display_fieldtypes
|
|||
from frappe.utils import (cint, flt, now, cstr, strip_html,
|
||||
sanitize_html, sanitize_email, cast_fieldtype)
|
||||
from frappe.utils.html_utils import unescape_html
|
||||
from frappe.model.docstatus import DocStatus
|
||||
|
||||
max_positive_value = {
|
||||
'smallint': 2 ** 15,
|
||||
|
|
@ -20,6 +22,7 @@ max_positive_value = {
|
|||
|
||||
DOCTYPES_FOR_DOCTYPE = ('DocType', 'DocField', 'DocPerm', 'DocType Action', 'DocType Link')
|
||||
|
||||
|
||||
def get_controller(doctype):
|
||||
"""Returns the **class** object of the given DocType.
|
||||
For `custom` type, returns `frappe.model.document.Document`.
|
||||
|
|
@ -172,7 +175,7 @@ class BaseDocument(object):
|
|||
...
|
||||
})
|
||||
"""
|
||||
if value==None:
|
||||
if value is None:
|
||||
value={}
|
||||
if isinstance(value, (dict, BaseDocument)):
|
||||
if not self.__dict__.get(key):
|
||||
|
|
@ -224,7 +227,7 @@ class BaseDocument(object):
|
|||
value.parentfield = key
|
||||
|
||||
if value.docstatus is None:
|
||||
value.docstatus = 0
|
||||
value.docstatus = DocStatus.draft()
|
||||
|
||||
if not getattr(value, "idx", None):
|
||||
value.idx = len(self.get(key) or []) + 1
|
||||
|
|
@ -272,7 +275,7 @@ class BaseDocument(object):
|
|||
)):
|
||||
d[fieldname] = str(d[fieldname])
|
||||
|
||||
if d[fieldname] == None and ignore_nulls:
|
||||
if d[fieldname] is None and ignore_nulls:
|
||||
del d[fieldname]
|
||||
|
||||
return d
|
||||
|
|
@ -282,8 +285,11 @@ class BaseDocument(object):
|
|||
if key not in self.__dict__:
|
||||
self.__dict__[key] = None
|
||||
|
||||
if key in ("idx", "docstatus") and self.__dict__[key] is None:
|
||||
self.__dict__[key] = 0
|
||||
if self.__dict__[key] is None:
|
||||
if key == "docstatus":
|
||||
self.docstatus = DocStatus.draft()
|
||||
elif key == "idx":
|
||||
self.__dict__[key] = 0
|
||||
|
||||
for key in self.get_valid_columns():
|
||||
if key not in self.__dict__:
|
||||
|
|
@ -304,6 +310,14 @@ class BaseDocument(object):
|
|||
def is_new(self):
|
||||
return self.get("__islocal")
|
||||
|
||||
@property
|
||||
def docstatus(self):
|
||||
return DocStatus(self.get("docstatus"))
|
||||
|
||||
@docstatus.setter
|
||||
def docstatus(self, value):
|
||||
self.__dict__["docstatus"] = DocStatus(cint(value))
|
||||
|
||||
def as_dict(self, no_nulls=False, no_default_fields=False, convert_dates_to_str=False):
|
||||
doc = self.get_valid_dict(convert_dates_to_str=convert_dates_to_str)
|
||||
doc["doctype"] = self.doctype
|
||||
|
|
@ -492,7 +506,7 @@ class BaseDocument(object):
|
|||
self.set(df.fieldname, flt(self.get(df.fieldname)))
|
||||
|
||||
if self.docstatus is not None:
|
||||
self.docstatus = cint(self.docstatus)
|
||||
self.docstatus = DocStatus(cint(self.docstatus))
|
||||
|
||||
def _get_missing_mandatory_fields(self):
|
||||
"""Get mandatory fields that do not have any values"""
|
||||
|
|
@ -581,7 +595,7 @@ class BaseDocument(object):
|
|||
setattr(self, df.fieldname, values.name)
|
||||
|
||||
for _df in fields_to_fetch:
|
||||
if self.is_new() or self.docstatus != 1 or _df.allow_on_submit:
|
||||
if self.is_new() or not self.docstatus.is_submitted() or _df.allow_on_submit:
|
||||
self.set_fetch_from_value(doctype, _df, values)
|
||||
|
||||
notify_link_count(doctype, docname)
|
||||
|
|
@ -591,7 +605,7 @@ class BaseDocument(object):
|
|||
|
||||
elif (df.fieldname != "amended_from"
|
||||
and (is_submittable or self.meta.is_submittable) and frappe.get_meta(doctype).is_submittable
|
||||
and cint(frappe.db.get_value(doctype, docname, "docstatus"))==2):
|
||||
and cint(frappe.db.get_value(doctype, docname, "docstatus")) == DocStatus.cancelled()):
|
||||
|
||||
cancelled_links.append((df.fieldname, docname, get_msg(df, docname)))
|
||||
|
||||
|
|
@ -646,8 +660,6 @@ class BaseDocument(object):
|
|||
value, comma_options))
|
||||
|
||||
def _validate_data_fields(self):
|
||||
from frappe.core.doctype.user.user import STANDARD_USERS
|
||||
|
||||
# data_field options defined in frappe.model.data_field_options
|
||||
for data_field in self.meta.get_data_fields():
|
||||
data = self.get(data_field.fieldname)
|
||||
|
|
@ -658,7 +670,7 @@ class BaseDocument(object):
|
|||
continue
|
||||
|
||||
if data_field_options == "Email":
|
||||
if (self.owner in STANDARD_USERS) and (data in STANDARD_USERS):
|
||||
if (self.owner in frappe.STANDARD_USERS) and (data in frappe.STANDARD_USERS):
|
||||
continue
|
||||
for email_address in frappe.utils.split_emails(data):
|
||||
frappe.utils.validate_email_address(email_address, throw=True)
|
||||
|
|
@ -807,8 +819,8 @@ class BaseDocument(object):
|
|||
or df.get("fieldtype") in ("Attach", "Attach Image", "Barcode", "Code")
|
||||
|
||||
# cancelled and submit but not update after submit should be ignored
|
||||
or self.docstatus==2
|
||||
or (self.docstatus==1 and not df.get("allow_on_submit"))):
|
||||
or self.docstatus.is_cancelled()
|
||||
or (self.docstatus.is_submitted() and not df.get("allow_on_submit"))):
|
||||
continue
|
||||
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -545,7 +545,7 @@ class DatabaseQuery(object):
|
|||
|
||||
elif f.operator.lower() in ("like", "not like") or (isinstance(f.value, str) and
|
||||
(not df or df.fieldtype not in ["Float", "Int", "Currency", "Percent", "Check"])):
|
||||
value = "" if f.value==None else f.value
|
||||
value = "" if f.value is None else f.value
|
||||
fallback = "''"
|
||||
|
||||
if f.operator.lower() in ("like", "not like") and isinstance(value, str):
|
||||
|
|
|
|||
|
|
@ -212,7 +212,7 @@ def check_permission_and_not_submitted(doc):
|
|||
.format(doc.doctype, doc.name), raise_exception=frappe.PermissionError)
|
||||
|
||||
# check if submitted
|
||||
if doc.docstatus == 1:
|
||||
if doc.docstatus.is_submitted():
|
||||
frappe.msgprint(_("{0} {1}: Submitted Record cannot be deleted. You must {2} Cancel {3} it first.").format(_(doc.doctype), doc.name, "<a href='https://docs.erpnext.com//docs/user/manual/en/setting-up/articles/delete-submitted-document' target='_blank'>", "</a>"),
|
||||
raise_exception=True)
|
||||
|
||||
|
|
|
|||
25
frappe/model/docstatus.py
Normal file
25
frappe/model/docstatus.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
|
||||
class DocStatus(int):
|
||||
def is_draft(self):
|
||||
return self == self.draft()
|
||||
|
||||
def is_submitted(self):
|
||||
return self == self.submitted()
|
||||
|
||||
def is_cancelled(self):
|
||||
return self == self.cancelled()
|
||||
|
||||
@classmethod
|
||||
def draft(cls):
|
||||
return cls(0)
|
||||
|
||||
@classmethod
|
||||
def submitted(cls):
|
||||
return cls(1)
|
||||
|
||||
@classmethod
|
||||
def cancelled(cls):
|
||||
return cls(2)
|
||||
|
|
@ -1,13 +1,16 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import frappe
|
||||
import hashlib
|
||||
import json
|
||||
import time
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
import frappe
|
||||
from frappe import _, msgprint, is_whitelisted
|
||||
from frappe.utils import flt, cstr, now, get_datetime_str, file_lock, date_diff
|
||||
from frappe.model.base_document import BaseDocument, get_controller
|
||||
from frappe.model.naming import set_new_name, gen_new_name_for_cancelled_doc
|
||||
from werkzeug.exceptions import NotFound, Forbidden
|
||||
import hashlib, json
|
||||
from frappe.model.naming import set_new_name
|
||||
from frappe.model.docstatus import DocStatus
|
||||
from frappe.model import optional_fields, table_fields
|
||||
from frappe.model.workflow import validate_workflow
|
||||
from frappe.model.workflow import set_workflow_state_on_action
|
||||
|
|
@ -17,6 +20,7 @@ from frappe.desk.form.document_follow import follow_document
|
|||
from frappe.core.doctype.server_script.server_script_utils import run_server_script_for_doc_event
|
||||
from frappe.utils.data import get_absolute_url
|
||||
|
||||
|
||||
# once_only validation
|
||||
# methods
|
||||
|
||||
|
|
@ -188,6 +192,8 @@ class Document(BaseDocument):
|
|||
is not set.
|
||||
|
||||
:param permtype: one of `read`, `write`, `submit`, `cancel`, `delete`"""
|
||||
import frappe.permissions
|
||||
|
||||
if self.flags.ignore_permissions:
|
||||
return True
|
||||
return frappe.permissions.has_permission(self.doctype, permtype, self, verbose=verbose)
|
||||
|
|
@ -209,13 +215,13 @@ class Document(BaseDocument):
|
|||
|
||||
self.flags.notifications_executed = []
|
||||
|
||||
if ignore_permissions!=None:
|
||||
if ignore_permissions is not None:
|
||||
self.flags.ignore_permissions = ignore_permissions
|
||||
|
||||
if ignore_links!=None:
|
||||
if ignore_links is not None:
|
||||
self.flags.ignore_links = ignore_links
|
||||
|
||||
if ignore_mandatory!=None:
|
||||
if ignore_mandatory is not None:
|
||||
self.flags.ignore_mandatory = ignore_mandatory
|
||||
|
||||
self.set("__islocal", True)
|
||||
|
|
@ -295,7 +301,7 @@ class Document(BaseDocument):
|
|||
|
||||
self.flags.notifications_executed = []
|
||||
|
||||
if ignore_permissions!=None:
|
||||
if ignore_permissions is not None:
|
||||
self.flags.ignore_permissions = ignore_permissions
|
||||
|
||||
self.flags.ignore_version = frappe.flags.in_test if ignore_version is None else ignore_version
|
||||
|
|
@ -305,9 +311,6 @@ class Document(BaseDocument):
|
|||
|
||||
self.check_permission("write", "save")
|
||||
|
||||
if self.docstatus == 2:
|
||||
self._rename_doc_on_cancel()
|
||||
|
||||
self.set_user_and_timestamp()
|
||||
self.set_docstatus()
|
||||
self.check_if_latest()
|
||||
|
|
@ -439,7 +442,7 @@ class Document(BaseDocument):
|
|||
values = self.as_dict()
|
||||
# format values
|
||||
for key, value in values.items():
|
||||
if value==None:
|
||||
if value is None:
|
||||
values[key] = ""
|
||||
return values
|
||||
|
||||
|
|
@ -472,7 +475,7 @@ class Document(BaseDocument):
|
|||
|
||||
# We'd probably want the creation and owner to be set via API
|
||||
# or Data import at some point, that'd have to be handled here
|
||||
if self.is_new():
|
||||
if self.is_new() and not (frappe.flags.in_patch or frappe.flags.in_migrate):
|
||||
self.creation = self.modified
|
||||
self.owner = self.modified_by
|
||||
|
||||
|
|
@ -487,8 +490,8 @@ class Document(BaseDocument):
|
|||
frappe.flags.currently_saving.append((self.doctype, self.name))
|
||||
|
||||
def set_docstatus(self):
|
||||
if self.docstatus==None:
|
||||
self.docstatus=0
|
||||
if self.docstatus is None:
|
||||
self.docstatus = DocStatus.draft()
|
||||
|
||||
for d in self.get_all_children():
|
||||
d.docstatus = self.docstatus
|
||||
|
|
@ -718,6 +721,7 @@ class Document(BaseDocument):
|
|||
else:
|
||||
tmp = frappe.db.sql("""select modified, docstatus from `tab{0}`
|
||||
where name = %s for update""".format(self.doctype), self.name, as_dict=True)
|
||||
|
||||
if not tmp:
|
||||
frappe.throw(_("Record does not exist"))
|
||||
else:
|
||||
|
|
@ -738,7 +742,7 @@ class Document(BaseDocument):
|
|||
else:
|
||||
self.check_docstatus_transition(0)
|
||||
|
||||
def check_docstatus_transition(self, docstatus):
|
||||
def check_docstatus_transition(self, to_docstatus):
|
||||
"""Ensures valid `docstatus` transition.
|
||||
Valid transitions are (number in brackets is `docstatus`):
|
||||
|
||||
|
|
@ -749,31 +753,32 @@ class Document(BaseDocument):
|
|||
|
||||
"""
|
||||
if not self.docstatus:
|
||||
self.docstatus = 0
|
||||
if docstatus==0:
|
||||
if self.docstatus==0:
|
||||
self.docstatus = DocStatus.draft()
|
||||
|
||||
if to_docstatus == DocStatus.draft():
|
||||
if self.docstatus.is_draft():
|
||||
self._action = "save"
|
||||
elif self.docstatus==1:
|
||||
elif self.docstatus.is_submitted():
|
||||
self._action = "submit"
|
||||
self.check_permission("submit")
|
||||
elif self.docstatus==2:
|
||||
elif self.docstatus.is_cancelled():
|
||||
raise frappe.DocstatusTransitionError(_("Cannot change docstatus from 0 (Draft) to 2 (Cancelled)"))
|
||||
else:
|
||||
raise frappe.ValidationError(_("Invalid docstatus"), self.docstatus)
|
||||
|
||||
elif docstatus==1:
|
||||
if self.docstatus==1:
|
||||
elif to_docstatus == DocStatus.submitted():
|
||||
if self.docstatus.is_submitted():
|
||||
self._action = "update_after_submit"
|
||||
self.check_permission("submit")
|
||||
elif self.docstatus==2:
|
||||
elif self.docstatus.is_cancelled():
|
||||
self._action = "cancel"
|
||||
self.check_permission("cancel")
|
||||
elif self.docstatus==0:
|
||||
elif self.docstatus.is_draft():
|
||||
raise frappe.DocstatusTransitionError(_("Cannot change docstatus from 1 (Submitted) to 0 (Draft)"))
|
||||
else:
|
||||
raise frappe.ValidationError(_("Invalid docstatus"), self.docstatus)
|
||||
|
||||
elif docstatus==2:
|
||||
elif to_docstatus == DocStatus.cancelled():
|
||||
raise frappe.ValidationError(_("Cannot edit cancelled document"))
|
||||
|
||||
def set_parent_in_children(self):
|
||||
|
|
@ -885,14 +890,14 @@ class Document(BaseDocument):
|
|||
if (frappe.flags.in_import and frappe.flags.mute_emails) or frappe.flags.in_patch or frappe.flags.in_install:
|
||||
return
|
||||
|
||||
if self.flags.notifications_executed==None:
|
||||
if self.flags.notifications_executed is None:
|
||||
self.flags.notifications_executed = []
|
||||
|
||||
from frappe.email.doctype.notification.notification import evaluate_alert
|
||||
|
||||
if self.flags.notifications == None:
|
||||
if self.flags.notifications is None:
|
||||
alerts = frappe.cache().hget('notifications', self.doctype)
|
||||
if alerts==None:
|
||||
if alerts is None:
|
||||
alerts = frappe.get_all('Notification', fields=['name', 'event', 'method'],
|
||||
filters={'enabled': 1, 'document_type': self.doctype})
|
||||
frappe.cache().hset('notifications', self.doctype, alerts)
|
||||
|
|
@ -927,14 +932,14 @@ class Document(BaseDocument):
|
|||
@whitelist.__func__
|
||||
def _submit(self):
|
||||
"""Submit the document. Sets `docstatus` = 1, then saves."""
|
||||
self.docstatus = 1
|
||||
self.docstatus = DocStatus.submitted()
|
||||
return self.save()
|
||||
|
||||
@whitelist.__func__
|
||||
def _cancel(self):
|
||||
"""Cancel the document. Sets `docstatus` = 2, then saves.
|
||||
"""
|
||||
self.docstatus = 2
|
||||
self.docstatus = DocStatus.cancelled()
|
||||
return self.save()
|
||||
|
||||
@whitelist.__func__
|
||||
|
|
@ -952,7 +957,7 @@ class Document(BaseDocument):
|
|||
frappe.delete_doc(self.doctype, self.name, ignore_permissions = ignore_permissions, flags=self.flags)
|
||||
|
||||
def run_before_save_methods(self):
|
||||
"""Run standard methods before `INSERT` or `UPDATE`. Standard Methods are:
|
||||
"""Run standard methods before `INSERT` or `UPDATE`. Standard Methods are:
|
||||
|
||||
- `validate`, `before_save` for **Save**.
|
||||
- `validate`, `before_submit` for **Submit**.
|
||||
|
|
@ -1378,11 +1383,6 @@ class Document(BaseDocument):
|
|||
from frappe.desk.doctype.tag.tag import DocTags
|
||||
return DocTags(self.doctype).get_tags(self.name).split(",")[1:]
|
||||
|
||||
def _rename_doc_on_cancel(self):
|
||||
new_name = gen_new_name_for_cancelled_doc(self)
|
||||
frappe.rename_doc(self.doctype, self.name, new_name, force=True, show_alert=False)
|
||||
self.name = new_name
|
||||
|
||||
def __repr__(self):
|
||||
name = self.name or "unsaved"
|
||||
doctype = self.__class__.__name__
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ def get_dynamic_link_map(for_delete=False):
|
|||
|
||||
Note: Will not map single doctypes
|
||||
'''
|
||||
if getattr(frappe.local, 'dynamic_link_map', None)==None or frappe.flags.in_test:
|
||||
if getattr(frappe.local, 'dynamic_link_map', None) is None or frappe.flags.in_test:
|
||||
# Build from scratch
|
||||
dynamic_link_map = {}
|
||||
for df in get_dynamic_links():
|
||||
|
|
|
|||
|
|
@ -1,14 +1,3 @@
|
|||
"""utilities to generate a document name based on various rules defined.
|
||||
|
||||
NOTE:
|
||||
Till version 13, whenever a submittable document is amended it's name is set to orig_name-X,
|
||||
where X is a counter and it increments when amended again and so on.
|
||||
|
||||
From Version 14, The naming pattern is changed in a way that amended documents will
|
||||
have the original name `orig_name` instead of `orig_name-X`. To make this happen
|
||||
the cancelled document naming pattern is changed to 'orig_name-CANC-X'.
|
||||
"""
|
||||
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
|
|
@ -40,7 +29,7 @@ def set_new_name(doc):
|
|||
doc.name = None
|
||||
|
||||
if getattr(doc, "amended_from", None):
|
||||
doc.name = _get_amended_name(doc)
|
||||
_set_amended_name(doc)
|
||||
return
|
||||
|
||||
elif getattr(doc.meta, "issingle", False):
|
||||
|
|
@ -256,18 +245,6 @@ def revert_series_if_last(key, name, doc=None):
|
|||
* prefix = #### and hashes = 2021 (hash doesn't exist)
|
||||
* will search hash in key then accordingly get prefix = ""
|
||||
"""
|
||||
if hasattr(doc, 'amended_from'):
|
||||
# Do not revert the series if the document is amended.
|
||||
if doc.amended_from:
|
||||
return
|
||||
|
||||
# Get document name by parsing incase of fist cancelled document
|
||||
if doc.docstatus == 2 and not doc.amended_from:
|
||||
if doc.name.endswith('-CANC'):
|
||||
name, _ = NameParser.parse_docname(doc.name, sep='-CANC')
|
||||
else:
|
||||
name, _ = NameParser.parse_docname(doc.name, sep='-CANC-')
|
||||
|
||||
if ".#" in key:
|
||||
prefix, hashes = key.rsplit(".", 1)
|
||||
if "#" not in hashes:
|
||||
|
|
@ -356,9 +333,16 @@ def append_number_if_name_exists(doctype, value, fieldname="name", separator="-"
|
|||
return value
|
||||
|
||||
|
||||
def _get_amended_name(doc):
|
||||
name, _ = NameParser(doc).parse_amended_from()
|
||||
return name
|
||||
def _set_amended_name(doc):
|
||||
am_id = 1
|
||||
am_prefix = doc.amended_from
|
||||
if frappe.db.get_value(doc.doctype, doc.amended_from, "amended_from"):
|
||||
am_id = cint(doc.amended_from.split("-")[-1]) + 1
|
||||
am_prefix = "-".join(doc.amended_from.split("-")[:-1]) # except the last hyphen
|
||||
|
||||
doc.name = am_prefix + "-" + str(am_id)
|
||||
return doc.name
|
||||
|
||||
|
||||
def _field_autoname(autoname, doc, skip_slicing=None):
|
||||
"""
|
||||
|
|
@ -399,83 +383,3 @@ def _format_autoname(autoname, doc):
|
|||
name = re.sub(r"(\{[\w | #]+\})", get_param_value_for_match, autoname_value)
|
||||
|
||||
return name
|
||||
|
||||
class NameParser:
|
||||
"""Parse document name and return parts of it.
|
||||
|
||||
NOTE: It handles cancellend and amended doc parsing for now. It can be expanded.
|
||||
"""
|
||||
def __init__(self, doc):
|
||||
self.doc = doc
|
||||
|
||||
def parse_amended_from(self):
|
||||
"""
|
||||
Cancelled document naming will be in one of these formats
|
||||
|
||||
* original_name-X-CANC - This is introduced to migrate old style naming to new style
|
||||
* original_name-CANC - This is introduced to migrate old style naming to new style
|
||||
* original_name-CANC-X - This is the new style naming
|
||||
|
||||
New style naming: In new style naming amended documents will have original name. That says,
|
||||
when a document gets cancelled we need rename the document by adding `-CANC-X` to the end
|
||||
so that amended documents can use the original name.
|
||||
|
||||
Old style naming: cancelled documents stay with original name and when amended, amended one
|
||||
gets a new name as `original_name-X`. To bring new style naming we had to change the existing
|
||||
cancelled document names and that is done by adding `-CANC` to cancelled documents through patch.
|
||||
"""
|
||||
if not getattr(self.doc, 'amended_from', None):
|
||||
return (None, None)
|
||||
|
||||
# Handle old style cancelled documents (original_name-X-CANC, original_name-CANC)
|
||||
if self.doc.amended_from.endswith('-CANC'):
|
||||
name, _ = self.parse_docname(self.doc.amended_from, '-CANC')
|
||||
amended_from_doc = frappe.get_all(
|
||||
self.doc.doctype,
|
||||
filters = {'name': self.doc.amended_from},
|
||||
fields = ['amended_from'],
|
||||
limit=1)
|
||||
|
||||
# Handle format original_name-X-CANC.
|
||||
if amended_from_doc and amended_from_doc[0].amended_from:
|
||||
return self.parse_docname(name, '-')
|
||||
return name, None
|
||||
|
||||
# Handle new style cancelled documents
|
||||
return self.parse_docname(self.doc.amended_from, '-CANC-')
|
||||
|
||||
@classmethod
|
||||
def parse_docname(cls, name, sep='-'):
|
||||
split_list = name.rsplit(sep, 1)
|
||||
|
||||
if len(split_list) == 1:
|
||||
return (name, None)
|
||||
return (split_list[0], split_list[1])
|
||||
|
||||
def get_cancelled_doc_latest_counter(tname, docname):
|
||||
"""Get the latest counter used for cancelled docs of given docname.
|
||||
"""
|
||||
name_prefix = f'{docname}-CANC-'
|
||||
|
||||
rows = frappe.db.sql("""
|
||||
select
|
||||
name
|
||||
from `tab{tname}`
|
||||
where
|
||||
name like %(name_prefix)s and docstatus=2
|
||||
""".format(tname=tname), {'name_prefix': name_prefix+'%'}, as_dict=1)
|
||||
|
||||
if not rows:
|
||||
return -1
|
||||
return max([int(row.name.replace(name_prefix, '') or -1) for row in rows])
|
||||
|
||||
def gen_new_name_for_cancelled_doc(doc):
|
||||
"""Generate a new name for cancelled document.
|
||||
"""
|
||||
if getattr(doc, "amended_from", None):
|
||||
name, _ = NameParser(doc).parse_amended_from()
|
||||
else:
|
||||
name = doc.name
|
||||
|
||||
counter = get_cancelled_doc_latest_counter(doc.doctype, name)
|
||||
return f'{name}-CANC-{counter+1}'
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ def rename_doc(
|
|||
|
||||
if doctype=='DocType':
|
||||
rename_doctype(doctype, old, new, force)
|
||||
update_customizations(old, new)
|
||||
|
||||
update_attachments(doctype, old, new)
|
||||
|
||||
|
|
@ -174,6 +175,8 @@ def update_user_settings(old, new, link_fields):
|
|||
else:
|
||||
continue
|
||||
|
||||
def update_customizations(old: str, new: str) -> None:
|
||||
frappe.db.set_value("Custom DocPerm", {"parent": old}, "parent", new, update_modified=False)
|
||||
|
||||
def update_attachments(doctype, old, new):
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.utils import cint
|
||||
from frappe import _
|
||||
import json
|
||||
from frappe.utils import cint
|
||||
from frappe.model.docstatus import DocStatus
|
||||
|
||||
class WorkflowStateError(frappe.ValidationError): pass
|
||||
class WorkflowTransitionError(frappe.ValidationError): pass
|
||||
|
|
@ -102,13 +103,13 @@ def apply_workflow(doc, action):
|
|||
doc.set(next_state.update_field, next_state.update_value)
|
||||
|
||||
new_docstatus = cint(next_state.doc_status)
|
||||
if doc.docstatus == 0 and new_docstatus == 0:
|
||||
if doc.docstatus.is_draft() and new_docstatus == DocStatus.draft():
|
||||
doc.save()
|
||||
elif doc.docstatus == 0 and new_docstatus == 1:
|
||||
elif doc.docstatus.is_draft() and new_docstatus == DocStatus.submitted():
|
||||
doc.submit()
|
||||
elif doc.docstatus == 1 and new_docstatus == 1:
|
||||
elif doc.docstatus.is_submitted() and new_docstatus == DocStatus.submitted():
|
||||
doc.save()
|
||||
elif doc.docstatus == 1 and new_docstatus == 2:
|
||||
elif doc.docstatus.is_submitted() and new_docstatus == DocStatus.cancelled():
|
||||
doc.cancel()
|
||||
else:
|
||||
frappe.throw(_('Illegal Document Status for {0}').format(next_state.state))
|
||||
|
|
@ -212,10 +213,10 @@ def bulk_workflow_approval(docnames, doctype, action):
|
|||
frappe.db.commit()
|
||||
except Exception as e:
|
||||
if not frappe.message_log:
|
||||
# Exception is raised manually and not from msgprint or throw
|
||||
# Exception is raised manually and not from msgprint or throw
|
||||
message = "{0}".format(e.__class__.__name__)
|
||||
if e.args:
|
||||
message += " : {0}".format(e.args[0])
|
||||
message += " : {0}".format(e.args[0])
|
||||
message_dict = {"docname": docname, "message": message}
|
||||
failed_transactions[docname].append(message_dict)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,37 +1,76 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
""" Patch Handler.
|
||||
|
||||
This file manages execution of manaully written patches. Patches are script
|
||||
that apply changes in database schema or data to accomodate for changes in the
|
||||
code.
|
||||
|
||||
Ways to specify patches:
|
||||
|
||||
1. patches.txt file specifies patches that run before doctype schema
|
||||
migration. Each line represents one patch (old format).
|
||||
2. patches.txt can alternatively also separate pre and post model sync
|
||||
patches by using INI like file format:
|
||||
```patches.txt
|
||||
[pre_model_sync]
|
||||
app.module.patch1
|
||||
app.module.patch2
|
||||
|
||||
|
||||
[post_model_sync]
|
||||
app.module.patch3
|
||||
```
|
||||
|
||||
When different sections are specified patches are executed in this order:
|
||||
1. Run pre_model_sync patches
|
||||
2. Reload/resync all doctype schema
|
||||
3. Run post_model_sync patches
|
||||
|
||||
Hence any patch that just needs to modify data but doesn't depend on
|
||||
old schema should be added to post_model_sync section of file.
|
||||
|
||||
3. simple python commands can be added by starting line with `execute:`
|
||||
`execute:` example: `execute:print("hello world")`
|
||||
"""
|
||||
Execute Patch Files
|
||||
|
||||
To run directly
|
||||
import configparser
|
||||
import time
|
||||
from enum import Enum
|
||||
from typing import List, Optional
|
||||
|
||||
python lib/wnf.py patch patch1, patch2 etc
|
||||
python lib/wnf.py patch -f patch1, patch2 etc
|
||||
import frappe
|
||||
|
||||
where patch1, patch2 is module name
|
||||
"""
|
||||
import frappe, frappe.permissions, time
|
||||
|
||||
class PatchError(Exception): pass
|
||||
class PatchError(Exception):
|
||||
pass
|
||||
|
||||
def run_all(skip_failing=False):
|
||||
|
||||
class PatchType(Enum):
|
||||
pre_model_sync = "pre_model_sync"
|
||||
post_model_sync = "post_model_sync"
|
||||
|
||||
|
||||
def run_all(skip_failing: bool = False, patch_type: Optional[PatchType] = None) -> None:
|
||||
"""run all pending patches"""
|
||||
executed = [p[0] for p in frappe.db.sql("""select patch from `tabPatch Log`""")]
|
||||
executed = set(frappe.get_all("Patch Log", fields="patch", pluck="patch"))
|
||||
|
||||
frappe.flags.final_patches = []
|
||||
|
||||
def run_patch(patch):
|
||||
try:
|
||||
if not run_single(patchmodule = patch):
|
||||
log(patch + ': failed: STOPPED')
|
||||
print(patch + ': failed: STOPPED')
|
||||
raise PatchError(patch)
|
||||
except Exception:
|
||||
if not skip_failing:
|
||||
raise
|
||||
else:
|
||||
log('Failed to execute patch')
|
||||
print('Failed to execute patch')
|
||||
|
||||
for patch in get_all_patches():
|
||||
patches = get_all_patches(patch_type=patch_type)
|
||||
|
||||
for patch in patches:
|
||||
if patch and (patch not in executed):
|
||||
run_patch(patch)
|
||||
|
||||
|
|
@ -40,18 +79,57 @@ def run_all(skip_failing=False):
|
|||
patch = patch.replace('finally:', '')
|
||||
run_patch(patch)
|
||||
|
||||
def get_all_patches():
|
||||
def get_all_patches(patch_type: Optional[PatchType] = None) -> List[str]:
|
||||
|
||||
if patch_type and not isinstance(patch_type, PatchType):
|
||||
frappe.throw(f"Unsupported patch type specified: {patch_type}")
|
||||
|
||||
patches = []
|
||||
for app in frappe.get_installed_apps():
|
||||
if app == "shopping_cart":
|
||||
continue
|
||||
# 3-to-4 fix
|
||||
if app=="webnotes":
|
||||
app="frappe"
|
||||
patches.extend(frappe.get_file_items(frappe.get_pymodule_path(app, "patches.txt")))
|
||||
patches.extend(get_patches_from_app(app, patch_type=patch_type))
|
||||
|
||||
return patches
|
||||
|
||||
def get_patches_from_app(app: str, patch_type: Optional[PatchType] = None) -> List[str]:
|
||||
""" Get patches from an app's patches.txt
|
||||
|
||||
patches.txt can be:
|
||||
1. ini like file with section for different patch_type
|
||||
2. plain text file with each line representing a patch.
|
||||
"""
|
||||
|
||||
patches_txt = frappe.get_pymodule_path(app, "patches.txt")
|
||||
|
||||
try:
|
||||
# Attempt to parse as ini file with pre/post patches
|
||||
# allow_no_value: patches are not key value pairs
|
||||
# delimiters = '\n' to avoid treating default `:` and `=` in execute as k:v delimiter
|
||||
parser = configparser.ConfigParser(allow_no_value=True, delimiters="\n")
|
||||
# preserve case
|
||||
parser.optionxform = str
|
||||
parser.read(patches_txt)
|
||||
|
||||
# empty file
|
||||
if not parser.sections():
|
||||
return []
|
||||
|
||||
if not patch_type:
|
||||
return [patch for patch in parser[PatchType.pre_model_sync.value]] + \
|
||||
[patch for patch in parser[PatchType.post_model_sync.value]]
|
||||
|
||||
if patch_type.value in parser.sections():
|
||||
return [patch for patch in parser[patch_type.value]]
|
||||
else:
|
||||
frappe.throw(frappe._("Patch type {} not found in patches.txt").format(patch_type))
|
||||
|
||||
except configparser.MissingSectionHeaderError:
|
||||
# treat as old format with each line representing a single patch
|
||||
# backward compatbility with old patches.txt format
|
||||
if not patch_type or patch_type == PatchType.pre_model_sync:
|
||||
return frappe.get_file_items(patches_txt)
|
||||
|
||||
return []
|
||||
|
||||
def reload_doc(args):
|
||||
import frappe.modules
|
||||
run_single(method = frappe.modules.reload_doc, methodargs = args)
|
||||
|
|
@ -73,7 +151,7 @@ def execute_patch(patchmodule, method=None, methodargs=None):
|
|||
frappe.db.begin()
|
||||
start_time = time.time()
|
||||
try:
|
||||
log('Executing {patch} in {site} ({db})'.format(patch=patchmodule or str(methodargs),
|
||||
print('Executing {patch} in {site} ({db})'.format(patch=patchmodule or str(methodargs),
|
||||
site=frappe.local.site, db=frappe.db.cur_db_name))
|
||||
if patchmodule:
|
||||
if patchmodule.startswith("finally:"):
|
||||
|
|
@ -96,7 +174,7 @@ def execute_patch(patchmodule, method=None, methodargs=None):
|
|||
frappe.db.commit()
|
||||
end_time = time.time()
|
||||
block_user(False)
|
||||
log('Success: Done in {time}s'.format(time = round(end_time - start_time, 3)))
|
||||
print('Success: Done in {time}s'.format(time = round(end_time - start_time, 3)))
|
||||
|
||||
return True
|
||||
|
||||
|
|
@ -109,10 +187,7 @@ def executed(patchmodule):
|
|||
if patchmodule.startswith('finally:'):
|
||||
# patches are saved without the finally: tag
|
||||
patchmodule = patchmodule.replace('finally:', '')
|
||||
done = frappe.db.get_value("Patch Log", {"patch": patchmodule})
|
||||
# if done:
|
||||
# print "Patch %s already executed in %s" % (patchmodule, frappe.db.cur_db_name)
|
||||
return done
|
||||
return frappe.db.get_value("Patch Log", {"patch": patchmodule})
|
||||
|
||||
def block_user(block, msg=None):
|
||||
"""stop/start execution till patch is run"""
|
||||
|
|
@ -128,6 +203,3 @@ def check_session_stopped():
|
|||
if frappe.db.get_global("__session_status")=='stop':
|
||||
frappe.msgprint(frappe.db.get_global("__session_status_message"))
|
||||
raise frappe.SessionStopped('Session Stopped')
|
||||
|
||||
def log(msg):
|
||||
print (msg)
|
||||
|
|
|
|||
|
|
@ -257,6 +257,12 @@ def make_boilerplate(template, doc, opts=None):
|
|||
pass
|
||||
|
||||
def get_list(self, args):
|
||||
pass
|
||||
|
||||
def get_count(self, args):
|
||||
pass
|
||||
|
||||
def get_stats(self, args):
|
||||
pass"""
|
||||
|
||||
with open(target_file_path, 'w') as target:
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
[pre_model_sync]
|
||||
frappe.patches.v12_0.remove_deprecated_fields_from_doctype #3
|
||||
execute:frappe.utils.global_search.setup_global_search_table()
|
||||
execute:frappe.reload_doc('core', 'doctype', 'doctype_action', force=True) #2019-09-23
|
||||
|
|
@ -87,7 +88,6 @@ frappe.patches.v11_0.set_missing_creation_and_modified_value_for_user_permission
|
|||
frappe.patches.v11_0.set_default_letter_head_source
|
||||
frappe.patches.v12_0.set_primary_key_in_series
|
||||
execute:frappe.delete_doc("Page", "modules", ignore_missing=True)
|
||||
frappe.patches.v11_0.set_default_letter_head_source
|
||||
frappe.patches.v12_0.setup_comments_from_communications
|
||||
frappe.patches.v12_0.replace_null_values_in_tables
|
||||
frappe.patches.v12_0.reset_home_settings
|
||||
|
|
@ -123,6 +123,9 @@ frappe.patches.v12_0.remove_parent_and_parenttype_from_print_formats
|
|||
frappe.patches.v12_0.remove_example_email_thread_notify
|
||||
execute:from frappe.desk.page.setup_wizard.install_fixtures import update_genders;update_genders()
|
||||
frappe.patches.v12_0.set_correct_url_in_files
|
||||
execute:frappe.reload_doc('core', 'doctype', 'doctype')
|
||||
execute:frappe.reload_doc('custom', 'doctype', 'property_setter')
|
||||
frappe.patches.v13_0.remove_invalid_options_for_data_fields
|
||||
frappe.patches.v13_0.website_theme_custom_scss
|
||||
frappe.patches.v13_0.make_user_type
|
||||
frappe.patches.v13_0.set_existing_dashboard_charts_as_public
|
||||
|
|
@ -153,7 +156,6 @@ frappe.patches.v13_0.rename_notification_fields
|
|||
frappe.patches.v13_0.remove_duplicate_navbar_items
|
||||
frappe.patches.v13_0.set_social_icons
|
||||
frappe.patches.v12_0.set_default_password_reset_limit
|
||||
execute:frappe.reload_doc('core', 'doctype', 'doctype', force=True)
|
||||
frappe.patches.v13_0.set_route_for_blog_category
|
||||
frappe.patches.v13_0.enable_custom_script
|
||||
frappe.patches.v13_0.update_newsletter_content_type
|
||||
|
|
@ -173,22 +175,24 @@ execute:frappe.delete_doc_if_exists('Page', 'workspace')
|
|||
execute:frappe.delete_doc_if_exists('Page', 'dashboard', force=1)
|
||||
frappe.core.doctype.page.patches.drop_unused_pages
|
||||
execute:frappe.get_doc('Role', 'Guest').save() # remove desk access
|
||||
frappe.patches.v13_0.remove_chat
|
||||
frappe.patches.v13_0.rename_desk_page_to_workspace # 02.02.2021
|
||||
frappe.patches.v13_0.delete_package_publish_tool
|
||||
frappe.patches.v13_0.rename_list_view_setting_to_list_view_settings
|
||||
frappe.patches.v13_0.remove_twilio_settings
|
||||
frappe.patches.v12_0.rename_uploaded_files_with_proper_name
|
||||
frappe.patches.v13_0.queryreport_columns
|
||||
execute:frappe.reload_doc('core', 'doctype', 'doctype')
|
||||
frappe.patches.v13_0.jinja_hook
|
||||
frappe.patches.v13_0.update_notification_channel_if_empty
|
||||
frappe.patches.v13_0.set_first_day_of_the_week
|
||||
frappe.patches.v14_0.drop_data_import_legacy
|
||||
frappe.patches.v14_0.rename_cancelled_documents
|
||||
frappe.patches.v14_0.copy_mail_data #08.03.21
|
||||
frappe.patches.v14_0.update_workspace2 # 20.09.2021
|
||||
frappe.patches.v14_0.save_ratings_in_fraction #23-12-2021
|
||||
frappe.patches.v14_0.transform_todo_schema
|
||||
frappe.patches.v14_0.remove_post_and_post_comment
|
||||
|
||||
[post_model_sync]
|
||||
frappe.patches.v14_0.drop_data_import_legacy
|
||||
frappe.patches.v14_0.copy_mail_data #08.03.21
|
||||
frappe.patches.v14_0.update_github_endpoints #08-11-2021
|
||||
frappe.patches.v14_0.remove_db_aggregation
|
||||
frappe.patches.v14_0.save_ratings_in_fraction #23-12-2021
|
||||
frappe.patches.v14_0.update_color_names_in_kanban_board_column
|
||||
frappe.patches.v14_0.transform_todo_schema
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ def execute():
|
|||
continue
|
||||
skip_for_doctype = user_permission.skip_for_doctype.split('\n')
|
||||
else: # while migrating from v10 -> v11
|
||||
if skip_for_doctype_map.get((user_permission.allow, user_permission.user)) == None:
|
||||
if skip_for_doctype_map.get((user_permission.allow, user_permission.user)) is None:
|
||||
skip_for_doctype = get_doctypes_to_skip(user_permission.allow, user_permission.user)
|
||||
# cache skip for doctype for same user and doctype
|
||||
skip_for_doctype_map[(user_permission.allow, user_permission.user)] = skip_for_doctype
|
||||
|
|
|
|||
17
frappe/patches/v13_0/remove_chat.py
Normal file
17
frappe/patches/v13_0/remove_chat.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import frappe
|
||||
import click
|
||||
|
||||
def execute():
|
||||
frappe.delete_doc_if_exists("DocType", "Chat Message")
|
||||
frappe.delete_doc_if_exists("DocType", "Chat Message Attachment")
|
||||
frappe.delete_doc_if_exists("DocType", "Chat Profile")
|
||||
frappe.delete_doc_if_exists("DocType", "Chat Token")
|
||||
frappe.delete_doc_if_exists("DocType", "Chat Room User")
|
||||
frappe.delete_doc_if_exists("DocType", "Chat Room")
|
||||
frappe.delete_doc_if_exists("Module Def", "Chat")
|
||||
|
||||
click.secho(
|
||||
"Chat Module is moved to a separate app and is removed from Frappe in version-13.\n"
|
||||
"Please install the app to continue using the chat feature: https://github.com/frappe/chat",
|
||||
fg="yellow",
|
||||
)
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# Copyright (c) 2022, Frappe and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe.model import data_field_options
|
||||
|
||||
|
||||
def execute():
|
||||
custom_field = frappe.qb.DocType('Custom Field')
|
||||
(frappe.qb
|
||||
.update(custom_field)
|
||||
.set(custom_field.options, None)
|
||||
.where(
|
||||
(custom_field.fieldtype == "Data")
|
||||
& (custom_field.options.notin(data_field_options)))
|
||||
).run()
|
||||
|
|
@ -3,9 +3,6 @@ import frappe
|
|||
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc("email", "doctype", "imap_folder")
|
||||
frappe.reload_doc("email", "doctype", "email_account")
|
||||
|
||||
# patch for all Email Account with the flag use_imap
|
||||
for email_account in frappe.get_list("Email Account", filters={"enable_incoming": 1, "use_imap": 1}):
|
||||
# get all data from Email Account
|
||||
|
|
|
|||
5
frappe/patches/v14_0/remove_post_and_post_comment.py
Normal file
5
frappe/patches/v14_0/remove_post_and_post_comment.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import frappe
|
||||
|
||||
def execute():
|
||||
frappe.delete_doc_if_exists("DocType", "Post")
|
||||
frappe.delete_doc_if_exists("DocType", "Post Comment")
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue