diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ed3be8..e82d16a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,9 @@ 0.5.2 (2023-??) ------------------ -* Feature: Allow configuring error email addresses via UI. +* Feature: Allow configuring error email addresses and email subject via UI. * Bugfix: . and .. should now be allowed to be used when specifying the templates directory. * Bugfix: corrected cron schedule incorrectly shifting back one day upon save. - 0.5.1 (2023-02-22) ------------------ diff --git a/notebooker/constants.py b/notebooker/constants.py index 6559ae5..a957a06 100644 --- a/notebooker/constants.py +++ b/notebooker/constants.py @@ -86,6 +86,7 @@ class NotebookResultBase(object): stdout = attr.ib(default=attr.Factory(list)) scheduler_job_id = attr.ib(default=None) mailfrom = attr.ib(default=None) + email_subject = attr.ib(default=None) is_slideshow = attr.ib(default=False) def saveable_output(self): @@ -123,10 +124,7 @@ class NotebookResultError(NotebookResultBase): scheduler_job_id = attr.ib(default=None) mailfrom = attr.ib(default=None) is_slideshow = attr.ib(default=False) - - @property - def email_subject(self): - return "" + email_subject = attr.ib(default=None) @property def raw_html(self): diff --git a/notebooker/execute_notebook.py b/notebooker/execute_notebook.py index 97d79ed..849c66b 100644 --- a/notebooker/execute_notebook.py +++ b/notebooker/execute_notebook.py @@ -227,6 +227,7 @@ def run_report( mailfrom=mailfrom, hide_code=hide_code, is_slideshow=is_slideshow, + email_subject=email_subject, ) logger.error( "Report run failed. Saving error result to mongo library %s@%s...", @@ -472,6 +473,7 @@ def run_report_in_subprocess( scheduler_job_id=None, run_synchronously=False, mailfrom=None, + email_subject=None, n_retries=3, is_slideshow=False, ) -> str: @@ -489,6 +491,7 @@ def run_report_in_subprocess( :param scheduler_job_id: `Optional[str]` if the job was triggered from the scheduler, this is the scheduler's job id :param run_synchronously: `bool` If True, then we will join the stderr monitoring thread until the job has completed :param mailfrom: `str` if passed, then this string will be used in the from field + :param email_subject: `str` if passed, then this string will be used in the email subject :param n_retries: The number of retries to attempt. :param is_slideshow: Whether the notebook is a reveal.js slideshow or not. :return: The unique job_id. @@ -511,6 +514,7 @@ def run_report_in_subprocess( hide_code=hide_code, scheduler_job_id=scheduler_job_id, is_slideshow=is_slideshow, + email_subject=email_subject, ) command = ( @@ -553,6 +557,7 @@ def run_report_in_subprocess( + (["--is-slideshow"] if is_slideshow else []) + ([f"--scheduler-job-id={scheduler_job_id}"] if scheduler_job_id is not None else []) + ([f"--mailfrom={mailfrom}"] if mailfrom is not None else []) + + ([f"--email-subject={email_subject}"] if email_subject else []) ) p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) diff --git a/notebooker/serialization/mongo.py b/notebooker/serialization/mongo.py index 1c3b29c..3097c91 100644 --- a/notebooker/serialization/mongo.py +++ b/notebooker/serialization/mongo.py @@ -169,11 +169,7 @@ def update_check_status(self, job_id: str, status: JobStatus, **extra): existing["status"] = status.value for k, v in extra.items(): if k == "error_info" and v: - self.result_data_store.put( - v, - filename=_error_info_filename(job_id), - encoding="utf-8", - ) + self.result_data_store.put(v, filename=_error_info_filename(job_id), encoding="utf-8") else: existing[k] = v self._save_raw_to_db(existing) @@ -192,6 +188,7 @@ def save_check_stub( hide_code: bool = False, scheduler_job_id: Optional[str] = None, is_slideshow: bool = False, + email_subject: Optional[str] = None, ) -> None: """Call this when we are just starting a check. Saves a "pending" job into storage.""" job_start_time = job_start_time or datetime.datetime.now() @@ -204,6 +201,7 @@ def save_check_stub( report_name=report_name, mailto=mailto, error_mailto=error_mailto, + email_subject=email_subject, generate_pdf_output=generate_pdf_output, overrides=overrides or {}, hide_code=hide_code, @@ -226,9 +224,7 @@ def save_check_result(self, notebook_result: Union[NotebookResultComplete, Noteb filename=filename_func(notebook_result.job_id), encoding="utf-8", ) - for json_attribute, filename_func in [ - ("raw_ipynb_json", _raw_json_filename), - ]: + for json_attribute, filename_func in [("raw_ipynb_json", _raw_json_filename)]: if getattr(notebook_result, json_attribute, None): self.result_data_store.put( json.dumps(getattr(notebook_result, json_attribute)), @@ -306,6 +302,7 @@ def _convert_result( stdout=result.get("stdout", []), scheduler_job_id=result.get("scheduler_job_id", None), is_slideshow=result.get("is_slideshow", False), + email_subject=result.get("email_subject", None), ) elif cls == NotebookResultPending: return NotebookResultPending( @@ -319,6 +316,7 @@ def _convert_result( report_title=result.get("report_title", result["report_name"]), mailto=result.get("mailto", ""), error_mailto=result.get("error_mailto", ""), + email_subject=result.get("email_subject", ""), hide_code=result.get("hide_code", False), stdout=result.get("stdout", []), scheduler_job_id=result.get("scheduler_job_id", None), @@ -343,6 +341,7 @@ def _convert_result( report_title=result.get("report_title", result["report_name"]), mailto=result.get("mailto", ""), error_mailto=result.get("error_mailto", ""), + email_subject=result.get("email_subject", ""), hide_code=result.get("hide_code", False), stdout=result.get("stdout", []), scheduler_job_id=result.get("scheduler_job_id", False), diff --git a/notebooker/utils/notebook_execution.py b/notebooker/utils/notebook_execution.py index 81e4962..3b58398 100644 --- a/notebooker/utils/notebook_execution.py +++ b/notebooker/utils/notebook_execution.py @@ -19,7 +19,9 @@ def _send_email(from_email: str, to_email: str, result: Union[NotebookResultComp report_title = ( result.report_title.decode("utf-8") if isinstance(result.report_title, bytes) else result.report_title ) - subject = result.email_subject or f"Notebooker: {report_title} report completed with status: {result.status.value}" + subject = f"Notebooker: {report_title} report completed with status: {result.status.value}" + if isinstance(result, NotebookResultComplete): + subject = result.email_subject or subject body = result.email_html or result.raw_html attachments = [] tmp_dir = None diff --git a/notebooker/web/routes/report_execution.py b/notebooker/web/routes/report_execution.py index d66520d..a6e0e69 100644 --- a/notebooker/web/routes/report_execution.py +++ b/notebooker/web/routes/report_execution.py @@ -128,6 +128,7 @@ class RunReportParams(NamedTuple): hide_code: bool scheduler_job_id: Optional[str] is_slideshow: bool + email_subject: Optional[str] def validate_run_params(report_name, params, issues: List[str]) -> RunReportParams: @@ -142,6 +143,7 @@ def validate_run_params(report_name, params, issues: List[str]) -> RunReportPara generate_pdf_output = params.get("generate_pdf") in ("on", "True", True) hide_code = params.get("hide_code") in ("on", "True", True) is_slideshow = params.get("is_slideshow") in ("on", "True", True) + email_subject = validate_title(params.get("email_subject") or "", issues) out = RunReportParams( report_title=report_title, @@ -152,6 +154,7 @@ def validate_run_params(report_name, params, issues: List[str]) -> RunReportPara hide_code=hide_code, scheduler_job_id=params.get("scheduler_job_id"), is_slideshow=is_slideshow, + email_subject=email_subject, ) logger.info(f"Validated params: {out}") return out @@ -174,6 +177,7 @@ def _handle_run_report( f"hide_code={params.hide_code} " f"scheduler_job_id={params.scheduler_job_id} " f"mailfrom={params.mailfrom} " + f"email_subject={params.email_subject} " f"is_slideshow={params.is_slideshow} " ) try: @@ -190,6 +194,7 @@ def _handle_run_report( hide_code=params.hide_code, scheduler_job_id=params.scheduler_job_id, mailfrom=params.mailfrom, + email_subject=params.email_subject, is_slideshow=params.is_slideshow, ) return ( @@ -255,6 +260,7 @@ def _rerun_report(job_id, prepare_only=False, run_synchronously=False): scheduler_job_id=None, # the scheduler will never call rerun run_synchronously=run_synchronously, is_slideshow=result.is_slideshow, + email_subject=result.email_subject, ) return new_job_id diff --git a/notebooker/web/routes/scheduling.py b/notebooker/web/routes/scheduling.py index af1c0fd..9bfc26c 100644 --- a/notebooker/web/routes/scheduling.py +++ b/notebooker/web/routes/scheduling.py @@ -76,6 +76,7 @@ def update_schedule(report_name): "mailto": params.mailto, "error_mailto": params.error_mailto, "mailfrom": params.mailfrom, + "email_subject": params.email_subject, "generate_pdf": params.generate_pdf_output, "hide_code": params.hide_code, "is_slideshow": params.is_slideshow, @@ -109,6 +110,7 @@ def create_schedule(report_name): "mailto": params.mailto, "error_mailto": params.error_mailto, "mailfrom": params.mailfrom, + "email_subject": params.email_subject, "generate_pdf": params.generate_pdf_output, "hide_code": params.hide_code, "scheduler_job_id": job_id, diff --git a/notebooker/web/scheduler.py b/notebooker/web/scheduler.py index ba29c92..2a31125 100644 --- a/notebooker/web/scheduler.py +++ b/notebooker/web/scheduler.py @@ -23,6 +23,7 @@ def run_report( mailfrom: Optional[str] = None, is_slideshow: bool = False, error_mailto: Optional[str] = None, + email_subject: Optional[str] = None, ): """ This is the entrypoint of the scheduler; APScheduler has to @@ -44,6 +45,7 @@ def run_report( mailfrom=mailfrom, n_retries=0, is_slideshow=is_slideshow, + email_subject=email_subject, ) else: # Fall back to using API. This will not work in readonly mode. @@ -64,6 +66,8 @@ def run_report( # natural. if mailfrom: payload["mailfrom"] = mailfrom + if email_subject: + payload["email_subject"] = email_subject logger.info(f"Running report at {url}, payload = {payload}") result = requests.post(url, params=urllib.parse.urlencode(payload)) logger.info(result.content) diff --git a/notebooker/web/static/notebooker/scheduler.js b/notebooker/web/static/notebooker/scheduler.js index 829fe4f..7b17532 100644 --- a/notebooker/web/static/notebooker/scheduler.js +++ b/notebooker/web/static/notebooker/scheduler.js @@ -141,6 +141,7 @@ function modifySchedulerModal(row) { mailto: row.params.mailto, error_mailto: row.params.error_mailto, mailfrom: row.params.mailfrom, + email_subject: row.params.email_subject, cronSchedule: row.cron_schedule, is_slideshow: row.params.is_slideshow, } @@ -169,6 +170,7 @@ function handleAddButtonClick() { mailto: "", error_mailto: "", mailfrom: "", + email_subject: "", cronSchedule: "", is_slideshow: "", } @@ -272,6 +274,7 @@ $(document).ready(() => { mailto: formObj.mailto, error_mailto: formObj.error_mailto, mailfrom: formObj.mailfrom, + email_subject: formObj.email_subject, generate_pdf: formObj.generate_pdf, hide_code: formObj.hide_code, is_slideshow: formObj.is_slideshow, diff --git a/notebooker/web/templates/results.html b/notebooker/web/templates/results.html index cd7808d..2a05e0d 100644 --- a/notebooker/web/templates/results.html +++ b/notebooker/web/templates/results.html @@ -62,6 +62,9 @@