diff --git a/rocky/reports/forms.py b/rocky/reports/forms.py index 9388d4fe4f4..58ec6a06956 100644 --- a/rocky/reports/forms.py +++ b/rocky/reports/forms.py @@ -40,10 +40,11 @@ class ReportScheduleStartDateChoiceForm(BaseRockyForm): class ReportScheduleStartDateForm(BaseRockyForm): start_date = forms.DateField( - label="", - widget=DateInput(format="%Y-%m-%d"), + label=_("Start date"), + widget=DateInput(format="%Y-%m-%d", attrs={"form": "generate_report"}), initial=lambda: datetime.now(tz=timezone.utc).date(), required=True, + input_formats=["%Y-%m-%d"], ) @@ -59,8 +60,8 @@ class ReportRecurrenceChoiceForm(BaseRockyForm): class ReportScheduleRecurrenceForm(BaseRockyForm): recurrence = forms.ChoiceField( - label="", - required=False, + label=_("Recurrence"), + required=True, widget=forms.Select(attrs={"form": "generate_report"}), choices=[("daily", _("Daily")), ("weekly", _("Weekly")), ("monthly", _("Monthly")), ("yearly", _("Yearly"))], ) diff --git a/rocky/reports/templates/partials/export_report_settings.html b/rocky/reports/templates/partials/export_report_settings.html index e012a3dc819..09e5384b0ae 100644 --- a/rocky/reports/templates/partials/export_report_settings.html +++ b/rocky/reports/templates/partials/export_report_settings.html @@ -15,33 +15,42 @@

{% translate "Report schedule" %}

or monthly. If you need the report just for a single occasion, select the one-time option. {% endblocktranslate %}

- {% include "partials/return_button.html" with btn_text="Change selection" %} -
{% csrf_token %} {% include "forms/report_form_fields.html" %} - -

{% translate "Recurrence" %}

{% include "partials/form/fieldset.html" with fields=report_schedule_form_recurrence_choice %} {% if is_scheduled_report %} - {% include "partials/form/fieldset.html" with fields=report_schedule_form_recurrence %} +

+ {% blocktranslate trimmed %} + The date you select will be the reference date for the data set for your report. + Please allow for up to 24 hours for your report to be ready. + {% endblocktranslate %} +

