diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py index d3af5d1fc6..54b650871f 100644 --- a/frappe/core/doctype/server_script/server_script.py +++ b/frappe/core/doctype/server_script/server_script.py @@ -3,6 +3,7 @@ from functools import partial from types import FunctionType, MethodType, ModuleType +from typing import TYPE_CHECKING import frappe from frappe import _ @@ -16,6 +17,9 @@ from frappe.utils.safe_exec import ( safe_exec, ) +if TYPE_CHECKING: + from frappe.core.doctype.scheduled_job_type.scheduled_job_type import ScheduledJobType + class ServerScript(Document): # begin: auto-generated types @@ -77,12 +81,10 @@ class ServerScript(Document): def validate(self): frappe.only_for("Script Manager", True) - self.sync_scheduled_jobs() - self.clear_scheduled_events() self.check_if_compilable_in_restricted_context() def on_update(self): - self.sync_scheduler_events() + self.sync_scheduled_job_type() def clear_cache(self): frappe.cache.delete_value("server_script_map") @@ -92,7 +94,10 @@ class ServerScript(Document): frappe.cache.delete_value("server_script_map") if self.script_type == "Scheduler Event": for job in self.scheduled_jobs: - frappe.delete_doc("Scheduled Job Type", job.name) + scheduled_job_type: "ScheduledJobType" = frappe.get_doc("Scheduled Job Type", job.name) + scheduled_job_type.stopped = True + scheduled_job_type.server_script = None + scheduled_job_type.save() def get_code_fields(self): return {"script": "py"} @@ -105,33 +110,35 @@ class ServerScript(Document): fields=["name", "stopped"], ) - def sync_scheduled_jobs(self): - """Sync Scheduled Job Type statuses if Server Script's disabled status is changed""" - if self.script_type != "Scheduler Event" or not self.has_value_changed("disabled"): + def sync_scheduled_job_type(self): + """Create or update Scheduled Job Type documents for Scheduler Event Server Scripts""" + if self.script_type != "Scheduler Event" or ( + (previous_script_type := self.has_value_changed("script_type")) + # True will be sent if its a new record + and previous_script_type.value not in (True, "Scheduler Event") + ): return - for scheduled_job in self.scheduled_jobs: - if bool(scheduled_job.stopped) != bool(self.disabled): - job = frappe.get_doc("Scheduled Job Type", scheduled_job.name) - job.stopped = self.disabled - job.save() - - def sync_scheduler_events(self): - """Create or update Scheduled Job Type documents for Scheduler Event Server Scripts""" - if not self.disabled and self.event_frequency and self.script_type == "Scheduler Event": - cron_format = self.cron_format if self.event_frequency == "Cron" else None - setup_scheduler_events( - script_name=self.name, frequency=self.event_frequency, cron_format=cron_format + if scheduled_script := frappe.db.get_value("Scheduled Job Type", {"server_script": self.name}): + scheduled_job_type: "ScheduledJobType" = frappe.get_doc("Scheduled Job Type", scheduled_script) + else: + scheduled_job_type: "ScheduledJobType" = frappe.get_doc( + { + "doctype": "Scheduled Job Type", + "server_script": self.name, + } ) - def clear_scheduled_events(self): - """Deletes existing scheduled jobs by Server Script if self.event_frequency or self.cron_format has changed""" - if ( - self.script_type == "Scheduler Event" - and (self.has_value_changed("event_frequency") or self.has_value_changed("cron_format")) - ) or (self.has_value_changed("script_type") and self.script_type != "Scheduler Event"): - for scheduled_job in self.scheduled_jobs: - frappe.delete_doc("Scheduled Job Type", scheduled_job.name, delete_permanently=1) + scheduled_job_type.update( + { + "method": frappe.scrub(f"{self.name}-{self.event_frequency}"), + "frequency": self.event_frequency, + "cron_format": self.cron_format, + "stopped": self.disabled, + } + ).save() + + frappe.msgprint(_("Scheduled execution for script {0} has updated").format(self.name)) def check_if_compilable_in_restricted_context(self): """Check compilation errors and send them back as warnings.""" @@ -247,43 +254,7 @@ class ServerScript(Document): return items -def setup_scheduler_events(script_name: str, frequency: str, cron_format: str | None = None): - """Creates or Updates Scheduled Job Type documents based on the specified script name and frequency - - Args: - script_name (str): Name of the Server Script document - frequency (str): Event label compatible with the Frappe scheduler - """ - method = frappe.scrub(f"{script_name}-{frequency}") - scheduled_script = frappe.db.get_value("Scheduled Job Type", {"method": method}) - - if not scheduled_script: - frappe.get_doc( - { - "doctype": "Scheduled Job Type", - "method": method, - "frequency": frequency, - "server_script": script_name, - "cron_format": cron_format, - } - ).insert() - - frappe.msgprint(_("Enabled scheduled execution for script {0}").format(script_name)) - - else: - doc = frappe.get_doc("Scheduled Job Type", scheduled_script) - - if doc.frequency == frequency: - return - - doc.frequency = frequency - doc.cron_format = cron_format - doc.save() - - frappe.msgprint(_("Scheduled execution for script {0} has updated").format(script_name)) - - -def execute_api_server_script(script=None, *args, **kwargs): +def execute_api_server_script(script: ServerScript, *args, **kwargs): # These are only added for compatibility with rate limiter. del args del kwargs diff --git a/frappe/core/doctype/server_script/server_script_utils.py b/frappe/core/doctype/server_script/server_script_utils.py index 418f3aea83..9237b66de5 100644 --- a/frappe/core/doctype/server_script/server_script_utils.py +++ b/frappe/core/doctype/server_script/server_script_utils.py @@ -43,7 +43,7 @@ def run_server_script_for_doc_event(doc, event): if scripts: # run all scripts for this doctype + event for script_name in scripts: - frappe.get_doc("Server Script", script_name).execute_doc(doc) + frappe.get_cached_doc("Server Script", script_name).execute_doc(doc) def get_server_script_map(): diff --git a/frappe/model/document.py b/frappe/model/document.py index f9c9ae7ae1..1b0c34f1fd 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -21,7 +21,7 @@ from frappe.model.naming import set_new_name, validate_name from frappe.model.utils import is_virtual_doctype from frappe.model.workflow import set_workflow_state_on_action, validate_workflow from frappe.types import DF -from frappe.utils import compare, cstr, date_diff, file_lock, flt, now +from frappe.utils import Truthy, compare, cstr, date_diff, file_lock, flt, now from frappe.utils.data import get_absolute_url, get_datetime, get_timedelta, getdate from frappe.utils.global_search import update_global_search @@ -468,7 +468,7 @@ class Document(BaseDocument): previous = self.get_doc_before_save() if not previous: - return True + return Truthy(context="New Document") previous_value = previous.get(fieldname) current_value = self.get(fieldname) @@ -480,7 +480,10 @@ class Document(BaseDocument): elif isinstance(previous_value, timedelta): current_value = get_timedelta(current_value) - return previous_value != current_value + if previous_value != current_value: + return Truthy(value=previous_value) + + return False def set_new_name(self, force=False, set_name=None, set_child_names=True): """Calls `frappe.naming.set_new_name` for parent and child docs.""" diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index 70a5e2223b..ceba8bab05 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -42,6 +42,8 @@ EMAIL_MATCH_PATTERN = re.compile( re.IGNORECASE, ) +UNSET = object() + def get_fullname(user=None): """get the full name (first name + last name) of the user from User""" @@ -1166,3 +1168,21 @@ class CallbackManager: def reset(self): self._functions.clear() + + +class Truthy: + def __init__(self, value=True, context=UNSET): + self.value = value + self.context = context + + def __bool__(self): + return True + + def __eq__(self, other: object) -> bool: + return True == other # noqa: E712 + + def __repr__(self) -> str: + _val = "UNSET" if self.value is UNSET else self.value + _ctx = "UNSET" if self.context is UNSET else self.context + + return f"Truthy(value={_val}, context={_ctx})"