diff --git a/netbox/extras/forms/reports.py b/netbox/extras/forms/reports.py new file mode 100644 index 000000000..658796bb5 --- /dev/null +++ b/netbox/extras/forms/reports.py @@ -0,0 +1,16 @@ +from django import forms + +from utilities.forms import BootstrapMixin, DateTimePicker + +__all__ = ( + 'ReportForm', +) + + +class ReportForm(BootstrapMixin, forms.Form): + schedule_at = forms.DateTimeField( + required=False, + widget=DateTimePicker(), + label="Schedule at", + help_text="Schedule execution of report to a set time", + ) \ No newline at end of file diff --git a/netbox/extras/forms/scripts.py b/netbox/extras/forms/scripts.py index 380b4364c..de55a3ee6 100644 --- a/netbox/extras/forms/scripts.py +++ b/netbox/extras/forms/scripts.py @@ -1,6 +1,6 @@ from django import forms -from utilities.forms import BootstrapMixin +from utilities.forms import BootstrapMixin, DateTimePicker __all__ = ( 'ScriptForm', @@ -14,17 +14,25 @@ class ScriptForm(BootstrapMixin, forms.Form): label="Commit changes", help_text="Commit changes to the database (uncheck for a dry-run)" ) + _schedule_at = forms.DateTimeField( + required=False, + widget=DateTimePicker(), + label="Schedule at", + help_text="Schedule execution of script to a set time", + ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - # Move _commit to the end of the form + # Move _commit and _schedule_at to the end of the form + schedule_at = self.fields.pop('_schedule_at') commit = self.fields.pop('_commit') + self.fields['_schedule_at'] = schedule_at self.fields['_commit'] = commit @property def requires_input(self): """ - A boolean indicating whether the form requires user input (ignore the _commit field). + A boolean indicating whether the form requires user input (ignore the _commit and _schedule_at fields). """ - return bool(len(self.fields) > 1) + return bool(len(self.fields) > 2) diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index 4873a1f9e..1d3c142c7 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -550,7 +550,11 @@ class JobResult(models.Model): ) queue = django_rq.get_queue("default") - queue.enqueue(func, job_id=str(job_result.job_id), job_result=job_result, **kwargs) + + if schedule_at := kwargs.pop("schedule_at", None): + queue.enqueue_at(schedule_at, func, job_id=str(job_result.job_id), job_result=job_result, **kwargs) + else: + queue.enqueue(func, job_id=str(job_result.job_id), job_result=job_result, **kwargs) return job_result diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 5b589c181..073496773 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -15,6 +15,7 @@ from utilities.utils import copy_safe_request, count_related, get_viewname, norm from utilities.views import ContentTypePermissionRequiredMixin from . import filtersets, forms, tables from .choices import JobResultStatusChoices +from .forms.reports import ReportForm from .models import * from .reports import get_report, get_reports, run_report from .scripts import get_scripts, run_script @@ -562,7 +563,7 @@ class ReportView(ContentTypePermissionRequiredMixin, View): return render(request, 'extras/report.html', { 'report': report, - 'run_form': ConfirmationForm(), + 'form': ReportForm(), }) def post(self, request, module, name): @@ -575,6 +576,12 @@ class ReportView(ContentTypePermissionRequiredMixin, View): if report is None: raise Http404 + schedule_at = None + form = ReportForm(request.POST) + + if form.is_valid(): + schedule_at = form.cleaned_data.get("schedule_at") + # Allow execution only if RQ worker process is running if not Worker.count(get_connection('default')): messages.error(request, "Unable to run report: RQ worker process not running.") @@ -589,7 +596,8 @@ class ReportView(ContentTypePermissionRequiredMixin, View): report.full_name, report_content_type, request.user, - job_timeout=report.job_timeout + job_timeout=report.job_timeout, + schedule_at=schedule_at, ) return redirect('extras:report_result', job_result_pk=job_result.pk) @@ -707,6 +715,7 @@ class ScriptView(ContentTypePermissionRequiredMixin, GetScriptMixin, View): elif form.is_valid(): commit = form.cleaned_data.pop('_commit') + schedule_at = form.cleaned_data.pop("_schedule_at") script_content_type = ContentType.objects.get(app_label='extras', model='script') @@ -719,6 +728,7 @@ class ScriptView(ContentTypePermissionRequiredMixin, GetScriptMixin, View): request=copy_safe_request(request), commit=commit, job_timeout=script.job_timeout, + schedule_at=schedule_at, ) return redirect('extras:script_result', job_result_pk=job_result.pk) diff --git a/netbox/project-static/package.json b/netbox/project-static/package.json index 256696947..808a49825 100644 --- a/netbox/project-static/package.json +++ b/netbox/project-static/package.json @@ -28,7 +28,7 @@ "clipboard": "^2.0.8", "color2k": "^1.2.4", "dayjs": "^1.10.4", - "flatpickr": "4.6.3", + "flatpickr": "4.6.13", "htmx.org": "^1.6.1", "just-debounce-it": "^1.4.0", "masonry-layout": "^4.2.2", diff --git a/netbox/templates/extras/report.html b/netbox/templates/extras/report.html index 391de6614..94f37571b 100644 --- a/netbox/templates/extras/report.html +++ b/netbox/templates/extras/report.html @@ -1,5 +1,6 @@ {% extends 'generic/object.html' %} {% load helpers %} +{% load form_helpers %} {% block title %}{{ report.name }}{% endblock %} @@ -33,18 +34,24 @@ {% block content %}