Merge pull request #26369 from gavindsouza/refactor-scheduled-server_script

refactor!: Server Script (Scheduler Event) & misc APIs
This commit is contained in:
Ankush Menat 2024-05-13 20:25:29 +05:30 committed by GitHub
commit bbf18d39cc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 62 additions and 68 deletions

View file

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

View file

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

View file

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

View file

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