Merge branch 'develop' of github.com:frappe/frappe into refactor-file

This commit is contained in:
Gavin D'souza 2022-04-21 11:45:50 +05:30
commit 1d84483289
55 changed files with 506 additions and 274 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

@ -10,6 +10,7 @@ context('Kanban Board', () => {
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');

View file

@ -241,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;
});
@ -258,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()

View file

@ -218,7 +218,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
@ -2077,25 +2076,36 @@ 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
)
if not traceback:
traceback = get_traceback()
if not title:
title = "Error"
return get_doc(
dict(
doctype="Error Log",
error=as_unicode(traceback),
method=title,
reference_doctype=reference_doctype,
reference_name=reference_name,
)
).insert(ignore_permissions=True)
def get_desk_link(doctype, name):
@ -2148,9 +2158,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

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

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

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

@ -1226,7 +1226,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
@ -1234,22 +1234,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

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

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

@ -255,7 +255,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

@ -124,7 +124,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."))
@ -307,7 +307,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

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

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

@ -595,6 +595,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 +635,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

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

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

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

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

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

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

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