diff --git a/intranet/apps/eighth/admin.py b/intranet/apps/eighth/admin.py index 1143767833..c452353475 100644 --- a/intranet/apps/eighth/admin.py +++ b/intranet/apps/eighth/admin.py @@ -36,9 +36,9 @@ class EighthBlockAdmin(SimpleHistoryAdmin): @admin.register(EighthScheduledActivity) class EighthScheduledActivityAdmin(SimpleHistoryAdmin): - list_display = ("activity", "block", "comments", "admin_comments", "cancelled", "attendance_taken") - list_filter = ("attendance_taken", "cancelled", "block") - ordering = ("-block", "activity__name") + list_display = ("activity", "block", "hidden_until", "comments", "admin_comments", "cancelled", "attendance_taken") + list_filter = ("attendance_taken", "cancelled", "block", "hidden_until") + ordering = ("-block", "hidden_until", "activity__name") search_fields = ("activity__name",) diff --git a/intranet/apps/eighth/exceptions.py b/intranet/apps/eighth/exceptions.py index 7b44b5bc44..18a2b28dbb 100644 --- a/intranet/apps/eighth/exceptions.py +++ b/intranet/apps/eighth/exceptions.py @@ -32,6 +32,7 @@ class SignupException(Exception): "You may not sign up for this restricted activity. You are not on the authorized list.", "This activity is restricted for this student." ), "Blacklisted": m("You are blacklisted from this activity.", "This student is blacklisted from this activity."), + "ActivityHidden": m("This activity is currently hidden and cannot be signed up for.", "This activity is currently hidden."), "OverrideBlockLocked": m( "An override block ({0}) has been locked. Signup is not allowed at this time.", "An override block ({1}) has been locked." ), diff --git a/intranet/apps/eighth/forms/admin/scheduling.py b/intranet/apps/eighth/forms/admin/scheduling.py index 6da24c5941..76b2166e13 100644 --- a/intranet/apps/eighth/forms/admin/scheduling.py +++ b/intranet/apps/eighth/forms/admin/scheduling.py @@ -49,6 +49,7 @@ class Meta: "restricted", "sticky", "both_blocks", + "hidden_until", "comments", "admin_comments", ] @@ -57,6 +58,7 @@ class Meta: "title": forms.TextInput(attrs={"size": 30}), "comments": forms.Textarea(attrs={"rows": 2, "cols": 30}), "admin_comments": forms.Textarea(attrs={"rows": 2, "cols": 30}), + "hidden_until": forms.DateTimeInput(attrs={"class": "datetimepicker"}), } def clean(self): diff --git a/intranet/apps/eighth/migrations/0073_eighthscheduledactivity_hidden_until_and_more.py b/intranet/apps/eighth/migrations/0073_eighthscheduledactivity_hidden_until_and_more.py new file mode 100644 index 0000000000..4c7ecf1f7c --- /dev/null +++ b/intranet/apps/eighth/migrations/0073_eighthscheduledactivity_hidden_until_and_more.py @@ -0,0 +1,22 @@ +# Generated by Django 5.2.8 on 2025-12-23 15:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("eighth", "0072_alter_eighthscheduledactivity_waitlist"), + ] + + operations = [ + migrations.AddField( + model_name="eighthscheduledactivity", + name="hidden_until", + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name="historicaleighthscheduledactivity", + name="hidden_until", + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/intranet/apps/eighth/models.py b/intranet/apps/eighth/models.py index dbec7b3a1e..aa35b46c66 100644 --- a/intranet/apps/eighth/models.py +++ b/intranet/apps/eighth/models.py @@ -385,6 +385,14 @@ def get_active_schedulings(self) -> QuerySet | Collection["EighthScheduledActivi return EighthScheduledActivity.objects.filter(activity=self, block__date__gte=date_start, block__date__lte=date_end) + @property + def is_hidden(self) -> bool: + """Returns whether the activity is hidden. + Returns: + Whether the activity is hidden. + """ + return self.get_active_schedulings().filter(hidden_until__isnull=False, hidden_until__gt=timezone.now()).exists() + @property def is_active(self) -> bool: """Returns whether an activity is "active." @@ -891,7 +899,8 @@ def title_with_flags(self) -> str: The full title for the scheduled activity, with flags. """ cancelled_str = " (Cancelled)" if self.cancelled else "" - name_with_flags = self.activity._name_with_flags(True, self.title) + cancelled_str # pylint: disable=protected-access + hidden_str = " (Hidden)" if self.is_hidden else "" + name_with_flags = self.activity._name_with_flags(True, self.title) + cancelled_str + hidden_str # pylint: disable=protected-access if self.special and not self.activity.special: name_with_flags = "Special: " + name_with_flags return name_with_flags @@ -914,6 +923,14 @@ def is_activity_sticky(self) -> bool: """ return self.sticky or self.activity.sticky + @property + def is_hidden(self) -> bool: + """Returns whether the scheduled activity is hidden. + Returns: + Whether this scheduled activity is hidden. + """ + return self.hidden_until is not None and timezone.now() < self.hidden_until + def get_true_sponsors(self) -> QuerySet | Collection[EighthSponsor]: # pylint: disable=unsubscriptable-object """Retrieves the sponsors for the scheduled activity, taking into account activity defaults and overrides. @@ -1222,6 +1239,10 @@ def add_user( exception = eighth_exceptions.SignupException() + if self.is_hidden: + if request is None or not request.user.is_eighth_admin: + exception.ActivityHidden = True + if user.grade and user.grade.number > 12: exception.SignupForbidden = True @@ -1637,6 +1658,8 @@ def __str__(self): suff = f" - {self.title}" if self.title else "" return f"{self.activity}{suff} on {self.block}{cancelled_str}" + hidden_until = models.DateTimeField(null=True, blank=True) + class EighthSignupManager(Manager): """Model manager for EighthSignup.""" diff --git a/intranet/apps/eighth/serializers.py b/intranet/apps/eighth/serializers.py index 6f187581b5..9772568420 100644 --- a/intranet/apps/eighth/serializers.py +++ b/intranet/apps/eighth/serializers.py @@ -6,6 +6,7 @@ from django.contrib.auth import get_user_model from django.db import transaction from django.db.models import Count +from django.utils import timezone from rest_framework import serializers from rest_framework.reverse import reverse @@ -151,6 +152,8 @@ def process_scheduled_activity( "sponsors": [], "restricted": scheduled_activity.get_restricted(), "restricted_for_user": restricted_for_user, + "hidden": scheduled_activity.is_hidden, + "hidden_until": scheduled_activity.hidden_until, "both_blocks": scheduled_activity.is_both_blocks(), "one_a_day": activity.one_a_day, "special": scheduled_activity.get_special(), @@ -231,6 +234,9 @@ def fetch_activity_list_with_metadata(self, block): .order_by("activity__administrative", "administrative", "activity__name") ) + if user is None or not user.is_eighth_admin: + scheduled_activities = scheduled_activities.exclude(hidden_until__isnull=False, hidden_until__gt=timezone.now()) + for scheduled_activity in scheduled_activities: # Avoid re-fetching scheduled_activity. activity_info = self.get_activity( diff --git a/intranet/apps/eighth/views/activities.py b/intranet/apps/eighth/views/activities.py index 2732803c43..b472bfcb35 100644 --- a/intranet/apps/eighth/views/activities.py +++ b/intranet/apps/eighth/views/activities.py @@ -8,7 +8,7 @@ from django.contrib import messages from django.contrib.auth import get_user_model from django.contrib.auth.decorators import login_required -from django.db.models import Count +from django.db.models import Count, Q from django.http import Http404, HttpResponse from django.shortcuts import get_object_or_404, render from django.utils import timezone @@ -35,6 +35,9 @@ def activity_view(request, activity_id=None): activity = get_object_or_404(EighthActivity, id=activity_id) scheduled_activities = EighthScheduledActivity.objects.filter(activity=activity) + if request.user.is_student and activity.is_hidden: + scheduled_activities = scheduled_activities.filter(Q(hidden_until__isnull=True) | Q(hidden_until__lte=timezone.now())) + show_all = "show_all" in request.GET if not show_all: first_date = timezone.localtime(timezone.now()).date() diff --git a/intranet/apps/eighth/views/admin/scheduling.py b/intranet/apps/eighth/views/admin/scheduling.py index 66fed30ff9..c146cc4031 100644 --- a/intranet/apps/eighth/views/admin/scheduling.py +++ b/intranet/apps/eighth/views/admin/scheduling.py @@ -85,6 +85,7 @@ def schedule_activity_view(request): "both_blocks", "comments", "admin_comments", + "hidden_until", ] if "rooms" in form.cleaned_data: for o in form.cleaned_data["rooms"]: @@ -234,6 +235,7 @@ def schedule_activity_view(request): "scheduled": not sched_act.cancelled, "cancelled": sched_act.cancelled, "sticky_students": sched_act.sticky_students.all(), + "hidden_until": sched_act.hidden_until, } ) except KeyError: diff --git a/intranet/templates/eighth/admin/schedule_activity.html b/intranet/templates/eighth/admin/schedule_activity.html index ee1499ec55..a53bb0b652 100644 --- a/intranet/templates/eighth/admin/schedule_activity.html +++ b/intranet/templates/eighth/admin/schedule_activity.html @@ -10,6 +10,7 @@ + + {% endblock %} {% block css %} {{ block.super }} + {% stylesheet 'eighth.schedule' %} {% endblock %} @@ -230,6 +239,9 @@