From dbcb28ffc458d686edee07b90b93bdff898e6524 Mon Sep 17 00:00:00 2001 From: Roelof Korporaal Date: Thu, 14 Nov 2024 18:29:20 +0100 Subject: [PATCH] Add bulk actions on report overview (#3777) Co-authored-by: Rieven Co-authored-by: ammar92 Co-authored-by: stephanie0x00 <9821756+stephanie0x00@users.noreply.github.com> Co-authored-by: Jan Klopper --- rocky/assets/js/grabSelectionOnModalOpen.js | 66 ++++ rocky/assets/js/reportActionForms.js | 210 ++++++++++ rocky/reports/report_types/definitions.py | 3 + .../modal_partials/delete_modal.html | 55 +++ .../modal_partials/rename_modal.html | 42 ++ .../modal_partials/rerun_modal.html | 49 +++ .../modal_partials/share_modal.html | 43 ++ .../report_overview/report_history_table.html | 367 +++++++++++------- rocky/reports/views/report_overview.py | 212 +++++++++- rocky/rocky/locale/django.pot | 118 +++++- rocky/tests/conftest.py | 335 ++++++++++++++++ rocky/tests/reports/test_report_overview.py | 136 +++++++ 12 files changed, 1485 insertions(+), 151 deletions(-) create mode 100644 rocky/assets/js/grabSelectionOnModalOpen.js create mode 100644 rocky/assets/js/reportActionForms.js create mode 100644 rocky/reports/templates/report_overview/modal_partials/delete_modal.html create mode 100644 rocky/reports/templates/report_overview/modal_partials/rename_modal.html create mode 100644 rocky/reports/templates/report_overview/modal_partials/rerun_modal.html create mode 100644 rocky/reports/templates/report_overview/modal_partials/share_modal.html create mode 100644 rocky/tests/reports/test_report_overview.py diff --git a/rocky/assets/js/grabSelectionOnModalOpen.js b/rocky/assets/js/grabSelectionOnModalOpen.js new file mode 100644 index 00000000000..838dd21b740 --- /dev/null +++ b/rocky/assets/js/grabSelectionOnModalOpen.js @@ -0,0 +1,66 @@ +import { onDomReady } from "./imports/utils.js"; +import { + renderRenameSelection, + renderDeleteSelection, + renderRerunSelection, +} from "./reportActionForms.js"; + +onDomReady(function () { + if (getSelection().length > 0) { + openDialogFromUrl(getAnchor()); + } else { + closeDialog(getAnchor()); + } +}); + +export function openDialogFromUrl(anchor) { + // If ID is present in the URL on DomReady, open the dialog immediately. + let id = window.location.hash.slice(1); + + if (id) { + let modal = document.querySelector("#" + id); + + if (anchor == "rename-modal") { + renderRenameSelection(modal, getSelection()); + } + if (anchor == "delete-modal") { + renderDeleteSelection(modal, getSelection()); + } + if (anchor == "rerun-modal") { + renderRerunSelection(modal, getSelection()); + } + } +} + +export function closeDialog(anchor) { + // If ID is present in the URL on DomReady, open the dialog immediately. + let id = window.location.hash.slice(1); + + if (id) { + let modal = document.querySelector("#" + id); + modal.close(); + } + + let baseUrl = window.location.toString().split("#")[0]; + window.history.pushState("", "Base URL", baseUrl); +} + +export function getSelection() { + let checkedItems = document.querySelectorAll(".report-checkbox:checked"); + return checkedItems; +} + +addEventListener("hashchange", function () { + if (getSelection().length > 0) { + openDialogFromUrl(getAnchor()); + } else { + closeDialog(getAnchor()); + } +}); + +function getAnchor() { + let currentUrl = document.URL; + let urlParts = currentUrl.split("#"); + + return urlParts.length > 1 ? urlParts[1] : null; +} diff --git a/rocky/assets/js/reportActionForms.js b/rocky/assets/js/reportActionForms.js new file mode 100644 index 00000000000..3f2523cc5a4 --- /dev/null +++ b/rocky/assets/js/reportActionForms.js @@ -0,0 +1,210 @@ +export function renderRenameSelection(modal, selection) { + let report_names = getReportNames(selection); + let report_types = getReportTypes(selection); + let references = []; + + selection.forEach((input_element) => { + references.push(input_element.value); + }); + + let table_element = document.getElementById("rename-table"); + let table_body = table_element.querySelector("tbody"); + let table_row = table_element.querySelector("tr.rename-table-row"); + + table_body.innerHTML = ""; + + for (let i = 0; i < references.length; i++) { + let table_row_copy = table_row.cloneNode(true); + + let type_ul = table_row_copy.querySelector("td.type ul"); + let name_input_element = table_row_copy.querySelector(".report-name-input"); + let reference_input_element = table_row_copy.querySelector( + ".report-reference-input", + ); + // let date_td = table_row_copy.querySelector("td.date"); + + name_input_element.setAttribute("value", report_names[i]); + reference_input_element.setAttribute("value", references[i]); + + type_ul.innerHTML = report_types[i]; + // date_td.innerText = "date"; + + table_body.appendChild(table_row_copy); + } +} + +export function renderDeleteSelection(modal, selection) { + let report_names = getReportNames(selection); + let report_types = getReportTypes(selection); + let reference_dates = getReportReferenceDates(selection); + let creation_dates = getReportCreationDates(selection); + let report_oois = getReportOOIs(selection); + let references = []; + + selection.forEach((input_element) => { + references.push(input_element.value); + }); + + let table_element = document.getElementById("delete-table"); + let table_body = table_element.querySelector("tbody"); + let table_row = table_element.querySelector("tr.delete-table-row"); + + table_body.innerHTML = ""; + + for (let i = 0; i < references.length; i++) { + let table_row_copy = table_row.cloneNode(true); + + let reference_input_element = table_row_copy.querySelector( + ".report-reference-input", + ); + + let type_ul = table_row_copy.querySelector("td.type ul"); + let reference_date_td = table_row_copy.querySelector("td.reference_date"); + let creation_date_td = table_row_copy.querySelector("td.creation_date"); + let ooi_td = table_row_copy.querySelector("td.input_objects"); + let name_span = table_row_copy.querySelector("td.name span.name-holder"); + + name_span.innerText = report_names[i]; + reference_input_element.setAttribute("value", references[i]); + + type_ul.innerHTML = report_types[i]; + reference_date_td.innerText = reference_dates[i]; + creation_date_td.innerText = creation_dates[i]; + ooi_td.innerHTML = report_oois[i]; + + table_body.appendChild(table_row_copy); + } +} + +export function renderRerunSelection(modal, selection) { + let report_names = getReportNames(selection); + let references = []; + let report_types = getReportTypes(selection); + let reference_dates = getReportReferenceDates(selection); + let creation_dates = getReportCreationDates(selection); + let report_oois = getReportOOIs(selection); + + selection.forEach((input_element) => { + references.push(input_element.value); + }); + + let table_element = document.getElementById("rerun-table"); + let table_body = table_element.querySelector("tbody"); + let table_row = table_element.querySelector("tr.rerun-table-row"); + + table_body.innerHTML = ""; + + for (let i = 0; i < references.length; i++) { + let table_row_copy = table_row.cloneNode(true); + + let reference_input_element = table_row_copy.querySelector( + ".report-reference-input", + ); + + let type_ul = table_row_copy.querySelector("td.type ul"); + let reference_date_td = table_row_copy.querySelector("td.reference_date"); + let creation_date_td = table_row_copy.querySelector("td.creation_date"); + let ooi_td = table_row_copy.querySelector("td.input_objects"); + let name_span = table_row_copy.querySelector("td.name span.name-holder"); + + name_span.innerText = report_names[i]; + reference_input_element.setAttribute("value", references[i]); + + type_ul.innerHTML = report_types[i]; + reference_date_td.innerText = reference_dates[i]; + creation_date_td.innerText = creation_dates[i]; + ooi_td.innerHTML = report_oois[i]; + + table_body.appendChild(table_row_copy); + } +} + +export function getReportNames(selection) { + let report_names = []; + + for (let i = 0; i < selection.length; i++) { + let report_name = selection[i] + .closest("tr") + ?.querySelector("td.report_name a")?.innerText; + + if (!report_name) { + continue; + } + + report_names.push(report_name); + } + + return report_names; +} + +export function getReportTypes(selection) { + let report_types_list = []; + + for (let i = 0; i < selection.length; i++) { + let report_types = selection[i] + .closest("tr") + ?.querySelector("td.report_types ul.tags")?.innerHTML; + + if (!report_types) { + continue; + } + + report_types_list.push(report_types); + } + + return report_types_list; +} + +export function getReportOOIs(selection) { + let report_oois_list = []; + + for (let i = 0; i < selection.length; i++) { + let report_oois = selection[i] + .closest("tr") + ?.querySelector("td.report_oois")?.innerHTML; + + if (!report_oois) { + continue; + } + + report_oois_list.push(report_oois); + } + + return report_oois_list; +} + +export function getReportReferenceDates(selection) { + let reference_dates = []; + + for (let i = 0; i < selection.length; i++) { + let reference_date = selection[i] + .closest("tr") + ?.querySelector("td.report_reference_date")?.innerHTML; + + if (!reference_date) { + continue; + } + + reference_dates.push(reference_date); + } + + return reference_dates; +} + +export function getReportCreationDates(selection) { + let creation_dates = []; + + for (let i = 0; i < selection.length; i++) { + let creation_date = selection[i] + .closest("tr") + ?.querySelector("td.report_creation_date")?.innerHTML; + + if (!creation_date) { + continue; + } + + creation_dates.push(creation_date); + } + + return creation_dates; +} diff --git a/rocky/reports/report_types/definitions.py b/rocky/reports/report_types/definitions.py index 6c21727b11b..112d6807c80 100644 --- a/rocky/reports/report_types/definitions.py +++ b/rocky/reports/report_types/definitions.py @@ -42,6 +42,9 @@ class BaseReport: def __init__(self, octopoes_api_connector: OctopoesAPIConnector): self.octopoes_api_connector = octopoes_api_connector + def collect_data(self, input_oois: Iterable[str], valid_time: datetime) -> dict[str, dict[str, Any]]: + raise NotImplementedError + @classmethod def class_attributes(cls) -> dict[str, Any]: return { diff --git a/rocky/reports/templates/report_overview/modal_partials/delete_modal.html b/rocky/reports/templates/report_overview/modal_partials/delete_modal.html new file mode 100644 index 00000000000..6020559b179 --- /dev/null +++ b/rocky/reports/templates/report_overview/modal_partials/delete_modal.html @@ -0,0 +1,55 @@ +{% load i18n %} + +{% component 'modal' size='dialog-large' modal_id='delete-modal' %} +{% fill 'header' %}{% translate "Delete the following report(s):" %}{% endfill %} +{% fill 'content' %} +

+ {% blocktranslate %} + Deleted reports are removed in the view from the moment of deletion. The report can still be accessed on timestamps before the deletion. Only the report is removed from the view, not the data it is based on. + {% endblocktranslate %} +

+

+ {% blocktranslate %} + It is still possible to generate a new report for same date. If the report is part of a combined report, it will remain available in the combined report. + {% endblocktranslate %} +

+
+ {% csrf_token %} + + + + {% comment %} {% endcomment %} + + + + + + + + + + + + + + + + +
{% translate "Report type" %}{% translate "Name" %}{% translate "Type" %}{% translate "Input objects" %}{% translate "Reference date" %}{% translate "Creation date" %}
+ + + +
    +
+
+
+{% endfill %} +{% fill "footer_buttons" %} + +{% endfill %} +{% endcomponent %} +{% component_css_dependencies %} diff --git a/rocky/reports/templates/report_overview/modal_partials/rename_modal.html b/rocky/reports/templates/report_overview/modal_partials/rename_modal.html new file mode 100644 index 00000000000..67cb6a6ef1a --- /dev/null +++ b/rocky/reports/templates/report_overview/modal_partials/rename_modal.html @@ -0,0 +1,42 @@ +{% load i18n %} + +{% component "modal" size="dialog-large" modal_id="rename-modal" %} +{% fill "header" %} +{% translate "Rename the following report(s):" %} +{% endfill %} +{% fill "content" %} +
+ {% csrf_token %} + + + + + + {% comment %} {% endcomment %} + + + + + + + {% comment %} {% endcomment %} + + +
{% translate "Report type" %}{% translate "Name" %}{% translate "Reference date" %}
+
    +
+
+ + +
+
+{% endfill %} +{% fill "footer_buttons" %} + +{% endfill %} +{% endcomponent %} +{% component_css_dependencies %} diff --git a/rocky/reports/templates/report_overview/modal_partials/rerun_modal.html b/rocky/reports/templates/report_overview/modal_partials/rerun_modal.html new file mode 100644 index 00000000000..666f56227c6 --- /dev/null +++ b/rocky/reports/templates/report_overview/modal_partials/rerun_modal.html @@ -0,0 +1,49 @@ +{% load i18n %} + +{% component 'modal' size='dialog-large' modal_id='rerun-modal' %} +{% fill 'header' %}{% translate "Rerun the following report(s):" %}{% endfill %} +{% fill 'content' %} +

+ {% blocktranslate %} + By submitting you're generating the selected reports again, using the current data. + {% endblocktranslate %} +

+
+ {% csrf_token %} + + + + + + + + + + + + + + + + + + + +
{% translate "Name" %}{% translate "Type" %}{% translate "Input objects" %}{% translate "Reference date" %}{% translate "Creation date" %}
+ + + +
    +
+
+
+{% endfill %} +{% fill "footer_buttons" %} + +{% endfill %} +{% endcomponent %} +{% component_css_dependencies %} diff --git a/rocky/reports/templates/report_overview/modal_partials/share_modal.html b/rocky/reports/templates/report_overview/modal_partials/share_modal.html new file mode 100644 index 00000000000..c465b49a988 --- /dev/null +++ b/rocky/reports/templates/report_overview/modal_partials/share_modal.html @@ -0,0 +1,43 @@ +{% load i18n %} + +{% component 'modal' size='dialog-large' modal_id='delete-modal' %} +{% fill 'header' %}{% translate "Delete the following report(s):" %}{% endfill %} +{% fill 'content' %} +

+ {% blocktranslate %} + Deleted reports are removed in the view from the moment of deletion. The report can still be accessed on timestamps before the deletion. Only the report is removed from the view, not the data it is based on. + {% endblocktranslate %} +

+

+ {% blocktranslate %} + It is still possible to generate a new report for same date. If the report is part of a combined report, it will remain available in the combined report. + {% endblocktranslate %} +

+
+ {% csrf_token %} + + + + {% comment %} {% endcomment %} + + {% comment %} {% endcomment %} + + + + + {% comment %} {% endcomment %} + + {% comment %} {% endcomment %} + + +
{% translate "Report type" %}{% translate "Name" %}{% translate "Reference date" %}
+ + +
+
+{% endfill %} +{% fill "footer_buttons" %} + +{% endfill %} +{% endcomponent %} +{% component_css_dependencies %} diff --git a/rocky/reports/templates/report_overview/report_history_table.html b/rocky/reports/templates/report_overview/report_history_table.html index 48f78251e39..5fbcd3526d8 100644 --- a/rocky/reports/templates/report_overview/report_history_table.html +++ b/rocky/reports/templates/report_overview/report_history_table.html @@ -1,151 +1,236 @@ {% load i18n %} +{% load static %} {% load ooi_extra %} {% load report_extra %} +{% load component_tags %} +{% load compress %} -{% if reports %} -

- {% blocktranslate with length=reports|length total=total_oois %}Showing {{ length }} of {{ total }} reports{% endblocktranslate %} -

-
- - - - - - - - - - {% if report.children_reports %}{% endif %} - - - - {% for report in reports %} - - - + + {% endif %} + {% endfor %} + +
{% translate "Reports:" %}
{% translate "Name" %}{% translate "Report type" %}{% translate "Input objects" %}{% translate "Reference date" %}{% translate "Creation date" %}
- {{ report.parent_report.name }} - -
    - {% if report.parent_report.report_type == "concatenated-report" or report.parent_report.report_type is None %} - {% for report_type, total_objects in report.report_type_summary.items %} - {% if forloop.counter0 < 2 %} -
  • {{ report_type|get_report_type_name }}
  • - {% endif %} - {% if forloop.counter0 == 2 %} -
  • + {{ report.report_type_summary|length|add:"-2" }}
  • +
    + + {% include "report_overview/modal_partials/rerun_modal.html" %} + {% include "report_overview/modal_partials/rename_modal.html" %} + {% include "report_overview/modal_partials/delete_modal.html" %} + +
    +
    + {% if reports %} +

    + {% blocktranslate with length=reports|length total=total_oois %}Showing {{ length }} of {{ total }} reports{% endblocktranslate %} +

    +
    +
    + + + + + + + + + + + {% if report.children_reports %}{% endif %} + + + + {% for report in reports %} + + + + + + + + {% if report.children_reports %} + {% endif %} - - - - - - {% if report.children_reports %} - - {% endif %} - - {% if report.children_reports %} - - + + + + + + + {% for report_type, total_objects in report.report_type_summary.items %} + + + + + {% endfor %} + +
    {% translate "Reports:" %}
    + + {% translate "Name" %}{% translate "Report type" %}{% translate "Input Objects" %}{% translate "Reference date" %}{% translate "Creation date" %}
    + + + {{ report.parent_report.name }} + +
      + {% if report.parent_report.report_type == "concatenated-report" or report.parent_report.report_type is None %} + {% for report_type, total_objects in report.report_type_summary.items %} + {% if forloop.counter0 < 2 %} +
    • {{ report_type|get_report_type_name }}
    • + {% endif %} + {% if forloop.counter0 == 2 %} +
    • + {{ report.report_type_summary|length|add:"-2" }}
    • + {% endif %} + {% endfor %} + {% else %} +
    • + {{ report.parent_report.report_type|get_report_type_name }} +
    • {% endif %} - {% endfor %} - {% else %} -
    • - {{ report.parent_report.report_type|get_report_type_name }} -
    • +
    +
    + {% if report.parent_report.input_oois|length == 1 %} + {{ report.parent_report.input_oois.0|human_readable }} + {% elif report.total_objects == 1 %} + {{ report.children_reports.0.input_oois.0|human_readable }} + {% else %} + {{ report.total_objects }} + {% endif %} + {{ report.parent_report.observed_at|date }}{{ report.parent_report.date_generated }} + + - {% if report.parent_report.input_oois|length == 1 %} - {{ report.parent_report.input_oois.0|human_readable }} - {% elif report.total_objects == 1 %} - {{ report.children_reports.0.input_oois.0|human_readable }} - {% else %} - {{ report.total_objects }} - {% endif %} - {{ report.parent_report.observed_at|date }}{{ report.parent_report.date_generated }} - -
    - - -
    {% translate "Report types" %}
    -

    - {% blocktranslate count counter=report.total_children_reports %} + + {% if report.children_reports %} +

    + - - {% endif %} - {% endfor %} - -
    {% translate "Subreports details:" %}
    + + +
    {% translate "Report types" %}
    +

    + {% blocktranslate count counter=report.total_children_reports %} This report consist of {{counter}} subreport with the following report type and object. {% plural %} This report consist of {{counter}} subreports with the following report types and objects. {% endblocktranslate %} -

    - - - - - - - - {% for report_type, total_objects in report.report_type_summary.items %} - - - - - {% endfor %} - -
    {% translate "Subreports details:" %}
    {% translate "Report type" %}{% translate "Objects" %}
    -
      -
    • {{ report_type|get_report_type_name }}
    • -
    -
    {{ total_objects }}
    - -
    - {% translate "Subreports" %} - ({{ report.children_reports|length }}/{{ report.total_children_reports }}) -
    - - - - - - - - - {% for child_report in report.children_reports %} - - - - - - {% endfor %} - -
    {% translate "Report type" %}{% translate "Object" %}{% translate "Report name" %}
    -
      -
    • - {{ child_report.report_type|get_report_type_name }} -
    • -
    -
    - {{ child_report.input_oois.0|human_readable }} - - {{ child_report.name }} -
    -
    - {% if report.total_children_reports > 5 %} - {% translate "View all subreports" %} - {% endif %} -
    -
    - -{% else %} -

    {% translate "No reports have been generated yet." %}

    -{% endif %} +

    +
    {% translate "Report type" %}{% translate "Objects" %}
    +
      +
    • {{ report_type|get_report_type_name }}
    • +
    +
    {{ total_objects }}
    + +
    + {% translate "Subreports" %} + ({{ report.children_reports|length }}/{{ report.total_children_reports }}) +
    + + + + + + + + + {% for child_report in report.children_reports %} + + + + + + {% endfor %} + +
    {% translate "Report type" %}{% translate "Object" %}{% translate "Report name" %}
    +
      +
    • + {{ child_report.report_type|get_report_type_name }} +
    • +
    +
    + {{ child_report.input_oois.0|human_readable }} + + {{ child_report.name }} +
    +
    + {% if report.total_children_reports > 5 %} + {% translate "View all subreports" %} + {% endif %} +
    +
+ +
+ {% else %} +

{% translate "No reports have been generated yet." %}

+ {% endif %} + +{% block html_at_end_body %} + {% compress js %} + + + + {% endcompress %} +{% endblock html_at_end_body %} diff --git a/rocky/reports/views/report_overview.py b/rocky/reports/views/report_overview.py index 46171a1ffa2..e6ff232ed2c 100644 --- a/rocky/reports/views/report_overview.py +++ b/rocky/reports/views/report_overview.py @@ -1,16 +1,21 @@ -from datetime import datetime +from datetime import datetime, timezone from typing import Any +from uuid import uuid4 import structlog from django.contrib import messages from django.urls import reverse from django.utils.translation import gettext_lazy as _ from django.views.generic import ListView +from pydantic import TypeAdapter, ValidationError +from tools.ooi_helpers import create_ooi -from octopoes.models import Reference +from octopoes.models import OOI, Reference from octopoes.models.exception import ObjectNotFoundException from octopoes.models.ooi.reports import Report, ReportRecipe -from reports.views.base import ReportBreadcrumbs, get_selection +from reports.report_types.aggregate_organisation_report.report import aggregate_reports +from reports.report_types.helpers import get_report_by_id +from reports.views.base import ReportBreadcrumbs, ReportDataDict, get_selection from rocky.paginator import RockyPaginator from rocky.views.mixins import OctopoesView, ReportList from rocky.views.scheduler import SchedulerView @@ -93,12 +98,213 @@ class ReportHistoryView(BreadcrumbsReportOverviewView, OctopoesView, ListView): paginator = RockyPaginator template_name = "report_overview/report_history.html" + def post(self, request, *args, **kwargs): + try: + self.run_bulk_actions() + except (ObjectNotFoundException, ValidationError): + messages.error(request, _("An unexpected error occurred, please check logs for more info.")) + return self.get(request, *args, **kwargs) + def get_queryset(self) -> ReportList: return ReportList(self.octopoes_api_connector, valid_time=self.observed_at) + def get_report_ooi(self, ooi_pk: str) -> Report: + return self.octopoes_api_connector.get(Reference.from_str(f"{ooi_pk}"), valid_time=datetime.now(timezone.utc)) + + def run_bulk_actions(self) -> None: + action = self.request.POST.get("action", "") + report_references = self.request.POST.getlist("report_reference", []) + + if action == "rename": + return self.rename_reports(report_references) + + if action == "delete": + return self.delete_reports(report_references) + + if action == "rerun": + return self.rerun_reports(report_references) + + def delete_reports(self, report_references: list[Reference]) -> None: + self.octopoes_api_connector.delete_many(report_references, datetime.now(timezone.utc)) + messages.success(self.request, _("Deletion successful.")) + + def rerun_reports(self, report_references: list[str]) -> None: + for report_id in report_references: + actual_report_ooi = self.get_report_ooi(report_id) + + # First create new parent report and then create all subreports with new parent reference + if actual_report_ooi.report_type == "concatenated-report": + self.rerun_concatenated_report(actual_report_ooi) + + elif actual_report_ooi.report_type == "aggregate-organisation-report": + self.rerun_aggregate_report(actual_report_ooi) + + elif actual_report_ooi.report_type == "multi-organization-report": + return messages.warning( + self.request, + _( + "Multi organization reports cannot be rescheduled. " + "It consists of imported data from different organizations and not based on new generated data." + ), + ) + + else: + self.rerun_single_report(actual_report_ooi) + messages.success(self.request, _("Rerun successful")) + + def get_input_data(self, report_ooi: Report) -> dict[str, Any]: + self.bytes_client.login() + + report_data = TypeAdapter(Any, config={"arbitrary_types_allowed": True}).validate_json( + self.bytes_client.get_raw(raw_id=report_ooi.data_raw_id) + ) + + return { + "input_data": { + "input_oois": report_data["input_data"]["input_oois"], + "report_types": report_data["input_data"]["report_types"], + "plugins": report_data["input_data"]["plugins"], + } + } + + def get_input_oois(self, ooi_pks: list[str]) -> list[OOI]: + return [ + self.octopoes_api_connector.get(Reference.from_str(ooi), valid_time=self.observed_at) for ooi in ooi_pks + ] + + def recreate_report( + self, report_ooi: Report, observed_at: datetime, bytes_id: str, parent_report_ooi: Report | None = None + ) -> Report: + """Recreate a report with new UUID, observed_at and (bytes) data.""" + + new_report_ooi = Report( + name=report_ooi.name, + report_type=report_ooi.report_type, + template=report_ooi.template, + report_id=uuid4(), + organization_code=report_ooi.organization_code, + organization_name=report_ooi.organization_name, + organization_tags=report_ooi.organization_tags, + data_raw_id=bytes_id, + date_generated=observed_at, + input_oois=report_ooi.input_oois, + observed_at=observed_at, + parent_report=report_ooi.parent_report if parent_report_ooi is None else parent_report_ooi.reference, + report_recipe=report_ooi.report_recipe, + has_parent=report_ooi.has_parent if parent_report_ooi is None else True, + ) + + create_ooi(self.octopoes_api_connector, self.bytes_client, new_report_ooi, observed_at) + + return new_report_ooi + + def rerun_single_report(self, report_ooi: Report) -> None: + observed_at = datetime.now(timezone.utc) + report_input_data = self.get_input_data(report_ooi) + report_type = get_report_by_id(report_ooi.report_type) + + report_data = report_type(self.octopoes_api_connector).collect_data(report_ooi.input_oois, observed_at) + bytes_id = self.bytes_client.upload_raw( + raw=ReportDataDict({"report_data": report_data} | report_input_data).model_dump_json().encode(), + manual_mime_types={"openkat/report"}, + ) + self.recreate_report(report_ooi, observed_at, bytes_id) + + def rerun_aggregate_report(self, report_ooi: Report) -> None: + observed_at = datetime.now(timezone.utc) + report_input_data = self.get_input_data(report_ooi) + report_types = report_input_data["input_data"]["report_types"] + + _, post_processed_data, _, _ = aggregate_reports( + self.octopoes_api_connector, + self.get_input_oois(report_ooi.input_oois), + report_types, + observed_at, + report_ooi.organization_code, + ) + + bytes_id = self.bytes_client.upload_raw( + raw=ReportDataDict(post_processed_data | report_input_data).model_dump_json().encode(), + manual_mime_types={"openkat/report"}, + ) + + self.recreate_report(report_ooi, observed_at, bytes_id) + + def rerun_concatenated_report(self, report_ooi: Report) -> None: + observed_at = datetime.now(timezone.utc) + report_input_data = self.get_input_data(report_ooi) + + bytes_id = self.bytes_client.upload_raw( + raw=ReportDataDict(report_input_data).model_dump_json().encode(), manual_mime_types={"openkat/report"} + ) + + parent_ooi = self.recreate_report(report_ooi, observed_at, bytes_id) + + subreports = self.octopoes_api_connector.query( + "Report. None: + report_names = self.request.POST.getlist("report_name", []) + error_reports = [] + + if not report_references or not report_names: + messages.error(self.request, _("Renaming failed. Empty report name found.")) + + if len(report_references) != len(report_names): + messages.error(self.request, _("Report names and reports does not match.")) + + for index, report_id in enumerate(report_references): + report_ooi = self.get_report_ooi(report_id) + report_ooi.name = report_names[index] + + try: + create_ooi(self.octopoes_api_connector, self.bytes_client, report_ooi, datetime.now(timezone.utc)) + except ValidationError: + error_reports.append(f'"{report_ooi.name}"') + + if not error_reports: + return messages.success(self.request, _("Reports successfully renamed.")) + + return messages.error(self.request, _("Report {} could not be renamed.").format(", ".join(error_reports))) + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["total_oois"] = len(self.object_list) + context["selected_reports"] = self.request.GET.getlist("report", []) return context diff --git a/rocky/rocky/locale/django.pot b/rocky/rocky/locale/django.pot index d396aab827b..245aa42c98a 100644 --- a/rocky/rocky/locale/django.pot +++ b/rocky/rocky/locale/django.pot @@ -32,6 +32,10 @@ msgstr "" #: reports/report_types/dns_report/report.html #: reports/report_types/tls_report/report.html #: reports/templates/partials/report_names_form.html +#: reports/templates/report_overview/modal_partials/delete_modal.html +#: reports/templates/report_overview/modal_partials/rename_modal.html +#: reports/templates/report_overview/modal_partials/rerun_modal.html +#: reports/templates/report_overview/modal_partials/share_modal.html #: reports/templates/report_overview/report_history_table.html #: reports/templates/report_overview/scheduled_reports_table.html #: reports/templates/report_overview/subreports_table.html @@ -1117,6 +1121,7 @@ msgid "Plugin description" msgstr "" #: katalogus/templates/partials/plugins.html +#: reports/templates/report_overview/report_history_table.html #: rocky/templates/organizations/organization_list.html #: rocky/templates/organizations/organization_settings.html msgid "Actions" @@ -1258,6 +1263,8 @@ msgid "Boefje ID" msgstr "" #: katalogus/templates/plugin_container_image.html +#: reports/templates/report_overview/modal_partials/delete_modal.html +#: reports/templates/report_overview/modal_partials/rerun_modal.html #: reports/templates/report_overview/report_history_table.html #: reports/templates/report_overview/scheduled_reports_table.html #: reports/templates/report_overview/subreports_table.html @@ -1357,6 +1364,8 @@ msgstr "" #: katalogus/templates/plugin_settings_delete.html #: katalogus/views/plugin_settings_delete.py +#: reports/templates/report_overview/modal_partials/delete_modal.html +#: reports/templates/report_overview/report_history_table.html #: rocky/templates/admin/delete_confirmation.html rocky/views/ooi_delete.py msgid "Delete" msgstr "" @@ -2023,6 +2032,8 @@ msgstr "" #: onboarding/templates/step_3c_setup_scan_ooi_detail.html #: reports/report_types/dns_report/report.html #: reports/templates/partials/report_ooi_list.html +#: reports/templates/report_overview/modal_partials/delete_modal.html +#: reports/templates/report_overview/modal_partials/rerun_modal.html #: reports/templates/summary/ooi_selection.html tools/forms/ooi.py #: rocky/templates/oois/ooi_list.html #: rocky/templates/partials/elements/ooi_tree_condensed_table.html @@ -2487,6 +2498,7 @@ msgstr "" #: reports/report_types/aggregate_organisation_report/appendix.html #: reports/templates/partials/plugin_overview_table.html +#: reports/templates/report_overview/modal_partials/rename_modal.html #: reports/templates/report_overview/report_history_table.html #: reports/templates/report_overview/scheduled_reports_table.html #: reports/templates/report_overview/subreports_table.html @@ -4097,6 +4109,70 @@ msgstr "" msgid "%(btn_text)s" msgstr "" +#: reports/templates/report_overview/modal_partials/delete_modal.html +#: reports/templates/report_overview/modal_partials/share_modal.html +msgid "Delete the following report(s):" +msgstr "" + +#: reports/templates/report_overview/modal_partials/delete_modal.html +#: reports/templates/report_overview/modal_partials/share_modal.html +msgid "" +"\n" +" Deleted reports are removed in the view from the moment of deletion. " +"The report can still be accessed on timestamps before the deletion. Only the " +"report is removed from the view, not the data it is based on.\n" +" " +msgstr "" + +#: reports/templates/report_overview/modal_partials/delete_modal.html +#: reports/templates/report_overview/modal_partials/share_modal.html +msgid "" +"\n" +" It is still possible to generate a new report for same date. If the " +"report is part of a combined report, it will remain available in the " +"combined report.\n" +" " +msgstr "" + +#: reports/templates/report_overview/modal_partials/delete_modal.html +#: reports/templates/report_overview/modal_partials/rerun_modal.html +msgid "Input objects" +msgstr "" + +#: reports/templates/report_overview/modal_partials/delete_modal.html +#: reports/templates/report_overview/modal_partials/rerun_modal.html +#: reports/templates/report_overview/report_history_table.html +#: reports/templates/report_overview/scheduled_reports_table.html +#: reports/templates/report_overview/subreports_table.html +msgid "Reference date" +msgstr "" + +#: reports/templates/report_overview/modal_partials/rename_modal.html +msgid "Rename the following report(s):" +msgstr "" + +#: reports/templates/report_overview/modal_partials/rename_modal.html +#: reports/templates/report_overview/report_history_table.html +msgid "Rename" +msgstr "" + +#: reports/templates/report_overview/modal_partials/rerun_modal.html +msgid "Rerun the following report(s):" +msgstr "" + +#: reports/templates/report_overview/modal_partials/rerun_modal.html +msgid "" +"\n" +" By submitting you're generating the selected reports again, using the " +"current data.\n" +" " +msgstr "" + +#: reports/templates/report_overview/modal_partials/rerun_modal.html +#: reports/templates/report_overview/report_history_table.html +msgid "Rerun" +msgstr "" + #: reports/templates/report_overview/report_history.html msgid "Reports history" msgstr "" @@ -4118,13 +4194,7 @@ msgid "Reports:" msgstr "" #: reports/templates/report_overview/report_history_table.html -msgid "Input objects" -msgstr "" - -#: reports/templates/report_overview/report_history_table.html -#: reports/templates/report_overview/scheduled_reports_table.html -#: reports/templates/report_overview/subreports_table.html -msgid "Reference date" +msgid "Input Objects" msgstr "" #: reports/templates/report_overview/report_history_table.html @@ -4399,6 +4469,40 @@ msgstr "" msgid "View report" msgstr "" +#: reports/views/report_overview.py +msgid "An unexpected error occurred, please check logs for more info." +msgstr "" + +#: reports/views/report_overview.py +msgid "Deletion successful." +msgstr "" + +#: reports/views/report_overview.py +msgid "" +"Multiorganization report cannot go through a rerun. It consists of imported " +"data from different organizations and not based on new generated data." +msgstr "" + +#: reports/views/report_overview.py +msgid "Rerun successful" +msgstr "" + +#: reports/views/report_overview.py +msgid "Renaming failed. Empty report name found." +msgstr "" + +#: reports/views/report_overview.py +msgid "Report names and reports does not match." +msgstr "" + +#: reports/views/report_overview.py +msgid "Reports successfully renamed." +msgstr "" + +#: reports/views/report_overview.py +msgid "Report {} could not be renamed." +msgstr "" + #: reports/views/view_helpers.py msgid "1: Select objects" msgstr "" diff --git a/rocky/tests/conftest.py b/rocky/tests/conftest.py index 97a9832e9a9..f73c57d106b 100644 --- a/rocky/tests/conftest.py +++ b/rocky/tests/conftest.py @@ -2005,3 +2005,338 @@ def multi_report_ooi(report_data_ooi_org_a, report_data_ooi_org_b): report_recipe=None, has_parent=False, ) + + +@pytest.fixture +def report_list(): + report_list: Paginated[tuple[Report, list[Report | None]]] = Paginated( + count=3, + items=[ + ( + Report( + object_type="Report", + scan_profile=None, + user_id=None, + primary_key="Report|f22f5195-3fde-44d1-b0d5-f74c1b27c984", + name="Findings Report for minvws.nl", + report_type="findings-report", + template="findings_report/report.html", + date_generated=datetime(2024, 11, 7, 15, 33, 55, 999999), + input_oois=["Hostname|internet|minvws.nl"], + report_id=UUID("f22f5195-3fde-44d1-b0d5-f74c1b27c984"), + organization_code="test", + organization_name="Test Organization", + organization_tags=[], + data_raw_id="3300354d-530f-4ecf-8485-e120f43ba3f1", + observed_at=datetime(2024, 11, 7, 23, 59, 59, 999999), + parent_report=None, + report_recipe=None, + has_parent=False, + ), + [], + ), + ( + Report( + object_type="Report", + scan_profile=None, + user_id=None, + primary_key="Report|c0e2c072-c57e-484c-b283-f4e0d1a2132c", + name="Aggregate Report", + report_type="aggregate-organisation-report", + template="aggregate_organisation_report/report.html", + date_generated=datetime(2024, 11, 7, 15, 32, 29, 999999), + input_oois=["Hostname|internet|minvws.nl"], + report_id=UUID("c0e2c072-c57e-484c-b283-f4e0d1a2132c"), + organization_code="test", + organization_name="Test Organization", + organization_tags=[], + data_raw_id="7e888ca9-cebc-4d6e-9f2c-5b45fa7101d4", + observed_at=datetime(2024, 11, 7, 15, 32, 29, 999999), + parent_report=None, + report_recipe=None, + has_parent=False, + ), + [], + ), + ( + Report( + object_type="Report", + scan_profile=None, + user_id=None, + primary_key="Report|99b5d510-d886-43a7-a29e-9973ddccfe67", + name="Concatenated Report for minvws.nl", + report_type="concatenated-report", + template="report.html", + date_generated=datetime(2024, 11, 7, 14, 12, 4, 999999), + input_oois=["Hostname|internet|minvws.nl"], + report_id=UUID("99b5d510-d886-43a7-a29e-9973ddccfe67"), + organization_code="test", + organization_name="Test Organization", + organization_tags=[], + data_raw_id="eb5c1226-7ab0-4e3f-8b41-7ab7016fa3fd", + observed_at=datetime(2024, 11, 7, 23, 59, 59, 999999), + parent_report=None, + report_recipe=None, + has_parent=False, + ), + [ + Report( + object_type="Report", + scan_profile=None, + user_id=None, + primary_key="Report|27f71380-7154-49af-846d-6b44bc944ec6", + name="Safe Connections Report for minvws.nl", + report_type="safe-connections-report", + template="safe_connections_report/report.html", + date_generated=datetime(2024, 11, 7, 14, 12, 18, 999999), + input_oois=["Hostname|internet|minvws.nl"], + report_id=UUID("27f71380-7154-49af-846d-6b44bc944ec6"), + organization_code="test", + organization_name="Test Organization", + organization_tags=[], + data_raw_id="27e8fa60-4675-4c22-b7a7-76152fc520b8", + observed_at=datetime(2024, 11, 7, 23, 59, 59, 999999), + parent_report=Reference("Report|99b5d510-d886-43a7-a29e-9973ddccfe67"), + report_recipe=None, + has_parent=True, + ), + Report( + object_type="Report", + scan_profile=None, + user_id=None, + primary_key="Report|4c627e99-76bf-4bcc-b1be-9b3e42a1e5b4", + name="DNS Report for minvws.nl", + report_type="dns-report", + template="dns_report/report.html", + date_generated=datetime(2024, 11, 7, 14, 12, 6, 999999), + input_oois=["Hostname|internet|minvws.nl"], + report_id=UUID("4c627e99-76bf-4bcc-b1be-9b3e42a1e5b4"), + organization_code="test", + organization_name="Test Organization", + organization_tags=[], + data_raw_id="775d62df-edf9-4c19-91cc-2cc1586a8111", + observed_at=datetime(2024, 11, 7, 23, 59, 59, 999999), + parent_report=Reference("Report|99b5d510-d886-43a7-a29e-9973ddccfe67"), + report_recipe=None, + has_parent=True, + ), + Report( + object_type="Report", + scan_profile=None, + user_id=None, + primary_key="Report|546f82ed-a6f1-4d06-8716-9c88fa0c5a79", + name="Name Server Report for minvws.nl", + report_type="name-server-report", + template="name_server_report/report.html", + date_generated=datetime(2024, 11, 7, 14, 12, 13, 999999), + input_oois=["Hostname|internet|minvws.nl"], + report_id=UUID("546f82ed-a6f1-4d06-8716-9c88fa0c5a79"), + organization_code="test", + organization_name="Test Organization", + organization_tags=[], + data_raw_id="6ea4268f-f8c9-4ccd-9f81-efb2bf60b215", + observed_at=datetime(2024, 11, 7, 23, 59, 59, 999999), + parent_report=Reference("Report|99b5d510-d886-43a7-a29e-9973ddccfe67"), + report_recipe=None, + has_parent=True, + ), + Report( + object_type="Report", + scan_profile=None, + user_id=None, + primary_key="Report|59d57dbd-c486-4ad4-9e72-287ecace5a7a", + name="Vulnerability Report for minvws.nl", + report_type="vulnerability-report", + template="vulnerability_report/report.html", + date_generated=datetime(2024, 11, 7, 14, 12, 21, 999999), + input_oois=["Hostname|internet|minvws.nl"], + report_id=UUID("59d57dbd-c486-4ad4-9e72-287ecace5a7a"), + organization_code="test", + organization_name="Test Organization", + organization_tags=[], + data_raw_id="efa648ef-7724-41cd-96c0-2c0d48b631bc", + observed_at=datetime(2024, 11, 7, 23, 59, 59, 999999), + parent_report=Reference("Report|99b5d510-d886-43a7-a29e-9973ddccfe67"), + report_recipe=None, + has_parent=True, + ), + Report( + object_type="Report", + scan_profile=None, + user_id=None, + primary_key="Report|65c12422-0663-466c-8beb-cf4aff891852", + name="Web System Report for minvws.nl", + report_type="web-system-report", + template="web_system_report/report.html", + date_generated=datetime(2024, 11, 7, 14, 12, 22, 999999), + input_oois=["Hostname|internet|minvws.nl"], + report_id=UUID("65c12422-0663-466c-8beb-cf4aff891852"), + organization_code="test", + organization_name="Test Organization", + organization_tags=[], + data_raw_id="d2623e9f-3f56-4c4f-b01c-abf4a6f5794d", + observed_at=datetime(2024, 11, 7, 23, 59, 59, 999999), + parent_report=Reference("Report|99b5d510-d886-43a7-a29e-9973ddccfe67"), + report_recipe=None, + has_parent=True, + ), + Report( + object_type="Report", + scan_profile=None, + user_id=None, + primary_key="Report|6f431e79-c913-447f-abf8-5b3b2e4ddd04", + name="Mail Report for minvws.nl", + report_type="mail-report", + template="mail_report/report.html", + date_generated=datetime(2024, 11, 7, 14, 12, 11, 999999), + input_oois=["Hostname|internet|minvws.nl"], + report_id=UUID("6f431e79-c913-447f-abf8-5b3b2e4ddd04"), + organization_code="test", + organization_name="Test Organization", + organization_tags=[], + data_raw_id="ba8e864a-770f-4a18-90d4-d7b8fba054ac", + observed_at=datetime(2024, 11, 7, 23, 59, 59, 999999), + parent_report=Reference("Report|99b5d510-d886-43a7-a29e-9973ddccfe67"), + report_recipe=None, + has_parent=True, + ), + Report( + object_type="Report", + scan_profile=None, + user_id=None, + primary_key="Report|9546466a-cb7d-4086-94cb-4f15aa2d8200", + name="System Report for minvws.nl", + report_type="systems-report", + template="systems_report/report.html", + date_generated=datetime(2024, 11, 7, 14, 12, 19, 999999), + input_oois=["Hostname|internet|minvws.nl"], + report_id=UUID("9546466a-cb7d-4086-94cb-4f15aa2d8200"), + organization_code="test", + organization_name="Test Organization", + organization_tags=[], + data_raw_id="771190cb-a570-4ddf-bf10-2b7cb6d7b852", + observed_at=datetime(2024, 11, 7, 23, 59, 59, 999999), + parent_report=Reference("Report|99b5d510-d886-43a7-a29e-9973ddccfe67"), + report_recipe=None, + has_parent=True, + ), + Report( + object_type="Report", + scan_profile=None, + user_id=None, + primary_key="Report|a5c8f206-cc74-4271-bd1a-231f9499e001", + name="IPv6 Report for minvws.nl", + report_type="ipv6-report", + template="ipv6_report/report.html", + date_generated=datetime(2024, 11, 7, 14, 12, 9, 999999), + input_oois=["Hostname|internet|minvws.nl"], + report_id=UUID("a5c8f206-cc74-4271-bd1a-231f9499e001"), + organization_code="test", + organization_name="Test Organization", + organization_tags=[], + data_raw_id="ef007438-6266-40c5-981b-a59a5e59d2a4", + observed_at=datetime(2024, 11, 7, 23, 59, 59, 999999), + parent_report=Reference("Report|99b5d510-d886-43a7-a29e-9973ddccfe67"), + report_recipe=None, + has_parent=True, + ), + Report( + object_type="Report", + scan_profile=None, + user_id=None, + primary_key="Report|c7095b19-9367-4432-96de-675caa51b621", + name="Open Ports Report for minvws.nl", + report_type="open-ports-report", + template="open_ports_report/report.html", + date_generated=datetime(2024, 11, 7, 14, 12, 14, 999999), + input_oois=["Hostname|internet|minvws.nl"], + report_id=UUID("c7095b19-9367-4432-96de-675caa51b621"), + organization_code="test", + organization_name="Test Organization", + organization_tags=[], + data_raw_id="e545a488-de8a-4750-8056-6a3b354d011f", + observed_at=datetime(2024, 11, 7, 23, 59, 59, 999999), + parent_report=Reference("Report|99b5d510-d886-43a7-a29e-9973ddccfe67"), + report_recipe=None, + has_parent=True, + ), + Report( + object_type="Report", + scan_profile=None, + user_id=None, + primary_key="Report|ec3592c3-1258-49a0-a76c-0c361f31d3ab", + name="Findings Report for minvws.nl", + report_type="findings-report", + template="findings_report/report.html", + date_generated=datetime(2024, 11, 7, 14, 12, 8, 999999), + input_oois=["Hostname|internet|minvws.nl"], + report_id=UUID("ec3592c3-1258-49a0-a76c-0c361f31d3ab"), + organization_code="test", + organization_name="Test Organization", + organization_tags=[], + data_raw_id="af8c8999-530d-45d5-a8b8-c823ee3c24b7", + observed_at=datetime(2024, 11, 7, 23, 59, 59, 999999), + parent_report=Reference("Report|99b5d510-d886-43a7-a29e-9973ddccfe67"), + report_recipe=None, + has_parent=True, + ), + Report( + object_type="Report", + scan_profile=None, + user_id=None, + primary_key="Report|f12bf3e3-0e51-4ab8-bb99-5b7dba3d9a6c", + name="RPKI Report for minvws.nl", + report_type="rpki-report", + template="rpki_report/report.html", + date_generated=datetime(2024, 11, 7, 14, 12, 16, 999999), + input_oois=["Hostname|internet|minvws.nl"], + report_id=UUID("f12bf3e3-0e51-4ab8-bb99-5b7dba3d9a6c"), + organization_code="test", + organization_name="Test Organization", + organization_tags=[], + data_raw_id="7b305f0d-c0a7-4ad5-af1e-31f81fc229c2", + observed_at=datetime(2024, 11, 7, 23, 59, 59, 999999), + parent_report=Reference("Report|99b5d510-d886-43a7-a29e-9973ddccfe67"), + report_recipe=None, + has_parent=True, + ), + ], + ), + ], + ) + return report_list + + +@pytest.fixture +def get_report_input_data_from_bytes(): + input_data = { + "input_data": { + "input_oois": ["Hostname|internet|minvws.nl"], + "report_types": [ + "ipv6-report", + "mail-report", + "name-server-report", + "open-ports-report", + "rpki-report", + "safe-connections-report", + "systems-report", + "vulnerability-report", + "web-system-report", + ], + "plugins": { + "required": [ + "rpki", + "webpage-analysis", + "ssl-certificates", + "security_txt_downloader", + "testssl-sh-ciphers", + "dns-records", + "dns-sec", + "ssl-version", + "nmap", + ], + "optional": ["masscan", "shodan", "nmap-ip-range", "nmap-udp", "nmap-ports"], + }, + } + } + return json.dumps(input_data).encode("utf-8") diff --git a/rocky/tests/reports/test_report_overview.py b/rocky/tests/reports/test_report_overview.py new file mode 100644 index 00000000000..c4fc478c15e --- /dev/null +++ b/rocky/tests/reports/test_report_overview.py @@ -0,0 +1,136 @@ +from uuid import uuid4 + +from pytest_django.asserts import assertContains +from reports.views.report_overview import ReportHistoryView + +from octopoes.models.exception import ObjectNotFoundException +from tests.conftest import setup_request + + +def test_report_overview_show_reports(rf, client_member, mock_organization_view_octopoes, report_list): + """ + Will send the selected oois to the report type selection page. + """ + + mock_organization_view_octopoes().list_reports.return_value = report_list + + response = ReportHistoryView.as_view()( + setup_request(rf.get("report_history"), client_member.user), organization_code=client_member.organization.code + ) + + assert response.status_code == 200 + + assertContains(response, "Showing 3 of 3 reports") + + +def test_report_overview_rename_reports( + rf, client_member, mock_organization_view_octopoes, mock_bytes_client, report_list +): + """ + Renames a report + """ + + mock_organization_view_octopoes().list_reports.return_value = report_list + report, subreports = report_list.items[0] + mock_organization_view_octopoes().get.return_value = report + + request = setup_request( + rf.post( + "report_history", + { + "action": "rename", + "report_name": "This is the new report name for testing", + "report_reference": report.primary_key, + }, + ), + client_member.user, + ) + + response = ReportHistoryView.as_view()(request, organization_code=client_member.organization.code) + + assert response.status_code == 200 + + assert list(request._messages)[0].message == "Reports successfully renamed." + + assertContains(response, "This is the new report name for testing") + + +def test_report_overview_rename_non_existant_report(rf, client_member, mock_organization_view_octopoes, report_list): + """ + Renames a report + """ + + mock_organization_view_octopoes().list_reports.return_value = report_list + report, subreports = report_list.items[0] + + request = setup_request( + rf.post( + "report_history", + { + "action": "rename", + "report_name": "This is the new report name for testing", + "report_reference": report.primary_key, + }, + ), + client_member.user, + ) + + response = ReportHistoryView.as_view()(request, organization_code=client_member.organization.code) + + mock_organization_view_octopoes().get.side_effect = ObjectNotFoundException("Object not found.") + + assert response.status_code == 200 + + assert ( + list(request._messages)[0].message == 'Report "This is the new report name for testing" could not be renamed.' + ) + + +def test_report_overview_delete_reports(rf, client_member, mock_organization_view_octopoes, report_list): + """ + Deletes a report + """ + + mock_organization_view_octopoes().list_reports.return_value = report_list + report, subreports = report_list.items[0] + mock_organization_view_octopoes().get.return_value = report + + request = setup_request( + rf.post("report_history", {"action": "delete", "report_reference": report.primary_key}), client_member.user + ) + + response = ReportHistoryView.as_view()(request, organization_code=client_member.organization.code) + + assert response.status_code == 200 + + assert list(request._messages)[0].message == "Deletion successful." + + +def test_report_overview_rerun_reports( + rf, client_member, mock_organization_view_octopoes, mock_bytes_client, get_report_input_data_from_bytes, report_list +): + """ + Rerun a report + """ + + mock_organization_view_octopoes().list_reports.return_value = report_list + + concatenated_report, subreports = report_list.items[2] # a concat report + + mock_organization_view_octopoes().get.return_value = concatenated_report + mock_bytes_client().get_raw.return_value = get_report_input_data_from_bytes + mock_bytes_client().upload_raw.return_value = str(uuid4()) + mock_organization_view_octopoes().query.return_value = subreports + + request = setup_request( + rf.post("report_history", {"action": "rerun", "report_reference": concatenated_report.primary_key}), + client_member.user, + ) + + response = ReportHistoryView.as_view()(request, organization_code=client_member.organization.code) + + assert response.status_code == 200 + + assert list(request._messages)[0].message == "Rerun successful" + + assertContains(response, concatenated_report.name)