Merge branch 'develop' into fix/ux_kb_driven_animations

This commit is contained in:
Suraj Shetty 2022-04-25 18:17:38 +05:30 committed by GitHub
commit dc60d7d7f0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
90 changed files with 1488 additions and 777 deletions

32
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,32 @@
name: Generate Semantic Release
on:
push:
branches:
- version-13
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout Entire Repository
uses: actions/checkout@v2
with:
fetch-depth: 0
persist-credentials: false
- name: Setup Node.js v14
uses: actions/setup-node@v2
with:
node-version: 14
- name: Setup dependencies
run: |
npm install @semantic-release/git @semantic-release/exec --no-save
- name: Create Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
GIT_AUTHOR_NAME: "Frappe PR Bot"
GIT_AUTHOR_EMAIL: "developers@frappe.io"
GIT_COMMITTER_NAME: "Frappe PR Bot"
GIT_COMMITTER_EMAIL: "developers@frappe.io"
run: npx semantic-release

View file

@ -53,3 +53,43 @@ pull_request_rules:
{{ title }} (#{{ number }})
{{ body }}
- name: backport to develop
conditions:
- label="backport develop"
actions:
backport:
branches:
- develop
assignees:
- "{{ author }}"
- name: backport to version-13-hotfix
conditions:
- label="backport version-13-hotfix"
actions:
backport:
branches:
- version-13-hotfix
assignees:
- "{{ author }}"
- name: backport to version-13-pre-release
conditions:
- label="backport version-13-pre-release"
actions:
backport:
branches:
- version-13-pre-release
assignees:
- "{{ author }}"
- name: backport to version-12-hotfix
conditions:
- label="backport version-12-hotfix"
actions:
backport:
branches:
- version-12-hotfix
assignees:
- "{{ author }}"

24
.releaserc Normal file
View file

@ -0,0 +1,24 @@
{
"branches": ["version-13"],
"plugins": [
"@semantic-release/commit-analyzer", {
"preset": "angular",
"releaseRules": [
{"breaking": true, "release": false}
]
},
"@semantic-release/release-notes-generator",
[
"@semantic-release/exec", {
"prepareCmd": 'sed -ir "s/[0-9]*\.[0-9]*\.[0-9]*/${nextRelease.version}/" frappe/__init__.py'
}
],
[
"@semantic-release/git", {
"assets": ["frappe/__init__.py"],
"message": "chore(release): Bumped to Version ${nextRelease.version}\n\n${nextRelease.notes}"
}
],
"@semantic-release/github"
]
}

View file

@ -20,6 +20,7 @@ context('Control Barcode', () => {
it('should generate barcode on setting a value', () => {
get_dialog_with_barcode().as('dialog');
cy.focused().blur();
cy.get('.frappe-control[data-fieldname=barcode]').findByRole('textbox')
.type('123456789')
.blur();
@ -36,6 +37,7 @@ context('Control Barcode', () => {
it('should reset when input is cleared', () => {
get_dialog_with_barcode().as('dialog');
cy.focused().blur();
cy.get('.frappe-control[data-fieldname=barcode]').findByRole('textbox')
.type('123456789')
.blur();

View file

@ -1,23 +1,27 @@
context('Date Control', () => {
before(() => {
cy.login();
cy.visit('/app/doctype');
return cy.window().its('frappe').then(frappe => {
return frappe.xcall('frappe.tests.ui_test_helpers.create_doctype', {
name: 'Test Date Control',
fields: [
{
"label": "Date",
"fieldname": "date",
"fieldtype": "Date",
"in_list_view": 1
},
]
});
});
cy.visit('/app');
});
function get_dialog(date_field_options) {
return cy.dialog({
title: 'Date',
fields: [{
"label": "Date",
"fieldname": "date",
"fieldtype": "Date",
"in_list_view": 1,
...date_field_options
}]
});
}
it('Selecting a date from the datepicker', () => {
cy.new_form('Test Date Control');
cy.clear_dialogs();
cy.clear_datepickers();
get_dialog().as('dialog');
cy.get_field('date', 'Date').click();
cy.get('.datepicker--nav-title').click();
cy.get('.datepicker--nav-title').click({force: true});
@ -28,12 +32,16 @@ context('Date Control', () => {
cy.get('.datepicker--months > .datepicker--cells > .datepicker--cell[data-month=0]').click();
cy.get('.datepicker--days > .datepicker--cells > .datepicker--cell[data-date=15]').click();
//Verifying if the selected date is displayed in the date field
cy.get_field('date', 'Date').should('have.value', '01-15-2020');
// Verify if the selected date is set the date field
cy.window().its('cur_dialog.fields_dict.date.value').should('be.equal', '2020-01-15');
});
it('Checking next and previous button', () => {
cy.get_field('date', 'Date').click();
cy.clear_dialogs();
cy.clear_datepickers();
get_dialog({ default: '2020-01-15' }).as('dialog');
cy.get_field('date', 'Date').click();
//Clicking on the next button in the datepicker
cy.get('.datepicker--nav-action[data-action=next]').click();
@ -42,7 +50,7 @@ context('Date Control', () => {
cy.get('.datepicker--cell[data-date=15]').click({force: true});
//Verifying if the selected date has been displayed in the date field
cy.get_field('date', 'Date').should('have.value', '02-15-2020');
cy.window().its('cur_dialog.fields_dict.date.value').should('be.equal', '2020-02-15');
cy.wait(500);
cy.get_field('date', 'Date').click();
@ -53,19 +61,22 @@ context('Date Control', () => {
cy.get('.datepicker--cell[data-date=15]').click({force: true});
//Verifying if the selected date has been displayed in the date field
cy.get_field('date', 'Date').should('have.value', '01-15-2020');
cy.window().its('cur_dialog.fields_dict.date.value').should('be.equal', '2020-01-15');
});
it('Clicking on "Today" button gives todays date', () => {
cy.get_field('date', 'Date').click();
cy.clear_dialogs();
cy.clear_datepickers();
get_dialog().as('dialog');
cy.get_field('date', 'Date').click();
//Clicking on "Today" button
cy.get('.datepicker--button').click();
//Picking up the todays date
const todays_date = Cypress.moment().format('MM-DD-YYYY');
//Verifying if clicking on "Today" button matches today's date
cy.get_field('date', 'Date').should('have.value', todays_date);
cy.window().then(win => {
expect(win.cur_dialog.fields_dict.date.value).to.be.equal(win.frappe.datetime.get_today());
});
});
});

View file

@ -16,7 +16,7 @@ context("Control Markdown Editor", () => {
cy.click_modal_primary_button("Upload");
cy.get_field("main_section_md", "Markdown Editor").should(
"contain",
"![](/files/sample_image.jpg)"
"![](/files/sample_image"
);
});
});

View file

@ -27,7 +27,7 @@ context('Form', () => {
cy.clear_filters();
cy.get('.standard-filter-section [data-fieldname="name"] input').type('Test Form Contact 3').blur();
cy.click_listview_row_item(0);
cy.click_listview_row_item_with_text('Test Form Contact 3');
cy.get('#page-Contact .page-head').findByTitle('Test Form Contact 3').should('exist');
cy.get('.prev-doc').should('be.visible').click();

View file

@ -0,0 +1,87 @@
context('Kanban Board', () => {
before(() => {
cy.login();
cy.visit('/app');
});
it('Create ToDo Kanban', () => {
cy.visit('/app/todo');
cy.get('.page-actions .custom-btn-group button').click();
cy.get('.page-actions .custom-btn-group ul.dropdown-menu li').contains('Kanban').click();
cy.focused().blur();
cy.fill_field('board_name', 'ToDo Kanban', 'Data');
cy.fill_field('field_name', 'Status', 'Select');
cy.click_modal_primary_button('Save');
cy.get('.title-text').should('contain', 'ToDo Kanban');
});
it('Create ToDo from kanban', () => {
cy.intercept({
method: 'POST',
url: 'api/method/frappe.client.save'
}).as('save-todo');
cy.click_listview_primary_button('Add ToDo');
cy.fill_field('description', 'Test Kanban ToDo', 'Text Editor');
cy.get('.modal-footer .btn-primary').last().click();
cy.wait('@save-todo');
});
it('Add and Remove fields', () => {
cy.visit('/app/todo/view/kanban/ToDo Kanban');
cy.intercept('POST', '/api/method/frappe.desk.doctype.kanban_board.kanban_board.save_settings').as('save-kanban');
cy.intercept('POST', '/api/method/frappe.desk.doctype.kanban_board.kanban_board.update_order').as('update-order');
cy.get('.page-actions .menu-btn-group > .btn').click();
cy.get('.page-actions .menu-btn-group .dropdown-menu li').contains('Kanban Settings').click();
cy.get('.add-new-fields').click();
cy.get('.checkbox-options .checkbox').contains('ID').click();
cy.get('.checkbox-options .checkbox').contains('Status').first().click();
cy.get('.checkbox-options .checkbox').contains('Priority').click();
cy.get('.modal-footer .btn-primary').last().click();
cy.get('.frappe-control .label-area').contains('Show Labels').click();
cy.click_modal_primary_button('Save');
cy.wait('@save-kanban');
cy.get('.kanban-column[data-column-value="Open"] .kanban-cards').as('open-cards');
cy.get('@open-cards').find('.kanban-card .kanban-card-doc').first().should('contain', 'ID:');
cy.get('@open-cards').find('.kanban-card .kanban-card-doc').first().should('contain', 'Status:');
cy.get('@open-cards').find('.kanban-card .kanban-card-doc').first().should('contain', 'Priority:');
cy.get('.page-actions .menu-btn-group > .btn').click();
cy.get('.page-actions .menu-btn-group .dropdown-menu li').contains('Kanban Settings').click();
cy.get_open_dialog().find('.frappe-control[data-fieldname="fields_html"] div[data-label="ID"] .remove-field').click();
cy.wait('@update-order');
cy.get_open_dialog().find('.frappe-control .label-area').contains('Show Labels').click();
cy.get('.modal-footer .btn-primary').last().click();
cy.wait('@save-kanban');
cy.get('@open-cards').find('.kanban-card .kanban-card-doc').first().should('not.contain', 'ID:');
});
// it('Drag todo', () => {
// cy.intercept({
// method: 'POST',
// url: 'api/method/frappe.desk.doctype.kanban_board.kanban_board.update_order_for_single_card'
// }).as('drag-completed');
// cy.get('.kanban-card-body')
// .contains('Test Kanban ToDo').first()
// .drag('[data-column-value="Closed"] .kanban-cards', { force: true });
// cy.wait('@drag-completed');
// });
});

View file

@ -16,7 +16,7 @@ context('Timeline Email', () => {
it('Adding email and verifying timeline content for email attachment', () => {
cy.visit('/app/todo');
cy.get('.list-row > .level-left > .list-subject').eq(0).click();
cy.click_listview_row_item_with_text('Test ToDo');
//Creating a new email
cy.get('.timeline-actions > .timeline-item > .action-buttons > .action-btn').click();
@ -47,7 +47,7 @@ context('Timeline Email', () => {
it('Deleting attachment and ToDo', () => {
cy.visit('/app/todo');
cy.get('.list-row > .level-left > .list-subject > .level-item.ellipsis > .ellipsis').eq(0).click();
cy.click_listview_row_item_with_text('Test ToDo');
//Removing the added attachment
cy.get('.attachment-row > .data-pill > .remove-btn > .icon').click();

View file

@ -2,7 +2,6 @@ context('Workspace 2.0', () => {
before(() => {
cy.visit('/login');
cy.login();
cy.visit('/app/website');
});
it('Navigate to page from sidebar', () => {
@ -13,6 +12,11 @@ context('Workspace 2.0', () => {
});
it('Create Private Page', () => {
cy.intercept({
method: 'POST',
url: 'api/method/frappe.desk.doctype.workspace.workspace.new_page'
}).as('new_page');
cy.get('.codex-editor__redactor .ce-block');
cy.get('.custom-actions button[data-label="Create%20Workspace"]').click();
cy.fill_field('title', 'Test Private Page', 'Data');
@ -27,12 +31,100 @@ context('Workspace 2.0', () => {
cy.wait(300);
cy.get('.sidebar-item-container[item-name="Test Private Page"]').should('have.attr', 'item-public', '0');
cy.wait(500);
cy.wait('@new_page');
});
it('Create Child Page', () => {
cy.intercept({
method: 'POST',
url: 'api/method/frappe.desk.doctype.workspace.workspace.new_page'
}).as('new_page');
cy.get('.codex-editor__redactor .ce-block');
cy.get('.custom-actions button[data-label="Create%20Workspace"]').click();
cy.fill_field('title', 'Test Child Page', 'Data');
cy.fill_field('parent', 'Test Private Page', 'Select');
cy.fill_field('icon', 'edit', 'Icon');
cy.get_open_dialog().find('.modal-header').click();
cy.get_open_dialog().find('.btn-primary').click();
// check if sidebar item is added in pubic section
cy.get('.sidebar-item-container[item-name="Test Child Page"]').should('have.attr', 'item-public', '0');
cy.get('.standard-actions .btn-primary[data-label="Save"]').click();
cy.wait(300);
cy.get('.sidebar-item-container[item-name="Test Child Page"]').should('have.attr', 'item-public', '0');
cy.wait('@new_page');
});
it('Duplicate Page', () => {
cy.intercept({
method: 'POST',
url: 'api/method/frappe.desk.doctype.workspace.workspace.duplicate_page'
}).as('page_duplicated');
cy.get('.codex-editor__redactor .ce-block');
cy.get('.standard-actions .btn-secondary[data-label=Edit]').click();
cy.get('.sidebar-item-container[item-name="Test Private Page"]').as('sidebar-item');
cy.get('@sidebar-item').find('.standard-sidebar-item').first().click();
cy.get('@sidebar-item').find('.dropdown-btn').first().click();
cy.get('@sidebar-item').find('.dropdown-list .dropdown-item').contains('Duplicate').first().click({force: true});
cy.get_open_dialog().fill_field('title', 'Duplicate Page', 'Data');
cy.click_modal_primary_button('Duplicate');
cy.wait('@page_duplicated');
});
it('Drag Sidebar Item', () => {
cy.intercept({
method: 'POST',
url: 'api/method/frappe.desk.doctype.workspace.workspace.sort_pages'
}).as('page_sorted');
cy.get('.sidebar-item-container[item-name="Duplicate Page"]').as('sidebar-item');
cy.get('@sidebar-item').find('.standard-sidebar-item').first().click();
cy.get('@sidebar-item').find('.drag-handle').first().move({ deltaX: 0, deltaY: 100 });
cy.get('.sidebar-item-container[item-name="Build"]').as('sidebar-item');
cy.get('@sidebar-item').find('.standard-sidebar-item').first().click();
cy.get('@sidebar-item').find('.drag-handle').first().move({ deltaX: 0, deltaY: 100 });
cy.wait('@page_sorted');
});
it('Edit Page Detail', () => {
cy.intercept({
method: 'POST',
url: 'api/method/frappe.desk.doctype.workspace.workspace.update_page'
}).as('page_updated');
cy.get('.sidebar-item-container[item-name="Test Private Page"]').as('sidebar-item');
cy.get('@sidebar-item').find('.standard-sidebar-item').first().click();
cy.get('@sidebar-item').find('.dropdown-btn').first().click();
cy.get('@sidebar-item').find('.dropdown-list .dropdown-item').contains('Edit').first().click({force: true});
cy.get_open_dialog().fill_field('title', ' 1', 'Data');
cy.get_open_dialog().find('input[data-fieldname="is_public"]').check();
cy.click_modal_primary_button('Update');
cy.get('.standard-sidebar-section:first .sidebar-item-container[item-name="Test Private Page"]').should('not.exist');
cy.get('.standard-sidebar-section:last .sidebar-item-container[item-name="Test Private Page 1"]').should('exist');
cy.wait('@page_updated');
});
it('Add New Block', () => {
cy.get('.sidebar-item-container[item-name="Duplicate Page"]').as('sidebar-item');
cy.get('@sidebar-item').find('.standard-sidebar-item').first().click();
cy.get('.ce-block').click().type('{enter}');
cy.get('.block-list-container .block-list-item').contains('Heading').click();
cy.get(":focus").type('Header');
@ -70,19 +162,24 @@ context('Workspace 2.0', () => {
cy.get('.standard-actions .btn-primary[data-label="Save"]').click();
});
it('Delete Private Page', () => {
it('Delete Duplicate Page', () => {
cy.intercept({
method: 'POST',
url: 'api/method/frappe.desk.doctype.workspace.workspace.delete_page'
}).as('page_deleted');
cy.get('.codex-editor__redactor .ce-block');
cy.get('.standard-actions .btn-secondary[data-label=Edit]').click();
cy.get('.sidebar-item-container[item-name="Test Private Page"]')
cy.get('.sidebar-item-container[item-name="Duplicate Page"]')
.find('.sidebar-item-control .setting-btn').click();
cy.get('.sidebar-item-container[item-name="Test Private Page"]')
cy.get('.sidebar-item-container[item-name="Duplicate Page"]')
.find('.dropdown-item[title="Delete Workspace"]').click({force: true});
cy.wait(300);
cy.get('.modal-footer > .standard-actions > .btn-modal-primary:visible').first().click();
cy.get('.standard-actions .btn-primary[data-label="Save"]').click();
cy.get('.codex-editor__redactor .ce-block');
cy.get('.sidebar-item-container[item-name="Test Private Page"]').should('not.exist');
cy.get('.sidebar-item-container[item-name="Duplicate Page"]').should('not.exist');
cy.wait('@page_deleted');
});
});

View file

@ -1,5 +1,6 @@
import 'cypress-file-upload';
import '@testing-library/cypress/add-commands';
import '@4tw/cypress-drag-drop';
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
@ -240,8 +241,20 @@ Cypress.Commands.add('clear_cache', () => {
});
Cypress.Commands.add('dialog', opts => {
return cy.window().then(win => {
var d = new win.frappe.ui.Dialog(opts);
return cy.window({ log: false }).its('frappe', { log: false }).then(frappe => {
Cypress.log({
name: "dialog",
displayName: "dialog",
message: 'frappe.ui.Dialog',
consoleProps: () => {
return {
options: opts,
dialog: d
}
}
});
var d = new frappe.ui.Dialog(opts);
d.show();
return d;
});
@ -257,6 +270,20 @@ Cypress.Commands.add('hide_dialog', () => {
cy.get('.modal:visible').should('not.exist');
});
Cypress.Commands.add('clear_dialogs', () => {
cy.window().then((win) => {
win.$('.modal, .modal-backdrop').remove();
});
cy.get('.modal').should('not.exist');
});
Cypress.Commands.add('clear_datepickers', () => {
cy.window().then((win) => {
win.$('.datepicker').remove();
});
cy.get('.datepicker').should('not.exist');
});
Cypress.Commands.add('insert_doc', (doctype, args, ignore_duplicate) => {
return cy
.window()
@ -325,6 +352,13 @@ Cypress.Commands.add('click_listview_row_item', (row_no) => {
cy.get('.list-row > .level-left > .list-subject > .level-item > .ellipsis').eq(row_no).click({force: true});
});
Cypress.Commands.add('click_listview_row_item_with_text', (text) => {
cy.get('.list-row > .level-left > .list-subject > .level-item > .ellipsis')
.contains(text)
.first()
.click({force: true});
});
Cypress.Commands.add('click_filter_button', () => {
cy.get('.filter-selector > .btn').click();
});

View file

@ -217,7 +217,6 @@ def init(site, sites_path=None, new_site=False):
local.module_app = None
local.app_modules = None
local.system_settings = _dict()
local.user = None
local.user_perms = None
@ -355,11 +354,11 @@ def cache() -> "RedisWrapper":
return redis_server
def get_traceback():
def get_traceback(with_context=False):
"""Returns error traceback."""
from frappe.utils import get_traceback
return get_traceback()
return get_traceback(with_context=with_context)
def errprint(msg):
@ -1211,18 +1210,35 @@ def reload_doc(module, dt=None, dn=None, force=False, reset_permissions=False):
@whitelist()
def rename_doc(*args, **kwargs):
def rename_doc(
doctype: str,
old: str,
new: str,
force: bool = False,
merge: bool = False,
*,
ignore_if_exists: bool = False,
show_alert: bool = True,
rebuild_search: bool = True,
) -> str:
"""
Renames a doc(dt, old) to doc(dt, new) and updates all linked fields of type "Link"
Calls `frappe.model.rename_doc.rename_doc`
"""
kwargs.pop("ignore_permissions", None)
kwargs.pop("cmd", None)
from frappe.model.rename_doc import rename_doc
return rename_doc(*args, **kwargs)
return rename_doc(
doctype=doctype,
old=old,
new=new,
force=force,
merge=merge,
ignore_if_exists=ignore_if_exists,
show_alert=show_alert,
rebuild_search=rebuild_search,
)
def get_module(modulename):
@ -2068,25 +2084,30 @@ def logger(
)
def log_error(message=None, title=_("Error")):
def log_error(title=None, message=None, reference_doctype=None, reference_name=None):
"""Log error to Error Log"""
# AI ALERT:
# Parameter ALERT:
# the title and message may be swapped
# the better API for this is log_error(title, message), and used in many cases this way
# this hack tries to be smart about whats a title (single line ;-)) and fixes it
traceback = None
if message:
if "\n" in title:
error, title = title, message
if "\n" in title: # traceback sent as title
traceback, title = title, message
else:
error = message
else:
error = get_traceback()
traceback = message
return get_doc(dict(doctype="Error Log", error=as_unicode(error), method=title)).insert(
ignore_permissions=True
)
title = title or "Error"
traceback = as_unicode(traceback or get_traceback(with_context=True))
return get_doc(
doctype="Error Log",
error=traceback,
method=title,
reference_doctype=reference_doctype,
reference_name=reference_name,
).insert(ignore_permissions=True)
def get_desk_link(doctype, name):
@ -2139,9 +2160,7 @@ def safe_eval(code, eval_globals=None, eval_locals=None):
def get_system_settings(key):
if key not in local.system_settings:
local.system_settings.update({key: db.get_single_value("System Settings", key)})
return local.system_settings.get(key)
return db.get_single_value("System Settings", key, cache=True)
def get_active_domains():

View file

@ -189,7 +189,7 @@ class AutoRepeat(Document):
if self.notify_by_email and self.recipients:
self.send_notification(new_doc)
except Exception:
error_log = frappe.log_error(frappe.get_traceback(), _("Auto Repeat Document Creation Failure"))
error_log = self.log_error("Auto repeat failed")
self.disable_auto_repeat()

View file

@ -5,7 +5,6 @@ import json
import frappe
from frappe.desk.notifications import clear_notifications, delete_notification_count_for
from frappe.model.document import Document
common_default_keys = ["__default", "__global"]

View file

@ -189,7 +189,10 @@ def insert(doc=None):
if isinstance(doc, str):
doc = json.loads(doc)
if doc.get("parenttype"):
doc = frappe._dict(doc)
if frappe.is_table(doc.doctype):
if not (doc.parenttype and doc.parent and doc.parentfield):
frappe.throw(_("parenttype, parent and parentfield are required to insert a child record"))
# inserting a child record
parent = frappe.get_doc(doc.parenttype, doc.parent)
parent.append(doc.parentfield, doc)

View file

@ -870,7 +870,7 @@ def run_ui_tests(
# install cypress
click.secho("Installing Cypress...", fg="yellow")
frappe.commands.popen(
"yarn add cypress@^6 cypress-file-upload@^5 @testing-library/cypress@^8 @cypress/code-coverage@^3 --no-lockfile"
"yarn add cypress@^6 cypress-file-upload@^5 @4tw/cypress-drag-drop@^2 @testing-library/cypress@^8 @cypress/code-coverage@^3 --no-lockfile"
)
# run for headless mode
@ -1024,6 +1024,7 @@ def set_config(context, key, value, global_=False, parse=False, as_dict=False):
def get_version(output):
"""Show the versions of all the installed apps."""
from git import Repo
from git.exc import InvalidGitRepositoryError
from frappe.utils.change_log import get_app_branch
from frappe.utils.commands import render_table
@ -1034,12 +1035,16 @@ def get_version(output):
for app in sorted(frappe.get_all_apps()):
module = frappe.get_module(app)
app_hooks = frappe.get_module(app + ".hooks")
repo = Repo(frappe.get_app_path(app, ".."))
app_info = frappe._dict()
try:
app_info.commit = Repo(frappe.get_app_path(app, "..")).head.object.hexsha[:7]
except InvalidGitRepositoryError:
app_info.commit = ""
app_info.app = app
app_info.branch = get_app_branch(app)
app_info.commit = repo.head.object.hexsha[:7]
app_info.version = getattr(app_hooks, f"{app_info.branch}_version", None) or module.__version__
data.append(app_info)

View file

@ -450,8 +450,7 @@ def get_contacts(email_strings: List[str], auto_create_contact=False) -> List[st
contact.insert(ignore_permissions=True)
contact_name = contact.name
except Exception:
traceback = frappe.get_traceback()
frappe.log_error(traceback)
contact.log_error("Unable to add contact")
if contact_name:
contacts.append(contact_name)

View file

@ -248,7 +248,7 @@ def mark_email_as_seen(name: str = None):
frappe.db.commit() # nosemgrep: this will be called in a GET request
except Exception:
frappe.log_error(frappe.get_traceback())
frappe.log_error("Unable to mark as seen", None, "Communication", name)
finally:
frappe.response.update(

View file

@ -113,7 +113,7 @@ def start_import(data_import):
except Exception:
frappe.db.rollback()
data_import.db_set("status", "Error")
frappe.log_error(title=data_import.name)
data_import.log_error("Data import failed")
finally:
frappe.flags.in_import = False

View file

@ -347,7 +347,7 @@
},
{
"default": "0",
"description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field",
"description": "Don't encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field",
"fieldname": "ignore_xss_filter",
"fieldtype": "Check",
"label": "Ignore XSS Filter"
@ -547,7 +547,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2022-03-02 17:07:32.117897",
"modified": "2022-04-19 12:27:28.641580",
"modified_by": "Administrator",
"module": "Core",
"name": "DocField",

View file

@ -1,148 +1,76 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "",
"beta": 0,
"creation": "2013-01-16 13:09:40",
"custom": 0,
"description": "Log of Scheduler Errors",
"docstatus": 0,
"doctype": "DocType",
"document_type": "System",
"editable_grid": 0,
"engine": "MyISAM",
"actions": [],
"creation": "2013-01-16 13:09:40",
"doctype": "DocType",
"document_type": "System",
"engine": "MyISAM",
"field_order": [
"seen",
"method",
"error",
"reference_doctype",
"reference_name"
],
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "seen",
"fieldtype": "Check",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Seen",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"default": "0",
"fieldname": "seen",
"fieldtype": "Check",
"hidden": 1,
"label": "Seen"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "method",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Title",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "method",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title",
"read_only": 1
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "error",
"fieldtype": "Code",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Error",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"fieldname": "error",
"fieldtype": "Code",
"label": "Error",
"read_only": 1
},
{
"fieldname": "reference_doctype",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Reference DocType",
"options": "DocType",
"search_index": 1
},
{
"fieldname": "reference_name",
"fieldtype": "Data",
"label": "Reference Name"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-warning-sign",
"idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2021-10-25 12:21:44.292471",
"modified_by": "Administrator",
"module": "Core",
"name": "Error Log",
"owner": "Administrator",
],
"icon": "fa fa-warning-sign",
"idx": 1,
"links": [],
"modified": "2022-04-18 17:25:47.406873",
"modified_by": "Administrator",
"module": "Core",
"name": "Error Log",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_order": "ASC",
"track_seen": 0
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "ASC",
"states": [],
"title_field": "method"
}

View file

@ -9,4 +9,8 @@ import frappe
class TestErrorLog(unittest.TestCase):
pass
def test_error_log(self):
"""let's do an error log on error log?"""
doc = frappe.new_doc("Error Log")
error = doc.log_error("This is an error")
self.assertEqual(error.doctype, "Error Log")

View file

@ -1043,7 +1043,7 @@ def attach_files_to_document(doc, event):
):
return
frappe.get_doc(
file_doc = frappe.get_doc(
doctype="File",
file_url=value,
attached_to_name=doc.name,
@ -1052,4 +1052,4 @@ def attach_files_to_document(doc, event):
folder="Home/Attachments",
).insert()
except Exception:
frappe.log_error(title=_("Error Attaching File"))
file_doc.log_error("Error Attaching File")

View file

@ -47,7 +47,7 @@ def run_background(prepared_report):
instance.save(ignore_permissions=True)
except Exception:
frappe.log_error(frappe.get_traceback())
report.log_error("Prepared report failed")
instance = frappe.get_doc("Prepared Report", prepared_report)
instance.status = "Error"
instance.error_message = frappe.get_traceback()

View file

@ -68,6 +68,8 @@
"prepared_report_section",
"enable_prepared_report_auto_deletion",
"prepared_report_expiry_period",
"column_break_64",
"max_auto_email_report_per_user",
"system_updates_section",
"disable_system_update_notification"
],
@ -445,7 +447,7 @@
"collapsible": 1,
"fieldname": "prepared_report_section",
"fieldtype": "Section Break",
"label": "Prepared Report"
"label": "Reports"
},
{
"default": "Frappe",
@ -485,12 +487,22 @@
"fieldtype": "Select",
"label": "First Day of the Week",
"options": "Sunday\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday"
},
{
"fieldname": "column_break_64",
"fieldtype": "Column Break"
},
{
"default": "20",
"fieldname": "max_auto_email_report_per_user",
"fieldtype": "Int",
"label": "Max auto email report per user"
}
],
"icon": "fa fa-cog",
"issingle": 1,
"links": [],
"modified": "2022-01-04 11:28:34.881192",
"modified": "2022-04-21 09:11:35.218721",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",

View file

@ -53,7 +53,6 @@ class SystemSettings(Document):
frappe.cache().delete_value("system_settings")
frappe.cache().delete_value("time_zone")
frappe.local.system_settings = {}
if frappe.flags.update_last_reset_password_date:
update_last_reset_password_date()

View file

@ -265,7 +265,7 @@ class User(Document):
except frappe.OutgoingEmailError:
# email server not set, don't send email
frappe.log_error(frappe.get_traceback())
self.log_error("Unable to send new password notification")
@Document.hook
def validate_reset_password(self):

View file

@ -22,11 +22,7 @@ JOB_COLORS = {"queued": "orange", "failed": "red", "started": "blue", "finished"
def get_info(view=None, queue_timeout=None, job_status=None) -> List[Dict]:
jobs = []
def add_job(job: "Job", name: str) -> None:
if job_status != "all" and job.get_status() != job_status:
return
if queue_timeout != "all" and not name.endswith(f":{queue_timeout}"):
return
def add_job(job: "Job", queue: str) -> None:
if job.kwargs.get("site") == frappe.local.site:
job_info = {
@ -34,7 +30,7 @@ def get_info(view=None, queue_timeout=None, job_status=None) -> List[Dict]:
or job.kwargs.get("kwargs", {}).get("job_type")
or str(job.kwargs.get("job_name")),
"status": job.get_status(),
"queue": name,
"queue": queue,
"creation": convert_utc_to_user_timezone(job.created_at),
"color": JOB_COLORS[job.get_status()],
}
@ -48,14 +44,21 @@ def get_info(view=None, queue_timeout=None, job_status=None) -> List[Dict]:
queues = get_queues()
for queue in queues:
for job in queue.jobs:
if job_status != "all" and job.get_status() != job_status:
return
if queue_timeout != "all" and not queue.name.endswith(f":{queue_timeout}"):
return
add_job(job, queue.name)
elif view == "Workers":
workers = get_workers()
for worker in workers:
current_job = worker.get_current_job()
if current_job and current_job.kwargs.get("site") == frappe.local.site:
add_job(current_job, job.origin)
if current_job:
if hasattr(current_job, "kwargs") and current_job.kwargs.get("site") == frappe.local.site:
add_job(current_job, current_job.origin)
else:
jobs.append({"queue": worker.name, "job_name": "busy", "status": "", "creation": ""})
else:
jobs.append({"queue": worker.name, "job_name": "idle", "status": "", "creation": ""})

View file

@ -596,6 +596,7 @@ docfield_properties = {
"in_preview": "Check",
"bold": "Check",
"no_copy": "Check",
"ignore_xss_filter": "Check",
"hidden": "Check",
"collapsible": "Check",
"collapsible_depends_on": "Data",

View file

@ -46,6 +46,7 @@
"report_hide",
"remember_last_selected_value",
"hide_border",
"ignore_xss_filter",
"property_depends_on_section",
"mandatory_depends_on",
"column_break_33",
@ -453,13 +454,20 @@
"hidden": 1,
"label": "Is System Generated",
"read_only": 1
},
{
"default": "0",
"description": "Don't encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field",
"fieldname": "ignore_xss_filter",
"fieldtype": "Check",
"label": "Ignore XSS Filter"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2022-03-31 12:05:11.799654",
"modified": "2022-04-13 22:31:14.162661",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form Field",

View file

@ -1066,7 +1066,7 @@ class Database(object):
now_datetime() - relativedelta(minutes=minutes),
)[0][0]
def get_db_table_columns(self, table):
def get_db_table_columns(self, table) -> List[str]:
"""Returns list of column names from given table."""
columns = frappe.cache().hget("table_columns", table)
if columns is None:
@ -1146,18 +1146,13 @@ class Database(object):
return frappe.db.is_missing_column(e)
def get_descendants(self, doctype, name):
"""Return descendants of the current record"""
node_location_indexes = self.get_value(doctype, name, ("lft", "rgt"))
if node_location_indexes:
lft, rgt = node_location_indexes
return self.sql_list(
"""select name from `tab{doctype}`
where lft > {lft} and rgt < {rgt}""".format(
doctype=doctype, lft=lft, rgt=rgt
)
)
else:
# when document does not exist
"""Return descendants of the group node in tree"""
from frappe.utils.nestedset import get_descendants_of
try:
return get_descendants_of(doctype, name, ignore_permissions=True)
except Exception:
# Can only happen if document doesn't exists - kept for backward compatibility
return []
def is_missing_table_or_column(self, e):
@ -1228,7 +1223,7 @@ class Database(object):
frappe.flags.touched_tables = set()
frappe.flags.touched_tables.update(tables)
def bulk_insert(self, doctype, fields, values, ignore_duplicates=False):
def bulk_insert(self, doctype, fields, values, ignore_duplicates=False, *, chunk_size=10_000):
"""
Insert multiple records at a time
@ -1236,22 +1231,20 @@ class Database(object):
:param fields: list of fields
:params values: list of list of values
"""
insert_list = []
fields = ", ".join("`" + field + "`" for field in fields)
values = list(values)
table = frappe.qb.DocType(doctype)
for idx, value in enumerate(values):
insert_list.append(tuple(value))
if idx and (idx % 10000 == 0 or idx < len(values) - 1):
self.sql(
"""INSERT {ignore_duplicates} INTO `tab{doctype}` ({fields}) VALUES {values}""".format(
ignore_duplicates="IGNORE" if ignore_duplicates else "",
doctype=doctype,
fields=fields,
values=", ".join(["%s"] * len(insert_list)),
),
tuple(insert_list),
)
insert_list = []
for start_index in range(0, len(values), chunk_size):
query = frappe.qb.into(table)
if ignore_duplicates:
# Pypika does not have same api for ignoring duplicates
if frappe.conf.db_type == "mariadb":
query = query.ignore()
elif frappe.conf.db_type == "postgres":
query = query.on_conflict().do_nothing()
values_to_insert = values[start_index : start_index + chunk_size]
query.columns(fields).insert(*values_to_insert).run()
def enqueue_jobs_after_commit():

View file

@ -338,7 +338,7 @@ def get_desktop_page(page):
"onboardings": workspace.onboardings,
}
except DoesNotExistError:
frappe.log_error(frappe.get_traceback())
frappe.log_error("Workspace Missing")
return {}
@ -472,7 +472,7 @@ def save_new_widget(doc, page, blocks, new_widgets):
""".format(
page, json_config, e
)
frappe.log_error(log, _("Could not save customization"))
doc.log_error("Could not save customization", log)
return False
return True

View file

@ -1,267 +1,124 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"actions": [],
"allow_rename": 1,
"autoname": "field:kanban_board_name",
"beta": 0,
"creation": "2016-10-19 12:26:04.809812",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"kanban_board_name",
"reference_doctype",
"field_name",
"column_break_4",
"private",
"show_labels",
"section_break_3",
"columns",
"filters",
"fields"
],
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "kanban_board_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Kanban Board Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 1
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "reference_doctype",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Reference Document Type",
"length": 0,
"no_copy": 0,
"options": "DocType",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"reqd": 1
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "field_name",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Field Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"reqd": 1
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_3",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"fieldtype": "Section Break"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "columns",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Columns",
"length": 0,
"no_copy": 0,
"options": "Kanban Board Column",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"options": "Kanban Board Column"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "filters",
"fieldtype": "Text",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"fieldtype": "Code",
"label": "Filters",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"options": "JSON",
"read_only": 1
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "private",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Private",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"read_only": 1
},
{
"fieldname": "fields",
"fieldtype": "Code",
"label": "Fields",
"options": "JSON",
"read_only": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "show_labels",
"fieldtype": "Check",
"label": "Show Labels",
"read_only": 1
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2019-09-05 14:22:27.664645",
"links": [],
"modified": "2022-04-13 12:10:20.284367",
"modified_by": "Administrator",
"module": "Desk",
"name": "Kanban Board",
"name_case": "",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"read": 1,
"role": "All"
},
{
"create": 1,
"delete": 1,
"if_owner": 1,
"read": 1,
"role": "All",
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"set_user_permissions": 0,
"role": "System Manager",
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"read_only": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 0,
"track_seen": 0
"states": [],
"track_changes": 1
}

View file

@ -24,7 +24,7 @@ class KanbanBoard(Document):
def validate_column_name(self):
for column in self.columns:
if not column.column_name:
frappe.msgprint(frappe._("Column Name cannot be empty"), raise_exception=True)
frappe.msgprint(_("Column Name cannot be empty"), raise_exception=True)
def get_permission_query_conditions(user):
@ -92,7 +92,6 @@ def update_order(board_name, order):
updated_cards = []
for col_name, cards in order_dict.items():
order_list = []
for card in cards:
column = frappe.get_value(doctype, {"name": card}, fieldname)
if column != col_name:
@ -246,3 +245,22 @@ def set_indicator(board_name, column_name, indicator):
board.save()
return board
@frappe.whitelist()
def save_settings(board_name: str, settings: str) -> Document:
settings = json.loads(settings)
doc = frappe.get_doc("Kanban Board", board_name)
fields = settings["fields"]
if not isinstance(fields, str):
fields = json.dumps(fields)
doc.fields = fields
doc.show_labels = settings["show_labels"]
doc.save()
resp = doc.as_dict()
resp["fields"] = frappe.parse_json(resp["fields"])
return resp

View file

@ -20,7 +20,7 @@ class NotificationLog(Document):
try:
send_notification_email(self)
except frappe.OutgoingEmailError:
frappe.log_error(message=frappe.get_traceback(), title=_("Failed to send notification email"))
self.log_error(_("Failed to send notification email"))
def get_permission_query_conditions(for_user):

View file

@ -1,7 +1,7 @@
{
"actions": [
{
"action": "app/console-log",
"action": "/app/console-log",
"action_type": "Route",
"label": "Logs"
},
@ -86,7 +86,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2022-04-09 16:35:32.345542",
"modified": "2022-04-15 14:15:58.398590",
"modified_by": "Administrator",
"module": "Desk",
"name": "System Console",
@ -106,4 +106,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View file

@ -80,7 +80,14 @@ class Workspace(Document):
# remove duplicate before adding
for idx, link in enumerate(self.links):
if link.label == card.get("label") and link.type == "Card Break":
if link.get("label") == card.get("label") and link.get("type") == "Card Break":
# count and set number of links for the card if link_count is 0
if link.link_count == 0:
for count, card_link in enumerate(self.links[idx + 1 :]):
if card_link.get("type") == "Card Break":
break
link.link_count = count + 1
del self.links[idx : idx + link.link_count + 1]
self.append(
@ -199,21 +206,31 @@ def update_page(name, title, icon, parent, public):
doc.sequence_id = frappe.db.count("Workspace", {"public": public}, cache=True)
doc.public = public
doc.for_user = "" if public else doc.for_user or frappe.session.user
doc.label = "{0}-{1}".format(title, doc.for_user) if doc.for_user else title
doc.label = new_name = "{0}-{1}".format(title, doc.for_user) if doc.for_user else title
doc.save(ignore_permissions=True)
if name != doc.label:
rename_doc("Workspace", name, doc.label, force=True, ignore_permissions=True)
if name != new_name:
rename_doc("Workspace", name, new_name, force=True, ignore_permissions=True)
# update new name and public in child pages
if child_docs:
for child in child_docs:
child_doc = frappe.get_doc("Workspace", child.name)
child_doc.parent_page = doc.title
child_doc.public = doc.public
if child_doc.public != public:
child_doc.public = public
child_doc.for_user = "" if public else child_doc.for_user or frappe.session.user
child_doc.label = new_child_name = (
"{0}-{1}".format(child_doc.title, child_doc.for_user)
if child_doc.for_user
else child_doc.title
)
child_doc.save(ignore_permissions=True)
return {"name": doc.title, "public": doc.public, "label": doc.label}
if child.name != new_child_name:
rename_doc("Workspace", child.name, new_child_name, force=True, ignore_permissions=True)
return {"name": title, "public": public, "label": new_name}
@frappe.whitelist()

View file

@ -314,7 +314,7 @@ def get_prepared_report_result(report, filters, dn="", user=None):
latest_report_data = {"columns": columns, "result": data}
except Exception:
frappe.log_error(frappe.get_traceback())
doc.log_error("Prepared report failed")
frappe.delete_doc("Prepared Report", doc.name)
frappe.db.commit()
doc = None

View file

@ -12,6 +12,7 @@ from frappe.model.document import Document
from frappe.model.naming import append_number_if_name_exists
from frappe.utils import (
add_to_date,
cint,
format_time,
get_link_to_form,
get_url_to_report,
@ -51,14 +52,18 @@ class AutoEmailReport(Document):
self.email_to = "\n".join(valid)
def validate_report_count(self):
"""check that there are only 3 enabled reports per user"""
count = frappe.db.sql(
"select count(*) from `tabAuto Email Report` where user=%s and enabled=1", self.user
)[0][0]
max_reports_per_user = frappe.local.conf.max_reports_per_user or 3
count = frappe.db.count("Auto Email Report", {"user": self.user, "enabled": 1})
max_reports_per_user = (
cint(frappe.local.conf.max_reports_per_user) # kept for backward compatibilty
or cint(frappe.db.get_single_value("System Settings", "max_auto_email_report_per_user"))
or 20
)
if count > max_reports_per_user + (-1 if self.flags.in_insert else 0):
frappe.throw(_("Only {0} emailed reports are allowed per user").format(max_reports_per_user))
msg = _("Only {0} emailed reports are allowed per user.").format(max_reports_per_user)
msg += " " + _("To allow more reports update limit in System Settings.")
frappe.throw(msg, title=_("Report limit reached"))
def validate_report_format(self):
"""check if user has select correct report format"""
@ -255,7 +260,9 @@ def send_daily():
try:
auto_email_report.send()
except Exception as e:
frappe.log_error(e, _("Failed to send {0} Auto Email Report").format(auto_email_report.name))
auto_email_report.log_error(
"Failed to send {0} Auto Email Report".format(auto_email_report.name)
)
def send_monthly():

View file

@ -473,7 +473,7 @@ class EmailAccount(Document):
frappe.db.rollback()
except Exception:
frappe.db.rollback()
frappe.log_error(title="EmailAccount.receive")
self.log_error(title="EmailAccount.receive")
if self.use_imap:
self.handle_bad_emails(mail.uid, mail.raw_message, frappe.get_traceback())
exceptions.append(frappe.get_traceback())
@ -521,7 +521,7 @@ class EmailAccount(Document):
# close connection to mailserver
email_server.logout()
except Exception:
frappe.log_error(title=_("Error while connecting to email account {0}").format(self.name))
self.log_error(title=_("Error while connecting to email account {0}").format(self.name))
return []
return mails
@ -667,7 +667,7 @@ class EmailAccount(Document):
try:
email_server = self.get_incoming_server(in_receive=True)
except Exception:
frappe.log_error(title=_("Error while connecting to email account {0}").format(self.name))
self.log_error("Email Connection Error")
if not email_server:
return
@ -679,7 +679,7 @@ class EmailAccount(Document):
message = safe_encode(message)
email_server.imap.append("Sent", "\\Seen", imaplib.Time2Internaldate(time.time()), message)
except Exception:
frappe.log_error(title="EmailAccount.append_email_to_sent_folder")
self.log_error("Unable to add to Sent folder")
@frappe.whitelist()

View file

@ -198,10 +198,7 @@ class SendMailContext:
traceback_string = "".join(traceback.format_tb(exc_tb))
traceback_string += f"\n Queue Name: {self.queue_doc.name}"
if self.is_background_task:
frappe.log_error(title="frappe.email.queue.flush", message=traceback_string)
else:
frappe.log_error(message=traceback_string)
self.queue_doc.log_error("Email sending failed", traceback_string)
@property
def smtp_session(self):
@ -625,11 +622,11 @@ class QueueBuilder:
mail_to_string = cstr(mail.as_string())
except frappe.InvalidEmailAddressError:
# bad Email Address - don't add to queue
frappe.log_error(
"Invalid Email ID Sender: {0}, Recipients: {1}, \nTraceback: {2} ".format(
self.log_error(
title="Invalid email address",
message="Invalid email address Sender: {0}, Recipients: {1}, \nTraceback: {2} ".format(
self.sender, ", ".join(self.final_recipients()), traceback.format_exc()
),
"Email Not Sent",
)
return

View file

@ -329,19 +329,17 @@ def send_scheduled_email():
pluck="name",
)
for newsletter in scheduled_newsletter:
for newsletter_name in scheduled_newsletter:
try:
frappe.get_doc("Newsletter", newsletter).queue_all()
newsletter = frappe.get_doc("Newsletter", newsletter_name)
newsletter.queue_all()
except Exception:
frappe.db.rollback()
# wasn't able to send emails :(
frappe.db.set_value("Newsletter", newsletter, "email_sent", 0)
message = (
f"Newsletter {newsletter} failed to send" "\n\n" f"Traceback: {frappe.get_traceback()}"
)
frappe.log_error(title="Send Newsletter", message=message)
frappe.db.set_value("Newsletter", newsletter_name, "email_sent", 0)
newsletter.log_error("Failed to send newsletter")
if not frappe.flags.in_test:
frappe.db.commit()

View file

@ -141,7 +141,7 @@ def get_context(context):
self.create_system_notification(doc, context)
except:
frappe.log_error(title="Failed to send notification", message=frappe.get_traceback())
self.log_error("Failed to send Notification")
if self.set_property_after_alert:
allow_update = True
@ -168,7 +168,7 @@ def get_context(context):
doc.save(ignore_permissions=True)
doc.flags.in_notification_update = False
except Exception:
frappe.log_error(title="Document update failed", message=frappe.get_traceback())
self.log_error("Document update failed")
def create_system_notification(self, doc, context):
subject = self.subject
@ -433,7 +433,7 @@ def evaluate_alert(doc, alert, event):
if event == "Value Change" and not doc.is_new():
if not frappe.db.has_column(doc.doctype, alert.value_changed):
alert.db_set("enabled", 0)
frappe.log_error("Notification {0} has been disabled due to missing field".format(alert.name))
alert.log_error("Notification {0} has been disabled due to missing field".format(alert.name))
return
doc_before_save = doc.get_doc_before_save()

View file

@ -170,7 +170,7 @@ def flush(from_test=False):
is_background_task = not from_test
func(email_queue_name=row.name, is_background_task=is_background_task)
except Exception:
frappe.log_error()
frappe.get_doc("Email Queue", row.name).log_error()
def get_queue():

View file

@ -123,7 +123,7 @@ class EmailServer:
except _socket.error:
# log performs rollback and logs error in Error Log
frappe.log_error("receive.connect_pop")
self.log_error("POP: Unable to connect")
# Invalid mail server -- due to refusing connection
frappe.msgprint(_("Invalid Mail Server. Please rectify and try again."))
@ -306,7 +306,7 @@ class EmailServer:
else:
# log performs rollback and logs error in Error Log
frappe.log_error("receive.get_messages", self.make_error_msg(msg_num, incoming_mail))
self.log_error("Unable to fetch email", self.make_error_msg(msg_num, incoming_mail))
self.errors = True
frappe.db.rollback()

View file

@ -213,5 +213,5 @@ def has_consumer_access(consumer, update_log):
else:
return frappe.safe_eval(condition, frappe._dict(doc=doc))
except Exception as e:
frappe.log_error(title="has_consumer_access error", message=e)
consumer.log_error("has_consumer_access error")
return False

View file

@ -226,7 +226,6 @@ scheduler_events = {
"frappe.sessions.clear_expired_sessions",
"frappe.email.doctype.notification.notification.trigger_daily_alerts",
"frappe.utils.scheduler.restrict_scheduler_events_if_dormant",
"frappe.email.doctype.auto_email_report.auto_email_report.send_daily",
"frappe.website.doctype.personal_data_deletion_request.personal_data_deletion_request.remove_unverified_record",
"frappe.desk.form.document_follow.send_daily_updates",
"frappe.social.doctype.energy_point_settings.energy_point_settings.allocate_review_points",
@ -241,6 +240,7 @@ scheduler_events = {
"frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_daily",
"frappe.utils.change_log.check_for_update",
"frappe.integrations.doctype.s3_backup_settings.s3_backup_settings.take_backups_daily",
"frappe.email.doctype.auto_email_report.auto_email_report.send_daily",
"frappe.integrations.doctype.google_drive.google_drive.daily_backup",
],
"weekly_long": [

View file

@ -105,7 +105,7 @@ def enqueue_webhook(doc, webhook):
if i != 2:
continue
else:
raise e
webhook.log_error("Webhook failed")
def log_request(url, headers, data, res):

View file

@ -159,13 +159,13 @@ class SiteMigration:
"""Run Migrate operation on site specified. This method initializes
and destroys connections to the site database.
"""
if not self.required_services_running():
raise SystemExit(1)
if site:
frappe.init(site=site)
frappe.connect()
if not self.required_services_running():
raise SystemExit(1)
self.setUp()
try:
self.pre_schema_updates()

View file

@ -40,6 +40,9 @@ data_fieldtypes = (
"JSON",
)
float_like_fields = {"Float", "Currency", "Percent"}
datetime_fields = {"Datetime", "Date", "Time"}
attachment_fieldtypes = (
"Attach",
"Attach Image",

View file

@ -2,10 +2,18 @@
# License: MIT. See LICENSE
import datetime
import json
from typing import Dict, List
import frappe
from frappe import _
from frappe.model import child_table_fields, default_fields, display_fieldtypes, table_fields
from frappe.model import (
child_table_fields,
datetime_fields,
default_fields,
display_fieldtypes,
float_like_fields,
table_fields,
)
from frappe.model.docstatus import DocStatus
from frappe.model.naming import set_new_name
from frappe.model.utils.link_count import notify_link_count
@ -252,10 +260,11 @@ class BaseDocument(object):
def get_valid_dict(
self, sanitize=True, convert_dates_to_str=False, ignore_nulls=False, ignore_virtual=False
):
) -> Dict:
d = frappe._dict()
for fieldname in self.meta.get_valid_columns():
d[fieldname] = self.get(fieldname)
# column is valid, we can use getattr
d[fieldname] = getattr(self, fieldname, None)
# if no need for sanitization and value is None, continue
if not sanitize and d[fieldname] is None:
@ -263,25 +272,24 @@ class BaseDocument(object):
df = self.meta.get_field(fieldname)
if df and df.get("is_virtual"):
if ignore_virtual:
del d[fieldname]
continue
if df:
if getattr(df, "is_virtual", False):
if ignore_virtual:
del d[fieldname]
continue
from frappe.utils.safe_exec import get_safe_globals
if d[fieldname] is None and (options := getattr(df, "options", None)):
from frappe.utils.safe_exec import get_safe_globals
if d[fieldname] is None:
if df.get("options"):
d[fieldname] = frappe.safe_eval(
code=df.get("options"),
code=options,
eval_globals=get_safe_globals(),
eval_locals={"doc": self},
)
else:
_val = getattr(self, fieldname, None)
if _val and not callable(_val):
d[fieldname] = _val
elif df:
if isinstance(d[fieldname], list) and df.fieldtype not in table_fields:
frappe.throw(_("Value for {0} cannot be a list").format(_(df.label)))
if df.fieldtype == "Check":
d[fieldname] = 1 if cint(d[fieldname]) else 0
@ -291,25 +299,20 @@ class BaseDocument(object):
elif df.fieldtype == "JSON" and isinstance(d[fieldname], dict):
d[fieldname] = json.dumps(d[fieldname], sort_keys=True, indent=4, separators=(",", ": "))
elif df.fieldtype in ("Currency", "Float", "Percent") and not isinstance(d[fieldname], float):
elif df.fieldtype in float_like_fields and not isinstance(d[fieldname], float):
d[fieldname] = flt(d[fieldname])
elif df.fieldtype in ("Datetime", "Date", "Time") and d[fieldname] == "":
elif (df.fieldtype in datetime_fields and d[fieldname] == "") or (
getattr(df, "unique", False) and cstr(d[fieldname]).strip() == ""
):
d[fieldname] = None
elif df.get("unique") and cstr(d[fieldname]).strip() == "":
# unique empty field should be set to None
d[fieldname] = None
if isinstance(d[fieldname], list) and df.fieldtype not in table_fields:
frappe.throw(_("Value for {0} cannot be a list").format(_(df.label)))
if convert_dates_to_str and isinstance(
d[fieldname], (datetime.datetime, datetime.date, datetime.time, datetime.timedelta)
):
d[fieldname] = str(d[fieldname])
if d[fieldname] is None and ignore_nulls:
if ignore_nulls and d[fieldname] is None:
del d[fieldname]
return d
@ -329,7 +332,7 @@ class BaseDocument(object):
if key not in self.__dict__:
self.__dict__[key] = None
def get_valid_columns(self):
def get_valid_columns(self) -> List[str]:
if self.doctype not in frappe.local.valid_columns:
if self.doctype in DOCTYPES_FOR_DOCTYPE:
from frappe.model.meta import get_table_columns
@ -342,7 +345,7 @@ class BaseDocument(object):
return frappe.local.valid_columns[self.doctype]
def is_new(self):
def is_new(self) -> bool:
return self.get("__islocal")
@property
@ -359,8 +362,8 @@ class BaseDocument(object):
no_default_fields=False,
convert_dates_to_str=False,
no_child_table_fields=False,
):
doc = self.get_valid_dict(convert_dates_to_str=convert_dates_to_str)
) -> Dict:
doc = self.get_valid_dict(convert_dates_to_str=convert_dates_to_str, ignore_nulls=no_nulls)
doc["doctype"] = self.doctype
for df in self.meta.get_table_fields():
@ -375,20 +378,15 @@ class BaseDocument(object):
for d in children
]
if no_nulls:
for k in list(doc):
if doc[k] is None:
del doc[k]
if no_default_fields:
for k in list(doc):
if k in default_fields:
del doc[k]
for key in default_fields:
if key in doc:
del doc[key]
if no_child_table_fields:
for k in list(doc):
if k in child_table_fields:
del doc[k]
for key in child_table_fields:
if key in doc:
del doc[key]
for key in (
"_user_tags",
@ -398,8 +396,8 @@ class BaseDocument(object):
"__run_link_triggers",
"__unsaved",
):
if self.get(key):
doc[key] = self.get(key)
if value := getattr(self, key, None):
doc[key] = value
return doc

View file

@ -1362,6 +1362,12 @@ class Document(BaseDocument):
).insert(ignore_permissions=True)
frappe.local.flags.commit = True
def log_error(self, title=None, message=None):
"""Helper function to create an Error Log"""
return frappe.log_error(
message=message, title=title, reference_doctype=self.doctype, reference_name=self.name
)
def get_signature(self):
"""Returns signature (hash) for private URL."""
return hashlib.sha224(get_datetime_str(self.creation).encode()).hexdigest()

View file

@ -6,7 +6,6 @@ from typing import TYPE_CHECKING, Optional, Union
import frappe
from frappe import _
from frappe.database.sequence import get_next_val, set_next_val
from frappe.model import log_types
from frappe.query_builder import DocType
from frappe.utils import cint, cstr, now_datetime
@ -36,6 +35,8 @@ def set_new_name(doc):
doc.name = None
if is_autoincremented(doc.doctype, meta):
from frappe.database.sequence import get_next_val
doc.name = get_next_val(doc.doctype)
return
@ -322,11 +323,14 @@ def get_default_naming_series(doctype):
def validate_name(doctype: str, name: Union[int, str], case: Optional[str] = None):
if not name:
frappe.throw(_("No Name Specified for {0}").format(doctype))
if isinstance(name, int):
if is_autoincremented(doctype):
from frappe.database.sequence import set_next_val
# this will set the sequence val to be the provided name and set it to be used
# so that the sequence will start from the next val of the setted val(name)
set_next_val(doctype, name, is_val_used=True)

View file

@ -254,8 +254,9 @@ def bulk_workflow_approval(docnames, doctype, action):
frappe.db.rollback()
frappe.log_error(
frappe.get_traceback(),
"Workflow {0} threw an error for {1} {2}".format(action, doctype, docname),
title="Workflow {0} threw an error for {1} {2}".format(action, doctype, docname),
reference_doctype="Workflow",
reference_name=action,
)
finally:
if not message_dict:

View file

@ -117,6 +117,7 @@ class ParallelTestRunner:
class ParallelTestResult(unittest.TextTestResult):
def startTest(self, test):
self.tb_locals = True
self._started_at = time.time()
super(unittest.TextTestResult, self).startTest(test)
test_class = unittest.util.strclass(test.__class__)

View file

@ -106,7 +106,6 @@ frappe.patches.v12_0.set_default_incoming_email_port
frappe.patches.v12_0.update_global_search
frappe.patches.v12_0.setup_tags
frappe.patches.v12_0.update_auto_repeat_status_and_not_submittable
frappe.patches.v12_0.copy_to_parent_for_tags
frappe.patches.v12_0.create_notification_settings_for_user
frappe.patches.v11_0.make_all_prepared_report_attachments_private #2019-11-26
frappe.patches.v12_0.setup_email_linking

View file

@ -1,7 +0,0 @@
import frappe
def execute():
frappe.db.sql("UPDATE `tabTag Link` SET parenttype=document_type")
frappe.db.sql("UPDATE `tabTag Link` SET parent=document_name")

View file

@ -375,7 +375,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
}
set_open_count() {
if (!this.data.transactions || !this.data.fieldname) {
if (!this.data || (!this.data.transactions || !this.data.fieldname)) {
return;
}

View file

@ -289,19 +289,23 @@ export default class GridRow {
var me = this;
if(this.doc && !this.grid.df.in_place_edit) {
// remove row
if(!this.open_form_button) {
this.open_form_button = $(`
<div class="btn-open-row">
<a>${frappe.utils.icon('edit', 'xs')}</a>
<div class="hidden-xs edit-grid-row">${ __("Edit") }</div>
</div>
`)
.appendTo($('<div class="col col-xs-1"></div>').appendTo(this.row))
.on('click', function() {
me.toggle_view(); return false;
});
if (!this.open_form_button) {
this.open_form_button = $('<div class="col col-xs-1"></div>').appendTo(this.row);
if(this.is_too_small()) {
if (!this.configure_columns) {
this.open_form_button = $(`
<div class="btn-open-row">
<a>${frappe.utils.icon('edit', 'xs')}</a>
<div class="hidden-xs edit-grid-row">${ __("Edit") }</div>
</div>
`)
.appendTo(this.open_form_button)
.on('click', function() {
me.toggle_view(); return false;
});
}
if (this.is_too_small()) {
// narrow
this.open_form_button.css({'margin-right': '-2px'});
}
@ -310,7 +314,9 @@ export default class GridRow {
}
add_column_configure_button() {
if (this.configure_columns) {
if (this.grid.df.in_place_edit && !this.frm) return;
if (this.configure_columns && this.frm) {
this.configure_columns_button = $(`
<div class="col grid-static-col col-xs-1 d-flex justify-content-center" style="cursor: pointer;">
<a>${frappe.utils.icon('setting-gear', 'sm', '', 'filter: opacity(0.5)')}</a>
@ -320,6 +326,10 @@ export default class GridRow {
.on('click', () => {
this.configure_dialog_for_columns_selector();
});
} else if (this.configure_columns && !this.frm) {
this.configure_columns_button = $(`
<div class="col grid-static-col col-xs-1"></div>
`).appendTo(this.row);
}
}
@ -595,6 +605,8 @@ export default class GridRow {
// to get update df for the row
let df = this.docfields.find(field => field.fieldname === col[0].fieldname);
this.set_dependant_property(df);
let colsize = col[1];
let txt = this.doc ?
frappe.format(this.doc[df.fieldname], df, null, this.doc) :
@ -633,6 +645,56 @@ export default class GridRow {
}
}
set_dependant_property(df) {
if (!df.reqd && df.mandatory_depends_on &&
this.evaluate_depends_on_value(df.mandatory_depends_on)) {
df.reqd = 1;
}
if (!df.read_only && df.read_only_depends_on &&
this.evaluate_depends_on_value(df.read_only_depends_on)) {
df.read_only = 1;
}
}
evaluate_depends_on_value(expression) {
let out = null;
let doc = this.doc;
if (!doc) return;
let parent = this.frm ? this.frm.doc : this.doc || null;
if (typeof (expression) === 'boolean') {
out = expression;
} else if (typeof (expression) === 'function') {
out = expression(doc);
} else if (expression.substr(0, 5)=='eval:') {
try {
out = frappe.utils.eval(expression.substr(5), { doc, parent });
if (parent && parent.istable && expression.includes('is_submittable')) {
out = true;
}
} catch (e) {
frappe.throw(__('Invalid "depends_on" expression'));
}
} else if (expression.substr(0, 3)=='fn:' && this.frm) {
out = this.frm.script_manager.trigger(expression.substr(3), this.doctype, this.docname);
} else {
var value = doc[expression];
if ($.isArray(value)) {
out = !!value.length;
} else {
out = !!value;
}
}
return out;
}
show_search_row() {
// show or remove search columns based on grid rows
this.show_search = this.frm && this.frm.doc &&

View file

@ -98,7 +98,7 @@ frappe.ui.form.Layout = class Layout {
// remove previous color
this.message.removeClass(this.message_color);
}
this.message_color = (color && ['yellow', 'blue', 'red'].includes(color)) ? color : 'blue';
this.message_color = (color && ['yellow', 'blue', 'red', 'green', 'orange'].includes(color)) ? color : 'blue';
if (html) {
if (html.substr(0, 1)!=='<') {
// wrap in a block
@ -439,7 +439,7 @@ frappe.ui.form.Layout = class Layout {
}
handle_tab(doctype, fieldname, shift) {
let grid_row = null,
let grid_row = null,
prev = null,
fields = this.fields_list,
focused = false;

View file

@ -130,7 +130,8 @@ frappe.ui.form.save = function (frm, action, callback, btn) {
folded = frm.layout.folded;
}
if (df.reqd && !frappe.model.has_value(doc.doctype, doc.name, df.fieldname)) {
if (is_docfield_mandatory(doc, df) &&
!frappe.model.has_value(doc.doctype, doc.name, df.fieldname)) {
has_errors = true;
error_fields[error_fields.length] = __(df.label);
// scroll to field
@ -173,6 +174,42 @@ frappe.ui.form.save = function (frm, action, callback, btn) {
return !has_errors;
};
let is_docfield_mandatory = function(doc, df) {
if (df.reqd) return true;
if (!df.mandatory_depends_on || !doc) return;
let out = null;
let expression = df.mandatory_depends_on;
let parent = frappe.get_meta(df.parent);
if (typeof (expression) === 'boolean') {
out = expression;
} else if (typeof (expression) === 'function') {
out = expression(doc);
} else if (expression.substr(0, 5) == 'eval:') {
try {
out = frappe.utils.eval(expression.substr(5), { doc, parent });
if (parent && parent.istable && expression.includes('is_submittable')) {
out = true;
}
} catch (e) {
frappe.throw(__('Invalid "mandatory_depends_on" expression'));
}
} else {
var value = doc[expression];
if ($.isArray(value)) {
out = !!value.length;
} else {
out = !!value;
}
}
return out;
};
const scroll_to = (fieldname) => {
frm.scroll_to_field(fieldname);
frm.scroll_set = true;

View file

@ -323,7 +323,7 @@ frappe.ui.form.Toolbar = class Toolbar {
}
// New
if(p[CREATE] && !this.frm.meta.issingle) {
if (p[CREATE] && !this.frm.meta.issingle && !this.frm.meta.in_create) {
this.page.add_menu_item(__("New {0}", [__(me.frm.doctype)]), function() {
frappe.new_doc(me.frm.doctype, true);
}, true, {

View file

@ -124,7 +124,7 @@ frappe.views.BaseList = class BaseList {
// df is passed
const df = fieldname;
fieldname = df.fieldname;
doctype = df.parent;
doctype = df.parent || doctype;
}
if (!this.fields) this.fields = [];

View file

@ -1580,15 +1580,22 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
}
if (frappe.user.has_role("System Manager")) {
items.push({
label: __("List Settings", null, "Button in list view menu"),
action: () => this.show_list_settings(),
standard: true,
});
if (this.get_view_settings) {
items.push(this.get_view_settings());
}
}
return items;
}
get_view_settings() {
return {
label: __("List Settings", null, "Button in list view menu"),
action: () => this.show_list_settings(),
standard: true,
};
}
show_list_settings() {
frappe.model.with_doctype(this.doctype, () => {
new ListSettings({

View file

@ -22,15 +22,17 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout {
super.make();
this.refresh();
// set default
$.each(this.fields_list, (_, field) => {
if (!is_null(field.df.default)) {
let def_value = field.df.default;
$.each(this.fields_list, function(i, field) {
if (field.df["default"]) {
let def_value = field.df["default"];
if (def_value === "Today" && field.df.fieldtype === "Date") {
if (def_value == 'Today' && field.df["fieldtype"] == 'Date') {
def_value = frappe.datetime.get_today();
}
this.set_value(field.df.fieldname, def_value);
field.set_input(def_value);
// if default and has depends_on, render its fields.
me.refresh_dependency();
}
})
@ -127,7 +129,6 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout {
if (f) {
f.set_value(val).then(() => {
f.set_input(val);
f.refresh();
this.refresh_dependency();
resolve();
});

View file

@ -630,8 +630,6 @@ frappe.provide("frappe.views");
if(!card) return;
make_dom();
render_card_meta();
add_task_link();
// edit_card_title();
}
function make_dom() {
@ -640,12 +638,35 @@ frappe.provide("frappe.views");
title: frappe.utils.html2text(card.title),
disable_click: card._disable_click ? 'disable-click' : '',
creation: card.creation,
doc_content: get_doc_content(card),
image_url: cur_list.get_image_url(card),
form_link: frappe.utils.get_form_link(card.doctype, card.name)
};
self.$card = $(frappe.render_template('kanban_card', opts))
.appendTo(wrapper);
}
function get_doc_content(card) {
let fields = [];
for (let field_name of cur_list.board.fields) {
let field = (
frappe.meta.get_docfield(card.doctype, field_name, card.name)
|| frappe.model.get_std_field(field_name)
);
let label = cur_list.board.show_labels ? `<span>${__(field.label)}: </span>` : '';
let value = frappe.format(card.doc[field_name], field);
fields.push(`
<div class="text-muted text-truncate">
${label}
<span>${value}</span>
</div>
`);
}
return fields.join("");
}
function get_tags_html(card) {
return card.tags
? `<div class="kanban-tags">
@ -688,12 +709,6 @@ frappe.provide("frappe.views");
.find('.kanban-assignments').append($assignees_group);
}
function add_task_link() {
let task_link = frappe.utils.get_form_link(card.doctype, card.name);
self.$card.find('.kanban-card-redirect')
.attr('href', task_link);
}
function get_assignees_group() {
return frappe.avatar_group(card.assigned_list, 3, {
css_class: 'avatar avatar-small',
@ -744,7 +759,7 @@ frappe.provide("frappe.views");
assigned_list: card.assigned_list || assigned_list,
comment_count: card.comment_count || comment_count,
color: card.color || null,
doc: doc
doc: doc || card
};
}

View file

@ -1,23 +1,24 @@
<div class="kanban-card-wrapper {{ disable_click }}" data-name="{{encodeURIComponent(name)}}">
<a class="kanban-card-redirect" href="#">
<div class="kanban-card content">
{% if(image_url) { %}
<div class="kanban-image">
<img src="{{image_url}}" alt="{{title}}">
</div>
{% } %}
<div class="kanban-card-body">
<div class="kanban-title-area">
<div class="kanban-card content">
{% if(image_url) { %}
<div class="kanban-image">
<img src="{{image_url}}" alt="{{title}}">
</div>
{% } %}
<div class="kanban-card-body">
<div class="kanban-title-area">
<a href="{{ form_link }}">
<div class="kanban-card-title ellipsis" title="{{title}}">
{{ title }}
</div>
<div class="kanban-card-creation">
{{ creation }}
</div>
</div>
<div class="kanban-card-meta">
</a>
<br>
<div class="kanban-card-doc text-muted">
{{ doc_content }}
</div>
</div>
<div class="kanban-card-meta">
</div>
</div>
</a>
</div>
</div>
</div>

View file

@ -0,0 +1,264 @@
export default class KanbanSettings {
constructor({ kanbanview, doctype, meta, settings }) {
if (!doctype) {
frappe.throw(__("DocType required"));
}
this.kanbanview = kanbanview;
this.doctype = doctype;
this.meta = meta;
this.settings = settings;
this.dialog = null;
this.fields = this.settings && this.settings.fields;
frappe.model.with_doctype("List View Settings", () => {
this.make();
this.get_fields();
this.setup_fields();
this.setup_remove_fields();
this.add_new_fields();
this.show_dialog();
});
}
make() {
this.dialog = new frappe.ui.Dialog({
title: __("{0} Settings", [__(this.doctype)]),
fields: [
{
fieldname: "show_labels",
label: __("Show Labels"),
fieldtype: "Check",
},
{
fieldname: "fields_html",
fieldtype: "HTML"
},
{
fieldname: "fields",
fieldtype: "Code",
hidden: 1
}
]
});
this.dialog.set_values(this.settings);
this.dialog.set_primary_action(__("Save"), () => {
frappe.show_alert({
message: __("Saving"),
indicator: "green"
});
frappe.call({
method:
"frappe.desk.doctype.kanban_board.kanban_board.save_settings",
args: {
board_name: this.settings.name,
settings: this.dialog.get_values()
},
callback: r => {
this.kanbanview.board = r.message;
this.kanbanview.render();
this.dialog.hide();
}
});
});
}
refresh() {
this.setup_fields();
this.add_new_fields();
this.setup_remove_fields();
}
show_dialog() {
if (!this.settings.fields) {
this.update_fields();
}
this.dialog.show();
}
setup_fields() {
const fields_html = this.dialog.get_field("fields_html");
const wrapper = fields_html.$wrapper[0];
let fields = "";
for (let fieldname of this.fields) {
let field = this.get_docfield(fieldname);
fields += `
<div class="control-input flex align-center form-control fields_order sortable"
style="display: block; margin-bottom: 5px;"
data-fieldname="${field.fieldname}"
data-label="${field.label}"
data-type="${field.type}">
<div class="row">
<div class="col-md-1">
${frappe.utils.icon("drag", "xs", "", "", "sortable-handle")}
</div>
<div class="col-md-10" style="padding-left:0px;">
${__(field.label)}
</div>
<div class="col-md-1">
<a class="text-muted remove-field" data-fieldname="${field.fieldname}">
${frappe.utils.icon("delete", "xs")}
</a>
</div>
</div>
</div>`;
}
fields_html.html(`
<div class="form-group">
<div class="clearfix">
<label class="control-label" style="padding-right: 0px;">Fields</label>
</div>
<div class="control-input-wrapper">
${fields}
</div>
<p class="help-box small text-muted">
<a class="add-new-fields text-muted">
+ Add / Remove Fields
</a>
</p>
</div>
`);
new Sortable(
wrapper.getElementsByClassName("control-input-wrapper")[0],
{
handle: ".sortable-handle",
draggable: ".sortable",
onUpdate: params => {
this.fields.splice(
params.newIndex,
0,
this.fields.splice(params.oldIndex, 1)[0]
);
this.dialog.set_value(
"fields",
JSON.stringify(this.fields)
);
this.refresh();
}
}
);
}
add_new_fields() {
let add_new_fields = this.get_dialog_fields_wrapper().getElementsByClassName(
"add-new-fields"
)[0];
add_new_fields.onclick = () => this.show_column_selector();
}
setup_remove_fields() {
let remove_fields = this.get_dialog_fields_wrapper().getElementsByClassName(
"remove-field"
);
for (let idx = 0; idx < remove_fields.length; idx++) {
remove_fields.item(idx).onclick = () =>
this.remove_fields(
remove_fields.item(idx).getAttribute("data-fieldname")
);
}
}
get_dialog_fields_wrapper() {
return this.dialog.get_field("fields_html").$wrapper[0];
}
remove_fields(fieldname) {
this.fields = this.fields.filter(field => field !== fieldname);
this.dialog.set_value("fields", JSON.stringify(this.fields));
this.refresh();
}
update_fields() {
const wrapper = this.dialog.get_field("fields_html").$wrapper[0];
let fields_order = wrapper.getElementsByClassName("fields_order");
this.fields = [];
for (let idx = 0; idx < fields_order.length; idx++) {
this.fields.push(
fields_order.item(idx).getAttribute("data-fieldname")
);
}
this.dialog.set_value("fields", JSON.stringify(this.fields));
}
show_column_selector() {
let dialog = new frappe.ui.Dialog({
title: __("{0} Fields", [__(this.doctype)]),
fields: [
{
label: __("Select Fields"),
fieldtype: "MultiCheck",
fieldname: "fields",
options: this.get_multiselect_fields(),
columns: 2
}
]
});
dialog.set_primary_action(__("Save"), () => {
this.fields = dialog.get_values().fields || [];
this.dialog.set_value("fields", JSON.stringify(this.fields));
this.refresh();
dialog.hide();
});
dialog.show();
}
get_fields() {
this.fields = this.settings.fields;
this.fields.uniqBy(f => f.fieldname);
}
get_docfield(field_name) {
return (
frappe.meta.get_docfield(this.doctype, field_name) ||
frappe.model.get_std_field(field_name)
);
}
get_multiselect_fields() {
const ignore_fields = [
"idx",
"lft",
"rgt",
"old_parent",
"_user_tags",
"_liked_by",
"_comments",
"_assign",
this.meta.title_field || "name"
];
const ignore_fieldtypes = [
"Attach Image",
"Text Editor",
"HTML Editor",
"Code",
"Color",
...frappe.model.no_value_type
];
return frappe.model.std_fields
.concat(this.kanbanview.get_fields_in_list_view())
.filter(
field =>
!ignore_fields.includes(field.fieldname) &&
!ignore_fieldtypes.includes(field.fieldtype)
)
.map(field => {
return {
label: __(field.label),
value: field.fieldname,
checked: this.fields.includes(field.fieldname)
};
});
}
}

View file

@ -1,3 +1,5 @@
import KanbanSettings from "./kanban_settings";
frappe.provide('frappe.views');
frappe.views.KanbanView = class KanbanView extends frappe.views.ListView {
@ -57,6 +59,7 @@ frappe.views.KanbanView = class KanbanView extends frappe.views.ListView {
.then(board => {
this.board = board;
this.board.filters_array = JSON.parse(this.board.filters || '[]');
this.board.fields = JSON.parse(this.board.fields || '[]');
this.filters = this.board.filters_array;
});
}
@ -187,6 +190,25 @@ frappe.views.KanbanView = class KanbanView extends frappe.views.ListView {
};
}
get_view_settings() {
return {
label: __("Kanban Settings", null, "Button in kanban view menu"),
action: () => this.show_kanban_settings(),
standard: true,
};
}
show_kanban_settings() {
frappe.model.with_doctype(this.doctype, () => {
new KanbanSettings({
kanbanview: this,
doctype: this.doctype,
settings: this.board,
meta: frappe.get_meta(this.doctype)
});
});
}
get required_libs() {
return [
'assets/frappe/js/lib/fluxify.min.js',

View file

@ -7,7 +7,7 @@ export default class Block {
make(block, block_name, widget_type = block) {
let block_data = this.config.page_data[block+'s'].items.find(obj => {
return frappe.utils.unescape_html(obj.label) == frappe.utils.unescape_html(block_name);
return frappe.utils.unescape_html(obj.label) == frappe.utils.unescape_html(__(block_name));
});
if (!block_data) return false;
this.wrapper.innerHTML = '';

View file

@ -31,7 +31,7 @@ export default class Card extends Block {
this.new('card', 'links');
if (this.data && this.data.card_name) {
let has_data = this.make('card', __(this.data.card_name), 'links');
let has_data = this.make('card', this.data.card_name, 'links');
if (!has_data) return;
}

View file

@ -32,7 +32,7 @@ export default class Chart extends Block {
this.new('chart');
if (this.data && this.data.chart_name) {
let has_data = this.make('chart', __(this.data.chart_name));
let has_data = this.make('chart', this.data.chart_name);
if (!has_data) return;
}

View file

@ -73,7 +73,7 @@ export default class Onboarding extends Block {
make(block, block_name) {
let block_data = this.config.page_data['onboardings'].items.find(obj => {
return obj.label == block_name;
return obj.label == __(block_name);
});
if (!block_data) return false;
this.wrapper.innerHTML = '';

View file

@ -51,7 +51,7 @@ export default class Shortcut extends Block {
this.new('shortcut');
if (this.data && this.data.shortcut_name) {
let has_data = this.make('shortcut', __(this.data.shortcut_name));
let has_data = this.make('shortcut', this.data.shortcut_name);
if (!has_data) return;
}

View file

@ -228,30 +228,35 @@ class CardDialog extends WidgetDialog {
}
process_data(data) {
data.links.map((item, idx) => {
let message = '';
let row = idx+1;
let message = '';
if (!item.link_type) {
message = "Following fields have missing values: <br><br><ul>";
message += `<li>Link Type in Row ${row}</li>`;
}
if (!data.links) {
message = "You must add atleast one link.";
} else {
data.links.map((item, idx) => {
let row = idx+1;
if (!item.link_to) {
message += `<li>Link To in Row ${row}</li>`;
}
if (!item.link_type) {
message = "Following fields have missing values: <br><br><ul>";
message += `<li>Link Type in Row ${row}</li>`;
}
if (message) {
message += "</ul>";
frappe.throw({
message: __(message),
title: __("Missing Values Required"),
indicator: 'orange'
});
}
if (!item.link_to) {
message += `<li>Link To in Row ${row}</li>`;
}
item.label = item.label ? item.label : item.link_to;
});
item.label = item.label ? item.label : item.link_to;
});
}
if (message) {
message += "</ul>";
frappe.throw({
message: __(message),
title: __("Missing Values Required"),
indicator: 'orange'
});
}
data.label = data.label ? data.label : data.chart_name;
return data;

View file

@ -343,11 +343,10 @@ textarea.form-control {
.duration-picker {
position: absolute;
z-index: 999;
border-radius: var(--border-radius);
box-shadow: var(--shadow-sm);
background: var(--popover-bg);
width: max-content;
&:after,
&:before {
border: solid transparent;
@ -466,4 +465,4 @@ button.data-pill {
top: 0;
right: 0;
cursor: pointer;
}
}

View file

@ -7,6 +7,7 @@
font-family: inherit;
}
/*rtl:begin:ignore*/
.ql-editor {
font-family: var(--font-stack);
color: var(--text-color);
@ -22,7 +23,15 @@
a[href] {
text-decoration: underline;
}
.ql-direction-rtl {
direction: rtl;
+ .table {
direction: ltr;
}
}
}
/*rtl:end:ignore*/
.ql-toolbar.ql-snow {
border-top-left-radius: var(--border-radius);
@ -70,6 +79,7 @@
min-height: 0;
max-height: none;
overflow: hidden;
resize: none;
}
}

View file

@ -139,6 +139,7 @@
}
.kanban-cards {
height: 100%;
max-height: calc(100vh - 250px);
margin: -5px;
padding: 5px;
@ -149,39 +150,120 @@
&::-webkit-scrollbar {
display: none;
}
}
.kanban-card {
@include flex(flex, space-between, null, column);
margin-top: var(--margin-sm);
min-height: 100px;
@include card(
$padding: 0,
$background-color: var(--kanban-card-bg)
);
.kanban-card-body {
padding: var(--padding-sm);
.kanban-card-wrapper {
position: relative;
display: block;
&:last-child .kanban-card {
margin-bottom: var(--margin-xl);
}
.kanban-card {
@include flex(flex, space-between, null, column);
margin-top: var(--margin-sm);
min-height: 100px;
@include card(
$padding: 0,
$background-color: var(--kanban-card-bg)
);
.kanban-image {
height: 125px;
img {
border-radius: var(--border-radius) var(--border-radius) 0 0;
object-position: top;
object-fit: cover;
margin: 0 auto;
height: 100%;
width: 100%;
min-width: 100%;
color: transparent;
position: relative;
}
@include broken-img(
$height: 125px,
$top: -4px,
)
}
.kanban-card-body {
cursor: grab;
padding: var(--padding-sm);
.kanban-title-area {
margin-bottom: 12px;
max-width: 90%;
font-size: var(--text-md);
font-weight: 500;
.kanban-card-doc {
.text-muted div {
display: inline;
}
}
.kanban-card-creation {
font-size: var(--text-md);
color: var(--text-muted);
margin-top: var(--margin-xs);
}
}
.kanban-card-meta {
.list-comment-count {
width: 30px;
}
.like-action:not(.liked) {
.icon use {
stroke: var(--text-muted);
}
}
.kanban-tags {
font-size: var(--text-sm);
margin-bottom: 8px;
.tag-pill {
border-radius: 100px;
height: 22px;
width: auto;
padding: 2px 8px;
margin-bottom: var(--margin-xs);
margin-right: var(--margin-xs);
}
}
.kanban-assignments {
display: flex;
float: right;
.avatar {
cursor: default;
width: 22px;
height: 22px;
}
.avatar-action {
width: 22px;
height: 22px;
.icon {
width: 12px;
height: 12px;
}
}
}
}
}
}
}
}
}
.kanban-card-wrapper {
position: relative;
.kanban-card-redirect {
display: block;
&:hover,
&:focus {
text-decoration: none;
}
}
&:last-child .kanban-card {
margin-bottom: var(--margin-xl);
}
}
.kanban-card:hover,
.new-card-area,
.edit-card-area {
@ -189,7 +271,6 @@
}
.kanban-card-wrapper:hover {
cursor: pointer;
text-decoration: none;
.kanban-card-edit {
@ -197,43 +278,6 @@
}
}
.kanban-title-area {
margin-bottom: 12px;
.kanban-card-title {
max-width: 90%;
font-size: var(--text-md);
font-weight: 500;
}
.kanban-card-creation {
font-size: var(--text-md);
color: var(--text-muted);
margin-top: var(--margin-xs);
}
}
.kanban-image {
height: 125px;
img {
border-radius: var(--border-radius) var(--border-radius) 0 0;
object-position: top;
object-fit: cover;
margin: 0 auto;
height: 100%;
width: 100%;
min-width: 100%;
color: transparent;
position: relative;
}
@include broken-img(
$height: 125px,
$top: -4px,
)
}
.kanban-card-edit {
position: absolute;
right: 10px;
@ -291,54 +335,6 @@
}
}
.kanban-card-meta {
.list-comment-count {
width: 30px;
}
.like-action:not(.liked) {
.icon use {
stroke: var(--text-muted);
}
}
.kanban-tags {
font-size: var(--text-sm);
margin-bottom: 8px;
.tag-pill {
border-radius: 100px;
height: 22px;
width: auto;
padding: 2px 8px;
margin-bottom: var(--margin-xs);
margin-right: var(--margin-xs);
}
}
.kanban-assignments {
display: flex;
float: right;
.avatar {
cursor: default;
width: 22px;
height: 22px;
}
.avatar-action {
width: 22px;
height: 22px;
.icon {
width: 12px;
height: 12px;
}
}
}
}
.kanban-empty-state {
width: 100%;
line-height: 400px;

View file

@ -30,6 +30,7 @@
.my-account-container {
max-width: 800px;
margin: auto;
margin-bottom: 4rem;
}
.account-info {

View file

@ -57,7 +57,7 @@ class EnergyPointRule(Document):
self.apply_only_once,
)
except Exception as e:
frappe.log_error(frappe.get_traceback(), "apply_energy_point")
self.log_error("Energy points failed")
def rule_condition_satisfied(self, doc):
if self.for_doc_event == "New":

View file

@ -15,6 +15,7 @@ import frappe
import frappe.utils.scheduler
from frappe.model.naming import revert_series_if_last
from frappe.modules import get_module_name, load_doctype_module
from frappe.utils import cint
unittest_runner = unittest.TextTestRunner
SLOW_TEST_THRESHOLD = 2
@ -177,10 +178,13 @@ def run_all_tests(
_add_test(app, path, filename, verbose, test_suite, ui_tests)
if junit_xml_output:
runner = unittest_runner(verbosity=1 + (verbose and 1 or 0), failfast=failfast)
runner = unittest_runner(verbosity=1 + cint(verbose), failfast=failfast)
else:
runner = unittest_runner(
resultclass=TimeLoggingTestResult, verbosity=1 + (verbose and 1 or 0), failfast=failfast
resultclass=TimeLoggingTestResult,
verbosity=1 + cint(verbose),
failfast=failfast,
tb_locals=verbose,
)
if profile:
@ -279,10 +283,13 @@ def _run_unittest(
test_suite.addTest(module_test_cases)
if junit_xml_output:
runner = unittest_runner(verbosity=1 + (verbose and 1 or 0), failfast=failfast)
runner = unittest_runner(verbosity=1 + cint(verbose), failfast=failfast)
else:
runner = unittest_runner(
resultclass=TimeLoggingTestResult, verbosity=1 + (verbose and 1 or 0), failfast=failfast
resultclass=TimeLoggingTestResult,
verbosity=1 + cint(verbose),
failfast=failfast,
tb_locals=verbose,
)
if profile:

View file

@ -141,3 +141,40 @@ class TestClient(unittest.TestCase):
self.assertEqual(get("ToDo", filters=filters_json).description, "test")
todo.delete()
def test_client_insert(self):
from frappe.client import insert
def get_random_title():
return "test-{0}".format(frappe.generate_hash())
# test insert dict
doc = {"doctype": "Note", "title": get_random_title(), "content": "test"}
note1 = insert(doc)
self.assertTrue(note1)
# test insert json
doc["title"] = get_random_title()
json_doc = frappe.as_json(doc)
note2 = insert(json_doc)
self.assertTrue(note2)
# test insert child doc without parent fields
child_doc = {"doctype": "Note Seen By", "user": "Administrator"}
with self.assertRaises(frappe.ValidationError):
insert(child_doc)
# test insert child doc with parent fields
child_doc = {
"doctype": "Note Seen By",
"user": "Administrator",
"parenttype": "Note",
"parent": note1.name,
"parentfield": "seen_by",
}
note3 = insert(child_doc)
self.assertTrue(note3)
# cleanup
frappe.delete_doc("Note", note1.name)
frappe.delete_doc("Note", note2.name)

View file

@ -4,6 +4,7 @@
import datetime
import inspect
import unittest
from math import ceil
from random import choice
from unittest.mock import patch
@ -445,6 +446,33 @@ class TestDB(unittest.TestCase):
self.assertEqual(frappe.db.exists(dt, [["name", "=", dn]]), dn)
def test_bulk_insert(self):
current_count = frappe.db.count("ToDo")
test_body = f"test_bulk_insert - {random_string(10)}"
chunk_size = 10
for number_of_values in (1, 2, 5, 27):
current_transaction_writes = frappe.db.transaction_writes
frappe.db.bulk_insert(
"ToDo",
["name", "description"],
[[f"ToDo Test Bulk Insert {i}", test_body] for i in range(number_of_values)],
ignore_duplicates=True,
chunk_size=chunk_size,
)
# check that all records were inserted
self.assertEqual(number_of_values, frappe.db.count("ToDo") - current_count)
# check if inserts were done in chunks
expected_number_of_writes = ceil(number_of_values / chunk_size)
self.assertEqual(
expected_number_of_writes, frappe.db.transaction_writes - current_transaction_writes
)
frappe.db.delete("ToDo", {"description": test_body})
@run_only_if(db_type_is.MARIADB)
class TestDDLCommandsMaria(unittest.TestCase):

View file

@ -137,12 +137,10 @@ def create_contact_records():
@frappe.whitelist()
def create_multiple_todo_records():
values = []
if frappe.db.get_all("ToDo", {"description": "Multiple ToDo 1"}):
return
for index in range(1, 1002):
values.append(("100{}".format(index), "Multiple ToDo {}".format(index)))
values = [("100{}".format(i), "Multiple ToDo {}".format(i)) for i in range(1, 1002)]
frappe.db.bulk_insert("ToDo", fields=["name", "description"], values=set(values))

View file

@ -18,6 +18,7 @@ from typing import Generator, Iterable
from urllib.parse import quote, urlparse
from redis.exceptions import ConnectionError
from traceback_with_variables import iter_exc_lines
from werkzeug.test import Client
import frappe
@ -255,7 +256,7 @@ def get_gravatar(email):
return gravatar_url
def get_traceback() -> str:
def get_traceback(with_context=False) -> str:
"""
Returns the traceback of the Exception
"""
@ -264,14 +265,19 @@ def get_traceback() -> str:
if not any([exc_type, exc_value, exc_tb]):
return ""
trace_list = traceback.format_exception(exc_type, exc_value, exc_tb)
bench_path = get_bench_path() + "/"
if with_context:
trace_list = iter_exc_lines()
tb = "\n".join(trace_list)
else:
trace_list = traceback.format_exception(exc_type, exc_value, exc_tb)
tb = "".join(cstr(t) for t in trace_list)
return "".join(cstr(t) for t in trace_list).replace(bench_path, "")
bench_path = get_bench_path() + "/"
return tb.replace(bench_path, "")
def log(event, details):
frappe.logger().info(details)
frappe.logger(event).info(details)
def dict_to_str(args, sep="&"):

View file

@ -92,7 +92,12 @@ def download_multi_pdf(doctype, name, format=None, no_letterhead=False, options=
pdf_options=options,
)
except Exception:
frappe.log_error("Permission Error on doc {} of doctype {}".format(doc_name, doctype_name))
frappe.log_error(
title="Error in Multi PDF download",
message="Permission Error on doc {} of doctype {}".format(doc_name, doctype_name),
reference_doctype=doctype_name,
reference_name=doc_name,
)
frappe.local.response.filename = "{}.pdf".format(name)
frappe.local.response.filecontent = read_multi_pdf(output)

View file

@ -15,6 +15,9 @@ import frappe.utils.data
from frappe import _
from frappe.frappeclient import FrappeClient
from frappe.handler import execute_cmd
from frappe.model.delete_doc import delete_doc
from frappe.model.mapper import get_mapped_doc
from frappe.model.rename_doc import rename_doc
from frappe.modules import scrub
from frappe.utils.background_jobs import enqueue, get_jobs
from frappe.website.utils import get_next_link, get_shade, get_toc
@ -110,12 +113,16 @@ def get_safe_globals():
errprint=frappe.errprint,
qb=frappe.qb,
get_meta=frappe.get_meta,
new_doc=frappe.new_doc,
get_doc=frappe.get_doc,
get_mapped_doc=get_mapped_doc,
get_last_doc=frappe.get_last_doc,
get_cached_doc=frappe.get_cached_doc,
get_list=frappe.get_list,
get_all=frappe.get_all,
get_system_settings=frappe.get_system_settings,
rename_doc=frappe.rename_doc,
rename_doc=rename_doc,
delete_doc=delete_doc,
utils=datautils,
get_url=frappe.utils.get_url,
render_template=frappe.render_template,

View file

@ -70,6 +70,8 @@ data-web-form="{{ name }}" data-web-form-doctype="{{ doc_type }}" data-login-req
<div class="comments" style="margin-top: 3rem;">
{% include 'templates/includes/comments/comments.html' %}
</div>
{%- else -%}
<div style="height: 3rem"></div>
{%- endif %} {# comments #}
{% endblock page_content %}

View file

@ -20,6 +20,7 @@ def get_response(path=None, http_status_code=200):
except frappe.PermissionError as e:
response = NotPermittedPage(endpoint, http_status_code, exception=e).render()
except Exception as e:
frappe.log_error(f"{path} failed")
response = ErrorPage(exception=e).render()
return response

View file

@ -63,10 +63,11 @@ semantic-version~=2.8.5
sqlparse~=0.4.1
stripe~=2.56.0
terminaltables~=3.1.0
traceback-with-variables~=2.0.4
urllib3~=1.26.4
Werkzeug~=2.0.3
Whoosh~=2.7.4
wrapt~=1.12.1
wrapt~=1.14.0
xlrd~=2.0.1
zxcvbn-python~=4.4.24
tenacity~=8.0.1