Merge branch 'develop' into link_title_refactor

This commit is contained in:
Suraj Shetty 2022-02-04 14:53:26 +05:30 committed by GitHub
commit 68d934ae0f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
211 changed files with 5425 additions and 3377 deletions

View file

@ -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

View file

@ -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')

View file

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

View file

@ -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');
@ -109,7 +126,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');
@ -120,7 +137,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", ""
);
});
});

View file

@ -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', {

View file

@ -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");
}

View file

@ -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

View file

@ -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();

View file

@ -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();

View file

@ -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()

View file

@ -143,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
@ -150,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):
@ -311,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:
@ -446,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)

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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",

View file

@ -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,7 +16,6 @@ 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, add_user_info
@ -108,8 +108,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")

View file

@ -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

View file

@ -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:

View file

@ -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,
)

View file

@ -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

View file

@ -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(

View file

@ -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.

View file

@ -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>
`);
}
});
},
});

View file

@ -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
}

View file

@ -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

View file

@ -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: {

View file

@ -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()

View file

@ -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(

View 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) {
// }
});

View 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"
}

View 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

View file

@ -0,0 +1,8 @@
# Copyright (c) 2021, Frappe Technologies and Contributors
# See license.txt
# import frappe
import unittest
class TestDataImportLog(unittest.TestCase):
pass

View file

@ -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

View file

@ -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)

View file

@ -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()

View file

@ -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

View file

@ -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()

View file

@ -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": []
}

View file

@ -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

View file

@ -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)]

View 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")

View file

@ -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()

View file

@ -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')

View file

@ -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)

View file

@ -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()

View file

@ -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:

View file

@ -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(),

View file

@ -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": "",

View file

@ -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",

View file

@ -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",

View file

@ -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)

View file

@ -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, "")

View file

@ -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)

View file

@ -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 &amp; 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",

View file

@ -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**"""

View file

@ -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):

View file

@ -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]:

View file

@ -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;")]

View file

@ -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

View file

@ -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",

View file

@ -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')):

View file

@ -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 = \

View file

@ -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)

View file

@ -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)

View file

@ -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",

View file

@ -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')

View file

@ -91,8 +91,8 @@ 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']
docinfo = frappe._dict(user_info = {})
@ -119,6 +119,7 @@ def get_docinfo(doc=None, doctype=None, name=None):
update_user_info(docinfo)
frappe.response["docinfo"] = docinfo
return docinfo
def add_comments(doc, docinfo):
# divide comments into separate lists

View file

@ -19,7 +19,7 @@ from frappe.utils import add_user_info
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)
@ -305,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))
@ -364,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("`")
@ -372,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
@ -438,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()
@ -560,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:
@ -598,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")

View file

@ -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):

View file

@ -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

View file

@ -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:

View file

@ -103,6 +103,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
View 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():

View file

@ -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

View file

@ -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"
}

View file

@ -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))

View file

@ -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`.
@ -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
@ -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)))
@ -805,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:

View file

@ -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
View 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)

View file

@ -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.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
@ -307,7 +311,7 @@ class Document(BaseDocument):
self.check_permission("write", "save")
if self.docstatus == 2:
if self.docstatus.is_cancelled():
self._rename_doc_on_cancel()
self.set_user_and_timestamp()
@ -474,7 +478,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
@ -490,7 +494,7 @@ class Document(BaseDocument):
def set_docstatus(self):
if self.docstatus is None:
self.docstatus=0
self.docstatus = DocStatus.draft()
for d in self.get_all_children():
d.docstatus = self.docstatus
@ -740,7 +744,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`):
@ -751,31 +755,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):
@ -929,14 +934,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__
@ -954,7 +959,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**.

View file

@ -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:

View file

@ -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)

View file

@ -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)

View file

@ -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:

View file

@ -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,8 @@ 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
@ -154,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
@ -174,22 +175,25 @@ 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

View 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",
)

View file

@ -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

View file

@ -0,0 +1,5 @@
import frappe
def execute():
frappe.delete_doc_if_exists("DocType", "Post")
frappe.delete_doc_if_exists("DocType", "Post Comment")

View file

