From e2c5a1a4814f0deebb6d2b568f52f6fe4ea308ea Mon Sep 17 00:00:00 2001 From: jbannister Date: Wed, 18 Jan 2023 15:57:21 +0000 Subject: [PATCH] Adding support for Reveal.js slideshows (#113) --- CHANGELOG.md | 4 +- notebooker/_entrypoints.py | 17 +++++---- notebooker/constants.py | 8 +++- notebooker/execute_notebook.py | 33 ++++++++++++++--- .../sample/slideshow_test.py | 33 +++++++++++++++++ notebooker/serialization/mongo.py | 12 +++--- notebooker/utils/conversion.py | 36 +++++++++++------- notebooker/web/routes/run_report.py | 37 ++++++++++++++----- notebooker/web/routes/scheduling.py | 2 + notebooker/web/scheduler.py | 2 + .../static/notebooker/one_click_notebooks.css | 4 ++ notebooker/web/static/notebooker/scheduler.js | 3 ++ notebooker/web/templates/run_report.html | 18 ++++++--- notebooker/web/templates/scheduler.html | 6 +++ tests/integration/test_templates.py | 7 +++- tests/integration/web/test_run_report.py | 7 ++-- tests/integration/web/test_scheduling.py | 33 +++++++---------- tests/unit/test_run_report.py | 4 +- 18 files changed, 191 insertions(+), 75 deletions(-) create mode 100644 notebooker/notebook_templates_example/sample/slideshow_test.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 84e47234..578eb515 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,10 @@ -0.4.6 (2022-??-??) +0.5.0 (2023-??-??) ------------------ +* Feature: Added support for [Reveal.js](https://revealjs.com/) notebook outputs * Bugfix: Small bugfix for synchronous report execution * Improvement: Delete functionality in mongo now also deletes files from GridFS - 0.4.5 (2022-09-29) ------------------ diff --git a/notebooker/_entrypoints.py b/notebooker/_entrypoints.py index a415e8d1..222f350d 100644 --- a/notebooker/_entrypoints.py +++ b/notebooker/_entrypoints.py @@ -64,15 +64,10 @@ def filesystem_default_value(dirname): help="If selected, notebooker will not try to pull the latest version of python templates from git.", ) @click.option( - "--default-mailfrom", - default=DEFAULT_MAILFROM_ADDRESS, - help="Set a new value for the default mailfrom setting." + "--default-mailfrom", default=DEFAULT_MAILFROM_ADDRESS, help="Set a new value for the default mailfrom setting." ) @click.option( - "--running-timeout", - default=DEFAULT_RUNNING_TIMEOUT, - help="Timeout in minutes for report execution", - type=int + "--running-timeout", default=DEFAULT_RUNNING_TIMEOUT, help="Timeout in minutes for report execution", type=int ) @click.option( "--serializer-cls", @@ -207,6 +202,12 @@ def start_webapp( default=None, help="Use this email in the From header of any sent email. If not passed, --default-mailfrom will be used", ) +@click.option( + "--is-slideshow", + default=False, + is_flag=True, + help="If specified, the notebook template's output will be treated as a Reveal.js slideshow.", +) @pass_config def execute_notebook( config: BaseConfig, @@ -224,6 +225,7 @@ def execute_notebook( prepare_notebook_only, scheduler_job_id, mailfrom, + is_slideshow, ): if report_name is None: raise ValueError("Error! Please provide a --report-name.") @@ -243,6 +245,7 @@ def execute_notebook( prepare_notebook_only, scheduler_job_id, mailfrom, + is_slideshow=is_slideshow, ) diff --git a/notebooker/constants.py b/notebooker/constants.py index dfca47c7..edbd9902 100644 --- a/notebooker/constants.py +++ b/notebooker/constants.py @@ -85,6 +85,7 @@ class NotebookResultBase(object): stdout = attr.ib(default=attr.Factory(list)) scheduler_job_id = attr.ib(default=None) mailfrom = attr.ib(default=None) + is_slideshow = attr.ib(default=False) def saveable_output(self): out = attr.asdict(self) @@ -103,6 +104,7 @@ class NotebookResultPending(NotebookResultBase): hide_code = attr.ib(default=False) scheduler_job_id = attr.ib(default=None) mailfrom = attr.ib(default=None) + is_slideshow = attr.ib(default=False) @attr.s() @@ -117,6 +119,7 @@ class NotebookResultError(NotebookResultBase): hide_code = attr.ib(default=False) scheduler_job_id = attr.ib(default=None) mailfrom = attr.ib(default=None) + is_slideshow = attr.ib(default=False) @property def email_subject(self): @@ -158,6 +161,7 @@ class NotebookResultComplete(NotebookResultBase): stdout = attr.ib(default=attr.Factory(list)) scheduler_job_id = attr.ib(default=None) mailfrom = attr.ib(default=None) + is_slideshow = attr.ib(default=False) def html_resources(self): """We have to save the raw images using Mongo GridFS - figure out where they will go here""" @@ -189,6 +193,7 @@ def saveable_output(self): "scheduler_job_id": self.scheduler_job_id, "raw_html": "", # backwards compatibility for versions<0.3.1 "mailfrom": self.mailfrom, + "is_slideshow": self.is_slideshow, } def __repr__(self): @@ -197,7 +202,7 @@ def __repr__(self): "job_start_time={job_start_time}, job_finish_time={job_finish_time}, update_time={update_time}, " "report_title={report_title}, overrides={overrides}, mailto={mailto}, mailfrom={mailfrom}" "email_subject={email_subject}, generate_pdf_output={generate_pdf_output}, hide_code={hide_code}, " - "scheduler_job_id={scheduler_job_id})".format( + "scheduler_job_id={scheduler_job_id}, is_slideshow={is_slideshow})".format( job_id=self.job_id, status=self.status, report_name=self.report_name, @@ -212,5 +217,6 @@ def __repr__(self): generate_pdf_output=self.generate_pdf_output, hide_code=self.hide_code, scheduler_job_id=self.scheduler_job_id, + is_slideshow=self.is_slideshow, ) ) diff --git a/notebooker/execute_notebook.py b/notebooker/execute_notebook.py index eb794524..88b04c7a 100644 --- a/notebooker/execute_notebook.py +++ b/notebooker/execute_notebook.py @@ -46,6 +46,7 @@ def _run_checks( py_template_subdir: str = "", scheduler_job_id: Optional[str] = None, mailfrom: Optional[str] = None, + is_slideshow: bool = False, ) -> NotebookResultComplete: """ This is the actual method which executes a notebook, whether running in the webapp or via the entrypoint. @@ -77,7 +78,8 @@ def _run_checks( If available, it will be part of the Error or Completed run report. mailfrom : `Optional[str]` If available, this will be the email used in the From header. - + is_slideshow: bool + Whether or not the output of this should use the equivalent of nbconvert --to slides Returns ------- @@ -102,14 +104,18 @@ def _run_checks( logger.info("Executing notebook at {} using parameters {} --> {}".format(ipynb_raw_path, overrides, output_ipynb)) pm.execute_notebook( - ipynb_raw_path, ipynb_executed_path, parameters=overrides, log_output=True, prepare_only=prepare_only + ipynb_raw_path, + ipynb_executed_path, + parameters=overrides, + log_output=True, + prepare_only=prepare_only, ) with open(ipynb_executed_path, "r") as f: raw_executed_ipynb = f.read() logger.info("Saving output notebook as HTML from {}".format(ipynb_executed_path)) - html, resources = ipython_to_html(ipynb_executed_path, job_id) - email_html, _ = ipython_to_html(ipynb_executed_path, job_id, hide_code=hide_code) + html, resources = ipython_to_html(ipynb_executed_path, job_id, is_slideshow=is_slideshow) + email_html, _ = ipython_to_html(ipynb_executed_path, job_id, hide_code=hide_code, is_slideshow=is_slideshow) pdf = ipython_to_pdf(raw_executed_ipynb, report_title, hide_code=hide_code) if generate_pdf_output else "" notebook_result = NotebookResultComplete( @@ -129,6 +135,7 @@ def _run_checks( overrides=overrides, scheduler_job_id=scheduler_job_id, mailfrom=mailfrom, + is_slideshow=is_slideshow, ) return notebook_result @@ -154,12 +161,17 @@ def run_report( py_template_subdir="", scheduler_job_id=None, mailfrom=None, + is_slideshow=False, ): job_id = job_id or str(uuid.uuid4()) stop_execution = os.getenv("NOTEBOOKER_APP_STOPPING") if stop_execution: - logger.info("Aborting attempt to run %s, jobid=%s as app is shutting down.", report_name, job_id) + logger.info( + "Aborting attempt to run %s, jobid=%s as app is shutting down.", + report_name, + job_id, + ) result_serializer.update_check_status(job_id, JobStatus.CANCELLED, error_info=CANCEL_MESSAGE) return try: @@ -170,7 +182,10 @@ def run_report( attempts_remaining, ) result_serializer.update_check_status( - job_id, report_name=report_name, job_start_time=job_submit_time, status=JobStatus.PENDING + job_id, + report_name=report_name, + job_start_time=job_submit_time, + status=JobStatus.PENDING, ) result = _run_checks( job_id, @@ -190,6 +205,7 @@ def run_report( py_template_subdir=py_template_subdir, scheduler_job_id=scheduler_job_id, mailfrom=mailfrom, + is_slideshow=is_slideshow, ) logger.info("Successfully got result.") result_serializer.save_check_result(result) @@ -208,6 +224,7 @@ def run_report( generate_pdf_output=generate_pdf_output, scheduler_job_id=scheduler_job_id, mailfrom=mailfrom, + is_slideshow=is_slideshow, ) logger.error( "Report run failed. Saving error result to mongo library %s@%s...", @@ -239,6 +256,7 @@ def run_report( py_template_subdir=py_template_subdir, scheduler_job_id=scheduler_job_id, mailfrom=mailfrom, + is_slideshow=is_slideshow, ) else: logger.info("Abandoning attempt to run report. It failed too many times.") @@ -327,6 +345,7 @@ def execute_notebook_entrypoint( prepare_notebook_only: bool, scheduler_job_id: Optional[str], mailfrom: Optional[str], + is_slideshow: bool, ): report_title = report_title or report_name output_dir, template_dir, _ = initialise_base_dirs(output_dir=config.OUTPUT_DIR, template_dir=config.TEMPLATE_DIR) @@ -351,6 +370,7 @@ def execute_notebook_entrypoint( logger.info("mailfrom = %s" % mailfrom) logger.info("pdf_output = %s", pdf_output) logger.info("hide_code = %s", hide_code) + logger.info("is_slideshow = %s", is_slideshow) logger.info("prepare_notebook_only = %s", prepare_notebook_only) logger.info("scheduler job id = %s", scheduler_job_id) logger.info("notebooker_disable_git = %s", notebooker_disable_git) @@ -384,6 +404,7 @@ def execute_notebook_entrypoint( py_template_subdir=py_template_subdir, scheduler_job_id=scheduler_job_id, mailfrom=mailfrom, + is_slideshow=is_slideshow, ) if result.mailto: send_result_email(result, config.DEFAULT_MAILFROM) diff --git a/notebooker/notebook_templates_example/sample/slideshow_test.py b/notebooker/notebook_templates_example/sample/slideshow_test.py new file mode 100644 index 00000000..9a3cd949 --- /dev/null +++ b/notebooker/notebook_templates_example/sample/slideshow_test.py @@ -0,0 +1,33 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.3.0 +# kernelspec: +# display_name: py3-local-pegasus +# language: python +# name: env +# --- + +# + tags=["parameters"] +the_range = 10 +# - + +# + [markdown] slideshow={"slide_type": "slide"} +# # This is slide number one + +# + [markdown] slideshow={"slide_type": "subslide"} +# ## This is slide two +# +# - I have some +# - things to talk +# - about. + +# + [markdown] slideshow={"slide_type": "slide"} +# # Okay onto the good stuff! +# - + +list(range(the_range)) diff --git a/notebooker/serialization/mongo.py b/notebooker/serialization/mongo.py index a2981e47..fd62b488 100644 --- a/notebooker/serialization/mongo.py +++ b/notebooker/serialization/mongo.py @@ -1,14 +1,13 @@ import datetime import json -from collections import Counter, defaultdict - -from abc import ABC +from collections import defaultdict from logging import getLogger from typing import Any, AnyStr, Dict, List, Optional, Tuple, Union, Iterator import click import gridfs import pymongo +from abc import ABC from gridfs import NoFile from notebooker.constants import JobStatus, NotebookResultComplete, NotebookResultError, NotebookResultPending @@ -53,7 +52,6 @@ def read_bytes_file(result_data_store, path): def load_files_from_gridfs(result_data_store: gridfs.GridFS, result: Dict, do_read=True) -> List[str]: - gridfs_filenames = [] all_html_output_paths = result.get("raw_html_resources", {}).get("outputs", []) gridfs_filenames.extend(all_html_output_paths) @@ -89,7 +87,6 @@ def load_files_from_gridfs(result_data_store: gridfs.GridFS, result: Dict, do_re class MongoResultSerializer(ABC): # This class is the interface between Mongo and the rest of the application - def __init__(self, database_name="notebooker", mongo_host="localhost", result_collection_name="NOTEBOOK_OUTPUT"): self.database_name = database_name self.mongo_host = mongo_host @@ -193,6 +190,7 @@ def save_check_stub( generate_pdf_output: bool = True, hide_code: bool = False, scheduler_job_id: Optional[str] = None, + is_slideshow: bool = False, ) -> 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() @@ -208,6 +206,7 @@ def save_check_stub( overrides=overrides or {}, hide_code=hide_code, scheduler_job_id=scheduler_job_id, + is_slideshow=is_slideshow, ) self._save_to_db(pending_result) @@ -303,6 +302,7 @@ def _convert_result( hide_code=result.get("hide_code", False), stdout=result.get("stdout", []), scheduler_job_id=result.get("scheduler_job_id", None), + is_slideshow=result.get("is_slideshow", False), ) elif cls == NotebookResultPending: return NotebookResultPending( @@ -318,6 +318,7 @@ def _convert_result( hide_code=result.get("hide_code", False), stdout=result.get("stdout", []), scheduler_job_id=result.get("scheduler_job_id", None), + is_slideshow=result.get("is_slideshow", False), ) elif cls == NotebookResultError: @@ -340,6 +341,7 @@ def _convert_result( hide_code=result.get("hide_code", False), stdout=result.get("stdout", []), scheduler_job_id=result.get("scheduler_job_id", False), + is_slideshow=result.get("is_slideshow", False), ) else: raise ValueError("Could not deserialise {} into result object.".format(result)) diff --git a/notebooker/utils/conversion.py b/notebooker/utils/conversion.py index 82b643d5..40b74938 100644 --- a/notebooker/utils/conversion.py +++ b/notebooker/utils/conversion.py @@ -6,7 +6,7 @@ import jupytext import nbformat import pkg_resources -from nbconvert import HTMLExporter, PDFExporter +from nbconvert import HTMLExporter, PDFExporter, SlidesExporter from nbconvert.exporters.exporter import ResourcesDict from traitlets.config import Config @@ -19,20 +19,27 @@ def get_resources_dir(job_id): return "{}/resources".format(job_id) -def ipython_to_html(ipynb_path: str, job_id: str, hide_code: bool = False) -> (nbformat.NotebookNode, Dict[str, Any]): - c = Config() - c.HTMLExporter.preprocessors = ["nbconvert.preprocessors.ExtractOutputPreprocessor"] - c.HTMLExporter.template_file = pkg_resources.resource_filename( - __name__, "../nbtemplates/notebooker_html_output.tpl" - ) - c.HTMLExporter.exclude_input = hide_code - c.HTMLExporter.exclude_output_prompt = hide_code - html_exporter_with_figs = HTMLExporter(config=c) - +def ipython_to_html( + ipynb_path: str, job_id: str, hide_code: bool = False, is_slideshow: bool = False +) -> (nbformat.NotebookNode, Dict[str, Any]): with open(ipynb_path, "r") as nb_file: nb = nbformat.reads(nb_file.read(), as_version=nbformat.v4.nbformat) - resources_dir = get_resources_dir(job_id) - html, resources = html_exporter_with_figs.from_notebook_node(nb, resources={"output_files_dir": resources_dir}) + c = Config() + if is_slideshow: + c.TagRemovePreprocessor.remove_cell_tags = ("injected-parameters", "parameters") + c.SlidesExporter.preprocessors = ["nbconvert.preprocessors.TagRemovePreprocessor"] + exporter = SlidesExporter(config=c) + html, resources = exporter.from_notebook_node(nb) + else: + c.HTMLExporter.preprocessors = ["nbconvert.preprocessors.ExtractOutputPreprocessor"] + c.HTMLExporter.template_file = pkg_resources.resource_filename( + __name__, "../nbtemplates/notebooker_html_output.tpl" + ) + c.HTMLExporter.exclude_input = hide_code + c.HTMLExporter.exclude_output_prompt = hide_code + html_exporter_with_figs = HTMLExporter(config=c) + resources_dir = get_resources_dir(job_id) + html, resources = html_exporter_with_figs.from_notebook_node(nb, resources={"output_files_dir": resources_dir}) resources = {k: v for (k, v) in resources.items() if not callable(v)} return html, resources @@ -49,7 +56,8 @@ def ipython_to_pdf(raw_executed_ipynb: str, report_title: str, hide_code: bool = resources["metadata"] = ResourcesDict() resources["metadata"]["name"] = report_title pdf, _ = pdf_exporter.from_notebook_node( - nbformat.reads(raw_executed_ipynb, as_version=nbformat.v4.nbformat), resources=resources + nbformat.reads(raw_executed_ipynb, as_version=nbformat.v4.nbformat), + resources=resources, ) return pdf diff --git a/notebooker/web/routes/run_report.py b/notebooker/web/routes/run_report.py index ea591810..c06ebd4b 100644 --- a/notebooker/web/routes/run_report.py +++ b/notebooker/web/routes/run_report.py @@ -161,6 +161,7 @@ def run_report( run_synchronously=False, mailfrom=None, n_retries=3, + is_slideshow=False, ) -> str: """ Actually run the report in earnest. @@ -175,6 +176,7 @@ def run_report( :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 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. """ job_id = str(uuid.uuid4()) @@ -191,6 +193,7 @@ def run_report( generate_pdf_output=generate_pdf_output, hide_code=hide_code, scheduler_job_id=scheduler_job_id, + is_slideshow=is_slideshow, ) app_config = current_app.config command = ( @@ -224,9 +227,11 @@ def run_report( json.dumps(overrides), "--pdf-output" if generate_pdf_output else "--no-pdf-output", "--hide-code" if hide_code else "--show-code", - "--n-retries", str(n_retries), + "--n-retries", + str(n_retries), ] + (["--prepare-notebook-only"] if prepare_only else []) + + (["--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 []) ) @@ -256,6 +261,7 @@ class RunReportParams(NamedTuple): generate_pdf_output: bool hide_code: bool scheduler_job_id: Optional[str] + is_slideshow: bool def validate_run_params(params, issues: List[str]) -> RunReportParams: @@ -268,6 +274,7 @@ def validate_run_params(params, issues: List[str]) -> RunReportParams: # "on" comes from HTML, "True" comes from urlencoded JSON params generate_pdf_output = params.get("generate_pdf") in ("on", "True") hide_code = params.get("hide_code") in ("on", "True") + is_slideshow = params.get("is_slideshow") in ("on", "True") out = RunReportParams( report_title=report_title, @@ -276,6 +283,7 @@ def validate_run_params(params, issues: List[str]) -> RunReportParams: generate_pdf_output=generate_pdf_output, hide_code=hide_code, scheduler_job_id=params.get("scheduler_job_id"), + is_slideshow=is_slideshow, ) logger.info(f"Validated params: {out}") return out @@ -288,14 +296,17 @@ def _handle_run_report( if issues: return jsonify({"status": "Failed", "content": ("\n".join(issues))}) report_name = convert_report_name_url_to_path(report_name) - logger.info(f"Handling run report with parameters report_name={report_name} " - f"report_title={params.report_title}" - f"mailto={params.mailto} " - f"overrides_dict={overrides_dict} " - f"generate_pdf_output={params.generate_pdf_output} " - f"hide_code={params.hide_code} " - f"scheduler_job_id={params.scheduler_job_id}" - f"mailfrom={params.mailfrom}") + logger.info( + f"Handling run report with parameters report_name={report_name} " + f"report_title={params.report_title}" + f"mailto={params.mailto} " + f"overrides_dict={overrides_dict} " + f"generate_pdf_output={params.generate_pdf_output} " + f"hide_code={params.hide_code} " + f"scheduler_job_id={params.scheduler_job_id} " + f"mailfrom={params.mailfrom} " + f"is_slideshow={params.is_slideshow} " + ) try: job_id = run_report( report_name, @@ -306,6 +317,7 @@ def _handle_run_report( hide_code=params.hide_code, scheduler_job_id=params.scheduler_job_id, mailfrom=params.mailfrom, + is_slideshow=params.is_slideshow, ) return ( jsonify({"id": job_id}), @@ -313,7 +325,11 @@ def _handle_run_report( {"Location": url_for("pending_results_bp.task_status", report_name=report_name, job_id=job_id)}, ) except RuntimeError as e: - return jsonify({"status": "Failed", "content": f"The job failed to initialise. Error: {str(e)}"}), 500, {} + return ( + jsonify({"status": "Failed", "content": f"The job failed to initialise. Error: {str(e)}"}), + 500, + {}, + ) @run_report_bp.route("/run_report_json/", methods=["POST"]) @@ -363,6 +379,7 @@ def _rerun_report(job_id, prepare_only=False, run_synchronously=False): prepare_only=prepare_only, scheduler_job_id=None, # the scheduler will never call rerun run_synchronously=run_synchronously, + is_slideshow=result.is_slideshow, ) return new_job_id diff --git a/notebooker/web/routes/scheduling.py b/notebooker/web/routes/scheduling.py index 2f3601be..94370e78 100644 --- a/notebooker/web/routes/scheduling.py +++ b/notebooker/web/routes/scheduling.py @@ -75,6 +75,7 @@ def update_schedule(report_name): "mailfrom": params.mailfrom, "generate_pdf": params.generate_pdf_output, "hide_code": params.hide_code, + "is_slideshow": params.is_slideshow, "scheduler_job_id": job_id, } job.modify(trigger=trigger, kwargs=params) @@ -107,6 +108,7 @@ def create_schedule(report_name): "generate_pdf": params.generate_pdf_output, "hide_code": params.hide_code, "scheduler_job_id": job_id, + "is_slideshow": params.is_slideshow, } logger.info(f"Creating job with params: {dict_params}") try: diff --git a/notebooker/web/scheduler.py b/notebooker/web/scheduler.py index 3542b07f..cbaef1da 100644 --- a/notebooker/web/scheduler.py +++ b/notebooker/web/scheduler.py @@ -20,6 +20,7 @@ def run_report( scheduler_job_id: str, # new parameters should be added below and be optional to avoid migrations mailfrom: Optional[str] = None, + is_slideshow: bool = False, ): """ This is the entrypoint of the scheduler; APScheduler has to @@ -36,6 +37,7 @@ def run_report( "generate_pdf": generate_pdf, "hide_code": hide_code, "scheduler_job_id": scheduler_job_id, + "is_slideshow": is_slideshow, } # This means that, if the default mailfrom changes, all already scheduled # jobs will use the new default in subsequent runs. Another approach could diff --git a/notebooker/web/static/notebooker/one_click_notebooks.css b/notebooker/web/static/notebooker/one_click_notebooks.css index 9d2f89a6..c6ec6bb0 100644 --- a/notebooker/web/static/notebooker/one_click_notebooks.css +++ b/notebooker/web/static/notebooker/one_click_notebooks.css @@ -95,3 +95,7 @@ td button i { .resultHeader { margin-left: 1em !important; } + +#resultsIframe { + height: 100%; +} \ No newline at end of file diff --git a/notebooker/web/static/notebooker/scheduler.js b/notebooker/web/static/notebooker/scheduler.js index 7a775ce7..39ca800e 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, mailfrom: row.params.mailfrom, cronSchedule: row.cron_schedule, + is_slideshow: row.params.is_slideshow, } ); setScheduleModalMode("Modify"); @@ -167,6 +168,7 @@ function handleAddButtonClick() { mailto: "", mailfrom: "", cronSchedule: "", + is_slideshow: "", } ); setScheduleModalMode("Add"); @@ -269,6 +271,7 @@ $(document).ready(() => { mailfrom: formObj.mailfrom, generate_pdf: formObj.generate_pdf, hide_code: formObj.hide_code, + is_slideshow: formObj.is_slideshow, }, success(data, status, request) { if (data.status === 'Failed') { diff --git a/notebooker/web/templates/run_report.html b/notebooker/web/templates/run_report.html index 0e6cb6b0..7cc263ae 100644 --- a/notebooker/web/templates/run_report.html +++ b/notebooker/web/templates/run_report.html @@ -54,11 +54,19 @@

Email from (optional):

-
- - +
+
+ + +
+
+
+
+ + +
diff --git a/notebooker/web/templates/scheduler.html b/notebooker/web/templates/scheduler.html index d0925159..5c933601 100644 --- a/notebooker/web/templates/scheduler.html +++ b/notebooker/web/templates/scheduler.html @@ -75,6 +75,12 @@
+
+
+ + +
+