diff --git a/AKDashboard/templates/AKDashboard/dashboard_row.html b/AKDashboard/templates/AKDashboard/dashboard_row.html index 36c1af1e08a44c32890061bdce9df9427869ef80..001d8b6f336cfd520b8744ccf12c8d71b9bfe933 100644 --- a/AKDashboard/templates/AKDashboard/dashboard_row.html +++ b/AKDashboard/templates/AKDashboard/dashboard_row.html @@ -64,13 +64,15 @@ </div> </a> {% if 'AKPreferencePoll'|check_app_installed and event.active %} - <a class="dashboard-box btn btn-primary" - href="{% url 'poll:poll' event_slug=event.slug %}"> - <div class="col-sm-12 col-md-3 col-lg-2 dashboard-button"> - <span class="fa fa-poll"></span> - <span class='text'>{% trans 'AK Preferences' %}</span> - </div> - </a> + {% if not event.poll_hidden or user.is_staff %} + <a class="dashboard-box btn btn-primary" + href="{% url 'poll:poll' event_slug=event.slug %}"> + <div class="col-sm-12 col-md-3 col-lg-2 dashboard-button"> + <span class="fa fa-poll"></span> + <span class='text'>{% trans 'AK Preferences' %}</span> + </div> + </a> + {% endif %} {% endif %} {% for button in event.dashboardbutton_set.all %} <a class="dashboard-box btn btn-{{ button.get_color_display }}" diff --git a/AKDashboard/tests.py b/AKDashboard/tests.py index 59328adf517d22a1793df63f67782254b0958f94..a45a7e22c2d1fe93dc1624667185d1b07009837a 100644 --- a/AKDashboard/tests.py +++ b/AKDashboard/tests.py @@ -29,6 +29,7 @@ class DashboardTests(TestCase): end=now(), active=True, plan_hidden=False, + poll_hidden=False, ) cls.default_category = AKCategory.objects.create( name="Test Category", @@ -146,6 +147,26 @@ class DashboardTests(TestCase): self.assertContains(response, "Current AKs") self.assertContains(response, "AK Wall") + def test_poll_hidden(self): + """ + Test visibility of poll buttons with regard to poll visibility status for a given event + """ + url_event_dashboard = reverse('dashboard:dashboard_event', kwargs={"slug": self.event.slug}) + + if apps.is_installed('AKPreferencePoll'): + # Poll hidden? No buttons should show up + self.event.poll_hidden = True + self.event.save() + response = self.client.get(url_event_dashboard) + self.assertNotContains(response, "AK Preferences") + + # Poll not hidden? + # Buttons to preference poll should be on the page + self.event.poll_hidden = False + self.event.save() + response = self.client.get(url_event_dashboard) + self.assertContains(response, "AK Preferences") + def test_dashboard_buttons(self): """ Make sure manually added buttons are displayed correctly diff --git a/AKModel/admin.py b/AKModel/admin.py index 0b17c5523d0e26263bd5487e72dd8b2796b6bdb6..8b7584f1328ff309655ddde1e18444273a106fdd 100644 --- a/AKModel/admin.py +++ b/AKModel/admin.py @@ -51,12 +51,16 @@ class EventAdmin(admin.ModelAdmin): wizard. """ model = Event - list_display = ['name', 'status_url', 'place', 'start', 'end', 'active', 'plan_hidden'] + list_display = ['name', 'status_url', 'place', 'start', 'end', 'active', 'plan_hidden', 'poll_hidden'] list_filter = ['active'] list_editable = ['active'] ordering = ['-start'] - readonly_fields = ['status_url', 'plan_hidden', 'plan_published_at', 'toggle_plan_visibility'] - actions = ['publish', 'unpublish'] + readonly_fields = [ + 'status_url', + 'plan_hidden', 'plan_published_at', 'toggle_plan_visibility', + 'poll_hidden', 'poll_published_at', 'toggle_poll_visibility', + ] + actions = ['publish_plan', 'unpublish_plan', 'publish_poll', 'unpublish_poll'] def add_view(self, request, form_url='', extra_context=None): # Override @@ -115,13 +119,31 @@ class EventAdmin(admin.ModelAdmin): text = _('Unpublish plan') return format_html("<a href='{url}'>{text}</a>", url=url, text=text) + @display(description=_("Toggle poll visibility")) + def toggle_poll_visibility(self, obj): + """ + Define a read-only field to toggle the visibility of the preference poll of this event + This will choose from two different link targets/views depending on the current visibility status + + :param obj: event to change the visibility of the plan for + :return: toggling link (HTML) + :rtype: str + """ + if obj.poll_hidden: + url = f"{reverse_lazy('admin:poll-publish')}?pks={obj.pk}" + text = _('Publish preference poll') + else: + url = f"{reverse_lazy('admin:poll-unpublish')}?pks={obj.pk}" + text = _('Unpublish preference poll') + return format_html("<a href='{url}'>{text}</a>", url=url, text=text) + def get_form(self, request, obj=None, change=False, **kwargs): # Override (update) form rendering to make sure the timezone of the event is used timezone.activate(obj.timezone) return super().get_form(request, obj, change, **kwargs) @action(description=_('Publish plan')) - def publish(self, request, queryset): + def publish_plan(self, request, queryset): """ Admin action to publish the plan """ @@ -129,7 +151,7 @@ class EventAdmin(admin.ModelAdmin): return HttpResponseRedirect(f"{reverse_lazy('admin:plan-publish')}?pks={','.join(str(pk) for pk in selected)}") @action(description=_('Unpublish plan')) - def unpublish(self, request, queryset): + def unpublish_plan(self, request, queryset): """ Admin action to hide the plan """ @@ -137,6 +159,23 @@ class EventAdmin(admin.ModelAdmin): return HttpResponseRedirect( f"{reverse_lazy('admin:plan-unpublish')}?pks={','.join(str(pk) for pk in selected)}") + @action(description=_('Publish preference poll')) + def publish_poll(self, request, queryset): + """ + Admin action to publish the preference poll + """ + selected = queryset.values_list('pk', flat=True) + return HttpResponseRedirect(f"{reverse_lazy('admin:poll-publish')}?pks={','.join(str(pk) for pk in selected)}") + + @action(description=_('Unpublish preference poll')) + def unpublish_poll(self, request, queryset): + """ + Admin action to hide the preference poll + """ + selected = queryset.values_list('pk', flat=True) + return HttpResponseRedirect( + f"{reverse_lazy('admin:poll-unpublish')}?pks={','.join(str(pk) for pk in selected)}") + class PrepopulateWithNextActiveEventMixin: """ diff --git a/AKModel/forms.py b/AKModel/forms.py index eb60a04071ba1267681176ccd32d7e79e6437492..7c655058e49d1886f0d2bbe80da907c934d7efda 100644 --- a/AKModel/forms.py +++ b/AKModel/forms.py @@ -38,9 +38,10 @@ class NewEventWizardStartForm(forms.ModelForm): """ class Meta: model = Event - fields = ['name', 'slug', 'timezone', 'plan_hidden'] + fields = ['name', 'slug', 'timezone', 'plan_hidden', 'poll_hidden'] widgets = { 'plan_hidden': forms.HiddenInput(), + 'poll_hidden': forms.HiddenInput(), } # Special hidden field for wizard state handling @@ -57,7 +58,7 @@ class NewEventWizardSettingsForm(forms.ModelForm): class Meta: model = Event fields = "__all__" - exclude = ['plan_published_at'] + exclude = ['plan_published_at', 'poll_published_at'] widgets = { 'name': forms.HiddenInput(), 'slug': forms.HiddenInput(), @@ -69,6 +70,7 @@ class NewEventWizardSettingsForm(forms.ModelForm): 'interest_end': DateTimeInput(), 'reso_deadline': DateTimeInput(), 'plan_hidden': forms.HiddenInput(), + 'poll_hidden': forms.HiddenInput(), } diff --git a/AKModel/migrations/0070_event_poll_hidden_event_poll_published_at.py b/AKModel/migrations/0070_event_poll_hidden_event_poll_published_at.py new file mode 100644 index 0000000000000000000000000000000000000000..4ca184058798a6623f9e99dc8a41803614d742a7 --- /dev/null +++ b/AKModel/migrations/0070_event_poll_hidden_event_poll_published_at.py @@ -0,0 +1,32 @@ +# Generated by Django 5.1.6 on 2025-04-04 11:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("AKModel", "0069_alter_akpreference_preference"), + ] + + operations = [ + migrations.AddField( + model_name="event", + name="poll_hidden", + field=models.BooleanField( + default=True, + help_text="Hides preference poll for non-staff users", + verbose_name="Poll Hidden", + ), + ), + migrations.AddField( + model_name="event", + name="poll_published_at", + field=models.DateTimeField( + blank=True, + help_text="Timestamp at which the preference poll was published", + null=True, + verbose_name="Poll published at", + ), + ), + ] diff --git a/AKModel/models.py b/AKModel/models.py index f86d85ba1a9ebbe07f1c306aa31095393a76b532..a635c662e2d28b9d6fd4c51a9c56f2c79a82fc07 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -158,6 +158,12 @@ class Event(models.Model): plan_published_at = models.DateTimeField(verbose_name=_('Plan published at'), blank=True, null=True, help_text=_('Timestamp at which the plan was published')) + poll_hidden = models.BooleanField(verbose_name=_('Poll Hidden'), + help_text=_('Hides preference poll for non-staff users'), + default=True) + poll_published_at = models.DateTimeField(verbose_name=_('Poll published at'), blank=True, null=True, + help_text=_('Timestamp at which the preference poll was published')) + base_url = models.URLField(verbose_name=_("Base URL"), help_text=_("Prefix for wiki link construction"), blank=True) wiki_export_template_name = models.CharField(verbose_name=_("Wiki Export Template Name"), blank=True, max_length=50) default_slot = models.DecimalField(max_digits=4, decimal_places=2, default=2, verbose_name=_('Default Slot Length'), diff --git a/AKModel/urls.py b/AKModel/urls.py index 9c10340546b787c92cbd02cb2c3dbbeb6fe1ff94..522a3171c0aec495307ecb9edf7e7d76c87a2cc3 100644 --- a/AKModel/urls.py +++ b/AKModel/urls.py @@ -4,8 +4,8 @@ from django.urls import include, path from rest_framework.routers import DefaultRouter import AKModel.views.api -from AKModel.views.manage import ExportSlidesView, PlanPublishView, PlanUnpublishView, DefaultSlotEditorView, \ - AKsByUserView, AKScheduleJSONImportView +from AKModel.views.manage import ExportSlidesView, PlanPublishView, PlanUnpublishView, PollPublishView, PollUnpublishView, \ + DefaultSlotEditorView, AKsByUserView, AKScheduleJSONImportView from AKModel.views.ak import AKRequirementOverview, AKCSVExportView, AKJSONExportView, AKWikiExportView, \ AKMessageDeleteView from AKModel.views.event_wizard import NewEventWizardStartView, NewEventWizardPrepareImportView, \ @@ -108,6 +108,8 @@ def get_admin_urls_event(admin_site): path('<slug:event_slug>/ak-slide-export/', admin_site.admin_view(ExportSlidesView.as_view()), name="ak_slide_export"), path('plan/publish/', admin_site.admin_view(PlanPublishView.as_view()), name="plan-publish"), path('plan/unpublish/', admin_site.admin_view(PlanUnpublishView.as_view()), name="plan-unpublish"), + path('poll/publish/', admin_site.admin_view(PollPublishView.as_view()), name="poll-publish"), + path('poll/unpublish/', admin_site.admin_view(PollUnpublishView.as_view()), name="poll-unpublish"), path('<slug:event_slug>/defaultSlots/', admin_site.admin_view(DefaultSlotEditorView.as_view()), name="default-slots-editor"), path('<slug:event_slug>/importRooms/', admin_site.admin_view(RoomBatchCreationView.as_view()), diff --git a/AKModel/views/manage.py b/AKModel/views/manage.py index 1a7f1930b079edc4061ac5bc0a145466640bd574..fa88adb3cb053a6e7aefda1a2ca5bc662d6a6598 100644 --- a/AKModel/views/manage.py +++ b/AKModel/views/manage.py @@ -159,6 +159,31 @@ class PlanUnpublishView(IntermediateAdminActionView): self.entities.update(plan_published_at=None, plan_hidden=True) +class PollPublishView(IntermediateAdminActionView): + """ + Admin action view: Publish the preference poll of one or multitple event(s) + """ + title = _('Publish preference poll') + model = Event + confirmation_message = _('Publish the plan(s) of:') + success_message = _('Preference poll published') + + def action(self, form): + self.entities.update(poll_published_at=Now(), poll_hidden=False) + + +class PollUnpublishView(IntermediateAdminActionView): + """ + Admin action view: Unpublish the preference poll of one or multitple event(s) + """ + title = _('Unpublish preference poll') + model = Event + confirmation_message = _('Unpublish the preference poll(s) of:') + success_message = _('Preference poll unpublished') + + def action(self, form): + self.entities.update(poll_published_at=None, poll_hidden=True) + class DefaultSlotEditorView(EventSlugMixin, IntermediateAdminView): """ Admin view: Allow to edit the default slots of an event diff --git a/AKPreferencePoll/tests.py b/AKPreferencePoll/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..14d51e65dae617c326acede6802641c062c770d7 --- /dev/null +++ b/AKPreferencePoll/tests.py @@ -0,0 +1,40 @@ +from django.urls import reverse +from django.test import TestCase + +from AKModel.models import Event +from AKModel.tests.test_views import BasicViewTests + +class PollViewTests(BasicViewTests, TestCase): + """ + Tests for AKPreferencePoll + """ + fixtures = ['model.json'] + APP_NAME = 'poll' + + def test_poll_redirect(self): + """ + Test: Make sure that user is redirected from poll to dashboard when poll is hidden + """ + event = Event.objects.get(slug='kif42') + _, url_poll = self._name_and_url(('poll', {'event_slug': event.slug})) + url_dashboard = reverse("dashboard:dashboard_event", kwargs={"slug": event.slug}) + + event.poll_hidden = True + event.save() + + self.client.logout() + response = self.client.get(url_poll) + + self.assertRedirects(response, url_dashboard, + msg_prefix=f"Redirect away from poll not working ({url_poll} -> {url_dashboard})") + + self.client.force_login(self.staff_user) + response = self.client.get(url_poll) + + self.assertEqual( + response.status_code, + 200, + msg=f"{url_poll} broken", + ) + + self.assertTemplateUsed(response, "AKPreferencePoll/poll.html", msg_prefix="Poll is not visible for staff user") diff --git a/AKPreferencePoll/views.py b/AKPreferencePoll/views.py index 8f36c4c220810c9370a1d8297acb5c09c87733c2..70ac8b43cecbe13a9616af5fb99b99ad712d80f3 100644 --- a/AKPreferencePoll/views.py +++ b/AKPreferencePoll/views.py @@ -1,7 +1,7 @@ from django import forms from django.contrib import messages from django.contrib.messages.views import SuccessMessageMixin -from django.http import HttpResponseRedirect +from django.shortcuts import redirect from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ from django.views.generic import FormView @@ -39,6 +39,13 @@ class PreferencePollCreateView(EventSlugMixin, SuccessMessageMixin, FormView): extra=0, ) + def get(self, request, *args, **kwargs): + s = super().get(request, *args, **kwargs) + # Don't show preference form when event is not active or poll is hidden -> redirect to dashboard + if not self.event.active or (self.event.poll_hidden and not request.user.is_staff): + return redirect(self.get_success_url()) + return s + def get_success_url(self): return reverse_lazy( "dashboard:dashboard_event", kwargs={"slug": self.event.slug} @@ -96,4 +103,4 @@ class PreferencePollCreateView(EventSlugMixin, SuccessMessageMixin, FormView): success_message = self.get_success_message(participant_form.cleaned_data) if success_message: messages.success(self.request, success_message) - return HttpResponseRedirect(self.get_success_url()) + return redirect(self.get_success_url())