From bca760686ad96b988d36ce47ed1a1f02abe3d37f Mon Sep 17 00:00:00 2001 From: Eduardo Silva Date: Wed, 28 Jan 2026 16:48:32 -0300 Subject: [PATCH] Add scheduling functionality with profile and slot management --- scheduler/forms.py | 67 ++++++++++ scheduler/models.py | 2 +- scheduler/views.py | 125 +++++++++++++++++- templates/generic_delete_confirmation.html | 24 ++++ templates/scheduler/scheduleprofile_form.html | 86 ++++++++++++ templates/scheduler/scheduleprofile_list.html | 48 +++++++ templates/template_parts/base_sidebar.html | 10 ++ wireguard_webadmin/urls.py | 7 + 8 files changed, 367 insertions(+), 2 deletions(-) create mode 100644 scheduler/forms.py create mode 100644 templates/generic_delete_confirmation.html create mode 100644 templates/scheduler/scheduleprofile_form.html create mode 100644 templates/scheduler/scheduleprofile_list.html diff --git a/scheduler/forms.py b/scheduler/forms.py new file mode 100644 index 0000000..fefa188 --- /dev/null +++ b/scheduler/forms.py @@ -0,0 +1,67 @@ +from crispy_forms.helper import FormHelper +from crispy_forms.layout import Layout, Submit, HTML, Div +from django import forms +from django.utils.translation import gettext_lazy as _ + +from scheduler.models import ScheduleProfile, ScheduleSlot + + +class ScheduleProfileForm(forms.ModelForm): + class Meta: + model = ScheduleProfile + fields = ['name'] + labels = { + 'name': _('Profile Name'), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_tag = False + self.helper.layout = Layout( + Div( + 'name', + css_class='col-12' + ) + ) + + +class ScheduleSlotForm(forms.ModelForm): + class Meta: + model = ScheduleSlot + fields = ['start_weekday', 'start_time', 'end_weekday', 'end_time'] + widgets = { + 'start_time': forms.TimeInput(attrs={'type': 'time'}), + 'end_time': forms.TimeInput(attrs={'type': 'time'}), + } + labels = { + 'start_weekday': _('Start Day'), + 'start_time': _('Start Time'), + 'end_weekday': _('End Day'), + 'end_time': _('End Time'), + } + + def __init__(self, *args, **kwargs): + cancel_url = kwargs.pop('cancel_url', '#') + super().__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.layout = Layout( + Div( + Div('start_weekday', css_class='col-md-6'), + Div('start_time', css_class='col-md-6'), + css_class='row' + ), + Div( + Div('end_weekday', css_class='col-md-6'), + Div('end_time', css_class='col-md-6'), + css_class='row' + ), + Div( + Div( + Submit('submit', _('Save'), css_class='btn btn-primary'), + HTML(f'{_("Cancel")}'), + css_class='col-12 d-flex justify-content-end gap-2 mt-3' + ), + css_class='row' + ) + ) diff --git a/scheduler/models.py b/scheduler/models.py index f99638b..d03c9cf 100644 --- a/scheduler/models.py +++ b/scheduler/models.py @@ -24,7 +24,7 @@ class ScheduleProfile(models.Model): class ScheduleSlot(models.Model): - profile = models.ForeignKey(ScheduleProfile, on_delete=models.CASCADE, related_name="slots") + profile = models.ForeignKey(ScheduleProfile, on_delete=models.CASCADE, related_name="time_interval") start_weekday = models.PositiveSmallIntegerField(choices=WEEK_DAYS) end_weekday = models.PositiveSmallIntegerField(choices=WEEK_DAYS) start_time = models.TimeField() diff --git a/scheduler/views.py b/scheduler/views.py index 60f00ef..175e314 100644 --- a/scheduler/views.py +++ b/scheduler/views.py @@ -1 +1,124 @@ -# Create your views here. +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.shortcuts import render, get_object_or_404, redirect +from django.urls import reverse +from django.utils.translation import gettext as _ + +from scheduler.forms import ScheduleProfileForm +from scheduler.forms import ScheduleSlotForm +from scheduler.models import ScheduleProfile, ScheduleSlot + + +@login_required +def view_scheduler_profile_list(request): + profiles = ScheduleProfile.objects.all().order_by('name') + context = { + 'profiles': profiles, + } + return render(request, 'scheduler/scheduleprofile_list.html', context) + + +@login_required +def view_manage_scheduler_profile(request): + profile_uuid = request.GET.get('uuid') + if profile_uuid: + profile = get_object_or_404(ScheduleProfile, uuid=profile_uuid) + title = _('Edit Schedule Profile') + slots = profile.time_interval.all() + else: + profile = None + title = _('Create Schedule Profile') + slots = None + + if request.method == 'POST': + form = ScheduleProfileForm(request.POST, instance=profile) + if form.is_valid(): + form.save() + messages.success(request, _('Schedule Profile saved successfully.')) + return redirect('scheduler_profile_list') + else: + form = ScheduleProfileForm(instance=profile) + + context = { + 'form': form, + 'title': title, + 'profile': profile, + 'slots': slots, + } + return render(request, 'scheduler/scheduleprofile_form.html', context) + + +@login_required +def view_delete_scheduler_profile(request): + profile_uuid = request.GET.get('uuid') + profile = get_object_or_404(ScheduleProfile, uuid=profile_uuid) + + if request.method == 'POST': + profile.delete() + messages.success(request, _('Schedule Profile deleted successfully.')) + return redirect('scheduler_profile_list') + + context = { + 'object': profile, + 'title': _('Delete Schedule Profile'), + 'cancel_url': reverse('scheduler_profile_list'), + 'text': _('Are you sure you want to delete the profile "%(name)s"?') % {'name': profile.name} + } + return render(request, 'generic_delete_confirmation.html', context) + + +@login_required +def view_manage_scheduler_slot(request): + slot_uuid = request.GET.get('uuid') + profile_uuid = request.GET.get('profile_uuid') + + if slot_uuid: + slot = get_object_or_404(ScheduleSlot, uuid=slot_uuid) + profile = slot.profile + title = _('Edit Time Interval') + else: + profile = get_object_or_404(ScheduleProfile, uuid=profile_uuid) + slot = None + title = _('Add Time Interval') + + cancel_url = f"{reverse('manage_scheduler_profile')}?uuid={profile.uuid}" + + if request.method == 'POST': + form = ScheduleSlotForm(request.POST, instance=slot, cancel_url=cancel_url) + if form.is_valid(): + new_slot = form.save(commit=False) + new_slot.profile = profile + new_slot.save() + messages.success(request, _('Time Interval saved successfully.')) + return redirect(cancel_url) + else: + form = ScheduleSlotForm(instance=slot, cancel_url=cancel_url) + + context = { + 'form': form, + 'title': title, + 'page_title': title, + 'profile': profile, + } + return render(request, 'generic_form.html', context) + + +@login_required +def view_delete_scheduler_slot(request): + slot_uuid = request.GET.get('uuid') + slot = get_object_or_404(ScheduleSlot, uuid=slot_uuid) + profile = slot.profile + cancel_url = f"{reverse('manage_scheduler_profile')}?uuid={profile.uuid}" + + if request.method == 'POST': + slot.delete() + messages.success(request, _('Time Interval deleted successfully.')) + return redirect(cancel_url) + + context = { + 'object': slot, + 'title': _('Delete Time Interval'), + 'cancel_url': cancel_url, + 'text': _('Are you sure you want to delete this time interval?') + } + return render(request, 'scheduler/generic_delete_confirm.html', context) diff --git a/templates/generic_delete_confirmation.html b/templates/generic_delete_confirmation.html new file mode 100644 index 0000000..14a1bd7 --- /dev/null +++ b/templates/generic_delete_confirmation.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block content %} +
+
+

{{ title }}

+
+
+ {% csrf_token %} +
+

{{ text }}

+
+ +
+
+{% endblock %} \ No newline at end of file diff --git a/templates/scheduler/scheduleprofile_form.html b/templates/scheduler/scheduleprofile_form.html new file mode 100644 index 0000000..0d19aec --- /dev/null +++ b/templates/scheduler/scheduleprofile_form.html @@ -0,0 +1,86 @@ +{% extends "base.html" %} +{% load i18n %} +{% load crispy_forms_tags %} + +{% block content %} +
+
+

{{ title }}

+
+
+ {% csrf_token %} +
+
+
+ {{ form|crispy }} +
+
+ + + {% if profile %} +
+
+
+

{% trans "Time Intervals" %}

+ + {% trans "Add Interval" %} + +
+
+ + + + + + + + + + + + {% for slot in slots %} + + + + + + + + {% empty %} + + + + {% endfor %} + +
{% trans "Start Day" %}{% trans "Start Time" %}{% trans "End Day" %}{% trans "End Time" %}{% trans "Actions" %}
{{ slot.get_start_weekday_display }}{{ slot.start_time }}{{ slot.get_end_weekday_display }}{{ slot.end_time }} + + + + + + +
+ {% trans "No time intervals found." %} +
+
+
+
+ {% endif %} + +
+
+ + + {% trans "Cancel" %} + +
+
+
+
+
+{% endblock %} diff --git a/templates/scheduler/scheduleprofile_list.html b/templates/scheduler/scheduleprofile_list.html new file mode 100644 index 0000000..7fdfaba --- /dev/null +++ b/templates/scheduler/scheduleprofile_list.html @@ -0,0 +1,48 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block content %} +
+
+

