diff --git a/run_dir/design/project_samples_old.html b/run_dir/design/project_samples_old.html index 60996fa6a..12ad2e874 100644 --- a/run_dir/design/project_samples_old.html +++ b/run_dir/design/project_samples_old.html @@ -135,13 +135,14 @@

User project description - {% if not multiqc %} - - {% else %} - - {% end %} + {% end %} + {% if 'project_summary' in reports %} +

+ Open Project Summary report + {% end %} + {% if 'sample_summary_reports' in reports %} + + {% for sample in reports['sample_summary_reports'] %} + {{ sample}} + {% end %} + {% end %} + + diff --git a/status/bioinfo_analysis.py b/status/bioinfo_analysis.py index bbf98849d..2b267a9de 100644 --- a/status/bioinfo_analysis.py +++ b/status/bioinfo_analysis.py @@ -4,6 +4,7 @@ import dateutil +from status.reports import MultiQCReportHandler from status.util import SafeHandler @@ -282,8 +283,8 @@ def get(self, project_id): if application in app_classes[key]: application = key break - # to check if multiqc report exists (get_multiqc() is defined in util.BaseHandler) - multiqc = self.get_multiqc(project_id) or "" + + multiqc = MultiQCReportHandler.get_multiqc(self.application, project_id) or "" self.write( t.generate( gs_globals=self.application.gs_globals, diff --git a/status/flowcell.py b/status/flowcell.py index 413a38992..67e8dd092 100644 --- a/status/flowcell.py +++ b/status/flowcell.py @@ -470,7 +470,7 @@ def __init__(self, application, request, **kwargs): super(SafeHandler, self).__init__(application, request, **kwargs) def get(self, name): - reports_dir = self.application.minknow_reports_path + reports_dir = os.path.join(self.application.reports_path, "minknow_reports") report_path = os.path.join(reports_dir, f"report_{name}.html") self.write(open(report_path).read()) @@ -483,7 +483,9 @@ def __init__(self, application, request, **kwargs): super(SafeHandler, self).__init__(application, request, **kwargs) def get(self, name): - reports_dir = self.application.toulligqc_reports_path + reports_dir = os.path.join( + self.application.reports_path, "other_reports", "toulligqc_reports" + ) report_path = os.path.join(reports_dir, f"report_{name}.html") self.write(open(report_path).read()) diff --git a/status/multiqc_report.py b/status/multiqc_report.py deleted file mode 100644 index d0fe92008..000000000 --- a/status/multiqc_report.py +++ /dev/null @@ -1,20 +0,0 @@ -from status.util import SafeHandler - - -class MultiQCReportHandler(SafeHandler): - def get(self, project): - type = self.get_argument("type") - # get_multiqc() is defined in BaseHandler - multiqc_report = self.get_multiqc(project) - if multiqc_report: - self.write(multiqc_report[type]) - else: - t = self.application.loader.load("error_page.html") - self.write( - t.generate( - gs_globals=self.application.gs_globals, - status="404", - reason="MultiQC Report Not Found", - user=self.get_current_user(), - ) - ) diff --git a/status/projects.py b/status/projects.py index ac5bf13c7..718f9e3ea 100644 --- a/status/projects.py +++ b/status/projects.py @@ -17,6 +17,11 @@ from ibm_cloud_sdk_core.api_exception import ApiException from zenpy import ZenpyException +from status.reports import ( + MultiQCReportHandler, + ProjectSummaryReportHandler, + SingleCellSampleSummaryReportHandler, +) from status.util import SafeHandler, dthandler lims = lims.Lims(BASEURI, USERNAME, PASSWORD) @@ -689,7 +694,9 @@ def project_info(self, project, view_with_sources=False): "_qc_": "QC MultiQC", "_pipeline_": "Pipeline MultiQC", } - for report_type in self.get_multiqc(project, read_file=False).keys(): + for report_type in MultiQCReportHandler.get_multiqc( + self.application, project, read_file=False + ).keys(): # Attempt to assign a name of the report type, otherwise default to the type itself report_name = type_to_name.get(report_type, report_type) reports[report_name] = f"/multiqc_report/{project}?type={report_type}" @@ -946,8 +953,26 @@ def get(self, project): worksets_view = self.application.worksets_db.view( "project/ws_name", descending=True ) - # to check if multiqc report exists (get_multiqc() is defined in util.BaseHandler) - multiqc = list(self.get_multiqc(project).keys()) + + reports = {} + multiqc = list( + MultiQCReportHandler.get_multiqc( + self.application, project, read_file=False + ).keys() + ) + if multiqc: + reports["multiqc"] = multiqc + if ProjectSummaryReportHandler.get_summary_report( + self.application, project, read_file=False + ): + reports["project_summary"] = True + sample_summary_reports = ( + SingleCellSampleSummaryReportHandler.get_sample_summary_report( + self.application, project + ) + ) + if sample_summary_reports: + reports["sample_summary_reports"] = sample_summary_reports self.write( t.generate( gs_globals=self.application.gs_globals, @@ -958,7 +983,7 @@ def get(self, project): lims_dashboard_url=self.application.settings["lims_dashboard_url"], prettify=prettify_css_names, worksets=worksets_view[project], - multiqc=multiqc, + reports=reports, lims_uri=BASEURI, ) ) diff --git a/status/reports.py b/status/reports.py new file mode 100644 index 000000000..480b0fcd8 --- /dev/null +++ b/status/reports.py @@ -0,0 +1,166 @@ +import os +from typing import Any, Optional, Union + +from status.util import SafeHandler + + +class MultiQCReportHandler(SafeHandler): + def get(self, project: str) -> None: + report_type = self.get_argument("type") + multiqc_report = self.get_multiqc(self.application, project) + if multiqc_report: + self.write(multiqc_report[report_type]) + else: + t = self.application.loader.load("error_page.html") + self.write( + t.generate( + gs_globals=self.application.gs_globals, + status="404", + reason="MultiQC Report Not Found", + user=self.get_current_user(), + ) + ) + + @staticmethod + def get_multiqc( + app: Any, project_id: str, read_file: bool = True + ) -> Union[str, dict, None]: + """ + Getting multiqc reports for requested project from the filesystem + Returns a string containing html if report exists, otherwise None + If read_file is false, the value of the dictionary will be the path to the file + """ + + project_name = "" + multiqc_reports = {} + query_res = app.cloudant.post_view( + db="projects", ddoc="projects", view="id_to_name", key=project_id + ).get_result() + if query_res["rows"]: + project_name = query_res["rows"][0]["value"] + + if project_name: + multiqc_path = os.path.join(app.reports_path, "mqc_reports") or "" + for report_type in ["_", "_qc_", "_pipeline_"]: + multiqc_name = f"{project_name}{report_type}multiqc_report.html" + multiqc_file_path = os.path.join(multiqc_path, multiqc_name) + if os.path.exists(multiqc_file_path): + if read_file: + with open(multiqc_file_path, encoding="utf-8") as multiqc_file: + html = multiqc_file.read() + multiqc_reports[report_type] = html + else: + multiqc_reports[report_type] = multiqc_file_path + return multiqc_reports + + +class ProjectSummaryReportHandler(SafeHandler): + """Handler for project summary reports generated using yggdrasil""" + + def get(self, project_id: str) -> None: + report = self.get_summary_report(self.application, project_id) + if report: + self.write(report) + else: + t = self.application.loader.load("error_page.html") + self.write( + t.generate( + gs_globals=self.application.gs_globals, + status="404", + reason="Project Summary Report Not Found", + user=self.get_current_user(), + ) + ) + + @staticmethod + def get_summary_report( + app: Any, project_id: str, read_file: bool = True + ) -> Union[str, bool, None]: + """If read_file is false, the function will return True if the file exists, otherwise None + If read_file is True, it returns a string containing the report in html if it exists""" + project_name = "" + + query_res = app.cloudant.post_view( + db="projects", ddoc="projects", view="id_to_name", key=project_id + ).get_result() + + if query_res["rows"]: + project_name = query_res["rows"][0]["value"] + + if project_name: + report_path = os.path.join( + app.reports_path, + "yggdrasil", + project_id, + f"{project_name}_project_summary.html", + ) + if os.path.exists(report_path): + if read_file: + with open(report_path, encoding="utf-8") as report_file: + return report_file.read() + else: + return True + else: + return None + + +class SingleCellSampleSummaryReportHandler(SafeHandler): + """Handler for Single Cell sample summary reports generated using yggdrasil""" + + def get(self, project_id: str, sample_id: str) -> None: + report = self.get_sample_summary_report( + self.application, project_id, sample_id=sample_id + ) + if report: + self.set_header("Content-Type", "application/pdf") + self.set_header( + "Content-Disposition", + f"inline; filename={sample_id}_single_cell_sample_summary_report.pdf", + ) + self.write(report) + else: + t = self.application.loader.load("error_page.html") + self.write( + t.generate( + gs_globals=self.application.gs_globals, + status="404", + reason="Single Cell Sample Summary Report Not Found", + user=self.get_current_user(), + ) + ) + + @staticmethod + def get_sample_summary_report( + app: Any, project_id: str, sample_id: Optional[str] = None + ) -> Union[bytes, list[str], None]: + """Returns a list of sample summary reports for the requested project if sample_id is None, + otherwise returns the report for the requested sample""" + + sample_summary_reports_path = os.path.join( + app.reports_path, "yggdrasil", project_id + ) + if sample_id: + report_path = os.path.join( + sample_summary_reports_path, sample_id, f"{sample_id}_report.pdf" + ) + if os.path.exists(report_path): + with open(report_path, "rb") as report_file: + return report_file.read() + else: + return None + + else: + reports = [] + if os.path.exists(sample_summary_reports_path): + for item in os.listdir(sample_summary_reports_path): + if os.path.isdir( + os.path.join(sample_summary_reports_path, item) + ) and item.startswith(f"{project_id}_"): + if os.path.exists( + os.path.join( + sample_summary_reports_path, item, f"{item}_report.pdf" + ) + ): + reports.append(item) + + return reports diff --git a/status/util.py b/status/util.py index 38fb741d4..584527b00 100644 --- a/status/util.py +++ b/status/util.py @@ -159,35 +159,6 @@ def write_error(self, status_code, **kwargs): ) ) - def get_multiqc(self, project_id, read_file=True): - """ - Getting multiqc reports for requested project from the filesystem - Returns a string containing html if report exists, otherwise None - If read_file is false, the value of the dictionary will be the path to the file - """ - view = self.application.projects_db.view("project/id_name_dates") - rows = view[project_id].rows - project_name = "" - multiqc_reports = {} - # get only the first one - for row in rows: - project_name = row.value.get("project_name", "") - break - - if project_name: - multiqc_path = self.application.multiqc_path or "" - for type in ["_", "_qc_", "_pipeline_"]: - multiqc_name = f"{project_name}{type}multiqc_report.html" - multiqc_file_path = os.path.join(multiqc_path, multiqc_name) - if os.path.exists(multiqc_file_path): - if read_file: - with open(multiqc_file_path, encoding="utf-8") as multiqc_file: - html = multiqc_file.read() - multiqc_reports[type] = html - else: - multiqc_reports[type] = multiqc_file_path - return multiqc_reports - @staticmethod def get_user_details(app, user_email): user_details = {} diff --git a/status_app.py b/status_app.py index fb51c651c..43b8f7506 100644 --- a/status_app.py +++ b/status_app.py @@ -65,7 +65,6 @@ SentInvoiceHandler, ) from status.lanes_ordered import LanesOrderedDataHandler, LanesOrderedHandler -from status.multiqc_report import MultiQCReportHandler from status.ngisweden_stats import NGISwedenHandler from status.ont_plot import ONTFlowcellPlotHandler, ONTFlowcellYieldHandler from status.pricing import ( @@ -124,6 +123,11 @@ qPCRPoolsHandler, ) from status.reads_plot import DataFlowcellYieldHandler, FlowcellPlotHandler +from status.reports import ( + MultiQCReportHandler, + ProjectSummaryReportHandler, + SingleCellSampleSummaryReportHandler, +) from status.running_notes import ( LatestStickyNoteHandler, LatestStickyNotesMultipleHandler, @@ -392,6 +396,7 @@ def __init__(self, settings): ("/projects", ProjectsHandler), ("/project_cards", ProjectCardsHandler), ("/proj_meta", ProjMetaCompareHandler), + ("/proj_summary_report/([^/]*)$", ProjectSummaryReportHandler), ("/reads_total/([^/]*)$", ReadsTotalHandler), ("/rec_ctrl_view/([^/]*)$", RecCtrlDataHandler), ("/sample_requirements", SampleRequirementsViewHandler), @@ -399,6 +404,10 @@ def __init__(self, settings): ("/sample_requirements_update", SampleRequirementsUpdateHandler), ("/sensorpush", SensorpushHandler), ("/sequencing_queues", SequencingQueuesHandler), + ( + "/singlecell_sample_summary_report/(P[^/]*)/([^/]*)$", + SingleCellSampleSummaryReportHandler, + ), ("/smartseq3_progress", SmartSeq3ProgressPageHandler), ("/suggestion_box", SuggestionBoxHandler), ("/user_management", UserManagementHandler), @@ -519,14 +528,15 @@ def __init__(self, settings): # to display instruments in the server status self.server_status = settings.get("server_status") - # project summary - multiqc tab - self.multiqc_path = settings.get("multiqc_path") - - # MinKNOW reports - self.minknow_reports_path = settings.get("minknow_reports_path") - - # ToulligQC reports - self.toulligqc_reports_path = settings.get("toulligqc_reports_path") + # project summary - reports tab + # Structure of the reports folder: + # / + # ├── other_reports/ + # │ └── toulligqc_reports/ + # ├── minknow_reports/ + # ├── mqc_reports/ + # └── yggdrasil// + self.reports_path = settings.get("reports_path") # lims backend credentials limsbackend_cred_loc = Path(