+
+
+ {% include "partials/form/fieldset.html" with fields=report_schedule_form_start_date %} + +
+
+ {% include "partials/form/fieldset.html" with fields=report_schedule_form_recurrence %} +
+
{% endif %}
{% csrf_token %} {% include "forms/report_form_fields.html" %} - {% if show_listed_report_names %} + {% if not is_scheduled_report %} {% include "partials/report_names_header.html" %} {% include "partials/report_names_form.html" %} - {% endif %} - {% if is_scheduled_report %} + {% else %} {% include "partials/report_names_header.html" with recurrence=True %} {% include "partials/form/fieldset.html" with fields=report_parent_name_form %} diff --git a/rocky/reports/views/base.py b/rocky/reports/views/base.py index f03947015a9..dcb5a42d167 100644 --- a/rocky/reports/views/base.py +++ b/rocky/reports/views/base.py @@ -254,12 +254,8 @@ def get_available_report_types(self) -> tuple[list[dict[str, str]] | dict[str, l def get_observed_at(self): return self.observed_at if self.observed_at < datetime.now(timezone.utc) else datetime.now(timezone.utc) - def show_report_names(self) -> bool: - recurrence_choice = self.request.POST.get("choose_recurrence", "once") - return recurrence_choice == "once" - def is_scheduled_report(self) -> bool: - recurrence_choice = self.request.POST.get("choose_recurrence", "") + recurrence_choice = self.request.POST.get("choose_recurrence", "once") return recurrence_choice == "repeat" def create_report_recipe(self, report_name_format: str, subreport_name_format: str, schedule: str) -> ReportRecipe: @@ -442,7 +438,6 @@ class ReportFinalSettingsView(BaseReportView, ReportBreadcrumbs, SchedulerView, report_type: type[BaseReport] | None = None task_type = "report" is_a_scheduled_report = False - show_listes_report_names = False def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: if not self.get_report_type_ids(): @@ -450,7 +445,6 @@ def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: return PostRedirect(self.get_previous()) self.is_a_scheduled_report = self.is_scheduled_report() - self.show_listes_report_names = self.show_report_names() return super().get(request, *args, **kwargs) @@ -496,13 +490,13 @@ def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["reports"] = self.get_report_names() + context["report_schedule_form_start_date"] = self.get_report_schedule_form_start_date() context["report_schedule_form_recurrence_choice"] = self.get_report_schedule_form_recurrence_choice() context["report_schedule_form_recurrence"] = self.get_report_schedule_form_recurrence() context["report_parent_name_form"] = self.get_report_parent_name_form() context["report_child_name_form"] = self.get_report_child_name_form() - context["show_listed_report_names"] = self.show_listes_report_names context["is_scheduled_report"] = self.is_a_scheduled_report context["created_at"] = datetime.now() @@ -517,7 +511,7 @@ def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: report_names = request.POST.getlist("report_name", []) reference_dates = request.POST.getlist("reference_date") - if self.show_report_names() and report_names: + if not self.is_scheduled_report() and report_names: final_report_names = list(zip(old_report_names, self.finalise_report_names(report_names, reference_dates))) report_ooi = self.save_report(final_report_names) @@ -531,12 +525,13 @@ def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: subreport_name_format = request.POST.get("child_report_name", "") recurrence = request.POST.get("recurrence", "") + deadline_at = request.POST.get("start_date", datetime.now(timezone.utc).date()) schedule = self.convert_recurrence_to_cron_expressions(recurrence) report_recipe = self.create_report_recipe(report_name_format, subreport_name_format, schedule) - self.create_report_schedule(report_recipe) + self.create_report_schedule(report_recipe, deadline_at) return redirect(reverse("scheduled_reports", kwargs={"organization_code": self.organization.code})) diff --git a/rocky/rocky/locale/django.pot b/rocky/rocky/locale/django.pot index 37ec61502d4..41c7d24c5d9 100644 --- a/rocky/rocky/locale/django.pot +++ b/rocky/rocky/locale/django.pot @@ -2396,6 +2396,10 @@ msgstr "" msgid "Different date" msgstr "" +#: reports/forms.py +msgid "Start date" +msgstr "" + #: reports/forms.py msgid "No, just once" msgstr "" @@ -2404,6 +2408,11 @@ msgstr "" msgid "Yes, repeat" msgstr "" +#: reports/forms.py +#: reports/templates/report_overview/scheduled_reports_table.html +msgid "Recurrence" +msgstr "" + #: reports/forms.py msgid "Daily" msgstr "" @@ -2420,10 +2429,6 @@ msgstr "" msgid "Yearly" msgstr "" -#: reports/forms.py -msgid "Start date" -msgstr "" - #: reports/forms.py msgid "day" msgstr "" @@ -3675,8 +3680,9 @@ msgid "" msgstr "" #: reports/templates/partials/export_report_settings.html -#: reports/templates/report_overview/scheduled_reports_table.html -msgid "Recurrence" +msgid "" +"The date you select will be the reference date for the data set for your " +"report. Please allow for up to 24 hours for your report to be ready." msgstr "" #: reports/templates/partials/export_report_settings.html @@ -3747,6 +3753,7 @@ msgid "Report names:" msgstr "" #: reports/templates/partials/report_names_form.html +#: rocky/templates/partials/form/field_input.html msgid "(Required)" msgstr "" @@ -6125,12 +6132,6 @@ msgstr "" msgid "Please enable plugin to start scanning." msgstr "" -#: rocky/templates/partials/form/field_input.html -#: rocky/templates/partials/form/field_input_checkbox.html -#: rocky/templates/partials/form/field_input_radio.html -msgid "This field is required" -msgstr "" - #: rocky/templates/partials/form/field_input.html msgid "Not set" msgstr "" @@ -6143,6 +6144,12 @@ msgstr "" msgid "Forgot password" msgstr "" +#: rocky/templates/partials/form/field_input.html +#: rocky/templates/partials/form/field_input_checkbox.html +#: rocky/templates/partials/form/field_input_radio.html +msgid "This field is required" +msgstr "" + #: rocky/templates/partials/form/field_input_errors.html #: rocky/templates/partials/form/form_errors.html #: rocky/templates/partials/notifications_block.html diff --git a/rocky/rocky/scheduler.py b/rocky/rocky/scheduler.py index b3cdbb5238d..e417b8fa696 100644 --- a/rocky/rocky/scheduler.py +++ b/rocky/rocky/scheduler.py @@ -151,6 +151,7 @@ class ScheduleRequest(BaseModel): scheduler_id: str data: dict schedule: str + deadline_at: str class ScheduleResponse(BaseModel): diff --git a/rocky/rocky/templates/oois/ooi_add.html b/rocky/rocky/templates/oois/ooi_add.html index 0d186fd792e..03ab88811f0 100644 --- a/rocky/rocky/templates/oois/ooi_add.html +++ b/rocky/rocky/templates/oois/ooi_add.html @@ -11,11 +11,10 @@

{% blocktranslate %}Add a {{ display_type }}{% endblocktranslate %}

{% translate "Here you can add the asset of the client. Findings can be added to these in the findings page." %}

- + {% csrf_token %} {% if form.non_field_errors %}
{{ form.non_field_errors }}
{% endif %}
- {% translate type %}
diff --git a/rocky/rocky/templates/oois/ooi_edit.html b/rocky/rocky/templates/oois/ooi_edit.html index 82c900d8def..ac6d1c2461e 100644 --- a/rocky/rocky/templates/oois/ooi_edit.html +++ b/rocky/rocky/templates/oois/ooi_edit.html @@ -11,11 +11,10 @@

{% blocktranslate %}Edit {{ type }}: {{ ooi_human_readable }}{% endblocktranslate %}

{% blocktranslate %}Primary key fields cannot be edited.{% endblocktranslate %}

- + {% csrf_token %} {% if form.non_field_errors %}
{{ form.non_field_errors }}
{% endif %}
- {% translate type %}
diff --git a/rocky/rocky/templates/partials/form/field_input.html b/rocky/rocky/templates/partials/form/field_input.html index 7c55eb623d5..c521f0e25ba 100644 --- a/rocky/rocky/templates/partials/form/field_input.html +++ b/rocky/rocky/templates/partials/form/field_input.html @@ -1,13 +1,16 @@ {% load i18n %}
- {{ field.label_tag }} + {% if field.field.required %} + + {% else %} + {{ field.label_tag }} + {% endif %}

{{ field.field.widget.attrs.description }}

{% if form_view != "vertical" %}
- {% if field.field.required %} - {% translate "This field is required" %} - {% endif %}
{% if not field.field.widget.attrs.fixed_paws %} {{ field }} @@ -35,7 +38,9 @@ {% else %}
{% if field.field.required %} - {% translate "This field is required" %} + {% endif %}
{{ field }} diff --git a/rocky/rocky/views/scheduler.py b/rocky/rocky/views/scheduler.py index 1527d0864c4..cdb47b5ae51 100644 --- a/rocky/rocky/views/scheduler.py +++ b/rocky/rocky/views/scheduler.py @@ -97,13 +97,13 @@ def get_report_schedule_form_start_date_choice(self): return self.report_schedule_form_start_date_choice(self.request.POST) def get_report_schedule_form_start_date(self): - return self.report_schedule_form_start_date(self.request.POST) + return self.report_schedule_form_start_date() def get_report_schedule_form_recurrence_choice(self): return self.report_schedule_form_recurrence_choice(self.request.POST) def get_report_schedule_form_recurrence(self): - return self.report_schedule_form_recurrence(self.request.POST) + return self.report_schedule_form_recurrence() def get_report_parent_name_form(self): return self.report_parent_name_form() @@ -120,14 +120,17 @@ def get_task_details(self, task_id: str) -> Task | None: return task - def create_report_schedule(self, report_recipe: ReportRecipe) -> ScheduleResponse | None: + def create_report_schedule(self, report_recipe: ReportRecipe, deadline_at: str) -> ScheduleResponse | None: try: report_task = ReportTask( organisation_id=self.organization.code, report_recipe_id=str(report_recipe.recipe_id) ).model_dump() schedule_request = ScheduleRequest( - scheduler_id=self.scheduler_id, data=report_task, schedule=report_recipe.cron_expression + scheduler_id=self.scheduler_id, + data=report_task, + schedule=report_recipe.cron_expression, + deadline_at=deadline_at, ) submit_schedule = self.scheduler_client.post_schedule(schedule=schedule_request) diff --git a/rocky/tests/reports/test_aggregate_report_flow.py b/rocky/tests/reports/test_aggregate_report_flow.py index 7fc3ad095a4..e415dfb8047 100644 --- a/rocky/tests/reports/test_aggregate_report_flow.py +++ b/rocky/tests/reports/test_aggregate_report_flow.py @@ -200,7 +200,7 @@ def test_save_aggregate_report_view( mock_bytes_client, ): """ - Will send data through post to aggregate report. + Will send data through post to aggregate report and immediately creates a report (not scheduled). """ katalogus_mocker = mocker.patch("reports.views.base.get_katalogus")() @@ -235,6 +235,55 @@ def test_save_aggregate_report_view( assert "report_id=Report" in response.url +def test_save_aggregate_report_view_scheduled( + rf, + client_member, + valid_time, + mock_organization_view_octopoes, + listed_hostnames, + rocky_health, + mocker, + boefje_dns_records, + mock_bytes_client, +): + """ + Will send data through post to aggregate report and creates a scheduled aggregate report. + """ + + katalogus_mocker = mocker.patch("reports.views.base.get_katalogus")() + katalogus_mocker.get_plugins.return_value = [boefje_dns_records] + + rocky_health_mocker = mocker.patch("reports.report_types.aggregate_organisation_report.report.get_rocky_health")() + rocky_health_mocker.return_value = rocky_health + + mock_bytes_client().upload_raw.return_value = "Report|1730b72f-b115-412e-ad44-dae6ab3edff9" + + mock_organization_view_octopoes().list_objects.return_value = Paginated[OOIType]( + count=len(listed_hostnames), items=listed_hostnames + ) + + request = setup_request( + rf.post( + "aggregate_report_save", + { + "observed_at": valid_time.strftime("%Y-%m-%d"), + "ooi": listed_hostnames, + "report_type": ["systems-report", "vulnerability-report"], + "choose_recurrence": "repeat", + "start_date": "2024-01-01", + "recurrence": "weekly", + "parent_report_name": ["Scheduled Aggregate Report %x"], + }, + ), + client_member.user, + ) + + response = SaveAggregateReportView.as_view()(request, organization_code=client_member.organization.code) + + assert response.status_code == 302 # after post follows redirect, this to first create report ID + assert response.url == f"/en/{client_member.organization.code}/reports/scheduled-reports/" + + def test_json_download_aggregate_report( rf, client_member, diff --git a/rocky/tests/reports/test_generate_report_flow.py b/rocky/tests/reports/test_generate_report_flow.py index 50dca3bbd55..ff3dc8d346d 100644 --- a/rocky/tests/reports/test_generate_report_flow.py +++ b/rocky/tests/reports/test_generate_report_flow.py @@ -224,3 +224,48 @@ def test_save_generate_report_view( assert response.status_code == 302 # after post follows redirect, this to first create report ID assert "report_id=Report" in response.url + + +def test_save_generate_report_view_scheduled( + rf, + client_member, + valid_time, + mock_organization_view_octopoes, + listed_hostnames, + mocker, + boefje_dns_records, + mock_bytes_client, +): + """ + Will send data through post to generate report with schedule. + """ + + katalogus_mocker = mocker.patch("reports.views.base.get_katalogus")() + katalogus_mocker.get_plugins.return_value = [boefje_dns_records] + + mock_bytes_client().upload_raw.return_value = "Report|e821aaeb-a6bd-427f-b064-e46837911a5d" + + mock_organization_view_octopoes().list_objects.return_value = Paginated[OOIType]( + count=len(listed_hostnames), items=listed_hostnames + ) + + request = setup_request( + rf.post( + "generate_report_view", + { + "observed_at": valid_time.strftime("%Y-%m-%d"), + "ooi": listed_hostnames, + "report_type": "dns-report", + "choose_recurrence": "repeat", + "start_date": "2024-01-01", + "recurrence": "daily", + "parent_report_name": [f"DNS report for {len(listed_hostnames)} objects"], + }, + ), + client_member.user, + ) + + response = SaveGenerateReportView.as_view()(request, organization_code=client_member.organization.code) + + assert response.status_code == 302 # after post follows redirect, this to first create report ID + assert response.url == f"/en/{client_member.organization.code}/reports/scheduled-reports/" diff --git a/rocky/tests/test_upload_raw.py b/rocky/tests/test_upload_raw.py index 1d29f2c9421..83d6d352e1d 100644 --- a/rocky/tests/test_upload_raw.py +++ b/rocky/tests/test_upload_raw.py @@ -36,7 +36,7 @@ def test_upload_empty(rf, redteam_member, mock_organization_view_octopoes, mock_ assert response.status_code == 200 mock_bytes_client().upload_raw.assert_not_called() - assertContains(response, "This field is required") + assertContains(response, "(Required)") def test_upload_raw(rf, redteam_member, mock_organization_view_octopoes, mock_bytes_client, network):