{% trans 'Schedule Profiles' %}

+
+
+ + + + + + + + + + {% for profile in profiles %} + + + + + + {% empty %} + + + + {% endfor %} + +
{% trans 'Name' %}{% trans 'Peers' %}{% trans 'Actions' %}
{{ profile.name }}{{ profile.peerscheduling_set.count }} + + + + + + +
{% trans 'No schedule profiles found.' %}
+
+ +
+{% endblock %} \ No newline at end of file diff --git a/templates/template_parts/base_sidebar.html b/templates/template_parts/base_sidebar.html index c2e1018..a80c3f0 100644 --- a/templates/template_parts/base_sidebar.html +++ b/templates/template_parts/base_sidebar.html @@ -99,6 +99,16 @@ + + diff --git a/wireguard_webadmin/urls.py b/wireguard_webadmin/urls.py index 721b798..0d2c827 100644 --- a/wireguard_webadmin/urls.py +++ b/wireguard_webadmin/urls.py @@ -32,6 +32,8 @@ from firewall.views import manage_firewall_rule, manage_redirect_rule, view_fire view_reset_firewall from intl_tools.views import view_change_language from routing_templates.views import view_manage_routing_template, view_routing_template_list +from scheduler.views import view_scheduler_profile_list, view_manage_scheduler_profile, view_delete_scheduler_profile, \ + view_manage_scheduler_slot, view_delete_scheduler_slot from user_manager.views import view_manage_user, view_peer_group_list, view_peer_group_manage, view_user_list from vpn_invite.views import view_email_settings, view_vpn_invite_list, view_vpn_invite_settings from vpn_invite_public.views import view_public_vpn_invite @@ -108,5 +110,10 @@ urlpatterns = [ path('cluster/settings/', cluster_settings, name='cluster_settings'), path('routing-templates/list/', view_routing_template_list, name='routing_template_list'), path('routing-templates/manage/', view_manage_routing_template, name='manage_routing_template'), + path('scheduler/profile/list/', view_scheduler_profile_list, name='scheduler_profile_list'), + path('scheduler/profile/manage/', view_manage_scheduler_profile, name='manage_scheduler_profile'), + path('scheduler/profile/delete/', view_delete_scheduler_profile, name='delete_scheduler_profile'), + path('scheduler/slot/manage/', view_manage_scheduler_slot, name='manage_scheduler_slot'), + path('scheduler/slot/delete/', view_delete_scheduler_slot, name='delete_scheduler_slot'), path('change_language/', view_change_language, name='change_language'), ]