Merge branch 'develop' into comm-send-after
This commit is contained in:
commit
ded94bf050
17 changed files with 292 additions and 130 deletions
|
|
@ -2,7 +2,11 @@ context("Awesome Bar", () => {
|
|||
before(() => {
|
||||
cy.visit("/login");
|
||||
cy.login();
|
||||
cy.visit("/app/website");
|
||||
cy.visit("/app/todo"); // Make sure ToDo filters are cleared.
|
||||
cy.clear_filters();
|
||||
cy.visit("/app/blog-post"); // Make sure Blog Post filters are cleared.
|
||||
cy.clear_filters();
|
||||
cy.visit("/app/website"); // Go to some other page.
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
@ -11,36 +15,61 @@ context("Awesome Bar", () => {
|
|||
cy.get("@awesome_bar").type("{selectall}");
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cy.visit("/app/todo"); // Make sure we're not bleeding any filters to the next spec.
|
||||
cy.clear_filters();
|
||||
});
|
||||
|
||||
it("navigates to doctype list", () => {
|
||||
cy.get("@awesome_bar").type("todo");
|
||||
cy.wait(100);
|
||||
cy.wait(100); // Wait a bit before hitting enter.
|
||||
cy.get(".awesomplete").findByRole("listbox").should("be.visible");
|
||||
cy.get("@awesome_bar").type("{enter}");
|
||||
cy.get(".title-text").should("contain", "To Do");
|
||||
cy.location("pathname").should("eq", "/app/todo");
|
||||
});
|
||||
|
||||
it("find text in doctype list", () => {
|
||||
it("finds text in doctype list", () => {
|
||||
cy.get("@awesome_bar").type("test in todo");
|
||||
cy.wait(100);
|
||||
cy.wait(150); // Wait a bit before hitting enter.
|
||||
cy.get("@awesome_bar").type("{enter}");
|
||||
cy.get(".title-text").should("contain", "To Do");
|
||||
cy.wait(200);
|
||||
const name_filter = cy.get('[data-original-title="ID"] > input');
|
||||
name_filter.should("have.value", "%test%");
|
||||
cy.clear_filters();
|
||||
cy.wait(200); // Wait a bit longer before checking the filter.
|
||||
cy.get('[data-original-title="ID"] > input').should("have.value", "%test%");
|
||||
});
|
||||
|
||||
it("filter preserved, now finds something else", () => {
|
||||
cy.visit("/app/todo");
|
||||
cy.get(".title-text").should("contain", "To Do");
|
||||
cy.wait(200); // Wait a bit longer before checking the filter.
|
||||
cy.get('[data-original-title="ID"] > input').as("filter");
|
||||
cy.get("@filter").should("have.value", "%test%");
|
||||
cy.get("@awesome_bar").type("anothertest in todo");
|
||||
cy.wait(200); // Wait a bit longer before hitting enter.
|
||||
cy.get("@awesome_bar").type("{enter}");
|
||||
cy.wait(200); // Wait a bit longer before checking the filter.
|
||||
cy.get("@filter").should("have.value", "%anothertest%");
|
||||
});
|
||||
|
||||
it("navigates to another doctype, filter not bleeding", () => {
|
||||
cy.get("@awesome_bar").type("blog post");
|
||||
cy.wait(150); // Wait a bit before hitting enter.
|
||||
cy.get("@awesome_bar").type("{enter}");
|
||||
cy.get(".title-text").should("contain", "Blog Post");
|
||||
cy.wait(200); // Wait a bit longer before checking the filter.
|
||||
cy.location("search").should("be.empty");
|
||||
});
|
||||
|
||||
it("navigates to new form", () => {
|
||||
cy.get("@awesome_bar").type("new blog post");
|
||||
cy.wait(100);
|
||||
cy.wait(150); // Wait a bit before hitting enter
|
||||
cy.get("@awesome_bar").type("{enter}");
|
||||
cy.get(".title-text:visible").should("have.text", "New Blog Post");
|
||||
});
|
||||
|
||||
it("calculates math expressions", () => {
|
||||
cy.get("@awesome_bar").type("55 + 32");
|
||||
cy.wait(100);
|
||||
cy.wait(150); // Wait a bit before hitting enter
|
||||
cy.get("@awesome_bar").type("{downarrow}{enter}");
|
||||
cy.get(".modal-title").should("contain", "Result");
|
||||
cy.get(".msgprint").should("contain", "55 + 32 = 87");
|
||||
|
|
|
|||
|
|
@ -202,7 +202,7 @@ def start_scheduler():
|
|||
def start_worker(
|
||||
queue, quiet=False, rq_username=None, rq_password=None, burst=False, strategy=None
|
||||
):
|
||||
"""Start a backgrond worker"""
|
||||
"""Start a background worker"""
|
||||
from frappe.utils.background_jobs import start_worker
|
||||
|
||||
start_worker(
|
||||
|
|
@ -225,7 +225,7 @@ def start_worker(
|
|||
@click.option("--quiet", is_flag=True, default=False, help="Hide Log Outputs")
|
||||
@click.option("--burst", is_flag=True, default=False, help="Run Worker in Burst mode.")
|
||||
def start_worker_pool(queue, quiet=False, num_workers=2, burst=False):
|
||||
"""Start a backgrond worker"""
|
||||
"""Start a pool of background workers"""
|
||||
from frappe.utils.background_jobs import start_worker_pool
|
||||
|
||||
start_worker_pool(
|
||||
|
|
|
|||
|
|
@ -23,38 +23,23 @@
|
|||
"float_precision",
|
||||
"currency_precision",
|
||||
"rounding_method",
|
||||
"sec_backup_limit",
|
||||
"backup_limit",
|
||||
"encrypt_backup",
|
||||
"background_workers",
|
||||
"enable_scheduler",
|
||||
"dormant_days",
|
||||
"permissions",
|
||||
"apply_strict_user_permissions",
|
||||
"column_break_21",
|
||||
"allow_guests_to_upload_files",
|
||||
"force_web_capture_mode_for_uploads",
|
||||
"allow_older_web_view_links",
|
||||
"security_tab",
|
||||
"security",
|
||||
"session_expiry",
|
||||
"document_share_key_expiry",
|
||||
"column_break_13",
|
||||
"column_break_txqh",
|
||||
"deny_multiple_sessions",
|
||||
"disable_user_pass_login",
|
||||
"login_methods_section",
|
||||
"allow_login_using_mobile_number",
|
||||
"allow_login_using_user_name",
|
||||
"disable_user_pass_login",
|
||||
"column_break_uhqk",
|
||||
"login_with_email_link",
|
||||
"login_with_email_link_expiry",
|
||||
"allow_error_traceback",
|
||||
"strip_exif_metadata_from_uploaded_images",
|
||||
"allow_older_web_view_links",
|
||||
"password_settings",
|
||||
"logout_on_password_reset",
|
||||
"force_user_to_reset_password",
|
||||
"reset_password_link_expiry_duration",
|
||||
"password_reset_limit",
|
||||
"column_break_31",
|
||||
"enable_password_policy",
|
||||
"minimum_password_score",
|
||||
"brute_force_security",
|
||||
"allow_consecutive_login_attempts",
|
||||
"column_break_34",
|
||||
|
|
@ -66,6 +51,16 @@
|
|||
"two_factor_method",
|
||||
"lifespan_qrcode_image",
|
||||
"otp_issuer_name",
|
||||
"password_tab",
|
||||
"password_settings",
|
||||
"logout_on_password_reset",
|
||||
"force_user_to_reset_password",
|
||||
"reset_password_link_expiry_duration",
|
||||
"password_reset_limit",
|
||||
"column_break_31",
|
||||
"enable_password_policy",
|
||||
"minimum_password_score",
|
||||
"email_tab",
|
||||
"email",
|
||||
"email_footer_address",
|
||||
"email_retry_limit",
|
||||
|
|
@ -75,17 +70,31 @@
|
|||
"attach_view_link",
|
||||
"welcome_email_template",
|
||||
"reset_password_template",
|
||||
"prepared_report_section",
|
||||
"max_auto_email_report_per_user",
|
||||
"files_tab",
|
||||
"files_section",
|
||||
"max_file_size",
|
||||
"allow_guests_to_upload_files",
|
||||
"force_web_capture_mode_for_uploads",
|
||||
"strip_exif_metadata_from_uploaded_images",
|
||||
"column_break_uqma",
|
||||
"allowed_file_extensions",
|
||||
"updates_tab",
|
||||
"system_updates_section",
|
||||
"disable_system_update_notification",
|
||||
"disable_change_log_notification",
|
||||
"backups_tab",
|
||||
"sec_backup_limit",
|
||||
"backup_limit",
|
||||
"encrypt_backup",
|
||||
"advanced_tab",
|
||||
"prepared_report_section",
|
||||
"max_auto_email_report_per_user",
|
||||
"background_workers",
|
||||
"enable_scheduler",
|
||||
"dormant_days",
|
||||
"telemetry_section",
|
||||
"enable_telemetry",
|
||||
"files_section",
|
||||
"max_file_size",
|
||||
"column_break_uqma",
|
||||
"allowed_file_extensions"
|
||||
"allow_error_traceback",
|
||||
"enable_telemetry"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -126,7 +135,6 @@
|
|||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "date_and_number_format",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Date and Number Format"
|
||||
|
|
@ -171,10 +179,8 @@
|
|||
"options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "sec_backup_limit",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Backups"
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"default": "3",
|
||||
|
|
@ -184,7 +190,6 @@
|
|||
"label": "Number of Backups"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "background_workers",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Background Workers"
|
||||
|
|
@ -198,7 +203,6 @@
|
|||
"label": "Enable Scheduled Jobs"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "permissions",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Permissions"
|
||||
|
|
@ -211,10 +215,8 @@
|
|||
"label": "Apply Strict User Permissions"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "security",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Security"
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"default": "170:00",
|
||||
|
|
@ -223,10 +225,6 @@
|
|||
"fieldtype": "Data",
|
||||
"label": "Session Expiry (idle timeout)"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_13",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Note: Multiple sessions will be allowed in case of mobile device",
|
||||
|
|
@ -255,7 +253,6 @@
|
|||
"label": "Show Full Error and Allow Reporting of Issues to the Developer"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "password_settings",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Password"
|
||||
|
|
@ -286,7 +283,6 @@
|
|||
"options": "2\n3\n4"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "brute_force_security",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Brute Force Security"
|
||||
|
|
@ -309,7 +305,6 @@
|
|||
"label": "Allow Login After Fail"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "two_factor_authentication",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Two Factor Authentication"
|
||||
|
|
@ -338,6 +333,7 @@
|
|||
},
|
||||
{
|
||||
"default": "OTP App",
|
||||
"depends_on": "enable_two_factor_auth",
|
||||
"description": "Choose authentication method to be used by all users",
|
||||
"fieldname": "two_factor_method",
|
||||
"fieldtype": "Select",
|
||||
|
|
@ -345,7 +341,7 @@
|
|||
"options": "OTP App\nSMS\nEmail"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.two_factor_method == \"OTP App\"",
|
||||
"depends_on": "eval:doc.enable_two_factor_auth && doc.two_factor_method == \"OTP App\"",
|
||||
"description": "Time in seconds to retain QR code image on server. Min:<strong>240</strong>",
|
||||
"fieldname": "lifespan_qrcode_image",
|
||||
"fieldtype": "Int",
|
||||
|
|
@ -359,10 +355,8 @@
|
|||
"label": "OTP Issuer Name"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "email",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Email"
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"description": "Your organization name and address for the email footer.",
|
||||
|
|
@ -430,7 +424,6 @@
|
|||
"label": "Include Web View Link in Email"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "prepared_report_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Reports"
|
||||
|
|
@ -456,10 +449,8 @@
|
|||
"label": "Encrypt Backups"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "system_updates_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "System Updates"
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
|
|
@ -547,7 +538,6 @@
|
|||
"label": "Disable Document Sharing"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "telemetry_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Telemetry"
|
||||
|
|
@ -578,10 +568,8 @@
|
|||
"label": "Force Web Capture Mode for Uploads"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "files_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Files"
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "max_file_size",
|
||||
|
|
@ -598,12 +586,60 @@
|
|||
"fieldname": "allowed_file_extensions",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Allowed File Extensions"
|
||||
},
|
||||
{
|
||||
"fieldname": "security_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Login"
|
||||
},
|
||||
{
|
||||
"fieldname": "email_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Email"
|
||||
},
|
||||
{
|
||||
"fieldname": "files_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Files"
|
||||
},
|
||||
{
|
||||
"fieldname": "updates_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Updates"
|
||||
},
|
||||
{
|
||||
"fieldname": "backups_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Backups"
|
||||
},
|
||||
{
|
||||
"fieldname": "advanced_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Advanced"
|
||||
},
|
||||
{
|
||||
"fieldname": "password_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Password"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_txqh",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "login_methods_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Login Methods"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_uhqk",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-10-17 16:12:28.145496",
|
||||
"modified": "2023-11-27 14:08:01.927794",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "System Settings",
|
||||
|
|
|
|||
|
|
@ -93,7 +93,6 @@ class SystemSettings(Document):
|
|||
time_zone: DF.Literal
|
||||
two_factor_method: DF.Literal["OTP App", "SMS", "Email"]
|
||||
welcome_email_template: DF.Link | None
|
||||
|
||||
# end: auto-generated types
|
||||
def validate(self):
|
||||
from frappe.twofactor import toggle_two_factor_auth
|
||||
|
|
|
|||
|
|
@ -1227,27 +1227,31 @@ def create_contact(user, ignore_links=False, ignore_mandatory=False):
|
|||
|
||||
contact_name = get_contact_name(user.email)
|
||||
if not contact_name:
|
||||
contact = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Contact",
|
||||
"first_name": user.first_name,
|
||||
"last_name": user.last_name,
|
||||
"user": user.name,
|
||||
"gender": user.gender,
|
||||
}
|
||||
)
|
||||
try:
|
||||
contact = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Contact",
|
||||
"first_name": user.first_name,
|
||||
"last_name": user.last_name,
|
||||
"user": user.name,
|
||||
"gender": user.gender,
|
||||
}
|
||||
)
|
||||
|
||||
if user.email:
|
||||
contact.add_email(user.email, is_primary=True)
|
||||
if user.email:
|
||||
contact.add_email(user.email, is_primary=True)
|
||||
|
||||
if user.phone:
|
||||
contact.add_phone(user.phone, is_primary_phone=True)
|
||||
if user.phone:
|
||||
contact.add_phone(user.phone, is_primary_phone=True)
|
||||
|
||||
if user.mobile_no:
|
||||
contact.add_phone(user.mobile_no, is_primary_mobile_no=True)
|
||||
contact.insert(
|
||||
ignore_permissions=True, ignore_links=ignore_links, ignore_mandatory=ignore_mandatory
|
||||
)
|
||||
if user.mobile_no:
|
||||
contact.add_phone(user.mobile_no, is_primary_mobile_no=True)
|
||||
|
||||
contact.insert(
|
||||
ignore_permissions=True, ignore_links=ignore_links, ignore_mandatory=ignore_mandatory
|
||||
)
|
||||
except frappe.DuplicateEntryError:
|
||||
pass
|
||||
else:
|
||||
contact = frappe.get_doc("Contact", contact_name)
|
||||
contact.first_name = user.first_name
|
||||
|
|
|
|||
|
|
@ -46,8 +46,29 @@ class BulkUpdate(Document):
|
|||
|
||||
@frappe.whitelist()
|
||||
def submit_cancel_or_update_docs(doctype, docnames, action="submit", data=None):
|
||||
docnames = frappe.parse_json(docnames)
|
||||
if isinstance(docnames, str):
|
||||
docnames = frappe.parse_json(docnames)
|
||||
|
||||
if len(docnames) < 20:
|
||||
return _bulk_action(doctype, docnames, action, data)
|
||||
elif len(docnames) <= 500:
|
||||
frappe.msgprint(_("Bulk operation is enqueued in background."), alert=True)
|
||||
frappe.enqueue(
|
||||
_bulk_action,
|
||||
doctype=doctype,
|
||||
docnames=docnames,
|
||||
action=action,
|
||||
data=data,
|
||||
queue="short",
|
||||
timeout=1000,
|
||||
)
|
||||
else:
|
||||
frappe.throw(
|
||||
_("Bulk operations only support up to 500 documents."), title=_("Too Many Documents")
|
||||
)
|
||||
|
||||
|
||||
def _bulk_action(doctype, docnames, action, data):
|
||||
if data:
|
||||
data = frappe.parse_json(data)
|
||||
|
||||
|
|
@ -85,5 +106,4 @@ def submit_cancel_or_update_docs(doctype, docnames, action="submit", data=None):
|
|||
|
||||
def show_progress(docnames, message, i, description):
|
||||
n = len(docnames)
|
||||
if n >= 10:
|
||||
frappe.publish_progress(float(i) * 100 / n, title=message, description=description)
|
||||
frappe.publish_progress(float(i) * 100 / n, title=message, description=description)
|
||||
|
|
|
|||
48
frappe/desk/doctype/bulk_update/test_bulk_update.py
Normal file
48
frappe/desk/doctype/bulk_update/test_bulk_update.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# Copyright (c) 2023, Frappe Technologies and Contributors
|
||||
# See LICENSE
|
||||
|
||||
import time
|
||||
|
||||
import frappe
|
||||
from frappe.core.doctype.doctype.test_doctype import new_doctype
|
||||
from frappe.desk.doctype.bulk_update.bulk_update import submit_cancel_or_update_docs
|
||||
from frappe.tests.utils import FrappeTestCase, timeout
|
||||
|
||||
|
||||
class TestBulkUpdate(FrappeTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
super().setUpClass()
|
||||
cls.doctype = new_doctype(is_submittable=1, custom=1).insert().name
|
||||
frappe.db.commit()
|
||||
for _ in range(50):
|
||||
frappe.new_doc(cls.doctype, some_fieldname=frappe.mock("name")).insert()
|
||||
|
||||
@timeout()
|
||||
def wait_for_assertion(self, assertion):
|
||||
"""Wait till an assertion becomes True"""
|
||||
while True:
|
||||
if assertion():
|
||||
break
|
||||
time.sleep(0.2)
|
||||
|
||||
def test_bulk_submit_in_background(self):
|
||||
unsubmitted = frappe.get_all(self.doctype, {"docstatus": 0}, limit=5, pluck="name")
|
||||
failed = submit_cancel_or_update_docs(self.doctype, unsubmitted, action="submit")
|
||||
self.assertEqual(failed, [])
|
||||
|
||||
def check_docstatus(docs, status):
|
||||
frappe.db.rollback()
|
||||
matching_docs = frappe.get_all(
|
||||
self.doctype, {"docstatus": status, "name": ("in", docs)}, pluck="name"
|
||||
)
|
||||
return set(matching_docs) == set(docs)
|
||||
|
||||
unsubmitted = frappe.get_all(self.doctype, {"docstatus": 0}, limit=20, pluck="name")
|
||||
submit_cancel_or_update_docs(self.doctype, unsubmitted, action="submit")
|
||||
|
||||
self.wait_for_assertion(lambda: check_docstatus(unsubmitted, 1))
|
||||
|
||||
submitted = frappe.get_all(self.doctype, {"docstatus": 1}, limit=20, pluck="name")
|
||||
submit_cancel_or_update_docs(self.doctype, submitted, action="cancel")
|
||||
self.wait_for_assertion(lambda: check_docstatus(submitted, 2))
|
||||
|
|
@ -102,8 +102,7 @@
|
|||
"fieldname": "url",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "URL",
|
||||
"options": "URL"
|
||||
"label": "URL"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.doc_view == \"Kanban\"",
|
||||
|
|
@ -116,7 +115,7 @@
|
|||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-07-18 16:12:53.546430",
|
||||
"modified": "2023-11-27 14:13:38.489737",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Workspace Shortcut",
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides {
|
|||
this.abort_setup(r.message.fail);
|
||||
}
|
||||
},
|
||||
error: () => this.abort_setup("Error in setup"),
|
||||
error: () => this.abort_setup(),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -213,7 +213,11 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides {
|
|||
|
||||
abort_setup(fail_msg) {
|
||||
this.$working_state.find(".state-icon-container").html("");
|
||||
fail_msg = fail_msg ? fail_msg : __("Failed to complete setup");
|
||||
fail_msg = fail_msg
|
||||
? fail_msg
|
||||
: frappe.last_response.setup_wizard_failure_message
|
||||
? frappe.last_response.setup_wizard_failure_message
|
||||
: __("Failed to complete setup");
|
||||
|
||||
this.update_setup_message("Could not start up: " + fail_msg);
|
||||
|
||||
|
|
@ -463,7 +467,7 @@ frappe.setup.slides_settings = [
|
|||
fieldtype: "Data",
|
||||
options: "Email",
|
||||
},
|
||||
{ fieldname: "password", label: __("Password"), fieldtype: "Password" },
|
||||
{ fieldname: "password", label: __("Password"), fieldtype: "Password", length: 512 },
|
||||
],
|
||||
|
||||
onload: function (slide) {
|
||||
|
|
|
|||
|
|
@ -83,11 +83,14 @@ def process_setup_stages(stages, user_input, is_background_task=False):
|
|||
task.get("fn")(task.get("args"))
|
||||
except Exception:
|
||||
handle_setup_exception(user_input)
|
||||
message = current_task.get("fail_msg") if current_task else "Failed to complete setup"
|
||||
frappe.log_error(title=f"Setup failed: {message}")
|
||||
if not is_background_task:
|
||||
return {"status": "fail", "fail": current_task.get("fail_msg")}
|
||||
frappe.response["setup_wizard_failure_message"] = message
|
||||
raise
|
||||
frappe.publish_realtime(
|
||||
"setup_task",
|
||||
{"status": "fail", "fail_msg": current_task.get("fail_msg")},
|
||||
{"status": "fail", "fail_msg": message},
|
||||
user=frappe.session.user,
|
||||
)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -163,6 +163,7 @@ def get_mapped_doc(
|
|||
if postprocess:
|
||||
postprocess(source_doc, target_doc)
|
||||
|
||||
ret_doc.run_method("after_mapping", source_doc)
|
||||
ret_doc.set_onload("load_after_mapping", True)
|
||||
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
import json
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, Union
|
||||
|
||||
import frappe
|
||||
|
|
@ -233,17 +234,30 @@ def get_workflow_field_value(workflow_name, field):
|
|||
|
||||
@frappe.whitelist()
|
||||
def bulk_workflow_approval(docnames, doctype, action):
|
||||
from collections import defaultdict
|
||||
|
||||
docnames = json.loads(docnames)
|
||||
if len(docnames) < 20:
|
||||
_bulk_workflow_action(docnames, doctype, action)
|
||||
elif len(docnames) <= 500:
|
||||
frappe.msgprint(_("Bulk {0} is enqueued in background.").format(action), alert=True)
|
||||
frappe.enqueue(
|
||||
_bulk_workflow_action,
|
||||
docnames=docnames,
|
||||
doctype=doctype,
|
||||
action=action,
|
||||
queue="short",
|
||||
timeout=1000,
|
||||
)
|
||||
else:
|
||||
frappe.throw(_("Bulk approval only support up to 500 documents."), title=_("Too Many Documents"))
|
||||
|
||||
|
||||
def _bulk_workflow_action(docnames, doctype, action):
|
||||
# dictionaries for logging
|
||||
failed_transactions = defaultdict(list)
|
||||
successful_transactions = defaultdict(list)
|
||||
|
||||
# WARN: message log is cleared
|
||||
print("Clearing frappe.message_log...")
|
||||
frappe.clear_messages()
|
||||
|
||||
docnames = json.loads(docnames)
|
||||
for (idx, docname) in enumerate(docnames, 1):
|
||||
message_dict = {}
|
||||
try:
|
||||
|
|
@ -308,7 +322,9 @@ def print_workflow_log(messages, title, doctype, indicator):
|
|||
html = f"<div>{doc}</div>"
|
||||
msg += html
|
||||
|
||||
frappe.msgprint(msg, title=_("Workflow Status"), indicator=indicator, is_minimizable=True)
|
||||
frappe.msgprint(
|
||||
msg, title=_("Workflow Status"), indicator=indicator, is_minimizable=True, realtime=True
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ from oauthlib.openid import RequestValidator
|
|||
|
||||
import frappe
|
||||
from frappe.auth import LoginManager
|
||||
from frappe.utils.data import get_system_timezone
|
||||
from frappe.utils.data import get_system_timezone, now_datetime
|
||||
|
||||
|
||||
class OAuthWebRequestValidator(RequestValidator):
|
||||
|
|
@ -240,13 +240,7 @@ class OAuthWebRequestValidator(RequestValidator):
|
|||
def validate_bearer_token(self, token, scopes, request):
|
||||
# Remember to check expiration and scope membership
|
||||
otoken = frappe.get_doc("OAuth Bearer Token", token)
|
||||
token_expiration_local = otoken.expiration_time.replace(
|
||||
tzinfo=pytz.timezone(get_system_timezone())
|
||||
)
|
||||
token_expiration_utc = token_expiration_local.astimezone(pytz.utc)
|
||||
is_token_valid = (
|
||||
datetime.datetime.now(pytz.UTC) < token_expiration_utc
|
||||
) and otoken.status != "Revoked"
|
||||
is_token_valid = (now_datetime() < otoken.expiration_time) and otoken.status != "Revoked"
|
||||
client_scopes = frappe.db.get_value("OAuth Client", otoken.client, "scopes").split(
|
||||
get_url_delimiter()
|
||||
)
|
||||
|
|
|
|||
|
|
@ -368,7 +368,11 @@ frappe.router = {
|
|||
window.open(sub_path, "_blank");
|
||||
frappe.open_in_new_tab = false;
|
||||
} else {
|
||||
this.push_state(sub_path);
|
||||
const route_options = frappe.route_options || {};
|
||||
const query_params = Object.entries(route_options)
|
||||
.map(([key, value]) => `${key}=` + encodeURIComponent(JSON.stringify(value)))
|
||||
.join("&");
|
||||
this.push_state(sub_path, query_params ? `?${query_params}` : "");
|
||||
}
|
||||
setTimeout(() => {
|
||||
frappe.after_ajax &&
|
||||
|
|
@ -469,12 +473,19 @@ frappe.router = {
|
|||
return "/app/" + (path_string || default_page);
|
||||
},
|
||||
|
||||
push_state(url) {
|
||||
// change the URL and call the router
|
||||
if (window.location.pathname !== url) {
|
||||
/**
|
||||
* Changes the URL and calls the router.
|
||||
*
|
||||
* @param {string} path - The desired URI path to replace or push,
|
||||
* without query string. Example: "/app/todo"
|
||||
* @param {string} query_params - The desired query parameter string.
|
||||
* @returns {void}
|
||||
*/
|
||||
push_state(path, query_params = "") {
|
||||
if (window.location.pathname !== path || window.location.search !== query_params) {
|
||||
// push/replace state so the browser looks fine
|
||||
const method = frappe.route_flags.replace_route ? "replaceState" : "pushState";
|
||||
history[method](null, null, url);
|
||||
history[method](null, null, path);
|
||||
|
||||
// now process the route
|
||||
this.route();
|
||||
|
|
|
|||
|
|
@ -147,10 +147,13 @@ frappe.ui.GroupBy = class {
|
|||
doctype_fields.forEach((field) => {
|
||||
// pick numeric fields for sum / avg
|
||||
if (frappe.model.is_numeric_field(field.fieldtype)) {
|
||||
let field_label = field.label
|
||||
? field.label
|
||||
: frappe.model.unscrub(field.fieldname);
|
||||
let option_text =
|
||||
doctype == this.doctype
|
||||
? field.label
|
||||
: `${field.label} (${__(doctype)})`;
|
||||
? field_label
|
||||
: `${__(field_label)} (${__(doctype)})`;
|
||||
this.aggregate_on_html += `<option data-doctype="${doctype}"
|
||||
value="${field.fieldname}">${__(option_text)}</option>`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -419,7 +419,6 @@ class ShortcutDialog extends WidgetDialog {
|
|||
fieldtype: "Data",
|
||||
fieldname: "url",
|
||||
label: "URL",
|
||||
options: "URL",
|
||||
default: "",
|
||||
depends_on: (s) => s.type == "URL",
|
||||
mandatory_depends_on: (s) => s.type == "URL",
|
||||
|
|
@ -547,7 +546,11 @@ class ShortcutDialog extends WidgetDialog {
|
|||
data.label = data.label ? data.label : frappe.model.unscrub(data.link_to);
|
||||
|
||||
if (data.url) {
|
||||
!validate_url(data.url) &&
|
||||
let _url = data.url;
|
||||
if (data.url.startsWith("/")) {
|
||||
_url = frappe.urllib.get_base_url() + data.url;
|
||||
}
|
||||
!validate_url(_url) &&
|
||||
frappe.throw({
|
||||
message: __("<b>{0}</b> is not a valid URL", [data.url]),
|
||||
title: __("Invalid URL"),
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestBot(FrappeTestCase):
|
||||
pass
|
||||
Loading…
Add table
Reference in a new issue