@ -5,7 +5,6 @@ from __future__ import unicode_literals
import frappe
def execute():
frappe.reload_doc("desk", "doctype", "kanban_board_column")
indicator_map = {
'blue': 'Blue',
'orange': 'Orange',

View file

@ -5,10 +5,10 @@ from frappe import _
def execute():
frappe.reload_doc('desk', 'doctype', 'workspace', force=True)
for seq, wspace in enumerate(frappe.get_all('Workspace', order_by='name asc')):
doc = frappe.get_doc('Workspace', wspace.name)
for seq, workspace in enumerate(frappe.get_all('Workspace', order_by='name asc')):
doc = frappe.get_doc('Workspace', workspace.name)
content = create_content(doc)
update_wspace(doc, seq, content)
update_workspace(doc, seq, content)
frappe.db.commit()
def create_content(doc):
@ -49,7 +49,7 @@ def create_content(doc):
del doc.links[doc.links.index(l)]
return content
def update_wspace(doc, seq, content):
def update_workspace(doc, seq, content):
if not doc.title and not doc.content and not doc.is_standard and not doc.public:
doc.sequence_id = seq + 1
doc.content = json.dumps(content)

View file

@ -23,7 +23,7 @@ def print_has_permission_check_logs(func):
frappe.flags['has_permission_check_logs'] = []
result = func(*args, **kwargs)
self_perm_check = True if not kwargs.get('user') else kwargs.get('user') == frappe.session.user
raise_exception = False if kwargs.get('raise_exception') == False else True
raise_exception = False if kwargs.get('raise_exception') is False else True
# print only if access denied
# and if user is checking his own permission

File diff suppressed because it is too large Load diff

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 104 KiB

View file

@ -96,6 +96,7 @@ import "./frappe/ui/sort_selector.js";
import "./frappe/change_log.html";
import "./frappe/ui/workspace_loading_skeleton.html";
import "./frappe/ui/workspace_sidebar_loading_skeleton.html";
import "./frappe/desk.js";
import "./frappe/query_string.js";

View file

@ -2,6 +2,7 @@ import "./jquery-bootstrap";
import "./frappe/class.js";
import "./frappe/polyfill.js";
import "./lib/md5.min.js";
import "./lib/moment.js";
import "./frappe/provide.js";
import "./frappe/format.js";
import "./frappe/utils/number_format.js";

View file

@ -331,7 +331,7 @@ frappe.data_import.ImportPreview = class ImportPreview {
is_row_imported(row) {
let serial_no = row[0].content;
return this.import_log.find(log => {
return log.success && log.row_indexes.includes(serial_no);
return log.success && JSON.parse(log.row_indexes || '[]').includes(serial_no);
});
}
};

View file

@ -214,19 +214,20 @@ frappe.Application = class Application {
email_password_prompt(email_account,user,i) {
var me = this;
const email_id = email_account[i]["email_id"];
let d = new frappe.ui.Dialog({
title: __('Password missing in Email Account'),
fields: [
{
'fieldname': 'password',
'fieldtype': 'Password',
'label': __('Please enter the password for: <b>{0}</b>', [email_account[i]["email_id"]]),
'label': __('Please enter the password for: <b>{0}</b>', [email_id], "Email Account"),
'reqd': 1
},
{
"fieldname": "submit",
"fieldtype": "Button",
"label": __("Submit")
"label": __("Submit", null, "Submit password for Email Account")
}
]
});

View file

@ -534,22 +534,21 @@ export default {
});
},
show_google_drive_picker() {
let dialog = cur_dialog;
dialog.hide();
this.close_dialog = true;
let google_drive = new GoogleDrivePicker({
pickerCallback: data => this.google_drive_callback(data, dialog),
pickerCallback: data => this.google_drive_callback(data),
...this.google_drive_settings
});
google_drive.loadPicker();
},
google_drive_callback(data, dialog) {
google_drive_callback(data) {
if (data.action == google.picker.Action.PICKED) {
this.upload_file({
file_url: data.docs[0].url,
file_name: data.docs[0].name
});
} else if (data.action == google.picker.Action.CANCEL) {
dialog.show();
cur_frm.attachments.new_attachment()
}
},
url_to_file(url, filename, mime_type) {

View file

@ -11,7 +11,8 @@ frappe.ui.form.ControlDateRange = class ControlDateRange extends frappe.ui.form.
language: "en",
range: true,
autoClose: true,
toggleSelected: false
toggleSelected: false,
firstDay: frappe.datetime.get_first_day_of_the_week_index()
};
this.datepicker_options.dateFormat =
(frappe.boot.sysdefaults.date_format || 'yyyy-mm-dd');

View file

@ -2,7 +2,7 @@ frappe.ui.form.ControlDynamicLink = class ControlDynamicLink extends frappe.ui.f
get_options() {
let options = '';
if (this.df.get_options) {
options = this.df.get_options();
options = this.df.get_options(this);
} else if (this.docname==null && cur_dialog) {
//for dialog box
options = cur_dialog.get_value(this.df.options);

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