From 7d0a6366c4ca2b344f8fb1949caf9055a26892d1 Mon Sep 17 00:00:00 2001
From: Krishnan Shankar <krishnans2006@gmail.com>
Date: Sat, 10 Feb 2024 21:43:36 -0500
Subject: [PATCH 01/42] feat(announcements): implement models/UI for club
 announcements

---
 intranet/apps/announcements/forms.py          |  38 +++--
 .../migrations/0033_announcement_activity.py  |  20 +++
 intranet/apps/announcements/models.py         |   8 +
 intranet/apps/announcements/notifications.py  |  11 ++
 intranet/apps/announcements/urls.py           |   2 +
 intranet/apps/announcements/views.py          |  29 +++-
 intranet/apps/dashboard/views.py              |  56 ++++++-
 .../0066_eighthactivity_officers.py           |  20 +++
 .../0067_eighthactivity_subscribers.py        |  20 +++
 .../migrations/0068_auto_20240213_1938.py     |  23 +++
 intranet/apps/eighth/models.py                |   6 +
 intranet/apps/eighth/serializers.py           |  33 +++-
 intranet/apps/eighth/urls.py                  |   2 +
 intranet/apps/eighth/views/signup.py          |  18 ++
 intranet/apps/users/models.py                 |  10 ++
 intranet/static/css/dashboard.scss            |  74 ++++++++
 intranet/static/js/common.js                  |   4 +
 .../templates/announcements/announcement.html |  34 +++-
 .../templates/announcements/club-request.html | 158 ++++++++++++++++++
 intranet/templates/announcements/request.html |   4 +-
 intranet/templates/dashboard/dashboard.html   |  53 +++++-
 intranet/templates/eighth/signup.html         |  16 ++
 22 files changed, 602 insertions(+), 37 deletions(-)
 create mode 100644 intranet/apps/announcements/migrations/0033_announcement_activity.py
 create mode 100644 intranet/apps/eighth/migrations/0066_eighthactivity_officers.py
 create mode 100644 intranet/apps/eighth/migrations/0067_eighthactivity_subscribers.py
 create mode 100644 intranet/apps/eighth/migrations/0068_auto_20240213_1938.py
 create mode 100644 intranet/templates/announcements/club-request.html

diff --git a/intranet/apps/announcements/forms.py b/intranet/apps/announcements/forms.py
index 2099248f7ef..f0c3e104bd5 100644
--- a/intranet/apps/announcements/forms.py
+++ b/intranet/apps/announcements/forms.py
@@ -8,30 +8,38 @@
 class AnnouncementForm(forms.ModelForm):
     """A form for generating an announcement."""
 
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-        self.fields["expiration_date"].help_text = "By default, announcements expire after two weeks. To change this, click in the box above."
+    expiration_date = forms.DateTimeInput()
+    notify_email_all = forms.BooleanField(required=False, label="Send Email to All")
+    update_added_date = forms.BooleanField(required=False, label="Update Added Date")
 
-        self.fields["notify_post"].help_text = "If this box is checked, students who have signed up for notifications will receive an email."
+    class Meta:
+        model = Announcement
+        fields = ["title", "author", "content", "groups", "expiration_date", "notify_post", "notify_email_all", "update_added_date", "pinned"]
+        help_texts = {
+            "expiration_date": "By default, announcements expire after two weeks. To change this, click in the box above.",
+            "notify_post": "If this box is checked, students who have signed up for notifications will receive an email.",
+            "notify_email_all": "This will send an email notification to all of the users who can see this post. This option does NOT take users' email notification preferences into account, so please use with care.",
+            "update_added_date": "If this announcement has already been added, update the added date to now so that the announcement is pushed to the top. If this option is not selected, the announcement will stay in its current position.",
+        }
 
-        self.fields["notify_email_all"].help_text = (
-            "This will send an email notification to all of the users who can see this post. This option "
-            "does NOT take users' email notification preferences into account, so please use with care."
-        )
 
-        self.fields["update_added_date"].help_text = (
-            "If this announcement has already been added, update the added date to now so that the "
-            "announcement is pushed to the top. If this option is not selected, the announcement will stay in "
-            "its current position."
-        )
+class ClubAnnouncementForm(forms.ModelForm):
+    """A form for posting a club announcement."""
+
+    def __init__(self, user, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.fields["activity"].queryset = user.officer_for_set
 
     expiration_date = forms.DateTimeInput()
-    notify_email_all = forms.BooleanField(required=False, label="Send Email to All")
     update_added_date = forms.BooleanField(required=False, label="Update Added Date")
 
     class Meta:
         model = Announcement
-        fields = ["title", "author", "content", "groups", "expiration_date", "notify_post", "notify_email_all", "update_added_date", "pinned"]
+        fields = ["title", "author", "content", "activity", "expiration_date", "update_added_date"]
+        help_texts = {
+            "expiration_date": "By default, announcements expire after two weeks. To change this, click in the box above.",
+            "update_added_date": "If this announcement has already been added, update the added date to now so that the announcement is pushed to the top. If this option is not selected, the announcement will stay in its current position.",
+        }
 
 
 class AnnouncementEditForm(forms.ModelForm):
diff --git a/intranet/apps/announcements/migrations/0033_announcement_activity.py b/intranet/apps/announcements/migrations/0033_announcement_activity.py
new file mode 100644
index 00000000000..874a89ff7d5
--- /dev/null
+++ b/intranet/apps/announcements/migrations/0033_announcement_activity.py
@@ -0,0 +1,20 @@
+# Generated by Django 3.2.20 on 2024-02-14 00:06
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('eighth', '0067_eighthactivity_subscribers'),
+        ('announcements', '0032_alter_warningannouncement_type'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='announcement',
+            name='activity',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='eighth.eighthactivity'),
+        ),
+    ]
diff --git a/intranet/apps/announcements/models.py b/intranet/apps/announcements/models.py
index 96fe49471c9..2683d6c34b9 100644
--- a/intranet/apps/announcements/models.py
+++ b/intranet/apps/announcements/models.py
@@ -11,6 +11,8 @@
 from ...utils.deletion import set_historical_user
 from ...utils.html import nullify_links
 
+from ..eighth.models import EighthActivity
+
 
 class AnnouncementManager(Manager):
     def visible_to_user(self, user):
@@ -110,6 +112,8 @@ class Announcement(models.Model):
     updated = models.DateTimeField(auto_now=True)
     groups = models.ManyToManyField(DjangoGroup, blank=True)
 
+    activity = models.ForeignKey(EighthActivity, null=True, blank=True, on_delete=models.CASCADE)
+
     expiration_date = models.DateTimeField(auto_now=False, default=timezone.make_aware(datetime(3000, 1, 1)))
 
     notify_post = models.BooleanField(default=True)
@@ -141,6 +145,10 @@ def is_this_year(self):
         """Return whether the announcement was created after July 1st of this school year."""
         return is_current_year(self.added)
 
+    @property
+    def is_club_announcement(self):
+        return self.activity is not None
+
     def is_visible(self, user):
         return self in Announcement.objects.visible_to_user(user)
 
diff --git a/intranet/apps/announcements/notifications.py b/intranet/apps/announcements/notifications.py
index 51e6d486842..0932d3510b4 100644
--- a/intranet/apps/announcements/notifications.py
+++ b/intranet/apps/announcements/notifications.py
@@ -7,6 +7,7 @@
 from django.contrib import messages
 from django.contrib.auth import get_user_model
 from django.core import exceptions
+from django.db.models import Q
 from django.urls import reverse
 from requests_oauthlib import OAuth1
 from sentry_sdk import capture_exception
@@ -118,6 +119,16 @@ def announcement_posted_email(request, obj, send_all=False):
                 .objects.filter(user_type="student", graduation_year__gte=get_senior_graduation_year())
                 .union(get_user_model().objects.filter(user_type__in=["teacher", "counselor"]))
             )
+        elif obj.club:
+            filter = Q(subscribed_to_set__contains=obj.club) & (
+                Q(user_type="student") & Q(graduation_year__gte=get_senior_graduation_year()) | Q(user_type__in=["teacher", "counselor"])
+            )
+            users = (
+                get_user_model()
+                .objects.filter(user_type="student", graduation_year__gte=get_senior_graduation_year(), subscribed_to_set__contains=obj.club)
+                .union(get_user_model().objects.filter(user_type__in=["teacher", "counselor"], subscribed_to_set__contains=obj.club))
+            )
+
         else:
             users = (
                 get_user_model()
diff --git a/intranet/apps/announcements/urls.py b/intranet/apps/announcements/urls.py
index f704fab8da0..7c43d863f29 100644
--- a/intranet/apps/announcements/urls.py
+++ b/intranet/apps/announcements/urls.py
@@ -5,8 +5,10 @@
 urlpatterns = [
     re_path(r"^$", views.view_announcements, name="view_announcements"),
     re_path(r"^/archive$", views.view_announcements_archive, name="announcements_archive"),
+    re_path(r"^/club$", views.view_club_announcements, name="club_announcements"),
     re_path(r"^/add$", views.add_announcement_view, name="add_announcement"),
     re_path(r"^/request$", views.request_announcement_view, name="request_announcement"),
+    re_path(r"^/club/post$", views.post_club_announcement_view, name="post_club_announcement"),
     re_path(r"^/request/success$", views.request_announcement_success_view, name="request_announcement_success"),
     re_path(r"^/request/success_self$", views.request_announcement_success_self_view, name="request_announcement_success_self"),
     re_path(r"^/approve/(?P<req_id>\d+)$", views.approve_announcement_view, name="approve_announcement"),
diff --git a/intranet/apps/announcements/views.py b/intranet/apps/announcements/views.py
index b1f92231e0d..62cab9d53f1 100644
--- a/intranet/apps/announcements/views.py
+++ b/intranet/apps/announcements/views.py
@@ -13,7 +13,7 @@
 from ..auth.decorators import announcements_admin_required, deny_restricted
 from ..dashboard.views import dashboard_view
 from ..groups.models import Group
-from .forms import AnnouncementAdminForm, AnnouncementEditForm, AnnouncementForm, AnnouncementRequestForm
+from .forms import AnnouncementAdminForm, AnnouncementEditForm, AnnouncementForm, AnnouncementRequestForm, ClubAnnouncementForm
 from .models import Announcement, AnnouncementRequest
 from .notifications import (
     admin_request_announcement_email,
@@ -40,6 +40,13 @@ def view_announcements_archive(request):
     return dashboard_view(request, show_widgets=False, show_expired=True, ignore_dashboard_types=["event"])
 
 
+@login_required
+@deny_restricted
+def view_club_announcements(request):
+    """Show the dashboard with only club posts."""
+    return dashboard_view(request, show_widgets=False, show_hidden_club=True, ignore_dashboard_types=["event"])
+
+
 def announcement_posted_hook(request, obj):
     """Runs whenever a new announcement is created, or a request is approved and posted.
 
@@ -123,6 +130,26 @@ def request_announcement_view(request):
     return render(request, "announcements/request.html", {"form": form, "action": "add"})
 
 
+def post_club_announcement_view(request):
+    if request.method == "POST":
+        form = ClubAnnouncementForm(request.user, request.POST)
+
+        if form.is_valid():
+            obj = form.save(commit=True)
+            obj.user = request.user
+            # SAFE HTML
+            obj.content = safe_html(obj.content)
+
+            obj.save()
+
+            return redirect("index")
+        else:
+            messages.error(request, "Error adding announcement")
+    else:
+        form = ClubAnnouncementForm(request.user)
+    return render(request, "announcements/club-request.html", {"form": form, "action": "add"})
+
+
 @login_required
 def request_announcement_success_view(request):
     return render(request, "announcements/success.html", {"type": "request"})
diff --git a/intranet/apps/dashboard/views.py b/intranet/apps/dashboard/views.py
index b2400653b8f..0dd286a942f 100644
--- a/intranet/apps/dashboard/views.py
+++ b/intranet/apps/dashboard/views.py
@@ -264,7 +264,31 @@ def announcements_sorting_key(item):
     return items
 
 
-def paginate_announcements_list(request, context, items):
+def split_club_announcements(items):
+    standard, club = [], []
+
+    for item in items:
+        if item.dashboard_type == "announcement" and item.is_club_announcement:
+            club.append(item)
+        else:
+            standard.append(item)
+
+    return standard, club
+
+
+def filter_hidden_club_announcements(user, user_hidden_announcements, club_items):
+    visible, hidden = [], []
+
+    for item in club_items:
+        if item.id in user_hidden_announcements or user not in item.activity.subscribers.all():
+            hidden.append(item)
+        else:
+            visible.append(item)
+
+    return visible, hidden
+
+
+def paginate_announcements_list(request, context, items, visible_club_items, hidden_club_items):
     """
     Paginate ``items`` in groups of 15
 
@@ -289,6 +313,13 @@ def paginate_announcements_list(request, context, items):
     context.update(
         {"items": items, "page_num": page_num, "prev_page": prev_page, "next_page": next_page, "more_items": more_items, "page_obj": paginator}
     )
+    club_items = visible_club_items[:15]
+
+    context.update(
+        {
+            "club_items": club_items,
+        }
+    )
 
     return context, items
 
@@ -383,7 +414,7 @@ def add_widgets_context(request, context):
 
 
 @login_required
-def dashboard_view(request, show_widgets=True, show_expired=False, ignore_dashboard_types=None, show_welcome=False):
+def dashboard_view(request, show_widgets=True, show_expired=False, show_hidden_club=False, ignore_dashboard_types=None, show_welcome=False):
     """Process and show the dashboard, which includes activities, events, and widgets."""
 
     user = request.user
@@ -432,6 +463,9 @@ def dashboard_view(request, show_widgets=True, show_expired=False, ignore_dashbo
         # Show all by default to 8th period office
         show_all = True
 
+    if not show_hidden_club:
+        show_hidden_club = "show_hidden_club" in request.GET
+
     is_index_page = request.path_info in ["/", ""]
 
     context = {
@@ -441,18 +475,25 @@ def dashboard_view(request, show_widgets=True, show_expired=False, ignore_dashbo
         "events_admin": events_admin,
         "is_index_page": is_index_page,
         "show_all": show_all,
+        "show_hidden_club": show_hidden_club,
         "show_expired": show_expired,
         "show_tjstar": settings.TJSTAR_BANNER_START_DATE <= now.date() <= settings.TJSTAR_DATE,
     }
 
+    user_hidden_announcements = Announcement.objects.hidden_announcements(user).values_list("id", flat=True)
+    user_hidden_events = Event.objects.hidden_events(user).values_list("id", flat=True)
+
     # Get list of announcements
     items = get_announcements_list(request, context)
 
-    # Paginate announcements list
-    context, items = paginate_announcements_list(request, context, items)
+    items, club_items = split_club_announcements(items)
 
-    user_hidden_announcements = Announcement.objects.hidden_announcements(user).values_list("id", flat=True)
-    user_hidden_events = Event.objects.hidden_events(user).values_list("id", flat=True)
+    # Paginate announcements list
+    if not show_hidden_club:
+        visible_club_items, hidden_club_items = filter_hidden_club_announcements(user, user_hidden_announcements, club_items)
+        context, items = paginate_announcements_list(request, context, items, visible_club_items, hidden_club_items)
+    else:
+        context, items = paginate_announcements_list(request, context, club_items, [], [])
 
     if ignore_dashboard_types is None:
         ignore_dashboard_types = []
@@ -482,6 +523,9 @@ def dashboard_view(request, show_widgets=True, show_expired=False, ignore_dashbo
     elif show_expired:
         dashboard_title = dashboard_header = "Announcement Archive"
         view_announcements_url = "announcements_archive"
+    elif show_hidden_club:
+        dashboard_title = dashboard_header = "Club Announcements"
+        view_announcements_url = "club_announcements"
     else:
         dashboard_title = dashboard_header = "Announcements"
 
diff --git a/intranet/apps/eighth/migrations/0066_eighthactivity_officers.py b/intranet/apps/eighth/migrations/0066_eighthactivity_officers.py
new file mode 100644
index 00000000000..8f55cd3cded
--- /dev/null
+++ b/intranet/apps/eighth/migrations/0066_eighthactivity_officers.py
@@ -0,0 +1,20 @@
+# Generated by Django 3.2.20 on 2024-02-11 02:29
+
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('eighth', '0065_auto_20220903_0038'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='eighthactivity',
+            name='officers',
+            field=models.ManyToManyField(blank=True, related_name='officer_for_set', to=settings.AUTH_USER_MODEL),
+        ),
+    ]
diff --git a/intranet/apps/eighth/migrations/0067_eighthactivity_subscribers.py b/intranet/apps/eighth/migrations/0067_eighthactivity_subscribers.py
new file mode 100644
index 00000000000..16e04244faf
--- /dev/null
+++ b/intranet/apps/eighth/migrations/0067_eighthactivity_subscribers.py
@@ -0,0 +1,20 @@
+# Generated by Django 3.2.20 on 2024-02-14 00:06
+
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('eighth', '0066_eighthactivity_officers'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='eighthactivity',
+            name='subscribers',
+            field=models.ManyToManyField(blank=True, related_name='subscribed_activity_set', to=settings.AUTH_USER_MODEL),
+        ),
+    ]
diff --git a/intranet/apps/eighth/migrations/0068_auto_20240213_1938.py b/intranet/apps/eighth/migrations/0068_auto_20240213_1938.py
new file mode 100644
index 00000000000..e325219e3c5
--- /dev/null
+++ b/intranet/apps/eighth/migrations/0068_auto_20240213_1938.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.2.20 on 2024-02-14 00:38
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('eighth', '0067_eighthactivity_subscribers'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='eighthactivity',
+            name='subscriptions_enabled',
+            field=models.BooleanField(default=False),
+        ),
+        migrations.AddField(
+            model_name='historicaleighthactivity',
+            name='subscriptions_enabled',
+            field=models.BooleanField(default=False),
+        ),
+    ]
diff --git a/intranet/apps/eighth/models.py b/intranet/apps/eighth/models.py
index 6fca981ebe8..f112f3e0ba0 100644
--- a/intranet/apps/eighth/models.py
+++ b/intranet/apps/eighth/models.py
@@ -185,6 +185,7 @@ class EighthActivity(AbstractBaseEighthModel):
         sponsors (:obj:`list` of :obj:`EighthSponsor`): The default activity-level sponsors for the activity.
             On an EighthScheduledActivity basis, you should NOT query this field.
             Instead, use scheduled_activity.get_true_sponsors()
+        officers (:obj:`list` of :obj:`User`): The activity's officers as chosen by a club sponsor.
         rooms (:obj:`list` of :obj:`EighthRoom`): The default activity-level rooms for the activity.
             On an EighthScheduledActivity basis, you should NOT query this field.
             Use scheduled_activity.get_true_rooms()
@@ -231,6 +232,7 @@ class EighthActivity(AbstractBaseEighthModel):
         favorites (:obj:`list` of :obj:`User`): A ManyToManyField of User objects who have favorited the activity.
         similarities (:obj:`list` of :obj:`EighthActivitySimilarity`): A ManyToManyField of EighthActivitySimilarity
             objects which are similar to this activity.
+        subscribers (:obj:`list` of :obj:`User`): Individual users subscribed to this activity's announcements.
         deleted (bool): Whether the activity still technically exists in the system, but was marked to be deleted.
     """
 
@@ -240,6 +242,7 @@ class EighthActivity(AbstractBaseEighthModel):
     name = models.CharField(max_length=100, validators=[validators.MinLengthValidator(4)])  # This should really be unique
     description = models.CharField(max_length=2000, blank=True)
     sponsors = models.ManyToManyField(EighthSponsor, blank=True)
+    officers = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name="officer_for_set", blank=True)
     rooms = models.ManyToManyField(EighthRoom, blank=True)
     default_capacity = models.SmallIntegerField(null=True, blank=True)
 
@@ -274,6 +277,9 @@ class EighthActivity(AbstractBaseEighthModel):
 
     similarities = models.ManyToManyField("EighthActivitySimilarity", related_name="activity_set", blank=True)
 
+    subscriptions_enabled = models.BooleanField(default=False)
+    subscribers = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name="subscribed_activity_set", blank=True)
+
     deleted = models.BooleanField(blank=True, default=False)
 
     history = HistoricalRecords()
diff --git a/intranet/apps/eighth/serializers.py b/intranet/apps/eighth/serializers.py
index 1e864654a4e..7ce1fb8828b 100644
--- a/intranet/apps/eighth/serializers.py
+++ b/intranet/apps/eighth/serializers.py
@@ -92,7 +92,14 @@ class EighthBlockDetailSerializer(serializers.Serializer):
     comments = serializers.CharField(max_length=100)
 
     def process_scheduled_activity(
-        self, scheduled_activity, request=None, user=None, favorited_activities=None, recommended_activities=None, available_restricted_acts=None
+        self,
+        scheduled_activity,
+        request=None,
+        user=None,
+        favorited_activities=None,
+        subscribed_activities=None,
+        recommended_activities=None,
+        available_restricted_acts=None,
     ):
         activity = scheduled_activity.activity
         if user:
@@ -127,6 +134,8 @@ def process_scheduled_activity(
             "description": activity.description,
             "cancelled": scheduled_activity.cancelled,
             "favorited": activity.id in favorited_activities,
+            "subscribed_to": activity.id in subscribed_activities,
+            "subscriptions_enabled": activity.subscriptions_enabled,
             "roster": {
                 "count": 0,
                 "capacity": 0,
@@ -161,12 +170,26 @@ def process_scheduled_activity(
         return activity_info
 
     def get_activity(
-        self, user, favorited_activities, recommended_activities, available_restricted_acts, block_id, activity_id, scheduled_activity=None
+        self,
+        user,
+        favorited_activities,
+        subscribed_activities,
+        recommended_activities,
+        available_restricted_acts,
+        block_id,
+        activity_id,
+        scheduled_activity=None,
     ):
         if scheduled_activity is None:
             scheduled_activity = EighthScheduledActivity.objects.get(block_id=block_id, activity_id=activity_id)
         return self.process_scheduled_activity(
-            scheduled_activity, self.context["request"], user, favorited_activities, recommended_activities, available_restricted_acts
+            scheduled_activity,
+            self.context["request"],
+            user,
+            favorited_activities,
+            subscribed_activities,
+            recommended_activities,
+            available_restricted_acts,
         )
 
     def get_scheduled_activity(self, scheduled_activity_id):
@@ -179,9 +202,11 @@ def fetch_activity_list_with_metadata(self, block):
         if user:
             favorited_activities = set(user.favorited_activity_set.values_list("id", flat=True))
             recommended_activities = user.recommended_activities
+            subscribed_activities = set(user.subscribed_activity_set.values_list("id", flat=True))
         else:
             favorited_activities = set()
             recommended_activities = set()
+            subscribed_activities = set()
 
         available_restricted_acts = EighthActivity.restricted_activities_available_to_user(user)
 
@@ -202,7 +227,7 @@ def fetch_activity_list_with_metadata(self, block):
         for scheduled_activity in scheduled_activities:
             # Avoid re-fetching scheduled_activity.
             activity_info = self.get_activity(
-                user, favorited_activities, recommended_activities, available_restricted_acts, None, None, scheduled_activity
+                user, favorited_activities, subscribed_activities, recommended_activities, available_restricted_acts, None, None, scheduled_activity
             )
             activity = scheduled_activity.activity
             scheduled_activity_to_activity_map[scheduled_activity.id] = activity.id
diff --git a/intranet/apps/eighth/urls.py b/intranet/apps/eighth/urls.py
index 5652682d0de..b1dcd001f01 100644
--- a/intranet/apps/eighth/urls.py
+++ b/intranet/apps/eighth/urls.py
@@ -14,6 +14,8 @@
     re_path(r"^/leave$", signup.leave_waitlist_view, name="leave_waitlist"),
     re_path(r"^/seen_feature$", signup.seen_new_feature_view, name="seen_new_feature"),
     re_path(r"^/signup/multi$", signup.eighth_multi_signup_view, name="eighth_multi_signup"),
+    re_path(r"^/signup/subscribe/(?P<activity_id>\d+)$", signup.subscribe_to_club, name="subscribe_to_club"),
+    re_path(r"^/signup/unsubscribe/(?P<activity_id>\d+)$", signup.unsubscribe_from_club, name="unsubscribe_from_club"),
     re_path(r"^/toggle_favorite$", signup.toggle_favorite_view, name="eighth_toggle_favorite"),
     re_path(r"^/absences$", attendance.eighth_absences_view, name="eighth_absences"),
     re_path(r"^/absences/(?P<user_id>\d+)$", attendance.eighth_absences_view, name="eighth_absences"),
diff --git a/intranet/apps/eighth/views/signup.py b/intranet/apps/eighth/views/signup.py
index 97be5c56d8a..929ead51704 100644
--- a/intranet/apps/eighth/views/signup.py
+++ b/intranet/apps/eighth/views/signup.py
@@ -386,6 +386,24 @@ def eighth_multi_signup_view(request):
         return render(request, "eighth/multi_signup.html", context)
 
 
+@login_required
+@deny_restricted
+def subscribe_to_club(request, activity_id):
+    activity = get_object_or_404(EighthActivity, id=activity_id)
+
+    activity.subscribers.add(request.user)
+
+    return redirect(request.META.get("HTTP_REFERER", "/"))
+
+
+def unsubscribe_from_club(request, activity_id):
+    activity = get_object_or_404(EighthActivity, id=activity_id)
+    if request.user in activity.subscribers.all():
+        activity.subscribers.remove(request.user)
+
+    return redirect(request.META.get("HTTP_REFERER", "/"))
+
+
 @login_required
 @deny_restricted
 def toggle_favorite_view(request):
diff --git a/intranet/apps/users/models.py b/intranet/apps/users/models.py
index d592100c261..2374ab22712 100644
--- a/intranet/apps/users/models.py
+++ b/intranet/apps/users/models.py
@@ -924,6 +924,16 @@ def is_eighth_sponsor(self) -> bool:
         """
         return EighthSponsor.objects.filter(user=self).exists()
 
+    @property
+    def is_eighth_officer(self) -> bool:
+        """Checks if this user is an officer of an eighth period activity.
+
+        Returns:
+            Whether this user is an officer of an eighth period activity.
+
+        """
+        return self.officer_for_set.exists()
+
     @property
     def frequent_signups(self):
         """Return a QuerySet of activity id's and counts for the activities that a given user
diff --git a/intranet/static/css/dashboard.scss b/intranet/static/css/dashboard.scss
index 0a4f5809ed8..16664858a17 100644
--- a/intranet/static/css/dashboard.scss
+++ b/intranet/static/css/dashboard.scss
@@ -23,6 +23,62 @@
     margin-bottom: 4px;
 }
 
+.club-announcements {
+    padding: 10px;
+    border-radius: 4px;
+    transition: max-height 0.2s ease-in-out;
+    text-align: left;
+
+    &:hover {
+        cursor: pointer;
+    }
+
+    &.collapsed {
+        max-height: 90px !important;
+        overflow: hidden;
+    }
+
+    &.collapsed::after {
+        content: "";
+        position: absolute;
+        z-index: 1;
+        bottom: 0;
+        left: 0;
+        pointer-events: none;
+        background-image: linear-gradient(to bottom, rgba(255, 255, 255, 0), #fce624 90%);
+        width: 100%;
+        height: 4em;
+    }
+
+    &::-webkit-scrollbar {
+        width: 7px;
+    }
+    &::-webkit-scrollbar-track {
+        background: #d6d6d6;
+    }
+    &::-webkit-scrollbar-thumb {
+        background: #888;
+    }
+    &::-webkit-scrollbar-thumb:hover {
+        background: #555;
+    }
+}
+
+.club-announcements-header {
+    text-align: center;
+}
+
+.club-announcements-content {
+    display: none;
+}
+
+.announcements-icon-wrapper:has(> .club-announcements-button) {
+    @media (max-width: 800px) {
+        display: block !important;
+        width: 100%;
+    }
+}
+
 .announcement {
     background-color: white;
     -webkit--radius: 5px;
@@ -131,6 +187,24 @@
     }
 }
 
+a.button {
+    &:hover {
+        color: white !important;
+    }
+
+    &.subscribe-button {
+        color: green;
+        float: right;
+        margin-left: 5px;
+    }
+
+    &.unsubscribe-button {
+        color: red;
+        float: right;
+        margin-left: 5px;
+    }
+}
+
 .announcement {
     h3 {
         &:hover .announcement-icon-wrapper .announcement-toggle,
diff --git a/intranet/static/js/common.js b/intranet/static/js/common.js
index ca83c829413..80e83144954 100644
--- a/intranet/static/js/common.js
+++ b/intranet/static/js/common.js
@@ -52,6 +52,10 @@ $(function() {
         $(".warning-toggle-icon").toggleClass("fa-chevron-down fa-chevron-up");
         $.cookie("collapseWarning", !collapseWarning, {path: "/", expires: 14})
     });
+    $(".club-announcements-header").click(function() {
+        $(".club-announcements-content").slideToggle();
+        $(".club-announcements-toggle-icon").toggleClass("fa-chevron-down fa-chevron-up");
+    });
     if(!collapseWarning) {
         $(".warning-content").show();
         $(".warning-toggle-icon").toggleClass("fa-chevron-down fa-chevron-up");
diff --git a/intranet/templates/announcements/announcement.html b/intranet/templates/announcements/announcement.html
index 43c90672608..7ab6b520d09 100644
--- a/intranet/templates/announcements/announcement.html
+++ b/intranet/templates/announcements/announcement.html
@@ -25,7 +25,21 @@ <h3>
             {{ announcement.title }}
         {% endif %}
 
-            <div class="announcement-icon-wrapper">
+        <div class="announcement-icon-wrapper">
+            {% if announcement.is_club_announcement %}
+                {% if request.user in announcement.activity.subscribers.all %}
+                    <a class="button small-button unsubscribe-button" id="unsubscribe-button" href="{% url 'unsubscribe_from_club' announcement.activity.id %}">
+                        <i class="fas fa-times"></i>
+                        Unsubscribe
+                    </a>
+                {% else %}
+                    <a class="button small-button subscribe-button" id="subscribe-button" href="{% url 'subscribe_to_club' announcement.activity.id %}">
+                        <i class="fas fa-check"></i>
+                        Subscribe
+                    </a>
+                {% endif %}
+            {% endif %}
+        
             {% if hide_announcements %}
                 <a href="#" class="announcement-toggle">
                 {% if announcement.id in user_hidden_announcements %}
@@ -48,17 +62,21 @@ <h3>
                     <i class="announcement-icon far fa-trash-alt"></i>
                 </a>
             {% endif %}
-            </div>
+        </div>
     </h3>
 
     <div class="announcement-metadata">
         by {{ announcement.get_author|escape }} &bull; <time class="timeago" datetime="{{ announcement.added|date:'c' }}">{{ announcement.added|fuzzy_date|capfirst }}</time> &bull; to
-        {% for group in announcement.groups.all %}
-            {{ group }}
-            {% if not forloop.last %},{% endif %}
-        {% empty %}
-            everyone
-        {% endfor %}
+        {% if announcement.is_club_announcement %}
+            {{ announcement.activity.name }}
+        {% else %}
+            {% for group in announcement.groups.all %}
+                {{ group }}
+                {% if not forloop.last %},{% endif %}
+            {% empty %}
+                everyone
+            {% endfor %}
+        {% endif %}
 
         {% if announcement|argument_request_user:"is_visible_requester" %}
             &bull; You approved this announcement.
diff --git a/intranet/templates/announcements/club-request.html b/intranet/templates/announcements/club-request.html
new file mode 100644
index 00000000000..07020b3a7e7
--- /dev/null
+++ b/intranet/templates/announcements/club-request.html
@@ -0,0 +1,158 @@
+{% extends "page_with_nav.html" %}
+{% load static %}
+{% load dates %}
+{% load pipeline %}
+
+{% block title %}
+    {{ block.super }} - {{ action|title }} Announcement
+{% endblock %}
+
+{% block js %}
+    {{ block.super }}
+    <script src="{% static 'vendor/ckeditor/ckeditor.js' %}"></script>
+    <script src="{% static 'js/vendor/chrono.min.js' %}"></script>
+    <script src="{% static 'vendor/datetimepicker-2.4.5/jquery.datetimepicker.js' %}"></script>
+    <script src="{% static 'vendor/selectize.js-0.12.4/dist/js/standalone/selectize.min.js' %}"></script>
+    <script src="{% static 'js/announcement.form.js' %}"></script>
+    <script>
+        $(function() {
+            var author = $("#id_author");
+            var exp = $("#id_expiration_date");
+            dateFormat = function(date) {
+                zero = function(v) { return v<10 ? "0"+v : v; }
+                return (date.getFullYear() + "-" +
+                        zero(date.getMonth()+1) + "-" +
+                        zero(date.getDate()) + " 23:59:59");
+            }
+            dateReset = function() {
+                var date = new Date();
+                date.setDate(date.getDate() + 14);
+                exp.val(dateFormat(date));
+            }
+            date3000 = function() {
+                var date = new Date("3000-01-01 00:00:00");
+                exp.val(dateFormat(date));
+            }
+
+
+            author.attr("placeholder", "{{ user.full_name|escape }}");
+            dateReset();
+
+            var notes = $("#id_notes");
+            if(notes.html().length < 1) {
+                notes.html("Enter something here!\n");
+            }
+
+            $(".helptext", exp.parent()).before("<h5 style='display: none' class='exp-header'><b>Suggested Expiration Dates</b></h4><ul class='exp-list'></ul>");
+            $(".helptext", exp.parent()).before("<span class='exp-buttons'>" +
+                                               "<a onclick='dateReset()' class='button small-button'>Reset to Default</a>" +
+                                               "<a onclick='date3000()' class='button small-button'>Don't Expire</a>" +
+                                               "</span>");
+            $(".exp-list").on("click", "a", function () {
+                exp.val(dateFormat(new Date($(this).data("date"))));
+            })
+            {% if request.user.is_teacher %}
+                $("#id_teachers_requested")[0].selectize.setValue({{ request.user.id }});
+            {% endif %}
+        });
+    </script>
+{% endblock %}
+
+{% block css %}
+    {{ block.super }}
+    <link rel="stylesheet" href="{% static 'vendor/datetimepicker-2.4.5/jquery.datetimepicker.css' %}">
+    <link rel="stylesheet" href="{% static 'vendor/selectize.js-0.12.4/dist/css/selectize.default.css' %}">
+    {% stylesheet 'announcements.form' %}
+    <style>
+
+        div.cke_chrome {
+            margin: 10px 0;
+        }
+
+        .announcements table {
+            width: 600px;
+        }
+
+        .announcements table th {
+            min-width: 120px;
+        }
+
+        .announcements table td {
+            padding: 10px 0;
+        }
+
+        .announcements input,
+        .announcements textarea {
+            width: 100%;
+        }
+
+        @media (max-width: 810px) {
+            .announcements table, .announcements #cke_id_content {
+                width: 342px !important;
+            }
+        }
+
+        @media (max-width: 550px) {
+            .announcements table, .announcements #cke_id_content {
+                width: 400px !important;
+            }
+        }
+
+        ol li {
+            margin-left: 40px;
+            list-style-type: circle;
+        }
+
+        #cke_id_content {
+            width: 600px;
+            margin-bottom: -15px;
+        }
+
+        .announcements .selectize-control {
+            display: inline-block;
+        }
+
+
+    </style>
+{% endblock %}
+
+{% block head %}
+    {% if dark_mode_enabled %}
+        {% stylesheet 'dark/base' %}
+        {% stylesheet 'dark/nav' %}
+        {% stylesheet 'dark/cke' %}
+    {% endif %}
+{% endblock %}
+
+{% block main %}
+<div class="announcements primary-content" style="padding: 0">
+    <h2>
+        {% if action != "add" %}{{ action|title }} {% endif %}Post Club Announcement
+    </h2>
+    <p>
+      Are you an 8th period officer looking to post an announcement for interested students? This page allows you to easily submit club announcements.
+    </p>
+    {% if not request.user.is_restricted %}
+      <p>Want to make an announcement for the whole student body? <b><a href="{% url 'request_announcement' %}">Request an announcement instead!</a></b></p>
+      <p>Want to make an announcement for a specific event or activity? <b><a href="{% url 'request_event' %}">Submit an event for approval instead!</a></b></p>
+      <br />
+    {% endif %}
+    <strong>Guidelines for Club Announcements:</strong><br>
+    <ol>
+        <li>Use correct English grammar, punctuation, and spelling; do not use all caps; keep posts concise when possible, and use active voice for better clarity.</li>
+        <li>If you have a link to an external website in your post, make sure that it can be accessed without having to register for that site or provide any personal information. Facebook links that require you to sign in cannot be used for this reason, and may be omitted. This is to improve compliance with the FCPS Network User Guidelines.</li>
+        <li>If you are talking about an activity or event in your post, please put the location and time in your post body. Otherwise people will have no idea where or when it is.</li>
+        <li>Please ensure your announcement meets all of our guidelines <a target="_blank" href="https://guides.tjhsst.edu/ion/ion-announcement-guidelines">here</a>.</li>
+    </ol>
+    <br>
+    We reserve the right to edit announcements at our discretion (e.g. to correct formatting or spelling errors). Please make sure your post complies with the guidelines.
+    <br><br>
+    <form action="" method="post" id="club_announcement_form">
+        <table>
+            {% csrf_token %}
+            {{ form.as_table }}
+            <tr><td><button type="submit" id="submit_announcement">Submit</button></td></tr>
+        </table>
+    </form>
+</div>
+{% endblock %}
diff --git a/intranet/templates/announcements/request.html b/intranet/templates/announcements/request.html
index d26b5a1b0aa..6b777ba4554 100644
--- a/intranet/templates/announcements/request.html
+++ b/intranet/templates/announcements/request.html
@@ -134,6 +134,8 @@ <h2>
     </p>
     {% if not request.user.is_restricted %}
     <p>Want to make an announcement for a specific event or activity? <b><a href="{% url 'request_event' %}">Submit an event for approval instead!</a></b></p>
+    <p>Want to post an announcement for your club's subscribers? <b><a href="{% url 'post_club_announcement' %}">Submit a club announcement instead!</a></b></p>
+    <br />  
     {% endif %}
     <strong>Guidelines for News Posts:</strong><br>
     To increase the chances that your post comes up quickly, please mind the following:<br>
@@ -144,7 +146,7 @@ <h2>
         <li>If you are talking about a club, activity, or event in your post, please put the location and time in your post body. Otherwise people will have no idea where or when it is.  Please also try to make your post unique; we do not need twenty posts all titled "Free food!"</li>
         <li>If there's a well-defined group, such as "The Class of 2016" that you'd like to limit your post to, add that as a note in the notes field. If we have that group in the system, then we will post it to that group; otherwise, we will do the best that we can. If you do not specify a group, your news post will be visible to all students and faculty.</li>
         <li>Please do not make requests for lost-and-found-type notices.</li>
-        <li>Please ensure your annoucement meets all of our guidelines <a target="_blank" href="https://guides.tjhsst.edu/ion/ion-announcement-guidelines">here</a>.</li>
+        <li>Please ensure your announcement meets all of our guidelines <a target="_blank" href="https://guides.tjhsst.edu/ion/ion-announcement-guidelines">here</a>.</li>
     </ol>
     <br>
     We reserve the right to edit requests at our discretion (e.g. to correct formatting or spelling errors). Please make sure your post complies with the guidelines.
diff --git a/intranet/templates/dashboard/dashboard.html b/intranet/templates/dashboard/dashboard.html
index 09c67caabb3..b1ec5eb5077 100644
--- a/intranet/templates/dashboard/dashboard.html
+++ b/intranet/templates/dashboard/dashboard.html
@@ -117,8 +117,15 @@ <h2>{{ dashboard_header }}</h2>
                     Request Post
                 </a>
             {% else %}
+            {% if more_club_items and not club_items and view_announcements_url != "club_announcements" %}
+                <a class="button club-announcements-button" href="{% url 'club_announcements' %}">
+                    <i class="fas fa-users"></i>
+                    Club Announcements
+                </a>
+            {% endif %}
             {% if announcements_admin %}
-                {% if "show_all" not in request.GET %}
+                {% if view_announcements_url == "club_announcements" %}
+                {% elif "show_all" not in request.GET %}
                     <a class="button" href="{% url view_announcements_url %}?{% query_transform request show_all=1 %}">
                         Show All
                     </a>
@@ -132,11 +139,22 @@ <h2>{{ dashboard_header }}</h2>
                     Add
                 </a>
             {% else %}
+              
+            {% if view_announcements_url == "club_announcements" %}
+                {% if request.user.is_eighth_officer %}
+                    <a class="button announcement-request" href="{% url 'post_club_announcement' %}">
+                        <i class="fas fa-plus"></i>
+                        New
+                    </a>
+                {% endif %}
+            {% else %}  
                 <a class="button announcement-request" href="{% url 'request_announcement' %}">
                     <i class="far fa-file-alt"></i>
                     Request Post
                 </a>
             {% endif %}
+
+            {% endif %}
             {% endif %}
             </span>
     </div>
@@ -181,6 +199,36 @@ <h3>
             </div>
         {% endif %}
 
+        {% if club_items %}
+            <div class="announcement club-announcements">
+                <h3 class="club-announcements-header">
+                    <i class="fas fa-chevron-down club-announcements-toggle-icon"></i>&nbsp;
+                    You have {{ club_items|length }} new club announcement{{ club_items|length|pluralize }}
+                </h3>
+                <div class="club-announcements-content">
+                    {% for item in club_items %}
+                        {% if not hide_announcements or not item.id in user_hidden_announcements %}
+                            {% with announcement=item show_icon=True %}
+                                {% include "announcements/announcement.html" %}
+                            {% endwith %}
+                        {% endif %}
+                    {% endfor %}
+                    {% if more_club_items and view_announcements_url != "club_announcements" %}
+                        <a class="button" href="{% url 'club_announcements' %}">
+                            <i class="fas fa-users"></i>
+                            Show All Club Announcements
+                        </a>
+                        {% if request.user.is_eighth_officer %}
+                            <a class="button announcement-request" href="{% url 'post_club_announcement' %}">
+                                <i class="fas fa-plus"></i>
+                                New
+                            </a>
+                        {% endif %}
+                    {% endif %}
+                </div>
+            </div>
+        {% endif %}
+
         {% if show_near_graduation_message %}
             {% include "dashboard/senior_forwarding.html" %}
         {% endif %}
@@ -207,7 +255,8 @@ <h3>
             </div>
         {% endfor %}
         {% if not request.user.is_restricted %}
-        {% if page_num == 1 and view_announcements_url != 'announcements_archive' %}
+
+        {% if page_num == 1 and view_announcements_url != "announcements_archive" and view_announcements_url != "club_announcements" %}
             <a href="{% url 'announcements_archive' %}" class="button" style="float:left"><i class="fas fa-archive" style="width: 13px"></i> View Archive</a>
         {% endif %}
         {% if page_obj.num_pages > 1 %}
diff --git a/intranet/templates/eighth/signup.html b/intranet/templates/eighth/signup.html
index a0186e72f71..adab7d35895 100644
--- a/intranet/templates/eighth/signup.html
+++ b/intranet/templates/eighth/signup.html
@@ -66,6 +66,7 @@
         }
         window.isEighthAdmin = {% if request.user.is_eighth_admin %}true{% else %}false{% endif %};
         window.waitlistEnabled = {% if waitlist_enabled %}true{% else %}false{% endif %};
+        window.subscribedTo = {% if subscribed_to %}true{% else %}false{% endif %};
         window.blockIsToday = {% if active_block.is_today %}true{% else %}false{% endif %};
         window.signupTime = new Date({{ active_block.date|date:'Y,m-1,j' }},{{ active_block.signup_time|time:'G,i' }});
         window.isSelfSignup = {% if request.user == user %}true{% else %}false{% endif %};
@@ -334,6 +335,21 @@ <h3 class="activity-detail-header">
                             <% } %>
                         <% } %>
                     <%}%>
+                    <br />
+                    <% if (subscriptions_enabled || true) { %>
+                        <!-- TODO -->
+                        <% if (subscribed_to) { %>
+                            <a class="button" id="unsubscribe-button" href="/eighth/signup/unsubscribe/<%= id %>">
+                                <i class="fas fa-rss"></i>
+                                Unsubscribe
+                            </a>
+                        <% } else { %>
+                            <a class="button" id="subscribe-button" href="/eighth/signup/subscribe/<%= id %>">
+                                <i class="fas fa-rss"></i>
+                                Subscribe
+                            </a>
+                        <% } %>
+                    <% } %>
                 <%}%>
 
                 <div class="error-feedback">

From ca4ddd4b777e4deef14946916551c63712991a63 Mon Sep 17 00:00:00 2001
From: Krishnan Shankar <krishnans2006@gmail.com>
Date: Mon, 11 Mar 2024 22:12:49 -0400
Subject: [PATCH 02/42] feat(announcements): animate club announcements on
 dashboard

---
 intranet/static/js/dashboard/announcements.js | 22 ++++++++++++++-----
 .../templates/announcements/announcement.html |  2 +-
 intranet/templates/dashboard/dashboard.html   | 16 ++------------
 3 files changed, 20 insertions(+), 20 deletions(-)

diff --git a/intranet/static/js/dashboard/announcements.js b/intranet/static/js/dashboard/announcements.js
index b22f2e26db2..bdd28a59cab 100644
--- a/intranet/static/js/dashboard/announcements.js
+++ b/intranet/static/js/dashboard/announcements.js
@@ -93,11 +93,23 @@ $(document).ready(function() {
                 .addClass("fa-expand")
                 .attr("title", icon.attr("data-hidden-title"));
 
-            setTimeout(function() {
-                announcement.addClass("hidden");
-            }, 450);
-            announcementContent.css("display", "");
-            announcementContent.slideUp(350);
+            if (announcement.hasClass("remove-on-collapse")) {
+                announcement.slideUp(350);
+                setTimeout(function() {
+                    announcement.remove();
+                    const numAnnouncementsSpan = $(".num-club-announcements");
+                    console.log(numAnnouncementsSpan);
+                    const numAnnouncements = numAnnouncementsSpan.text().match(/\d+/);
+                    numAnnouncementsSpan.text(numAnnouncements - 1);
+                    $(".club-announcements:has(.club-announcements-content:not(:has(.announcement)))").slideUp(350);
+                }, 450);
+            } else {
+                setTimeout(function() {
+                    announcement.addClass("hidden");
+                }, 450);
+                announcementContent.css("display", "");
+                announcementContent.slideUp(350);
+            }
         }
     };
 
diff --git a/intranet/templates/announcements/announcement.html b/intranet/templates/announcements/announcement.html
index 7ab6b520d09..03e6635288c 100644
--- a/intranet/templates/announcements/announcement.html
+++ b/intranet/templates/announcements/announcement.html
@@ -7,7 +7,7 @@
     <script src="{% static 'js/vendor/jquery.timeago.js' %}"></script>
 {% endblock %}
 
-<div class="announcement{% if hide_announcements and announcement.id in user_hidden_announcements %} hidden{% endif %}{% if announcement.pinned %} pinned{% endif %}" data-id="{{ announcement.id }}" id="announcement-{{ announcement.id }}">
+<div class="announcement{% if announcement.is_club_announcement and view_announcements_url != "club_announcements" %} remove-on-collapse{% endif %}{% if hide_announcements and announcement.id in user_hidden_announcements %} hidden{% endif %}{% if announcement.pinned %} pinned{% endif %}" data-id="{{ announcement.id }}" id="announcement-{{ announcement.id }}">
     <h3>
         {% if show_icon and not announcement.pinned %}
             <i class="far fa-file-alt dashboard-item-icon" title="Announcement"></i>
diff --git a/intranet/templates/dashboard/dashboard.html b/intranet/templates/dashboard/dashboard.html
index b1ec5eb5077..db7af4bcdb8 100644
--- a/intranet/templates/dashboard/dashboard.html
+++ b/intranet/templates/dashboard/dashboard.html
@@ -117,7 +117,7 @@ <h2>{{ dashboard_header }}</h2>
                     Request Post
                 </a>
             {% else %}
-            {% if more_club_items and not club_items and view_announcements_url != "club_announcements" %}
+            {% if view_announcements_url != "club_announcements" %}
                 <a class="button club-announcements-button" href="{% url 'club_announcements' %}">
                     <i class="fas fa-users"></i>
                     Club Announcements
@@ -203,7 +203,7 @@ <h3>
             <div class="announcement club-announcements">
                 <h3 class="club-announcements-header">
                     <i class="fas fa-chevron-down club-announcements-toggle-icon"></i>&nbsp;
-                    You have {{ club_items|length }} new club announcement{{ club_items|length|pluralize }}
+                  You have <span class="num-club-announcements">{{ club_items|length }}</span> new club announcement{{ club_items|length|pluralize }}
                 </h3>
                 <div class="club-announcements-content">
                     {% for item in club_items %}
@@ -213,18 +213,6 @@ <h3 class="club-announcements-header">
                             {% endwith %}
                         {% endif %}
                     {% endfor %}
-                    {% if more_club_items and view_announcements_url != "club_announcements" %}
-                        <a class="button" href="{% url 'club_announcements' %}">
-                            <i class="fas fa-users"></i>
-                            Show All Club Announcements
-                        </a>
-                        {% if request.user.is_eighth_officer %}
-                            <a class="button announcement-request" href="{% url 'post_club_announcement' %}">
-                                <i class="fas fa-plus"></i>
-                                New
-                            </a>
-                        {% endif %}
-                    {% endif %}
                 </div>
             </div>
         {% endif %}

From 94777f48ab554d490bdb04dea88c7a1b36e2d34c Mon Sep 17 00:00:00 2001
From: Krishnan Shankar <krishnans2006@gmail.com>
Date: Tue, 12 Mar 2024 20:12:30 -0400
Subject: [PATCH 03/42] feat(announcements): filter club announcements by
 subscription status

also fix subscribe/unsubscribe button styling
---
 intranet/apps/dashboard/views.py              | 19 +++++----
 intranet/static/css/dark/dashboard.scss       | 10 +++++
 intranet/static/css/dashboard.scss            | 41 +++++++++++++++++--
 intranet/static/js/dashboard/announcements.js | 36 ++++++++++++++++
 .../templates/announcements/announcement.html |  2 +-
 intranet/templates/dashboard/dashboard.html   | 11 ++++-
 6 files changed, 104 insertions(+), 15 deletions(-)

diff --git a/intranet/apps/dashboard/views.py b/intranet/apps/dashboard/views.py
index 0dd286a942f..31475dea716 100644
--- a/intranet/apps/dashboard/views.py
+++ b/intranet/apps/dashboard/views.py
@@ -276,19 +276,21 @@ def split_club_announcements(items):
     return standard, club
 
 
-def filter_hidden_club_announcements(user, user_hidden_announcements, club_items):
-    visible, hidden = [], []
+def filter_club_announcements(user, user_hidden_announcements, club_items):
+    visible, hidden, unsubscribed = [], [], []
 
     for item in club_items:
-        if item.id in user_hidden_announcements or user not in item.activity.subscribers.all():
+        if user not in item.activity.subscribers.all():
+            unsubscribed.append(item)
+        elif item.id in user_hidden_announcements:
             hidden.append(item)
         else:
             visible.append(item)
 
-    return visible, hidden
+    return visible, hidden, unsubscribed
 
 
-def paginate_announcements_list(request, context, items, visible_club_items, hidden_club_items):
+def paginate_announcements_list(request, context, items, visible_club_items, more_club_items):
     """
     Paginate ``items`` in groups of 15
 
@@ -488,11 +490,12 @@ def dashboard_view(request, show_widgets=True, show_expired=False, show_hidden_c
 
     items, club_items = split_club_announcements(items)
 
-    # Paginate announcements list
     if not show_hidden_club:
-        visible_club_items, hidden_club_items = filter_hidden_club_announcements(user, user_hidden_announcements, club_items)
-        context, items = paginate_announcements_list(request, context, items, visible_club_items, hidden_club_items)
+        # Dashboard
+        visible_club_items, hidden_club_items, other_club_items = filter_club_announcements(user, user_hidden_announcements, club_items)
+        context, items = paginate_announcements_list(request, context, items, visible_club_items, hidden_club_items or other_club_items)
     else:
+        # Club announcements only
         context, items = paginate_announcements_list(request, context, club_items, [], [])
 
     if ignore_dashboard_types is None:
diff --git a/intranet/static/css/dark/dashboard.scss b/intranet/static/css/dark/dashboard.scss
index 6f847a424a5..741bd9914db 100644
--- a/intranet/static/css/dark/dashboard.scss
+++ b/intranet/static/css/dark/dashboard.scss
@@ -33,3 +33,13 @@
     color: #6060FF;
 }
 
+.club-announcement-filters > .club-announcement-filter {
+    background-color: black;
+    border-color: $darkborder;
+}
+
+a.button {
+    &:hover {
+        color: white !important;
+    }
+}
diff --git a/intranet/static/css/dashboard.scss b/intranet/static/css/dashboard.scss
index 16664858a17..0e319b8dfd7 100644
--- a/intranet/static/css/dashboard.scss
+++ b/intranet/static/css/dashboard.scss
@@ -172,7 +172,7 @@
     float: right;
     display: none;
 
-    .announcement:hover & {
+    .announcement:not(.club-announcements):hover & {
         display: block;
     }
 
@@ -187,11 +187,44 @@
     }
 }
 
-a.button {
-    &:hover {
-        color: white !important;
+.club-announcement-filters {
+    display: flex;
+    justify-content: space-between;
+    flex-grow: 1;
+
+    > .club-announcement-filter {
+        background-color: white;
+        border: 1px solid rgb(216, 216, 216);
+        padding: 6px 10px;
+        margin-bottom: 6px;
+        position: relative;
+
+        text-align: center;
+        font-size: 14px;
+        width: 100%;
+
+        cursor: pointer;
+        font-weight: bolder;
+
+        &.active {
+            background-color: rgb(44, 103, 186);
+            color: white;
+        }
+
+        &.subscribed-filter {
+            border-right: none;
+            border-top-left-radius: 5px;
+            border-bottom-left-radius: 5px;
+        }
+
+        &.unsubscribed-filter {
+            border-top-right-radius: 5px;
+            border-bottom-right-radius: 5px;
+        }
     }
+}
 
+a.button {
     &.subscribe-button {
         color: green;
         float: right;
diff --git a/intranet/static/js/dashboard/announcements.js b/intranet/static/js/dashboard/announcements.js
index bdd28a59cab..7abe9b79e00 100644
--- a/intranet/static/js/dashboard/announcements.js
+++ b/intranet/static/js/dashboard/announcements.js
@@ -129,4 +129,40 @@ $(document).ready(function() {
         var btn = $(".announcement-toggle", $(this).parent());
         announcementToggle.call(btn);
     });
+
+    const subscribedFilter = $(".subscribed-filter");
+    const unsubscribedFilter = $(".unsubscribed-filter");
+
+    function filterClubAnnouncements() {
+        if (subscribedFilter.hasClass("active")) {
+            $(".announcement").each(function() {
+                if ($(this).hasClass("subscribed")) {
+                    $(this).show();
+                } else {
+                    $(this).hide();
+                }
+            });
+        } else if (unsubscribedFilter.hasClass("active")) {
+            $(".announcement").each(function() {
+                if ($(this).hasClass("subscribed")) {
+                    $(this).hide();
+                } else {
+                    $(this).show();
+                }
+            });
+        }
+    }
+    filterClubAnnouncements();
+
+    subscribedFilter.click(function() {
+        $(".unsubscribed-filter").removeClass("active");
+        $(this).addClass("active");
+        filterClubAnnouncements();
+    });
+
+    unsubscribedFilter.click(function() {
+        $(".subscribed-filter").removeClass("active");
+        $(this).addClass("active");
+        filterClubAnnouncements();
+    });
 });
diff --git a/intranet/templates/announcements/announcement.html b/intranet/templates/announcements/announcement.html
index 03e6635288c..f9a12f578eb 100644
--- a/intranet/templates/announcements/announcement.html
+++ b/intranet/templates/announcements/announcement.html
@@ -7,7 +7,7 @@
     <script src="{% static 'js/vendor/jquery.timeago.js' %}"></script>
 {% endblock %}
 
-<div class="announcement{% if announcement.is_club_announcement and view_announcements_url != "club_announcements" %} remove-on-collapse{% endif %}{% if hide_announcements and announcement.id in user_hidden_announcements %} hidden{% endif %}{% if announcement.pinned %} pinned{% endif %}" data-id="{{ announcement.id }}" id="announcement-{{ announcement.id }}">
+<div class="announcement{% if view_announcements_url == "club_announcements" %}{% if request.user in announcement.activity.subscribers.all %} subscribed{% else %} unsubscribed{% endif %}{% endif %}{% if announcement.is_club_announcement and view_announcements_url != "club_announcements" %} remove-on-collapse{% endif %}{% if hide_announcements and announcement.id in user_hidden_announcements %} hidden{% endif %}{% if announcement.pinned %} pinned{% endif %}" data-id="{{ announcement.id }}" id="announcement-{{ announcement.id }}">
     <h3>
         {% if show_icon and not announcement.pinned %}
             <i class="far fa-file-alt dashboard-item-icon" title="Announcement"></i>
diff --git a/intranet/templates/dashboard/dashboard.html b/intranet/templates/dashboard/dashboard.html
index db7af4bcdb8..ed7efad711f 100644
--- a/intranet/templates/dashboard/dashboard.html
+++ b/intranet/templates/dashboard/dashboard.html
@@ -117,8 +117,8 @@ <h2>{{ dashboard_header }}</h2>
                     Request Post
                 </a>
             {% else %}
-            {% if view_announcements_url != "club_announcements" %}
-                <a class="button club-announcements-button" href="{% url 'club_announcements' %}">
+            {% if view_announcements_url != "club_announcements" and more_club_items %}
+                <a class="button club-announcements-button" href="{% url 'club_announcements' %}{% if "show_all" in request.GET %}?show_all=1{% endif %}">
                     <i class="fas fa-users"></i>
                     Club Announcements
                 </a>
@@ -220,6 +220,13 @@ <h3 class="club-announcements-header">
         {% if show_near_graduation_message %}
             {% include "dashboard/senior_forwarding.html" %}
         {% endif %}
+    
+        {% if view_announcements_url == "club_announcements" %}
+            <div class="club-announcement-filters">
+                <div class="club-announcement-filter subscribed-filter active">Your Subscriptions</div>
+                <div class="club-announcement-filter unsubscribed-filter">Other Club Announcements</div>
+            </div>
+        {% endif %}
 
         {% for item in items %}
             {% if item.dashboard_type in ignore_dashboard_types %}

From 9353de5501e1b4a8fde8c970f2da909a5aab8e19 Mon Sep 17 00:00:00 2001
From: Alan Zhu <2025azhu@tjhsst.edu>
Date: Fri, 29 Mar 2024 00:17:57 -0400
Subject: [PATCH 04/42] refactor: update year on search, announcement request
 pages

---
 intranet/apps/announcements/forms.py |  8 +++-----
 intranet/templates/search/tips.html  | 12 ++++++------
 2 files changed, 9 insertions(+), 11 deletions(-)

diff --git a/intranet/apps/announcements/forms.py b/intranet/apps/announcements/forms.py
index f0c3e104bd5..476de634133 100644
--- a/intranet/apps/announcements/forms.py
+++ b/intranet/apps/announcements/forms.py
@@ -1,4 +1,5 @@
 from django import forms
+from django.conf import settings
 from django.contrib.auth import get_user_model
 
 from ..users.forms import SortedTeacherMultipleChoiceField
@@ -77,10 +78,7 @@ class AnnouncementRequestForm(forms.ModelForm):
 
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
-        self.fields["title"].help_text = (
-            "The title of the announcement that will appear on Intranet. Please enter "
-            "a title more specific than just \"[Club name]'s Intranet Posting'."
-        )
+        self.fields["title"].help_text = "The title of the announcement that will appear on Intranet."
         self.fields["author"].help_text = (
             "If you want this post to have a custom author entry, such as "
             '"Basket Weaving Club" or "TJ Faculty," enter that name here. '
@@ -91,7 +89,7 @@ def __init__(self, *args, **kwargs):
         self.fields["notes"].help_text = (
             "Any information about this announcement you wish to share with the Intranet "
             "administrators and teachers selected above. If you want to restrict this posting "
-            "to a specific group of students, such as the Class of 2016, enter that request here."
+            f"to a specific group of students, such as the Class of {settings.SENIOR_GRADUATION_YEAR}, enter that request here."
         )
         self.fields["teachers_requested"] = SortedTeacherMultipleChoiceField(
             queryset=get_user_model().objects.get_approve_announcements_users_sorted(), show_username=True
diff --git a/intranet/templates/search/tips.html b/intranet/templates/search/tips.html
index 0eae98c6b77..d6c4d69eca6 100644
--- a/intranet/templates/search/tips.html
+++ b/intranet/templates/search/tips.html
@@ -9,8 +9,8 @@ <h3>Advanced Search Tips</h3>
         <li><em>middle</em></li>
         <li><em>nickname</em></li>
         <li><em>id</em> (e.g. 31863)</li>
-        <li><em>username</em> (e.g. 2015elowman)</li>
-        <li><em>gradyear</em> (e.g. 2015)</li>
+        <li><em>username</em> (e.g. {{ DJANGO_SETTINGS.SENIOR_GRADUATION_YEAR }}jdoe)</li>
+        <li><em>gradyear</em> (e.g. {{ DJANGO_SETTINGS.SENIOR_GRADUATION_YEAR }})</li>
         <li><em>grade</em> (9-12, staff)</li>
         <li><em>sex</em></li>
         <li><em>email</em></li>
@@ -20,9 +20,9 @@ <h3>Advanced Search Tips</h3>
     <b>Examples</b>
     <ul>
         <li>
-            To find all members of the class of 2016:
+            To find all members of the class of {{ DJANGO_SETTINGS.SENIOR_GRADUATION_YEAR }}:
             <br>
-            <em class="search-example">gradyear:2016</em>
+            <em class="search-example">gradyear:{{ DJANGO_SETTINGS.SENIOR_GRADUATION_YEAR }}</em>
         </li>
         <li>
             To find all females:
@@ -30,9 +30,9 @@ <h3>Advanced Search Tips</h3>
             <em class="search-example">sex:female</em>
         </li>
         <li>
-            To find all males in the class of 2016:
+            To find all males in the class of {{ DJANGO_SETTINGS.SENIOR_GRADUATION_YEAR }}:
             <br>
-            <em class="search-example">gradyear:2016 sex:male</em>
+            <em class="search-example">gradyear:{{ DJANGO_SETTINGS.SENIOR_GRADUATION_YEAR }} sex:male</em>
         </li>
         <li>
             To find senior girls with first names containing "an" followed by "a":

From d50a7ebf628e5bdd6fdcfa641a6739e5edb5eede Mon Sep 17 00:00:00 2001
From: Alan Zhu <2025azhu@tjhsst.edu>
Date: Fri, 29 Mar 2024 00:20:38 -0400
Subject: [PATCH 05/42] refactor(announcements): reword guidelines and remove
 redundant code

---
 intranet/settings/__init__.py                 |   1 +
 intranet/static/css/announcements.form.scss   |   1 +
 .../static/css/announcements.request.scss     |  45 ++++++++
 intranet/static/js/announcement.form.js       |  43 +++++++
 .../templates/announcements/add_modify.html   |  27 -----
 .../templates/announcements/announcement.html |   1 -
 intranet/templates/announcements/approve.html |  29 +----
 intranet/templates/announcements/request.html | 109 ++----------------
 8 files changed, 104 insertions(+), 152 deletions(-)
 create mode 100644 intranet/static/css/announcements.request.scss

diff --git a/intranet/settings/__init__.py b/intranet/settings/__init__.py
index 3dd7ee6d7c3..2fbdcbbef79 100644
--- a/intranet/settings/__init__.py
+++ b/intranet/settings/__init__.py
@@ -344,6 +344,7 @@
     "groups",
     "board",
     "announcements.form",
+    "announcements.request",
     "polls.form",
     "preferences",
     "signage.base",
diff --git a/intranet/static/css/announcements.form.scss b/intranet/static/css/announcements.form.scss
index d8dafd4b46a..7c2a5579ac4 100644
--- a/intranet/static/css/announcements.form.scss
+++ b/intranet/static/css/announcements.form.scss
@@ -84,3 +84,4 @@ div.cke_chrome {
         padding-left: 14px;
     }
 }
+
diff --git a/intranet/static/css/announcements.request.scss b/intranet/static/css/announcements.request.scss
new file mode 100644
index 00000000000..c88a23ec49a
--- /dev/null
+++ b/intranet/static/css/announcements.request.scss
@@ -0,0 +1,45 @@
+.announcements {
+    table {
+        width: 600px;
+
+        th {
+            min-width: 120px;
+        }
+
+        td {
+            padding: 10px 0;
+        }
+    }
+
+    #cke_id_content {
+        width: 600px;
+        margin-bottom: -15px;
+    }
+
+    table, #cke_id_content {
+        @media (max-width: 810px) {
+                width: 342px !important;
+            }
+
+        @media (max-width: 550px) {
+            width: 400px !important;
+        }
+    }
+
+    input, textarea {
+        width: 100%;
+    }
+
+    .selectize-control {
+        display: inline-block;
+    }
+}
+
+ol li {
+    margin-left: 40px;
+    list-style-type: circle;
+}
+
+div.cke_chrome {
+    margin: 10px 0;
+}
\ No newline at end of file
diff --git a/intranet/static/js/announcement.form.js b/intranet/static/js/announcement.form.js
index c5c2184adc0..090c0d0a501 100644
--- a/intranet/static/js/announcement.form.js
+++ b/intranet/static/js/announcement.form.js
@@ -103,4 +103,47 @@ $(function() {
             $(".exp-list").append(`<li><a class='exp-suggest-item' data-date='${use_date}'>"${dates[i].text}" - ${use_date}</a></li>`);
         }
     });
+
+    var exp = $("#id_expiration_date");
+
+    dateReset(exp);
+
+    $(".helptext", exp.parent()).before("<h5 style='display: none' class='exp-header'><b>Suggested Expiration Dates</b></h4><ul class='exp-list'></ul>");
+    $(".helptext", exp.parent()).before("<span class='exp-buttons'>" +
+        "<a id='date-reset-btn' class='button small-button'>Reset to Default</a>" +
+        "<a id='no-expire-btn' class='button small-button'>Don't Expire</a>" +
+        "</span>");
+
+    $(".exp-list").on("click", "a", function () {
+        exp.val(dateFormat(new Date($(this).data("date"))));
+    })
+
+    $("#date-reset-btn").click(function () {
+        dateReset(exp);
+    });
+
+    $("#no-expire-btn").click(function () {
+        date3000(exp);
+    });
 });
+
+function dateReset(exp) {
+    var date = new Date();
+    date.setDate(date.getDate() + 14);
+    exp.val(dateFormat(date));
+}
+
+function date3000(exp) {
+    var date = new Date("3000-01-01 00:00:00");
+    exp.val(dateFormat(date));
+}
+
+function dateFormat(date) {
+    return (date.getFullYear() + "-" +
+        zero(date.getMonth() + 1) + "-" +
+        zero(date.getDate()) + " 23:59:59");
+}
+
+function zero(v) {
+    return v < 10 ? "0" + v : v;
+}
\ No newline at end of file
diff --git a/intranet/templates/announcements/add_modify.html b/intranet/templates/announcements/add_modify.html
index 318a3f8ec8f..155b5a232b5 100644
--- a/intranet/templates/announcements/add_modify.html
+++ b/intranet/templates/announcements/add_modify.html
@@ -16,40 +16,13 @@
     <script src="{% static 'js/announcement.form.js' %}"></script>
     <script>
         $(function() {
-            var author = $("#id_author");
-            var exp = $("#id_expiration_date");
-            dateFormat = function(date) {
-                zero = function(v) { return v<10 ? "0"+v : v; }
-                return (date.getFullYear() + "-" +
-                        zero(date.getMonth()+1) + "-" +
-                        zero(date.getDate()) + " 23:59:59");
-            }
-            dateReset = function() {
-                var date = new Date();
-                date.setDate(date.getDate() + 14);
-                exp.val(dateFormat(date));
-            }
-            date3000 = function() {
-                var date = new Date("3000-01-01 00:00:00");
-                exp.val(dateFormat(date));
-            }
             {% if action == "add" %}
                 $("#id_update_added_date").parent().parent().remove()
-
                 author.attr("placeholder", "{{ user.full_name_nick|escape }}");
                 dateReset()
-
             {% elif announcement %}
                 author.attr("placeholder", "{{ announcement.user.full_name_nick|escape }}");
             {% endif %}
-            $(".helptext", exp.parent()).before("<h5 style='display: none' class='exp-header'><b>Suggested Expiration Dates</b></h4><ul class='exp-list'></ul>");
-            $(".helptext", exp.parent()).before("<span class='exp-buttons'>" +
-                                               "<a onclick='dateReset()' class='button small-button'>Reset to Default</a>" +
-                                               "<a onclick='date3000()' class='button small-button'>Don't Expire</a>" +
-                                               "</span>");
-            $(".exp-list").on("click", "a", function () {
-                exp.val(dateFormat(new Date($(this).data("date"))));
-            })
         });
     </script>
 {% endblock %}
diff --git a/intranet/templates/announcements/announcement.html b/intranet/templates/announcements/announcement.html
index f9a12f578eb..ad9af209aa1 100644
--- a/intranet/templates/announcements/announcement.html
+++ b/intranet/templates/announcements/announcement.html
@@ -39,7 +39,6 @@ <h3>
                     </a>
                 {% endif %}
             {% endif %}
-        
             {% if hide_announcements %}
                 <a href="#" class="announcement-toggle">
                 {% if announcement.id in user_hidden_announcements %}
diff --git a/intranet/templates/announcements/approve.html b/intranet/templates/announcements/approve.html
index 98b9229ab2f..39cd0c26246 100644
--- a/intranet/templates/announcements/approve.html
+++ b/intranet/templates/announcements/approve.html
@@ -16,36 +16,11 @@
     <script src="{% static 'js/announcement.form.js' %}"></script>
     <script>
         $(function() {
-            var author = $("#id_author");
-            var exp = $("#id_expiration_date");
-            dateFormat = function(date) {
-                zero = function(v) { return v<10 ? "0"+v : v; }
-                return (date.getFullYear() + "-" +
-                        zero(date.getMonth()+1) + "-" +
-                        zero(date.getDate()) + " 23:59:59");
-            }
-            dateReset = function() {
-                var date = new Date();
-                date.setDate(date.getDate() + 14);
-                exp.val(dateFormat(date));
-            }
-            date3000 = function() {
-                var date = new Date("3000-01-01 00:00:00");
-                exp.val(dateFormat(date));
-            }
             {% if admin_approve %}
-                author.attr("placeholder", "{{ req.get_author|escape }}")
+                $("#id_author").attr("placeholder", "{{ req.get_author|escape }}")
             {% else %}
-                author.attr("placeholder", "{{ announcement.user.full_name_nick|escape }}");
+                $("#id_author").attr("placeholder", "{{ announcement.user.full_name_nick|escape }}");
             {% endif %}
-            $(".helptext", exp.parent()).before("<h5 style='display: none' class='exp-header'><b>Suggested Expiration Dates</b></h4><ul class='exp-list'></ul>");
-            $(".helptext", exp.parent()).before("<span class='exp-buttons'>" +
-                                               "<a onclick='dateReset()' class='button small-button'>Reset to Default</a>" +
-                                               "<a onclick='date3000()' class='button small-button'>Don't Expire</a>" +
-                                               "</span>");
-            $(".exp-list").on("click", "a", function () {
-                exp.val(dateFormat(new Date($(this).data("date"))));
-            })
         });
     </script>
 {% endblock %}
diff --git a/intranet/templates/announcements/request.html b/intranet/templates/announcements/request.html
index 6b777ba4554..74b1f454a86 100644
--- a/intranet/templates/announcements/request.html
+++ b/intranet/templates/announcements/request.html
@@ -14,43 +14,10 @@
     <script src="{% static 'vendor/datetimepicker-2.4.5/jquery.datetimepicker.js' %}"></script>
     <script src="{% static 'vendor/selectize.js-0.12.4/dist/js/standalone/selectize.min.js' %}"></script>
     <script src="{% static 'js/announcement.form.js' %}"></script>
+    <script src="{% static 'js/announcement.request.js' %}"></script>
     <script>
         $(function() {
-            var author = $("#id_author");
-            var exp = $("#id_expiration_date");
-            dateFormat = function(date) {
-                zero = function(v) { return v<10 ? "0"+v : v; }
-                return (date.getFullYear() + "-" +
-                        zero(date.getMonth()+1) + "-" +
-                        zero(date.getDate()) + " 23:59:59");
-            }
-            dateReset = function() {
-                var date = new Date();
-                date.setDate(date.getDate() + 14);
-                exp.val(dateFormat(date));
-            }
-            date3000 = function() {
-                var date = new Date("3000-01-01 00:00:00");
-                exp.val(dateFormat(date));
-            }
-
-
-            author.attr("placeholder", "{{ user.full_name|escape }}");
-            dateReset();
-
-            var notes = $("#id_notes");
-            if(notes.html().length < 1) {
-                notes.html("Enter something here!\n");
-            }
-
-            $(".helptext", exp.parent()).before("<h5 style='display: none' class='exp-header'><b>Suggested Expiration Dates</b></h4><ul class='exp-list'></ul>");
-            $(".helptext", exp.parent()).before("<span class='exp-buttons'>" +
-                                               "<a onclick='dateReset()' class='button small-button'>Reset to Default</a>" +
-                                               "<a onclick='date3000()' class='button small-button'>Don't Expire</a>" +
-                                               "</span>");
-            $(".exp-list").on("click", "a", function () {
-                exp.val(dateFormat(new Date($(this).data("date"))));
-            })
+            $("#id_author").attr("placeholder", "{{ user.full_name|escape }}");
             {% if request.user.is_teacher %}
                 $("#id_teachers_requested")[0].selectize.setValue({{ request.user.id }});
             {% endif %}
@@ -63,57 +30,7 @@
     <link rel="stylesheet" href="{% static 'vendor/datetimepicker-2.4.5/jquery.datetimepicker.css' %}">
     <link rel="stylesheet" href="{% static 'vendor/selectize.js-0.12.4/dist/css/selectize.default.css' %}">
     {% stylesheet 'announcements.form' %}
-    <style>
-
-        div.cke_chrome {
-            margin: 10px 0;
-        }
-
-        .announcements table {
-            width: 600px;
-        }
-
-        .announcements table th {
-            min-width: 120px;
-        }
-
-        .announcements table td {
-            padding: 10px 0;
-        }
-
-        .announcements input,
-        .announcements textarea {
-            width: 100%;
-        }
-
-        @media (max-width: 810px) {
-            .announcements table, .announcements #cke_id_content {
-                width: 342px !important;
-            }
-        }
-
-        @media (max-width: 550px) {
-            .announcements table, .announcements #cke_id_content {
-                width: 400px !important;
-            }
-        }
-
-        ol li {
-            margin-left: 40px;
-            list-style-type: circle;
-        }
-
-        #cke_id_content {
-            width: 600px;
-            margin-bottom: -15px;
-        }
-
-        .announcements .selectize-control {
-            display: inline-block;
-        }
-
-
-    </style>
+    {% stylesheet 'announcements.request' %}
 {% endblock %}
 
 {% block head %}
@@ -134,22 +51,20 @@ <h2>
     </p>
     {% if not request.user.is_restricted %}
     <p>Want to make an announcement for a specific event or activity? <b><a href="{% url 'request_event' %}">Submit an event for approval instead!</a></b></p>
-    <p>Want to post an announcement for your club's subscribers? <b><a href="{% url 'post_club_announcement' %}">Submit a club announcement instead!</a></b></p>
-    <br />  
+
+    {% if request.user.is_club_officer %}
+    <p>Want to post an announcement for your club's members? <b><a href="{% url 'post_club_announcement' %}">Submit a club announcement instead!</a></b></p>
+    {% endif %}
+    <br />
     {% endif %}
     <strong>Guidelines for News Posts:</strong><br>
-    To increase the chances that your post comes up quickly, please mind the following:<br>
     <ol>
-        <li>Use correct English grammar, punctuation, and spelling; do not use all caps; keep posts concise when possible, and use active voice for better clarity.</li>
-        <li>Embedded images are not permitted directly in news posts except in special circumstances.</li>
-        <li>If you have a link to an external website in your post, make sure that it can be accessed without having to register for that site or provide any personal information. Facebook links that require you to sign in cannot be used for this reason, and may be omitted. This is to improve compliance with the FCPS Network User Guidelines.</li>
-        <li>If you are talking about a club, activity, or event in your post, please put the location and time in your post body. Otherwise people will have no idea where or when it is.  Please also try to make your post unique; we do not need twenty posts all titled "Free food!"</li>
-        <li>If there's a well-defined group, such as "The Class of 2016" that you'd like to limit your post to, add that as a note in the notes field. If we have that group in the system, then we will post it to that group; otherwise, we will do the best that we can. If you do not specify a group, your news post will be visible to all students and faculty.</li>
-        <li>Please do not make requests for lost-and-found-type notices.</li>
-        <li>Please ensure your announcement meets all of our guidelines <a target="_blank" href="https://guides.tjhsst.edu/ion/ion-announcement-guidelines">here</a>.</li>
+        <li>Use correct English grammar, punctuation, and spelling; do not use all caps; keep posts concise.</li>
+        <li>Do not submit repeat announcements; if an announcement about your topic has already been posted, do not request another.</li>
+        <li>See detailed guidelines <a target="_blank" href="https://guides.tjhsst.edu/ion/ion-announcement-guidelines">here</a>.</li>
     </ol>
     <br>
-    We reserve the right to edit requests at our discretion (e.g. to correct formatting or spelling errors). Please make sure your post complies with the guidelines.
+    Intranet Administrators may modify requests at their discretion.
     <br><br>
     <form action="" method="post" id="announcement_form">
         <table>

From 790c928de79b229fe0b1865ff0bc0a80c16e25fd Mon Sep 17 00:00:00 2001
From: Alan Zhu <2025azhu@tjhsst.edu>
Date: Fri, 29 Mar 2024 01:18:50 -0400
Subject: [PATCH 06/42] perf(announcements): add prefetch query in status view
 and tighten perms

Greatly reduces load time
---
 intranet/apps/announcements/views.py | 13 +++++++++----
 1 file changed, 9 insertions(+), 4 deletions(-)

diff --git a/intranet/apps/announcements/views.py b/intranet/apps/announcements/views.py
index 62cab9d53f1..e54a28d33b2 100644
--- a/intranet/apps/announcements/views.py
+++ b/intranet/apps/announcements/views.py
@@ -84,6 +84,7 @@ def announcement_approved_hook(request, obj, req):
 
 
 @login_required
+@deny_restricted
 def request_announcement_view(request):
     """The request announcement page."""
     if request.method == "POST":
@@ -142,7 +143,8 @@ def post_club_announcement_view(request):
 
             obj.save()
 
-            return redirect("index")
+            messages.success(request, "Successfully posted announcement.")
+            return redirect("club_announcements")
         else:
             messages.error(request, "Error adding announcement")
     else:
@@ -151,11 +153,13 @@ def post_club_announcement_view(request):
 
 
 @login_required
+@deny_restricted
 def request_announcement_success_view(request):
     return render(request, "announcements/success.html", {"type": "request"})
 
 
 @login_required
+@deny_restricted
 def request_announcement_success_self_view(request):
     return render(request, "announcements/success.html", {"type": "request", "self": True})
 
@@ -276,11 +280,12 @@ def admin_approve_announcement_view(request, req_id):
 @announcements_admin_required
 @deny_restricted
 def admin_request_status_view(request):
-    all_waiting = AnnouncementRequest.objects.filter(posted=None, rejected=False).this_year()
+    prefetch_fields = ["user", "teachers_requested", "teachers_approved", "posted", "posted_by", "rejected_by"]
+    all_waiting = AnnouncementRequest.objects.filter(posted=None, rejected=False).this_year().prefetch_related(*prefetch_fields)
     awaiting_teacher = all_waiting.filter(teachers_approved__isnull=True)
     awaiting_approval = all_waiting.filter(teachers_approved__isnull=False)
-    approved = AnnouncementRequest.objects.exclude(posted=None).this_year()
-    rejected = AnnouncementRequest.objects.filter(rejected=True).this_year()
+    approved = AnnouncementRequest.objects.exclude(posted=None).this_year().prefetch_related(*prefetch_fields)
+    rejected = AnnouncementRequest.objects.filter(rejected=True).this_year().prefetch_related(*prefetch_fields)
 
     context = {"awaiting_teacher": awaiting_teacher, "awaiting_approval": awaiting_approval, "approved": approved, "rejected": rejected}
 

From ff65c64d93d7df56bf3cc88ddb1a00ae74318d5a Mon Sep 17 00:00:00 2001
From: Alan Zhu <2025azhu@tjhsst.edu>
Date: Fri, 29 Mar 2024 01:33:20 -0400
Subject: [PATCH 07/42] feat(announcements): continue adding functionality and
 permissions

---
 intranet/apps/announcements/admin.py          |   4 +-
 intranet/apps/announcements/forms.py          |  24 +-
 intranet/apps/announcements/models.py         |  14 +-
 intranet/apps/announcements/notifications.py  |  11 +-
 intranet/apps/announcements/urls.py           |   3 +-
 intranet/apps/announcements/views.py          |  59 +++-
 intranet/apps/dashboard/views.py              |  21 +-
 .../apps/eighth/forms/admin/activities.py     |  22 ++
 .../0069_alter_eighthsponsor_user.py          |  21 ++
 .../0070_eighthactivity_club_sponsors.py      |  20 ++
 intranet/apps/eighth/models.py                |  13 +-
 intranet/apps/eighth/views/signup.py          |   8 +-
 intranet/apps/users/models.py                 |  10 +-
 intranet/static/css/dark/dashboard.scss       |   4 +
 intranet/static/css/dashboard.scss            |  83 ++++--
 intranet/static/css/eighth.admin.scss         |  10 +
 intranet/static/js/announcement.form.js       |   4 +-
 intranet/static/js/common.js                  |   4 -
 intranet/static/js/dashboard/announcements.js | 277 +++++++++---------
 intranet/static/js/eighth/admin.js            |  30 +-
 intranet/static/js/eighth/signup.js           |  60 +++-
 .../templates/announcements/add_modify.html   |   1 -
 .../templates/announcements/announcement.html |  46 ++-
 .../templates/announcements/club-request.html | 119 ++------
 intranet/templates/announcements/request.html |   2 +-
 intranet/templates/announcements/view.html    |  14 +-
 intranet/templates/dashboard/dashboard.html   |  47 +--
 .../templates/eighth/admin/edit_activity.html |  15 +-
 intranet/templates/eighth/signup.html         |  46 +--
 29 files changed, 626 insertions(+), 366 deletions(-)
 create mode 100644 intranet/apps/eighth/migrations/0069_alter_eighthsponsor_user.py
 create mode 100644 intranet/apps/eighth/migrations/0070_eighthactivity_club_sponsors.py

diff --git a/intranet/apps/announcements/admin.py b/intranet/apps/announcements/admin.py
index b3bfc532005..a80e2268696 100644
--- a/intranet/apps/announcements/admin.py
+++ b/intranet/apps/announcements/admin.py
@@ -4,8 +4,8 @@
 
 
 class AnnouncementAdmin(admin.ModelAdmin):
-    list_display = ("title", "user", "author", "added")
-    list_filter = ("added", "updated")
+    list_display = ("title", "user", "author", "activity", "added")
+    list_filter = ("added", "updated", "activity")
     ordering = ("-added",)
     raw_id_fields = ("user",)
     search_fields = ("title", "content", "user__first_name", "user__last_name", "user__username")
diff --git a/intranet/apps/announcements/forms.py b/intranet/apps/announcements/forms.py
index 476de634133..37fa37ac515 100644
--- a/intranet/apps/announcements/forms.py
+++ b/intranet/apps/announcements/forms.py
@@ -1,10 +1,15 @@
+import logging
+
 from django import forms
 from django.conf import settings
 from django.contrib.auth import get_user_model
 
+from ..eighth.models import EighthActivity
 from ..users.forms import SortedTeacherMultipleChoiceField
 from .models import Announcement, AnnouncementRequest
 
+logger = logging.getLogger(__name__)
+
 
 class AnnouncementForm(forms.ModelForm):
     """A form for generating an announcement."""
@@ -29,17 +34,28 @@ class ClubAnnouncementForm(forms.ModelForm):
 
     def __init__(self, user, *args, **kwargs):
         super().__init__(*args, **kwargs)
-        self.fields["activity"].queryset = user.officer_for_set
+
+        if user.is_announcements_admin:
+            self.fields["activity"].queryset = EighthActivity.objects.filter(subscriptions_enabled=True)
+        elif user.is_club_officer:
+            self.fields["activity"].queryset = EighthActivity.objects.filter(subscriptions_enabled=True, officers=user)
+        elif user.is_club_sponsor:
+            self.fields["activity"].queryset = user.club_sponsor_for_set.filter(subscriptions_enabled=True)
+        else:
+            self.fields["activity"].queryset = []
+        self.fields["activity"].required = True
+
+        if "instance" in kwargs:  # Don't allow changing the activity once the announcement has been created
+            self.fields["activity"].widget.attrs["disabled"] = True
+            self.fields["activity"].required = False
 
     expiration_date = forms.DateTimeInput()
-    update_added_date = forms.BooleanField(required=False, label="Update Added Date")
 
     class Meta:
         model = Announcement
-        fields = ["title", "author", "content", "activity", "expiration_date", "update_added_date"]
+        fields = ["activity", "title", "content", "expiration_date"]
         help_texts = {
             "expiration_date": "By default, announcements expire after two weeks. To change this, click in the box above.",
-            "update_added_date": "If this announcement has already been added, update the added date to now so that the announcement is pushed to the top. If this option is not selected, the announcement will stay in its current position.",
         }
 
 
diff --git a/intranet/apps/announcements/models.py b/intranet/apps/announcements/models.py
index 2683d6c34b9..390ed796086 100644
--- a/intranet/apps/announcements/models.py
+++ b/intranet/apps/announcements/models.py
@@ -10,7 +10,6 @@
 from ...utils.date import get_date_range_this_year, is_current_year
 from ...utils.deletion import set_historical_user
 from ...utils.html import nullify_links
-
 from ..eighth.models import EighthActivity
 
 
@@ -90,7 +89,7 @@ class Announcement(models.Model):
             The title of the announcement
         content
             The HTML content of the news post
-        authors
+        author
             The name of the author
         added
             The date the announcement was added
@@ -152,6 +151,13 @@ def is_club_announcement(self):
     def is_visible(self, user):
         return self in Announcement.objects.visible_to_user(user)
 
+    def can_modify(self, user):
+        return (
+            user.is_announcements_admin
+            or self.is_club_announcement
+            and (self.is_visible_submitter(user) or user.club_sponsor_for_set.filter(id=self.activity.id).exists())
+        )
+
     # False, not None. This can be None if no AnnouncementRequest exists for this Announcement,
     # and we should not reevaluate in that case.
     _announcementrequest = False  # type: AnnouncementRequest
@@ -165,13 +171,13 @@ def announcementrequest(self):
 
     def is_visible_requester(self, user):
         try:
-            return self.announcementrequest_set.filter(teachers_requested__id=user.id).exists()
+            return self.announcementrequest_set.filter(teachers_requested=user).exists()
         except get_user_model().DoesNotExist:
             return False
 
     def is_visible_submitter(self, user):
         try:
-            return (self.announcementrequest and user.id == self.announcementrequest.user_id) or self.user_id == user.id
+            return self.user == user or self.announcementrequest and user == self.announcementrequest.user
         except get_user_model().DoesNotExist:
             return False
 
diff --git a/intranet/apps/announcements/notifications.py b/intranet/apps/announcements/notifications.py
index 0932d3510b4..4b7c66cf4d8 100644
--- a/intranet/apps/announcements/notifications.py
+++ b/intranet/apps/announcements/notifications.py
@@ -7,7 +7,6 @@
 from django.contrib import messages
 from django.contrib.auth import get_user_model
 from django.core import exceptions
-from django.db.models import Q
 from django.urls import reverse
 from requests_oauthlib import OAuth1
 from sentry_sdk import capture_exception
@@ -119,14 +118,12 @@ def announcement_posted_email(request, obj, send_all=False):
                 .objects.filter(user_type="student", graduation_year__gte=get_senior_graduation_year())
                 .union(get_user_model().objects.filter(user_type__in=["teacher", "counselor"]))
             )
-        elif obj.club:
-            filter = Q(subscribed_to_set__contains=obj.club) & (
-                Q(user_type="student") & Q(graduation_year__gte=get_senior_graduation_year()) | Q(user_type__in=["teacher", "counselor"])
-            )
+        elif obj.activity:
+            subject = f"Club Announcement for {obj.activity.name}: {obj.title}"
             users = (
                 get_user_model()
-                .objects.filter(user_type="student", graduation_year__gte=get_senior_graduation_year(), subscribed_to_set__contains=obj.club)
-                .union(get_user_model().objects.filter(user_type__in=["teacher", "counselor"], subscribed_to_set__contains=obj.club))
+                .objects.filter(user_type="student", graduation_year__gte=get_senior_graduation_year(), subscribed_to_set__contains=obj.activity)
+                .union(get_user_model().objects.filter(user_type__in=["teacher", "counselor"], subscribed_to_set__contains=obj.activity))
             )
 
         else:
diff --git a/intranet/apps/announcements/urls.py b/intranet/apps/announcements/urls.py
index 7c43d863f29..91c482d50ec 100644
--- a/intranet/apps/announcements/urls.py
+++ b/intranet/apps/announcements/urls.py
@@ -8,7 +8,8 @@
     re_path(r"^/club$", views.view_club_announcements, name="club_announcements"),
     re_path(r"^/add$", views.add_announcement_view, name="add_announcement"),
     re_path(r"^/request$", views.request_announcement_view, name="request_announcement"),
-    re_path(r"^/club/post$", views.post_club_announcement_view, name="post_club_announcement"),
+    re_path(r"^/club/add$", views.add_club_announcement_view, name="add_club_announcement"),
+    re_path(r"^/club/modify/(?P<announcement_id>\d+)$", views.modify_club_announcement_view, name="modify_club_announcement"),
     re_path(r"^/request/success$", views.request_announcement_success_view, name="request_announcement_success"),
     re_path(r"^/request/success_self$", views.request_announcement_success_self_view, name="request_announcement_success_self"),
     re_path(r"^/approve/(?P<req_id>\d+)$", views.approve_announcement_view, name="approve_announcement"),
diff --git a/intranet/apps/announcements/views.py b/intranet/apps/announcements/views.py
index e54a28d33b2..5b1d9bd979a 100644
--- a/intranet/apps/announcements/views.py
+++ b/intranet/apps/announcements/views.py
@@ -15,13 +15,8 @@
 from ..groups.models import Group
 from .forms import AnnouncementAdminForm, AnnouncementEditForm, AnnouncementForm, AnnouncementRequestForm, ClubAnnouncementForm
 from .models import Announcement, AnnouncementRequest
-from .notifications import (
-    admin_request_announcement_email,
-    announcement_approved_email,
-    announcement_posted_email,
-    announcement_posted_twitter,
-    request_announcement_email,
-)
+from .notifications import (admin_request_announcement_email, announcement_approved_email, announcement_posted_email, announcement_posted_twitter,
+                            request_announcement_email)
 
 logger = logging.getLogger(__name__)
 
@@ -131,7 +126,13 @@ def request_announcement_view(request):
     return render(request, "announcements/request.html", {"form": form, "action": "add"})
 
 
-def post_club_announcement_view(request):
+@login_required
+@deny_restricted
+def add_club_announcement_view(request):
+    if not (request.user.is_announcements_admin or request.user.is_club_officer or request.user.is_club_sponsor):
+        messages.error(request, "You do not have permission to post club announcements.")
+        return redirect("club_announcements")
+
     if request.method == "POST":
         form = ClubAnnouncementForm(request.user, request.POST)
 
@@ -143,13 +144,47 @@ def post_club_announcement_view(request):
 
             obj.save()
 
-            messages.success(request, "Successfully posted announcement.")
+            messages.success(request, "Successfully posted club announcement.")
             return redirect("club_announcements")
         else:
-            messages.error(request, "Error adding announcement")
+            messages.error(request, "Error adding club announcement")
     else:
         form = ClubAnnouncementForm(request.user)
-    return render(request, "announcements/club-request.html", {"form": form, "action": "add"})
+    return render(request, "announcements/club-request.html", {"form": form, "action": "post"})
+
+
+@login_required
+@deny_restricted
+def modify_club_announcement_view(request, announcement_id):
+    announcement = get_object_or_404(Announcement, id=announcement_id)
+
+    if not announcement.is_club_announcement:
+        messages.error(request, "This announcement is not a club announcement.")
+        return redirect("club_announcements")
+
+    if not announcement.can_modify(request.user):
+        messages.error(request, "You do not have permission to modify this club announcement.")
+        return redirect("club_announcements")
+
+    if request.method == "POST":
+        form = ClubAnnouncementForm(request.user, request.POST, instance=announcement)
+
+        if form.is_valid():
+            obj = form.save(commit=True)
+            obj.user = request.user
+            obj.activity = announcement.activity
+            # SAFE HTML
+            obj.content = safe_html(obj.content)
+
+            obj.save()
+
+            messages.success(request, "Successfully modified club announcement.")
+            return redirect("club_announcements")
+        else:
+            messages.error(request, "Error modifying club announcement")
+    else:
+        form = ClubAnnouncementForm(request.user, instance=announcement)
+    return render(request, "announcements/club-request.html", {"form": form, "action": "modify"})
 
 
 @login_required
@@ -359,7 +394,7 @@ def modify_announcement_view(request, announcement_id=None):
             logger.info("Admin %s modified announcement: %s (%s)", request.user, announcement, announcement.id)
             return redirect("index")
         else:
-            messages.error(request, "Error adding announcement")
+            messages.error(request, "Error modifying announcement")
     else:
         announcement = get_object_or_404(Announcement, id=announcement_id)
         form = AnnouncementEditForm(instance=announcement)
diff --git a/intranet/apps/dashboard/views.py b/intranet/apps/dashboard/views.py
index 31475dea716..c1faae131e5 100644
--- a/intranet/apps/dashboard/views.py
+++ b/intranet/apps/dashboard/views.py
@@ -238,11 +238,11 @@ def get_announcements_list(request, context):
 
     # Load information on the user who posted the announcement
     # Unless the announcement has a custom author (some do, but not all), we will need the user information to construct the byline,
-    announcements = announcements.select_related("user")
+    announcements = announcements.select_related("user", "activity")
 
     # We may query the announcement request multiple times while checking if the user submitted or approved the announcement.
     # prefetch_related() will still make a separate query for each request, but the results are cached if we check them multiple times
-    announcements = announcements.prefetch_related("announcementrequest_set")
+    announcements = announcements.prefetch_related("announcementrequest_set", "groups")
 
     if context["events_admin"] and context["show_all"]:
         events = Event.objects.all()
@@ -252,6 +252,7 @@ def get_announcements_list(request, context):
         # Unlike announcements, show events for the rest of the day after they occur.
         midnight = timezone.localtime().replace(hour=0, minute=0, second=0, microsecond=0)
         events = Event.objects.visible_to_user(user).filter(time__gte=midnight, show_on_dashboard=True)
+    events = events.select_related("user").prefetch_related("groups")
 
     def announcements_sorting_key(item):
         if context["show_expired"] or context["show_all"]:
@@ -269,7 +270,8 @@ def split_club_announcements(items):
 
     for item in items:
         if item.dashboard_type == "announcement" and item.is_club_announcement:
-            club.append(item)
+            if item.activity.subscriptions_enabled:
+                club.append(item)
         else:
             standard.append(item)
 
@@ -280,12 +282,13 @@ def filter_club_announcements(user, user_hidden_announcements, club_items):
     visible, hidden, unsubscribed = [], [], []
 
     for item in club_items:
-        if user not in item.activity.subscribers.all():
-            unsubscribed.append(item)
-        elif item.id in user_hidden_announcements:
-            hidden.append(item)
-        else:
-            visible.append(item)
+        if item.activity.subscriptions_enabled:
+            if user not in item.activity.subscribers.all():
+                unsubscribed.append(item)
+            elif item.id in user_hidden_announcements:
+                hidden.append(item)
+            else:
+                visible.append(item)
 
     return visible, hidden, unsubscribed
 
diff --git a/intranet/apps/eighth/forms/admin/activities.py b/intranet/apps/eighth/forms/admin/activities.py
index 0f4d8953252..3d0a6a71edc 100644
--- a/intranet/apps/eighth/forms/admin/activities.py
+++ b/intranet/apps/eighth/forms/admin/activities.py
@@ -136,9 +136,18 @@ def __init__(self, *args, **kwargs):
         student_objects = get_user_model().objects.get_students()
         self.fields["users_allowed"].queryset = student_objects
         self.fields["users_blacklisted"].queryset = student_objects
+        self.fields["officers"].queryset = student_objects
+        self.fields["club_sponsors"].queryset = get_user_model().objects.filter(user_type__in=["teacher", "counselor"])
 
         self.fields["presign"].label = "2 day pre-signup"
         self.fields["default_capacity"].help_text = "Overrides the sum of each room's capacity above, if set."
+        self.fields["subscriptions_enabled"].label = "Enable club announcements"
+        self.fields["subscriptions_enabled"].help_text = "Allow students to subscribe to receive announcements for this activity through Ion."
+        self.fields["officers"].help_text = "Student officers can send club announcements to subscribers."
+        self.fields["club_sponsors"].help_text = (
+            "Club sponsors can manage this club's announcements. May be different from the activity's scheduled sponsors."
+        )
+        self.fields["subscribers"].help_text = "Students who subscribe to this activity will receive club announcements."
 
         # These fields are rendered on the right of the page on the edit activity page.
         self.right_fields = {
@@ -152,6 +161,15 @@ def __init__(self, *args, **kwargs):
             "seniors_allowed",
         }
 
+        self.club_announcements_fields = set(
+            [
+                "subscriptions_enabled",
+                "club_sponsors",
+                "officers",
+                "subscribers",
+            ]
+        )
+
     class Meta:
         model = EighthActivity
         fields = [
@@ -181,6 +199,10 @@ class Meta:
             "fri_a",
             "fri_b",
             "admin_comments",
+            "subscriptions_enabled",
+            "club_sponsors",
+            "officers",
+            "subscribers",
         ]
         widgets = {
             "description": forms.Textarea(attrs={"rows": 9, "cols": 46}),
diff --git a/intranet/apps/eighth/migrations/0069_alter_eighthsponsor_user.py b/intranet/apps/eighth/migrations/0069_alter_eighthsponsor_user.py
new file mode 100644
index 00000000000..3e8b62f3d04
--- /dev/null
+++ b/intranet/apps/eighth/migrations/0069_alter_eighthsponsor_user.py
@@ -0,0 +1,21 @@
+# Generated by Django 3.2.25 on 2024-03-30 04:11
+
+from django.conf import settings
+from django.db import migrations, models
+import intranet.utils.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('eighth', '0068_auto_20240213_1938'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='eighthsponsor',
+            name='user',
+            field=models.OneToOneField(blank=True, null=True, on_delete=intranet.utils.deletion.set_historical_user, related_name='sponsor_obj', to=settings.AUTH_USER_MODEL),
+        ),
+    ]
diff --git a/intranet/apps/eighth/migrations/0070_eighthactivity_club_sponsors.py b/intranet/apps/eighth/migrations/0070_eighthactivity_club_sponsors.py
new file mode 100644
index 00000000000..32122833a90
--- /dev/null
+++ b/intranet/apps/eighth/migrations/0070_eighthactivity_club_sponsors.py
@@ -0,0 +1,20 @@
+# Generated by Django 3.2.25 on 2024-04-01 02:04
+
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('eighth', '0069_alter_eighthsponsor_user'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='eighthactivity',
+            name='club_sponsors',
+            field=models.ManyToManyField(blank=True, related_name='club_sponsor_for_set', to=settings.AUTH_USER_MODEL),
+        ),
+    ]
diff --git a/intranet/apps/eighth/models.py b/intranet/apps/eighth/models.py
index f112f3e0ba0..d759cecab44 100644
--- a/intranet/apps/eighth/models.py
+++ b/intranet/apps/eighth/models.py
@@ -74,7 +74,7 @@ class EighthSponsor(AbstractBaseEighthModel):
 
     first_name = models.CharField(max_length=50)
     last_name = models.CharField(max_length=50)
-    user = models.OneToOneField(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=set_historical_user)
+    user = models.OneToOneField(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=set_historical_user, related_name="sponsor_obj")
     department = models.CharField(max_length=20, choices=DEPARTMENTS, default="general")
     full_time = models.BooleanField(default=True)
     online_attendance = models.BooleanField(default=True)
@@ -242,7 +242,6 @@ class EighthActivity(AbstractBaseEighthModel):
     name = models.CharField(max_length=100, validators=[validators.MinLengthValidator(4)])  # This should really be unique
     description = models.CharField(max_length=2000, blank=True)
     sponsors = models.ManyToManyField(EighthSponsor, blank=True)
-    officers = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name="officer_for_set", blank=True)
     rooms = models.ManyToManyField(EighthRoom, blank=True)
     default_capacity = models.SmallIntegerField(null=True, blank=True)
 
@@ -271,15 +270,19 @@ class EighthActivity(AbstractBaseEighthModel):
     fri_a = models.BooleanField("Meets Friday A", default=False)
     fri_b = models.BooleanField("Meets Friday B", default=False)
 
+    # For club announcements
+    subscriptions_enabled = models.BooleanField(default=False)
+    subscribers = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name="subscribed_activity_set", blank=True)
+    officers = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name="officer_for_set", blank=True)
+    # Can be different from the sponsor(s) listed for scheduling purposes
+    club_sponsors = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name="club_sponsor_for_set", blank=True)
+
     admin_comments = models.CharField(max_length=1000, blank=True)
 
     favorites = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name="favorited_activity_set", blank=True)
 
     similarities = models.ManyToManyField("EighthActivitySimilarity", related_name="activity_set", blank=True)
 
-    subscriptions_enabled = models.BooleanField(default=False)
-    subscribers = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name="subscribed_activity_set", blank=True)
-
     deleted = models.BooleanField(blank=True, default=False)
 
     history = HistoricalRecords()
diff --git a/intranet/apps/eighth/views/signup.py b/intranet/apps/eighth/views/signup.py
index 929ead51704..1d4c434db92 100644
--- a/intranet/apps/eighth/views/signup.py
+++ b/intranet/apps/eighth/views/signup.py
@@ -391,13 +391,19 @@ def eighth_multi_signup_view(request):
 def subscribe_to_club(request, activity_id):
     activity = get_object_or_404(EighthActivity, id=activity_id)
 
-    activity.subscribers.add(request.user)
+    if activity.subscriptions_enabled:
+        activity.subscribers.add(request.user)
+    else:
+        messages.error(request, "Subscriptions are not enabled for this activity.")
 
     return redirect(request.META.get("HTTP_REFERER", "/"))
 
 
+@login_required
+@deny_restricted
 def unsubscribe_from_club(request, activity_id):
     activity = get_object_or_404(EighthActivity, id=activity_id)
+
     if request.user in activity.subscribers.all():
         activity.subscribers.remove(request.user)
 
diff --git a/intranet/apps/users/models.py b/intranet/apps/users/models.py
index 2374ab22712..040a9506d7e 100644
--- a/intranet/apps/users/models.py
+++ b/intranet/apps/users/models.py
@@ -925,7 +925,7 @@ def is_eighth_sponsor(self) -> bool:
         return EighthSponsor.objects.filter(user=self).exists()
 
     @property
-    def is_eighth_officer(self) -> bool:
+    def is_club_officer(self) -> bool:
         """Checks if this user is an officer of an eighth period activity.
 
         Returns:
@@ -934,6 +934,12 @@ def is_eighth_officer(self) -> bool:
         """
         return self.officer_for_set.exists()
 
+    @property
+    def is_club_sponsor(self) -> bool:
+        """Used only for club announcements permissions. Not used for eighth period scheduling.
+        Use User.is_eighth_sponsor for that instead."""
+        return self.club_sponsor_for_set.exists()
+
     @property
     def frequent_signups(self):
         """Return a QuerySet of activity id's and counts for the activities that a given user
@@ -1266,7 +1272,7 @@ def attribute_is_public(self, permission: str) -> bool:
 
 
 PERMISSIONS_NAMES = {
-    prefix: [name[len(prefix) + 1 :] for name in dir(UserProperties) if name.startswith(prefix + "_")] for prefix in ["self", "parent"]
+    prefix: [name[len(prefix) + 1:] for name in dir(UserProperties) if name.startswith(prefix + "_")] for prefix in ["self", "parent"]
 }
 
 
diff --git a/intranet/static/css/dark/dashboard.scss b/intranet/static/css/dark/dashboard.scss
index 741bd9914db..b2b7d017836 100644
--- a/intranet/static/css/dark/dashboard.scss
+++ b/intranet/static/css/dark/dashboard.scss
@@ -43,3 +43,7 @@ a.button {
         color: white !important;
     }
 }
+
+a.club-announcement-meta-link:hover {
+    color: rgb(196, 196, 196) !important;
+}
diff --git a/intranet/static/css/dashboard.scss b/intranet/static/css/dashboard.scss
index 0e319b8dfd7..1d24ddf721d 100644
--- a/intranet/static/css/dashboard.scss
+++ b/intranet/static/css/dashboard.scss
@@ -29,10 +29,6 @@
     transition: max-height 0.2s ease-in-out;
     text-align: left;
 
-    &:hover {
-        cursor: pointer;
-    }
-
     &.collapsed {
         max-height: 90px !important;
         overflow: hidden;
@@ -66,10 +62,32 @@
 
 .club-announcements-header {
     text-align: center;
+    margin-bottom: 0;
 }
 
 .club-announcements-content {
     display: none;
+    margin-top: 5px;
+}
+
+.club-announcements-container {
+    .announcement, .announcement-meta {
+        display: none;
+    }
+}
+
+a.club-announcement-meta-link, a.club-announcement-meta-link:visited {
+    color: rgb(144, 144, 144);
+    text-decoration: underline;
+
+    &:hover {
+        color: rgb(66, 66, 66);
+    }
+}
+
+.club-announcements-toggle-icon {
+    float: right;
+    margin-top: 4px;
 }
 
 .announcements-icon-wrapper:has(> .club-announcements-button) {
@@ -77,9 +95,13 @@
         display: block !important;
         width: 100%;
     }
+
+    @media (max-width: 550px) {
+        margin-bottom: 6px;
+    }
 }
 
-.announcement {
+.announcement, .club-announcements, .announcement-meta {
     background-color: white;
     -webkit--radius: 5px;
     -moz--radius: 5px;
@@ -98,6 +120,16 @@
             cursor: pointer;
             color: $grey !important;
         }
+
+        &:hover .announcement-icon-wrapper .announcement-toggle,
+        .announcement-icon-wrapper .announcement-toggle:hover,
+        .announcement-icon-wrapper:hover .announcement-toggle:hover {
+            color: rgb(32, 66, 224);
+        }
+
+        .announcement-icon-wrapper:hover .announcement-toggle {
+            color: $grey;
+        }
     }
 
     &.announcement-meta h3 {
@@ -155,6 +187,18 @@
             }
         }
     }
+
+    &.club-announcements {
+        background-color: rgb(231, 231, 231);
+    }
+
+    &-icon {
+        cursor: pointer;
+    }
+
+    &.hidden .announcement-toggle-content {
+        display: none;
+    }
 }
 
 .announcements-icon-wrapper {
@@ -238,28 +282,6 @@ a.button {
     }
 }
 
-.announcement {
-    h3 {
-        &:hover .announcement-icon-wrapper .announcement-toggle,
-        .announcement-icon-wrapper .announcement-toggle:hover,
-        .announcement-icon-wrapper:hover .announcement-toggle:hover {
-            color: rgb(32, 66, 224);
-        }
-
-        .announcement-icon-wrapper:hover .announcement-toggle {
-            color: $grey;
-        }
-    }
-
-    &-icon {
-        cursor: pointer;
-    }
-
-    &.hidden .announcement-toggle-content {
-        display: none;
-    }
-}
-
 .event.hidden .event-toggle-content {
     display: none;
 }
@@ -349,6 +371,13 @@ div[data-placeholder]:not(:focus):not([data-div-placeholder-content]):before {
     .event h3:hover & {
         opacity: 1;
     }
+
+    &.fa-users {
+        width: 36px;
+        font-size: 28px;
+        position: relative;
+        top: 2px;
+    }
 }
 
 .main div.primary-content {
diff --git a/intranet/static/css/eighth.admin.scss b/intranet/static/css/eighth.admin.scss
index be3dde677bd..77a542959d6 100644
--- a/intranet/static/css/eighth.admin.scss
+++ b/intranet/static/css/eighth.admin.scss
@@ -224,3 +224,13 @@ input[type="submit"],
 input[type="reset"] {
     border: 1px solid rgba(0, 72, 171, 0.3);
 }
+
+form[name=edit_form] input[type=checkbox] {
+    position: relative;
+    top: 5px;
+}
+
+tr.club-announcements-field > td, tr.club-announcements-field > th {
+    padding-bottom: 15px;
+    vertical-align: top;
+}
diff --git a/intranet/static/js/announcement.form.js b/intranet/static/js/announcement.form.js
index 090c0d0a501..69e668caf14 100644
--- a/intranet/static/js/announcement.form.js
+++ b/intranet/static/js/announcement.form.js
@@ -5,12 +5,13 @@ $(function() {
         placeholder: "Everyone"
     });
 
-    var reset = $("#id_expiration_date").val() !== "3000-01-01 00:00:00";
     $("#id_expiration_date").datetimepicker({
         lazyInit: true,
         format: "Y-m-d H:i:s"
     });
 
+    $("select#id_activity").selectize();
+
     // for approval page
     $("select#id_teachers_requested").selectize({
         plugins: ["remove_button"],
@@ -44,7 +45,6 @@ $(function() {
     var editor = CKEDITOR.replace("content", {
         width: "600px"
     });
-    var end_index = 0;
 
     editor.on("instanceReady", function () {
         // TODO: Don't duplicate this function. Bad!
diff --git a/intranet/static/js/common.js b/intranet/static/js/common.js
index 80e83144954..ca83c829413 100644
--- a/intranet/static/js/common.js
+++ b/intranet/static/js/common.js
@@ -52,10 +52,6 @@ $(function() {
         $(".warning-toggle-icon").toggleClass("fa-chevron-down fa-chevron-up");
         $.cookie("collapseWarning", !collapseWarning, {path: "/", expires: 14})
     });
-    $(".club-announcements-header").click(function() {
-        $(".club-announcements-content").slideToggle();
-        $(".club-announcements-toggle-icon").toggleClass("fa-chevron-down fa-chevron-up");
-    });
     if(!collapseWarning) {
         $(".warning-content").show();
         $(".warning-toggle-icon").toggleClass("fa-chevron-down fa-chevron-up");
diff --git a/intranet/static/js/dashboard/announcements.js b/intranet/static/js/dashboard/announcements.js
index 7abe9b79e00..1d72285c992 100644
--- a/intranet/static/js/dashboard/announcements.js
+++ b/intranet/static/js/dashboard/announcements.js
@@ -1,168 +1,177 @@
 /* global $ */
 $(document).ready(function() {
-
-    $("div[data-placeholder]").on("keydown keypress input", function() {
-        if (this.textContent) {
-            this.dataset.divPlaceholderContent = 'true';
-        } else {
-            delete this.dataset.divPlaceholderContent;
-        }
-    });
-
-    function updatePartiallyHidden() {
-        if(window.disable_partially_hidden_announcements) {
-            return;
-        }
-
-        $(".announcement:not(.toggled):not(.hidden).partially-hidden").each(function() {
-            var content = $(this).find(".announcement-content");
-            if(content.height() <= 200) {
-                $(this).removeClass("partially-hidden");
-                content.off("click");
-            }
-        });
-        $(".announcement:not(.toggled):not(.hidden):not(.partially-hidden)").each(function() {
-            var content = $(this).find(".announcement-content");
-            if(content.height() > 200) {
-                $(this).addClass("partially-hidden");
-                content.click(function() {
-                    announcementToggle.call($(this).closest(".announcement"));
-                });
-            }
-            else {
-                content.off("click");
-            }
-        });
-    }
     updatePartiallyHidden();
-    $(window).resize(function() {setTimeout(updatePartiallyHidden, 0);});
-
-    function announcementToggle() {
-        var announcement = $(this).closest(".announcement");
-        var announcementContent = $(".announcement-toggle-content", announcement);
-        var icon = $(this).children(0);
-        var id = announcement.attr("data-id");
-
-        if(announcement.hasClass("partially-hidden")) {
-            announcement.addClass("toggled");
-
-            announcement.find(".announcement-content").off("click");
-
-            announcementContent.animate(
-                {"max-height": announcement.find(".announcement-content").height()},
-                {
-                    "duration": 350,
-                    complete: function() {
-                        announcement.removeClass("partially-hidden");
-                        announcementContent.css("max-height", "");
-                    }
-                }
-            );
-            return;
-        }
-
-        if (!id) {
-            console.error("Couldn't toggle invalid announcement ID");
-            return;
-        }
-
-        var hidden = announcement.hasClass("hidden");
-        var action = hidden ? "show" : "hide";
 
-        $.post("/announcements/" + action + "?" + id, {
-            announcement_id: id
-        }, function() {
-            console.info("Announcement", id, action);
-        });
-
-        announcement.addClass("toggled");
-
-        if (action === "show") {
-            icon.removeClass("fa-expand")
-                .addClass("fa-compress")
-                .attr("title", icon.attr("data-visible-title"));
-
-            setTimeout(function() {
-                announcement.removeClass("hidden");
-            }, 450);
+    filterClubAnnouncements();
 
-            announcementContent.css("display", "");
-            announcementContent.slideDown(350);
-        } else {
-            icon.removeClass("fa-compress")
-                .addClass("fa-expand")
-                .attr("title", icon.attr("data-hidden-title"));
-
-            if (announcement.hasClass("remove-on-collapse")) {
-                announcement.slideUp(350);
-                setTimeout(function() {
-                    announcement.remove();
-                    const numAnnouncementsSpan = $(".num-club-announcements");
-                    console.log(numAnnouncementsSpan);
-                    const numAnnouncements = numAnnouncementsSpan.text().match(/\d+/);
-                    numAnnouncementsSpan.text(numAnnouncements - 1);
-                    $(".club-announcements:has(.club-announcements-content:not(:has(.announcement)))").slideUp(350);
-                }, 450);
-            } else {
-                setTimeout(function() {
-                    announcement.addClass("hidden");
-                }, 450);
-                announcementContent.css("display", "");
-                announcementContent.slideUp(350);
-            }
+    $(".club-announcements-header").click(function () {
+        let content = $(".club-announcements-content");
+        if (!content.is(":visible")) {  // Avoid FOUC
+            content.show();
+            updatePartiallyHidden();
+            content.hide();
         }
-    };
+        content.slideToggle();
+        $(".club-announcements-toggle-icon").toggleClass("fa-chevron-down fa-chevron-up");
+    });
 
-    $(".announcement[data-id] h3").click(function(e) {
+    $(".announcement[data-id] h3").click(function (e) {
         if (e.target !== this) return;
         var btn = $(".announcement-toggle", $(this));
         announcementToggle.call(btn);
     });
 
-    $(".announcement[data-id] h3 .announcement-toggle").click(function(e) {
+    $(".announcement[data-id] h3 .announcement-toggle").click(function (e) {
         e.preventDefault();
         announcementToggle.call($(this));
     });
 
-    $(".announcement[data-id] h3 .dashboard-item-icon").click(function(e) {
+    $(".announcement[data-id] h3 .dashboard-item-icon").click(function (e) {
         e.preventDefault();
         var btn = $(".announcement-toggle", $(this).parent());
         announcementToggle.call(btn);
     });
 
-    const subscribedFilter = $(".subscribed-filter");
-    const unsubscribedFilter = $(".unsubscribed-filter");
+    $(window).resize(function () { setTimeout(updatePartiallyHidden, 0); });
 
-    function filterClubAnnouncements() {
-        if (subscribedFilter.hasClass("active")) {
-            $(".announcement").each(function() {
-                if ($(this).hasClass("subscribed")) {
-                    $(this).show();
-                } else {
-                    $(this).hide();
-                }
-            });
-        } else if (unsubscribedFilter.hasClass("active")) {
-            $(".announcement").each(function() {
-                if ($(this).hasClass("subscribed")) {
-                    $(this).hide();
-                } else {
-                    $(this).show();
-                }
-            });
+    $("div[data-placeholder]").on("keydown keypress input", function () {
+        if (this.textContent) {
+            this.dataset.divPlaceholderContent = 'true';
+        } else {
+            delete this.dataset.divPlaceholderContent;
         }
-    }
-    filterClubAnnouncements();
+    });
 
-    subscribedFilter.click(function() {
+    $(".subscribed-filter").click(function () {
         $(".unsubscribed-filter").removeClass("active");
         $(this).addClass("active");
         filterClubAnnouncements();
     });
 
-    unsubscribedFilter.click(function() {
+    $(".unsubscribed-filter").click(function () {
         $(".subscribed-filter").removeClass("active");
         $(this).addClass("active");
         filterClubAnnouncements();
     });
+
 });
+
+function updatePartiallyHidden() {
+    if (window.disable_partially_hidden_announcements) {
+        return;
+    }
+
+    $(".announcement:not(.toggled):not(.hidden).partially-hidden").each(function () {
+        var content = $(this).find(".announcement-content");
+        if (content.height() <= 200) {
+            $(this).removeClass("partially-hidden");
+            content.off("click");
+        }
+    });
+    $(".announcement:not(.toggled):not(.hidden):not(.partially-hidden)").each(function () {
+        var content = $(this).find(".announcement-content");
+        if (content.height() > 200) {
+            $(this).addClass("partially-hidden");
+            content.click(function () {
+                announcementToggle.call($(this).closest(".announcement"));
+            });
+        }
+        else {
+            content.off("click");
+        }
+    });
+}
+
+function announcementToggle() {
+    var announcement = $(this).closest(".announcement");
+    var announcementContent = $(".announcement-toggle-content", announcement);
+    var icon = $(this).children(0);
+    var id = announcement.attr("data-id");
+
+    if (announcement.hasClass("partially-hidden")) {
+        announcement.addClass("toggled");
+
+        announcement.find(".announcement-content").off("click");
+
+        announcementContent.animate(
+            { "max-height": announcement.find(".announcement-content").height() },
+            {
+                "duration": 350,
+                complete: function () {
+                    announcement.removeClass("partially-hidden");
+                    announcementContent.css("max-height", "");
+                }
+            }
+        );
+        return;
+    }
+
+    if (!id) {
+        console.error("Couldn't toggle invalid announcement ID");
+        return;
+    }
+
+    var hidden = announcement.hasClass("hidden");
+    var action = hidden ? "show" : "hide";
+
+    $.post("/announcements/" + action + "?" + id, {
+        announcement_id: id
+    });
+
+    announcement.addClass("toggled");
+
+    if (action === "show") {
+        icon.removeClass("fa-expand")
+            .addClass("fa-compress")
+            .attr("title", icon.attr("data-visible-title"));
+
+        setTimeout(function () {
+            announcement.removeClass("hidden");
+        }, 450);
+
+        announcementContent.css("display", "");
+        announcementContent.slideDown(350);
+    } else {
+        icon.removeClass("fa-compress")
+            .addClass("fa-expand")
+            .attr("title", icon.attr("data-hidden-title"));
+
+        if (announcement.hasClass("remove-on-collapse")) {
+            announcement.slideUp(350);
+            setTimeout(function () {
+                announcement.remove();
+                const numAnnouncementsSpan = $(".num-club-announcements");
+                const numAnnouncements = numAnnouncementsSpan.text().match(/\d+/);
+                numAnnouncementsSpan.text(numAnnouncements - 1);
+                $(".club-announcements:has(.club-announcements-content:not(:has(.announcement)))").slideUp(350);
+            }, 450);
+        } else {
+            setTimeout(function () {
+                announcement.addClass("hidden");
+            }, 450);
+            announcementContent.css("display", "");
+            announcementContent.slideUp(350);
+        }
+    }
+};
+
+function filterClubAnnouncements() {
+    if ($(".subscribed-filter").hasClass("active")) {
+        $(".announcement").each(function () {
+            if ($(this).hasClass("subscribed")) {
+                $(this).fadeIn();
+            } else {
+                $(this).hide();
+            }
+        });
+    } else if ($(".unsubscribed-filter").hasClass("active")) {
+        $(".announcement").each(function () {
+            if ($(this).hasClass("subscribed")) {
+                $(this).hide();
+            } else {
+                $(this).fadeIn();
+            }
+        });
+    }
+    updatePartiallyHidden();
+}
\ No newline at end of file
diff --git a/intranet/static/js/eighth/admin.js b/intranet/static/js/eighth/admin.js
index f31683c2bfa..1e3b2a30bbd 100644
--- a/intranet/static/js/eighth/admin.js
+++ b/intranet/static/js/eighth/admin.js
@@ -128,15 +128,15 @@ $(function() {
     });
 
     // Disable *_allowed form elements if Restricted isn't checked
-    var updateRestrictedFormFields = function() {
-        var restricted = $("#id_restricted").prop("checked");
-        $("#id_restricted").parents("tr").nextAll().each(function(index, tr) {
-            $(tr).find("input").attr("readonly", !restricted);
+    function updateDisabledFormFields(el) {
+        var checked = $(el).prop("checked");
+        $(el).parents("tr").nextAll().each(function(index, tr) {
+            $(tr).find("input").attr("readonly", !checked);
             if ($(tr).find("input").attr("type") === "checkbox") {
-                $(tr).find("input").attr("disabled", !restricted);
+                $(tr).find("input").attr("disabled", !checked);
             }
             $(tr).find("select").each(function(index, select) {
-                if (restricted) {
+                if (checked) {
                     select.selectize.enable();
                 } else {
                     select.selectize.disable();
@@ -144,15 +144,25 @@ $(function() {
             }).attr('disabled', false);
         });
 
-        // Blacklist should be always enabled
+        // Blacklist should always be enabled
         $("#id_users_blacklisted").parent("td").find("input").attr("readonly", false);
-        var select = $("#id_users_blacklisted").parent("td").find("select")[0].selectize.enable();
+        $("#id_users_blacklisted").parent("td").find("select")[0].selectize.enable();
     }
 
-    $("#id_restricted").click(updateRestrictedFormFields);
+    $("#id_restricted").click(function() {
+        updateDisabledFormFields($(this));
+    });
+
+    $("#id_subscriptions_enabled").click(function() {
+        updateDisabledFormFields($(this));
+    });
 
     if ($("#id_restricted").length > 0) {
-        updateRestrictedFormFields();
+        updateDisabledFormFields($("#id_restricted"));
+    }
+
+    if ($("#id_subscriptions_enabled").length > 0) {
+        updateDisabledFormFields($("#id_subscriptions_enabled"));
     }
 
     $("#only-show-overbooked").click(function() {
diff --git a/intranet/static/js/eighth/signup.js b/intranet/static/js/eighth/signup.js
index 6576e06e252..a0f494b7046 100644
--- a/intranet/static/js/eighth/signup.js
+++ b/intranet/static/js/eighth/signup.js
@@ -44,7 +44,9 @@ $(function() {
             "click button#leave-waitlist": "leaveWaitlistClickHandler",
             "click a#roster-button": "rosterClickHandler",
             "click a#roster-waitlist-button": "rosterWaitlistClickHandler",
-            "click button#close-activity-detail": "closeActivityDetail"
+            "click button#close-activity-detail": "closeActivityDetail",
+            "click a#subscribe-button": "subscribeClickHandler",
+            "click a#unsubscribe-button": "unsubscribeClickHandler"
         },
 
         render: function() {
@@ -142,6 +144,62 @@ $(function() {
             $(".primary-content.eighth-signup").removeClass("activity-detail-selected");
             $("#activity-detail").removeClass("selected");
             $("li.selected[data-activity-id]").removeClass("selected");
+        },
+
+        subscribeClickHandler: function(e) {
+            e.preventDefault();
+            var target = e.target;
+            var spinnerEl = document.getElementById("signup-spinner");
+            var spinner = new Spinner(spinnerOptions).spin(spinnerEl);
+            var url = $(target).attr("href");
+            var name = $(target).parent().parent().parent().find(".activity-detail-link").text().trim();
+            $.post(url, function(data) {
+                spinner.spin(false);
+                $(target).html("<i class='fas fa-rss'></i> Unsubscribe");
+                $(target).attr("id", "unsubscribe-button");
+                $(target).attr("href", url.replace("subscribe", "unsubscribe"));
+                Messenger().success({
+                    message: 'Subscribed to club announcements for ' + name + '.',
+                    hideAfter: 2,
+                    showCloseButton: true
+                });
+            }).fail(function (xhr, status, error) {
+                spinner.spin(false);
+                console.error(xhr.responseText);
+                Messenger().error({
+                    message: 'An error occurred subscribing to club announcements for ' + name + '. Try refreshing the page.',
+                    hideAfter: 5,
+                    showCloseButton: false
+                });
+            });
+        },
+
+        unsubscribeClickHandler: function (e) {
+            e.preventDefault();
+            var target = e.target;
+            var spinnerEl = document.getElementById("signup-spinner");
+            var spinner = new Spinner(spinnerOptions).spin(spinnerEl);
+            var url = $(target).attr("href");
+            var name = $(target).parent().parent().parent().find(".activity-detail-link").text().trim();
+            $.post(url, function (data) {
+                spinner.spin(false);
+                $(target).html("<i class='fas fa-rss'></i> Subscribe");
+                $(target).attr("id", "subscribe-button");
+                $(target).attr("href", url.replace("unsubscribe", "subscribe"));
+                Messenger().error({
+                    message: 'Unsubscribed from club announcements for ' + name + '.',
+                    hideAfter: 2,
+                    showCloseButton: true
+                });
+            }).fail(function (xhr, status, error) {
+                spinner.spin(false);
+                console.error(xhr.responseText);
+                Messenger().error({
+                    message: 'An error occurred unsubscribing from club announcements for ' + name + '. Try refreshing the page.',
+                    hideAfter: 5,
+                    showCloseButton: false
+                });
+            });
         }
     });
 
diff --git a/intranet/templates/announcements/add_modify.html b/intranet/templates/announcements/add_modify.html
index 155b5a232b5..dbbdb8c0a6b 100644
--- a/intranet/templates/announcements/add_modify.html
+++ b/intranet/templates/announcements/add_modify.html
@@ -19,7 +19,6 @@
             {% if action == "add" %}
                 $("#id_update_added_date").parent().parent().remove()
                 author.attr("placeholder", "{{ user.full_name_nick|escape }}");
-                dateReset()
             {% elif announcement %}
                 author.attr("placeholder", "{{ announcement.user.full_name_nick|escape }}");
             {% endif %}
diff --git a/intranet/templates/announcements/announcement.html b/intranet/templates/announcements/announcement.html
index ad9af209aa1..1af23e6d9c7 100644
--- a/intranet/templates/announcements/announcement.html
+++ b/intranet/templates/announcements/announcement.html
@@ -7,9 +7,27 @@
     <script src="{% static 'js/vendor/jquery.timeago.js' %}"></script>
 {% endblock %}
 
-<div class="announcement{% if view_announcements_url == "club_announcements" %}{% if request.user in announcement.activity.subscribers.all %} subscribed{% else %} unsubscribed{% endif %}{% endif %}{% if announcement.is_club_announcement and view_announcements_url != "club_announcements" %} remove-on-collapse{% endif %}{% if hide_announcements and announcement.id in user_hidden_announcements %} hidden{% endif %}{% if announcement.pinned %} pinned{% endif %}" data-id="{{ announcement.id }}" id="announcement-{{ announcement.id }}">
+<div data-id="{{ announcement.id }}" id="announcement-{{ announcement.id }}"
+    class="announcement
+    {% if view_announcements_url == "club_announcements" %}
+        {% if request.user in announcement.activity.subscribers.all %}
+            subscribed
+        {% else %}
+            unsubscribed
+        {% endif %}
+    {% endif %}
+    {% if announcement.is_club_announcement and view_announcements_url != "club_announcements" %}
+        remove-on-collapse
+    {% endif %}
+    {% if hide_announcements and announcement.id in user_hidden_announcements %}
+        hidden
+    {% endif %}
+    {% if announcement.pinned %}
+        pinned
+    {% endif %}">
+
     <h3>
-        {% if show_icon and not announcement.pinned %}
+        {% if show_icon and not announcement.pinned and not announcement.is_club_announcement %}
             <i class="far fa-file-alt dashboard-item-icon" title="Announcement"></i>
         {% endif %}
 
@@ -17,6 +35,10 @@ <h3>
             <i class="fas fa-bookmark dashboard-item-icon" title="Pinned announcement"></i>
         {% endif %}
 
+        {% if announcement.is_club_announcement %}
+            <i class="fas fa-users dashboard-item-icon" title="Club announcement"></i>
+        {% endif %}
+
         {% if link_to_announcement and not request.user.is_restricted %}
         <a href="{% url 'view_announcement' announcement.id %}" class="announcement-link">
             {{ announcement.title }}
@@ -28,12 +50,18 @@ <h3>
         <div class="announcement-icon-wrapper">
             {% if announcement.is_club_announcement %}
                 {% if request.user in announcement.activity.subscribers.all %}
-                    <a class="button small-button unsubscribe-button" id="unsubscribe-button" href="{% url 'unsubscribe_from_club' announcement.activity.id %}">
+                    <a class="button small-button unsubscribe-button"
+                        id="unsubscribe-button"
+                        href="{% url 'unsubscribe_from_club' announcement.activity.id %}"
+                        title="Unsubscribe from {{ announcement.activity.name }} club announcements">
                         <i class="fas fa-times"></i>
                         Unsubscribe
                     </a>
                 {% else %}
-                    <a class="button small-button subscribe-button" id="subscribe-button" href="{% url 'subscribe_to_club' announcement.activity.id %}">
+                    <a class="button small-button subscribe-button"
+                        id="subscribe-button"
+                        href="{% url 'subscribe_to_club' announcement.activity.id %}"
+                        title="Subscribe to {{ announcement.activity.name }} club announcements">
                         <i class="fas fa-check"></i>
                         Subscribe
                     </a>
@@ -53,10 +81,16 @@ <h3>
                     <i class="fas fa-paperclip fa-flip-horizontal"></i>
                 </a>
             {% endif %}
-            {% if request.user.is_announcements_admin %}
+            {% if announcement|argument_request_user:"can_modify" %}
+                {% if announcement.is_club_announcement %}
+                    <a href="{% url 'modify_club_announcement' announcement.id %}" title="Modify club announcement">
+                        <i class="announcement-icon fas fa-pencil-alt"></i>
+                    </a>
+                {% else %}
                 <a href="{% url 'modify_announcement' announcement.id %}" title="Modify announcement">
                     <i class="announcement-icon fas fa-pencil-alt"></i>
                 </a>
+                {% endif %}
                 <a href="{% url 'delete_announcement' announcement.id %}" class="announcement-delete" title="Delete announcement">
                     <i class="announcement-icon far fa-trash-alt"></i>
                 </a>
@@ -67,7 +101,7 @@ <h3>
     <div class="announcement-metadata">
         by {{ announcement.get_author|escape }} &bull; <time class="timeago" datetime="{{ announcement.added|date:'c' }}">{{ announcement.added|fuzzy_date|capfirst }}</time> &bull; to
         {% if announcement.is_club_announcement %}
-            {{ announcement.activity.name }}
+            <a class="club-announcement-meta-link" href="{% url 'eighth_activity' announcement.activity.id %}">{{ announcement.activity.name }}</a>
         {% else %}
             {% for group in announcement.groups.all %}
                 {{ group }}
diff --git a/intranet/templates/announcements/club-request.html b/intranet/templates/announcements/club-request.html
index 07020b3a7e7..a854470fb61 100644
--- a/intranet/templates/announcements/club-request.html
+++ b/intranet/templates/announcements/club-request.html
@@ -16,41 +16,6 @@
     <script src="{% static 'js/announcement.form.js' %}"></script>
     <script>
         $(function() {
-            var author = $("#id_author");
-            var exp = $("#id_expiration_date");
-            dateFormat = function(date) {
-                zero = function(v) { return v<10 ? "0"+v : v; }
-                return (date.getFullYear() + "-" +
-                        zero(date.getMonth()+1) + "-" +
-                        zero(date.getDate()) + " 23:59:59");
-            }
-            dateReset = function() {
-                var date = new Date();
-                date.setDate(date.getDate() + 14);
-                exp.val(dateFormat(date));
-            }
-            date3000 = function() {
-                var date = new Date("3000-01-01 00:00:00");
-                exp.val(dateFormat(date));
-            }
-
-
-            author.attr("placeholder", "{{ user.full_name|escape }}");
-            dateReset();
-
-            var notes = $("#id_notes");
-            if(notes.html().length < 1) {
-                notes.html("Enter something here!\n");
-            }
-
-            $(".helptext", exp.parent()).before("<h5 style='display: none' class='exp-header'><b>Suggested Expiration Dates</b></h4><ul class='exp-list'></ul>");
-            $(".helptext", exp.parent()).before("<span class='exp-buttons'>" +
-                                               "<a onclick='dateReset()' class='button small-button'>Reset to Default</a>" +
-                                               "<a onclick='date3000()' class='button small-button'>Don't Expire</a>" +
-                                               "</span>");
-            $(".exp-list").on("click", "a", function () {
-                exp.val(dateFormat(new Date($(this).data("date"))));
-            })
             {% if request.user.is_teacher %}
                 $("#id_teachers_requested")[0].selectize.setValue({{ request.user.id }});
             {% endif %}
@@ -63,57 +28,7 @@
     <link rel="stylesheet" href="{% static 'vendor/datetimepicker-2.4.5/jquery.datetimepicker.css' %}">
     <link rel="stylesheet" href="{% static 'vendor/selectize.js-0.12.4/dist/css/selectize.default.css' %}">
     {% stylesheet 'announcements.form' %}
-    <style>
-
-        div.cke_chrome {
-            margin: 10px 0;
-        }
-
-        .announcements table {
-            width: 600px;
-        }
-
-        .announcements table th {
-            min-width: 120px;
-        }
-
-        .announcements table td {
-            padding: 10px 0;
-        }
-
-        .announcements input,
-        .announcements textarea {
-            width: 100%;
-        }
-
-        @media (max-width: 810px) {
-            .announcements table, .announcements #cke_id_content {
-                width: 342px !important;
-            }
-        }
-
-        @media (max-width: 550px) {
-            .announcements table, .announcements #cke_id_content {
-                width: 400px !important;
-            }
-        }
-
-        ol li {
-            margin-left: 40px;
-            list-style-type: circle;
-        }
-
-        #cke_id_content {
-            width: 600px;
-            margin-bottom: -15px;
-        }
-
-        .announcements .selectize-control {
-            display: inline-block;
-        }
-
-
-    </style>
+    {% stylesheet 'announcements.request' %}
 {% endblock %}
 
 {% block head %}
@@ -127,31 +42,47 @@
 {% block main %}
 <div class="announcements primary-content" style="padding: 0">
     <h2>
-        {% if action != "add" %}{{ action|title }} {% endif %}Post Club Announcement
+        {{ action|title }} Club Announcement
     </h2>
+
+    {% if action != "modify" %}
     <p>
-      Are you an 8th period officer looking to post an announcement for interested students? This page allows you to easily submit club announcements.
+      Are you an 8th period {% if request.user.is_teacher %}sponsor{% else %}officer{% endif %}
+      looking to post an announcement for club members? This page allows you to easily submit club announcements.
     </p>
     {% if not request.user.is_restricted %}
       <p>Want to make an announcement for the whole student body? <b><a href="{% url 'request_announcement' %}">Request an announcement instead!</a></b></p>
       <p>Want to make an announcement for a specific event or activity? <b><a href="{% url 'request_event' %}">Submit an event for approval instead!</a></b></p>
       <br />
     {% endif %}
+    {% endif %}
     <strong>Guidelines for Club Announcements:</strong><br>
     <ol>
-        <li>Use correct English grammar, punctuation, and spelling; do not use all caps; keep posts concise when possible, and use active voice for better clarity.</li>
-        <li>If you have a link to an external website in your post, make sure that it can be accessed without having to register for that site or provide any personal information. Facebook links that require you to sign in cannot be used for this reason, and may be omitted. This is to improve compliance with the FCPS Network User Guidelines.</li>
-        <li>If you are talking about an activity or event in your post, please put the location and time in your post body. Otherwise people will have no idea where or when it is.</li>
-        <li>Please ensure your announcement meets all of our guidelines <a target="_blank" href="https://guides.tjhsst.edu/ion/ion-announcement-guidelines">here</a>.</li>
+        <li>Use correct English grammar, punctuation, and spelling; do not use all caps; keep posts concise.</li>
+        <li>Do not submit an excessive number of announcements, especially if they are about similar topics.</li>
+        <li>You are submitting to an official school website. Your announcement must be relevant to your club and contain appropriate content.</li>
+        <li>All announcement submissions, edits, and deletions are logged and may be reviewed by club sponsors and administrators.</li>
+        <li>See detailed guidelines <a target="_blank" href="https://guides.tjhsst.edu/ion/ion-announcement-guidelines">here</a>.</li>
     </ol>
     <br>
-    We reserve the right to edit announcements at our discretion (e.g. to correct formatting or spelling errors). Please make sure your post complies with the guidelines.
+    Intranet Administrators and club sponsors may modify announcements at their discretion.
     <br><br>
     <form action="" method="post" id="club_announcement_form">
         <table>
             {% csrf_token %}
             {{ form.as_table }}
-            <tr><td><button type="submit" id="submit_announcement">Submit</button></td></tr>
+            <tr>
+                <td></td>
+                <td>
+                    {% if request.user.is_student %}
+                    <hr>
+                    <input type="checkbox" required style="position: relative; top: 2px">
+                    I have read and agree to the club announcement guidelines above.
+                    <br>
+                    {% endif %}
+                    <button type="submit" id="submit_announcement" style="width: 200px">Submit</button>
+                </td>
+            </tr>
         </table>
     </form>
 </div>
diff --git a/intranet/templates/announcements/request.html b/intranet/templates/announcements/request.html
index 74b1f454a86..5455659a098 100644
--- a/intranet/templates/announcements/request.html
+++ b/intranet/templates/announcements/request.html
@@ -53,7 +53,7 @@ <h2>
     <p>Want to make an announcement for a specific event or activity? <b><a href="{% url 'request_event' %}">Submit an event for approval instead!</a></b></p>
 
     {% if request.user.is_club_officer %}
-    <p>Want to post an announcement for your club's members? <b><a href="{% url 'post_club_announcement' %}">Submit a club announcement instead!</a></b></p>
+    <p>Want to post an announcement for your club's members? <b><a href="{% url 'add_club_announcement' %}">Submit a club announcement instead!</a></b></p>
     {% endif %}
     <br />
     {% endif %}
diff --git a/intranet/templates/announcements/view.html b/intranet/templates/announcements/view.html
index 45917fe9c4e..80c27c7d225 100644
--- a/intranet/templates/announcements/view.html
+++ b/intranet/templates/announcements/view.html
@@ -30,11 +30,21 @@
 {% block main %}
 <div class="announcements primary-content">
     <div class="announcements-header">
-        <h2>Announcement: {{ announcement.title|escape }}</h2>
+        <h2>
+            {% if announcement.is_club_announcement %}
+                Club Announcement for
+                <a href="{% url 'eighth_activity' announcement.activity.id %}">{{ announcement.activity|escape }}</a>:
+            {% else %}
+            Announcement:
+            {% endif %}
+            {{ announcement.title|escape }}
+        </h2>
     </div>
 
     <div class="announcements-container">
-        {% include "announcements/announcement.html" %}
+        {% with show_icon=True %}
+            {% include "announcements/announcement.html" %}
+        {% endwith %}
     </div>
 </div>
 {% endblock %}
diff --git a/intranet/templates/dashboard/dashboard.html b/intranet/templates/dashboard/dashboard.html
index ed7efad711f..9f42891d808 100644
--- a/intranet/templates/dashboard/dashboard.html
+++ b/intranet/templates/dashboard/dashboard.html
@@ -117,7 +117,7 @@ <h2>{{ dashboard_header }}</h2>
                     Request Post
                 </a>
             {% else %}
-            {% if view_announcements_url != "club_announcements" and more_club_items %}
+            {% if view_announcements_url != "club_announcements" %}
                 <a class="button club-announcements-button" href="{% url 'club_announcements' %}{% if "show_all" in request.GET %}?show_all=1{% endif %}">
                     <i class="fas fa-users"></i>
                     Club Announcements
@@ -125,29 +125,39 @@ <h2>{{ dashboard_header }}</h2>
             {% endif %}
             {% if announcements_admin %}
                 {% if view_announcements_url == "club_announcements" %}
+                    <a class="button announcement-request" href="{% url 'add_club_announcement' %}">
+                        <i class="fas fa-plus"></i>
+                        Post
+                    </a>
                 {% elif "show_all" not in request.GET %}
                     <a class="button" href="{% url view_announcements_url %}?{% query_transform request show_all=1 %}">
                         Show All
                     </a>
                 {% else %}
-                    <a class="button" href="{% url view_announcements_url %}">
-                        <i class="fas fa-times"></i> Don't Show All
+                    {% if "show_all" not in request.GET %}
+                        <a class="button" href="{% url view_announcements_url %}?show_all=1">
+                            Show All
+                        </a>
+                    {% else %}
+                        <a class="button" href="{% url view_announcements_url %}">
+                            <i class="fas fa-times"></i> Don't Show All
+                        </a>
+                    {% endif %}
+                    <a class="button announcement-add" href="{% url 'add_announcement' %}">
+                        <i class="fas fa-plus"></i>
+                        Add
                     </a>
                 {% endif %}
-                <a class="button announcement-add" href="{% url 'add_announcement' %}">
-                    <i class="fas fa-plus"></i>
-                    Add
-                </a>
             {% else %}
-              
+
             {% if view_announcements_url == "club_announcements" %}
-                {% if request.user.is_eighth_officer %}
-                    <a class="button announcement-request" href="{% url 'post_club_announcement' %}">
+                {% if request.user.is_club_officer or request.user.is_club_sponsor %}
+                    <a class="button announcement-request" href="{% url 'add_club_announcement' %}">
                         <i class="fas fa-plus"></i>
-                        New
+                        Post
                     </a>
                 {% endif %}
-            {% else %}  
+            {% else %}
                 <a class="button announcement-request" href="{% url 'request_announcement' %}">
                     <i class="far fa-file-alt"></i>
                     Request Post
@@ -159,7 +169,7 @@ <h2>{{ dashboard_header }}</h2>
             </span>
     </div>
 
-    <div class="announcements-container">
+    <div class="announcements-container {% if view_announcements_url == "club_announcements" %}club-announcements-container{% endif %}">
         {% if show_homecoming %}
             {% include "special/hoco_ribbon.html" %}
         {% endif %}
@@ -168,7 +178,7 @@ <h2>{{ dashboard_header }}</h2>
         {% endif %}
         {% if announcements_admin %}
             {% if awaiting_teacher or awaiting_approval %}
-                <div class="announcement announcement-meta">
+                <div class="announcement-meta">
                 {% if awaiting_approval %}
                     <h3>
                     <i class="fas fa-exclamation-circle" style="color: red"></i> There {% if awaiting_approval|length == 1 %}is 1 announcement request{% else %}are {{ awaiting_approval|length }} announcement requests{% endif %} waiting for admin approval</h3>
@@ -200,10 +210,11 @@ <h3>
         {% endif %}
 
         {% if club_items %}
-            <div class="announcement club-announcements">
+            <div class="club-announcements">
                 <h3 class="club-announcements-header">
-                    <i class="fas fa-chevron-down club-announcements-toggle-icon"></i>&nbsp;
-                  You have <span class="num-club-announcements">{{ club_items|length }}</span> new club announcement{{ club_items|length|pluralize }}
+                    <i class="fas fa-users"></i>&nbsp;
+                    You have <span class="num-club-announcements">{{ club_items|length }}</span> new club announcement{{ club_items|length|pluralize }}
+                    <i class="fas fa-chevron-down club-announcements-toggle-icon"></i>
                 </h3>
                 <div class="club-announcements-content">
                     {% for item in club_items %}
@@ -220,7 +231,7 @@ <h3 class="club-announcements-header">
         {% if show_near_graduation_message %}
             {% include "dashboard/senior_forwarding.html" %}
         {% endif %}
-    
+
         {% if view_announcements_url == "club_announcements" %}
             <div class="club-announcement-filters">
                 <div class="club-announcement-filter subscribed-filter active">Your Subscriptions</div>
diff --git a/intranet/templates/eighth/admin/edit_activity.html b/intranet/templates/eighth/admin/edit_activity.html
index d7cb15b1b15..e55b410ee7c 100644
--- a/intranet/templates/eighth/admin/edit_activity.html
+++ b/intranet/templates/eighth/admin/edit_activity.html
@@ -142,7 +142,7 @@ <h4>Select an Activity:</h4>
             {{ hidden }}
         {% endfor %}
         {% for field in form.visible_fields %}
-            {% if field.name not in form.right_fields %}
+            {% if field.name not in form.right_fields and field.name not in form.club_announcements_fields %}
                 <tr>
                     <th>{{ field.label }}</th><td>{{ field.errors }} {{ field }} {% if field.help_text %}<br><span class="helptext">{{ field.help_text }}</span>{% endif %}</td>
                 </tr>
@@ -232,6 +232,19 @@ <h2>Restriction List:</h2>
             <br>
             <button onclick="addGroup()">Add A Group</button>
         {% endif %}
+
+        <br>
+        <h2>Club Announcements:</h2>
+        <table class="edit-table">
+        {% for field in form.visible_fields %}
+            {% if field.name in form.club_announcements_fields %}
+                <tr class="club-announcements-field">
+                    <th>{{ field.label }}</th>
+                    <td>{{ field.errors }} {{ field }} {% if field.help_text %}<span class="helptext">{{ field.help_text }}</span>{% endif %}</td>
+                </tr>
+            {% endif %}
+        {% endfor %}
+        </table>
     </div>
 </form>
 {% endblock %}
diff --git a/intranet/templates/eighth/signup.html b/intranet/templates/eighth/signup.html
index adab7d35895..3b899fd6f79 100644
--- a/intranet/templates/eighth/signup.html
+++ b/intranet/templates/eighth/signup.html
@@ -66,7 +66,6 @@
         }
         window.isEighthAdmin = {% if request.user.is_eighth_admin %}true{% else %}false{% endif %};
         window.waitlistEnabled = {% if waitlist_enabled %}true{% else %}false{% endif %};
-        window.subscribedTo = {% if subscribed_to %}true{% else %}false{% endif %};
         window.blockIsToday = {% if active_block.is_today %}true{% else %}false{% endif %};
         window.signupTime = new Date({{ active_block.date|date:'Y,m-1,j' }},{{ active_block.signup_time|time:'G,i' }});
         window.isSelfSignup = {% if request.user == user %}true{% else %}false{% endif %};
@@ -83,11 +82,11 @@
     <script>
     {% if request.GET.activity %}
         $(function() {
-                $("li[data-activity-id='{{ request.GET.activity|escape }}']").click();
+            $("li[data-activity-id='{{ request.GET.activity|escape }}']").click();
         })
     {% elif active_block_current_signup %}
         $(function() {
-                $("li[data-activity-id='{{ active_block_current_signup|escape }}']").click();
+            $("li[data-activity-id='{{ active_block_current_signup|escape }}']").click();
         })
     {% endif %}
     {% if not real_user.is_eighth_admin %}
@@ -312,6 +311,19 @@ <h3 class="activity-detail-header">
                             Get Notifications
                         </button>
                         <% } %>
+                        <% if (subscriptions_enabled) { %>
+                            <% if (subscribed_to) { %>
+                                <a class="button" id="unsubscribe-button" href="/eighth/signup/unsubscribe/<%= id %>">
+                                    <i class="fas fa-rss"></i>
+                                    Unsubscribe
+                                </a>
+                            <% } else { %>
+                                <a class="button" id="subscribe-button" href="/eighth/signup/subscribe/<%= id %>">
+                                    <i class="fas fa-rss"></i>
+                                    Subscribe
+                                </a>
+                            <% } %>
+                        <% } %>
                         <div id="signup-spinner-container">
                             <div id="signup-spinner"></div>
                         </div>
@@ -335,27 +347,25 @@ <h3 class="activity-detail-header">
                             <% } %>
                         <% } %>
                     <%}%>
-                    <br />
-                    <% if (subscriptions_enabled || true) { %>
-                        <!-- TODO -->
-                        <% if (subscribed_to) { %>
-                            <a class="button" id="unsubscribe-button" href="/eighth/signup/unsubscribe/<%= id %>">
-                                <i class="fas fa-rss"></i>
-                                Unsubscribe
-                            </a>
-                        <% } else { %>
-                            <a class="button" id="subscribe-button" href="/eighth/signup/subscribe/<%= id %>">
-                                <i class="fas fa-rss"></i>
-                                Subscribe
-                            </a>
-                        <% } %>
-                    <% } %>
                 <%}%>
 
                 <div class="error-feedback">
                 </div>
             </div>
             <% if (!showingRosterButton) { %>
+                <% if (subscriptions_enabled) { %>
+                    <% if (subscribed_to) { %>
+                        <a class="button" id="unsubscribe-button" href="/eighth/signup/unsubscribe/<%= id %>">
+                            <i class="fas fa-rss"></i>
+                            Unsubscribe
+                        </a>
+                    <% } else { %>
+                        <a class="button" id="subscribe-button" href="/eighth/signup/subscribe/<%= id %>">
+                            <i class="fas fa-rss"></i>
+                            Subscribe
+                        </a>
+                    <% } %>
+                <% } %>
                 <% if(isEighthAdmin) { %>
                     <a class="button" id="roster-button" data-endpoint="/eighth/roster/raw" href="/eighth/roster/<%= scheduled_activity_id %>">
                         View Roster

From ae02f8db348bd524aea2d5fa1c04d33312d9e781 Mon Sep 17 00:00:00 2001
From: Krishnan Shankar <krishnans2006@gmail.com>
Date: Sun, 7 Apr 2024 01:29:40 -0400
Subject: [PATCH 08/42] style(announcements): dark theme fixes

---
 intranet/static/css/dark/dashboard.scss | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/intranet/static/css/dark/dashboard.scss b/intranet/static/css/dark/dashboard.scss
index b2b7d017836..eaed2c8adff 100644
--- a/intranet/static/css/dark/dashboard.scss
+++ b/intranet/static/css/dark/dashboard.scss
@@ -4,7 +4,7 @@
     opacity: 0.75;
 }
 
-.announcement {
+.announcement, .club-announcements, .announcement-meta {
     background: black;
     border-color: $darkborder;
 
@@ -23,6 +23,10 @@
                         black 80%
         );
     }
+
+    &.club-announcements {
+        background-color: rgb(24, 24, 24);
+    }
 }
 
 .announcement-icon-wrapper > a, .announcement h3 .announcement-icon-wrapper:hover .announcement-toggle {

From 424691144bb65a9fa923faa068b6497cbcdaf33e Mon Sep 17 00:00:00 2001
From: Krishnan Shankar <krishnans2006@gmail.com>
Date: Sun, 7 Apr 2024 01:31:30 -0400
Subject: [PATCH 09/42] fix(announcements): handle some edge cases

---
 intranet/apps/announcements/views.py          | 25 ++++++++++++++++---
 intranet/static/js/dashboard/announcements.js |  8 ++++++
 intranet/templates/dashboard/dashboard.html   |  2 +-
 3 files changed, 31 insertions(+), 4 deletions(-)

diff --git a/intranet/apps/announcements/views.py b/intranet/apps/announcements/views.py
index 5b1d9bd979a..dfb7419cb69 100644
--- a/intranet/apps/announcements/views.py
+++ b/intranet/apps/announcements/views.py
@@ -15,8 +15,13 @@
 from ..groups.models import Group
 from .forms import AnnouncementAdminForm, AnnouncementEditForm, AnnouncementForm, AnnouncementRequestForm, ClubAnnouncementForm
 from .models import Announcement, AnnouncementRequest
-from .notifications import (admin_request_announcement_email, announcement_approved_email, announcement_posted_email, announcement_posted_twitter,
-                            request_announcement_email)
+from .notifications import (
+    admin_request_announcement_email,
+    announcement_approved_email,
+    announcement_posted_email,
+    announcement_posted_twitter,
+    request_announcement_email,
+)
 
 logger = logging.getLogger(__name__)
 
@@ -129,7 +134,11 @@ def request_announcement_view(request):
 @login_required
 @deny_restricted
 def add_club_announcement_view(request):
-    if not (request.user.is_announcements_admin or request.user.is_club_officer or request.user.is_club_sponsor):
+    is_announcements_admin = request.user.is_announcements_admin
+    is_club_sponsor = request.user.is_club_sponsor
+    is_club_officer = request.user.is_club_officer
+
+    if not (is_announcements_admin or is_club_sponsor or is_club_officer):
         messages.error(request, "You do not have permission to post club announcements.")
         return redirect("club_announcements")
 
@@ -150,6 +159,16 @@ def add_club_announcement_view(request):
             messages.error(request, "Error adding club announcement")
     else:
         form = ClubAnnouncementForm(request.user)
+
+        if not form.fields["activity"].queryset.exists():
+            if is_announcements_admin:
+                messages.error(request, "No clubs have enabled this feature yet.")
+            elif is_club_sponsor:
+                messages.error(request, "Please enable club announcements for your club.")
+            else:
+                messages.error(request, "Please ask your club sponsor to enable posting announcements for your club.")
+            return redirect("club_announcements")
+
     return render(request, "announcements/club-request.html", {"form": form, "action": "post"})
 
 
diff --git a/intranet/static/js/dashboard/announcements.js b/intranet/static/js/dashboard/announcements.js
index 1d72285c992..9b5e8b7a438 100644
--- a/intranet/static/js/dashboard/announcements.js
+++ b/intranet/static/js/dashboard/announcements.js
@@ -158,6 +158,10 @@ function announcementToggle() {
 function filterClubAnnouncements() {
     if ($(".subscribed-filter").hasClass("active")) {
         $(".announcement").each(function () {
+            if ($(this).hasClass("exclude-subscribed-filer")) {
+                $(this).fadeIn();
+                return;
+            }
             if ($(this).hasClass("subscribed")) {
                 $(this).fadeIn();
             } else {
@@ -166,6 +170,10 @@ function filterClubAnnouncements() {
         });
     } else if ($(".unsubscribed-filter").hasClass("active")) {
         $(".announcement").each(function () {
+            if ($(this).hasClass("exclude-subscribed-filer")) {
+                $(this).fadeIn();
+                return;
+            }
             if ($(this).hasClass("subscribed")) {
                 $(this).hide();
             } else {
diff --git a/intranet/templates/dashboard/dashboard.html b/intranet/templates/dashboard/dashboard.html
index 9f42891d808..8a182869150 100644
--- a/intranet/templates/dashboard/dashboard.html
+++ b/intranet/templates/dashboard/dashboard.html
@@ -252,7 +252,7 @@ <h3 class="club-announcements-header">
                 {% endwith %}
             {% endif %}
         {% empty %}
-            <div class="announcement">
+            <div class="announcement exclude-subscribed-filer">
                 {% if not request.user.is_restricted %}
                 There are no announcements to display at this time.
                 {% else %}

From b6e91d75dc775cc52c0646d0950363e72b30dc5f Mon Sep 17 00:00:00 2001
From: Krishnan Shankar <krishnans2006@gmail.com>
Date: Sun, 7 Apr 2024 01:39:18 -0400
Subject: [PATCH 10/42] style(announcements): improve club announcement form

---
 intranet/templates/announcements/club-request.html | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/intranet/templates/announcements/club-request.html b/intranet/templates/announcements/club-request.html
index a854470fb61..28379609c19 100644
--- a/intranet/templates/announcements/club-request.html
+++ b/intranet/templates/announcements/club-request.html
@@ -76,8 +76,10 @@ <h2>
                 <td>
                     {% if request.user.is_student %}
                     <hr>
-                    <input type="checkbox" required style="position: relative; top: 2px">
-                    I have read and agree to the club announcement guidelines above.
+                      <label>
+                        <input type="checkbox" required style="position: relative; top: 2px">
+                        I have read and agree to the club announcement guidelines above.
+                      </label>
                     <br>
                     {% endif %}
                     <button type="submit" id="submit_announcement" style="width: 200px">Submit</button>

From 347047fc9517f26aeb86ff5345f5a3ac3a52ab52 Mon Sep 17 00:00:00 2001
From: Krishnan Shankar <krishnans2006@gmail.com>
Date: Sun, 7 Apr 2024 02:19:37 -0400
Subject: [PATCH 11/42] feat(eighth): add subscribe and unsubscribe buttons
 everywhere

---
 intranet/templates/eighth/activity.html       | 16 +++++++++++++++-
 intranet/templates/eighth/multi_signup.html   | 13 +++++++++++++
 intranet/templates/eighth/profile_signup.html | 13 +++++++++++++
 3 files changed, 41 insertions(+), 1 deletion(-)

diff --git a/intranet/templates/eighth/activity.html b/intranet/templates/eighth/activity.html
index 03bd122f71b..c28f2ab7b5c 100644
--- a/intranet/templates/eighth/activity.html
+++ b/intranet/templates/eighth/activity.html
@@ -64,6 +64,20 @@
         </a>
     {% endif %}
 
+    {% if activity.subscriptions_enabled %}
+        {% if request.user in activity.subscribers.all %}
+            <a class="button" href="{% url 'unsubscribe_from_club' activity.id %}">
+                <i class="fas fa-rss"></i>
+                Unsubscribe
+            </a>
+        {% else %}
+            <a class="button" href="{% url 'subscribe_to_club' activity.id %}">
+                <i class="fas fa-rss"></i>
+                Subscribe
+            </a>
+        {% endif %}
+    {% endif %}
+
     <h2 style="padding-bottom: 0">Activity: {{ activity }}</h2>
     {% if activity.special %}
         <span class="badge green" title="This is a special activity.">Special</span>
@@ -200,7 +214,7 @@ <h3>
         </tbody>
         </table>
         <br>
-        All meetings of this activity are not guaranteed to appear on this page.
+        Not all meetings of this activity are guaranteed to appear on this page.
         <br>
         <b>Activity schedules are not fixed, and may change. Check Eighth Period schedules.</b>
         <br>
diff --git a/intranet/templates/eighth/multi_signup.html b/intranet/templates/eighth/multi_signup.html
index 0927e005e99..d780804abbc 100644
--- a/intranet/templates/eighth/multi_signup.html
+++ b/intranet/templates/eighth/multi_signup.html
@@ -301,6 +301,19 @@ <h3 class="activity-detail-header">
                             Get Notifications
                         </button>
                         <% } %>
+                        <% if (subscriptions_enabled) { %>
+                            <% if (subscribed_to) { %>
+                                <a class="button" id="unsubscribe-button" href="/eighth/signup/unsubscribe/<%= id %>">
+                                    <i class="fas fa-rss"></i>
+                                    Unsubscribe
+                                </a>
+                            <% } else { %>
+                                <a class="button" id="subscribe-button" href="/eighth/signup/subscribe/<%= id %>">
+                                    <i class="fas fa-rss"></i>
+                                    Subscribe
+                                </a>
+                            <% } %>
+                        <% } %>
                         <div id="signup-spinner-container">
                             <div id="signup-spinner"></div>
                         </div>
diff --git a/intranet/templates/eighth/profile_signup.html b/intranet/templates/eighth/profile_signup.html
index e10ddebcf55..48ec00146b9 100644
--- a/intranet/templates/eighth/profile_signup.html
+++ b/intranet/templates/eighth/profile_signup.html
@@ -293,6 +293,19 @@ <h3 class="activity-detail-header">
                             Get Notifications
                         </button>
                         <% } %>
+                        <% if (subscriptions_enabled) { %>
+                            <% if (subscribed_to) { %>
+                                <a class="button" id="unsubscribe-button" href="/eighth/signup/unsubscribe/<%= id %>">
+                                    <i class="fas fa-rss"></i>
+                                    Unsubscribe
+                                </a>
+                            <% } else { %>
+                                <a class="button" id="subscribe-button" href="/eighth/signup/subscribe/<%= id %>">
+                                    <i class="fas fa-rss"></i>
+                                    Subscribe
+                                </a>
+                            <% } %>
+                        <% } %>
                         <div id="signup-spinner-container">
                             <div id="signup-spinner"></div>
                         </div>

From d548e8a6f382a9ecd093e104f9af520c3e41f17a Mon Sep 17 00:00:00 2001
From: Krishnan Shankar <krishnans2006@gmail.com>
Date: Sun, 7 Apr 2024 22:54:58 -0400
Subject: [PATCH 12/42] feat(eighth): add activity settings page for club
 announcements control

---
 intranet/apps/auth/decorators.py              |  3 +
 intranet/apps/eighth/forms/activities.py      | 30 +++++++
 intranet/apps/eighth/urls.py                  |  1 +
 intranet/apps/eighth/views/activities.py      | 30 ++++++-
 .../templates/eighth/activity_settings.html   | 78 +++++++++++++++++++
 .../templates/eighth/take_attendance.html     |  8 ++
 6 files changed, 148 insertions(+), 2 deletions(-)
 create mode 100644 intranet/apps/eighth/forms/activities.py
 create mode 100644 intranet/templates/eighth/activity_settings.html

diff --git a/intranet/apps/auth/decorators.py b/intranet/apps/auth/decorators.py
index c99db081cac..c1005d5b3d4 100644
--- a/intranet/apps/auth/decorators.py
+++ b/intranet/apps/auth/decorators.py
@@ -26,6 +26,9 @@ def in_admin_group(user):
 #: Restrict the wrapped view to eighth admins
 eighth_admin_required = admin_required("eighth")
 
+# Restrict the wrapped view to eighth sponsors
+eighth_sponsor_required = user_passes_test(lambda u: not u.is_anonymous and u.is_eighth_sponsor)
+
 #: Restrict the wrapped view to announcements admins
 announcements_admin_required = admin_required("announcements")
 
diff --git a/intranet/apps/eighth/forms/activities.py b/intranet/apps/eighth/forms/activities.py
new file mode 100644
index 00000000000..91b9928ac78
--- /dev/null
+++ b/intranet/apps/eighth/forms/activities.py
@@ -0,0 +1,30 @@
+from typing import List  # noqa
+
+from django import forms
+from django.contrib.auth import get_user_model
+
+from ..models import EighthActivity
+
+
+class ActivitySettingsForm(forms.ModelForm):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.fields["officers"].queryset = get_user_model().objects.get_students()
+        self.fields["club_sponsors"].queryset = get_user_model().objects.filter(user_type__in=["teacher", "counselor"])
+
+        self.fields["subscriptions_enabled"].label = "Enable club announcements"
+        self.fields["subscriptions_enabled"].help_text = "Allow students to subscribe to receive announcements for this activity through Ion."
+        self.fields["club_sponsors"].label = "Teacher moderators"
+        self.fields[
+            "club_sponsors"
+        ].help_text = "Teacher moderators can post and manage this club's announcements. You should include club sponsors here."
+        self.fields["officers"].label = "Student officers"
+        self.fields["officers"].help_text = "Student officers can send club announcements to subscribers."
+
+    class Meta:
+        model = EighthActivity
+        fields = [
+            "subscriptions_enabled",
+            "club_sponsors",
+            "officers",
+        ]
diff --git a/intranet/apps/eighth/urls.py b/intranet/apps/eighth/urls.py
index b1dcd001f01..2d47e43df7f 100644
--- a/intranet/apps/eighth/urls.py
+++ b/intranet/apps/eighth/urls.py
@@ -39,6 +39,7 @@
     re_path(r"^/roster/raw/waitlist/(?P<scheduled_activity_id>\d+)$", attendance.raw_waitlist_view, name="eighth_raw_waitlist"),
     # Activity Info (for students/teachers)
     re_path(r"^/activity/(?P<activity_id>\d+)$", activities.activity_view, name="eighth_activity"),
+    re_path(r"^/activity/(?P<activity_id>\d+)/settings$", activities.settings_view, name="eighth_activity_settings"),
     re_path(r"^/activity/statistics/global$", activities.stats_global_view, name="eighth_statistics_global"),
     re_path(r"^/activity/statistics/multiple$", activities.stats_multiple_view, name="eighth_statistics_multiple"),
     re_path(r"^/activity/statistics/(?P<activity_id>\d+)$", activities.stats_view, name="eighth_statistics"),
diff --git a/intranet/apps/eighth/views/activities.py b/intranet/apps/eighth/views/activities.py
index 563ec3f2fec..aaf52e25fdc 100644
--- a/intranet/apps/eighth/views/activities.py
+++ b/intranet/apps/eighth/views/activities.py
@@ -5,10 +5,11 @@
 from io import BytesIO
 
 from django.conf import settings
+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.http import HttpResponse
+from django.http import HttpResponse, Http404
 from django.shortcuts import get_object_or_404, render
 from django.utils import timezone
 from reportlab.lib.pagesizes import letter
@@ -16,10 +17,11 @@
 from reportlab.lib.units import inch
 from reportlab.platypus import PageBreak, Paragraph, SimpleDocTemplate, Spacer, Table
 
+from ..forms.activities import ActivitySettingsForm
 from ....utils.date import get_date_range_this_year, get_senior_graduation_year
 from ....utils.helpers import is_entirely_digit
 from ....utils.serialization import safe_json
-from ...auth.decorators import deny_restricted
+from ...auth.decorators import deny_restricted, eighth_sponsor_required
 from ..forms.admin.activities import ActivityMultiSelectForm
 from ..models import EighthActivity, EighthBlock, EighthScheduledActivity, EighthSignup
 from ..utils import get_start_date
@@ -50,6 +52,30 @@ def activity_view(request, activity_id=None):
     return render(request, "eighth/activity.html", context)
 
 
+@eighth_sponsor_required
+def settings_view(request, activity_id=None):
+    activity = get_object_or_404(EighthActivity, id=activity_id)
+
+    if not request.user.sponsor_obj:
+        raise Http404
+
+    if request.user.sponsor_obj not in activity.sponsors.all():
+        raise Http404
+
+    if request.method == "POST":
+        form = ActivitySettingsForm(request.POST, instance=activity)
+        if form.is_valid():
+            form.save()
+        else:
+            messages.error(request, "There was an error saving the activity settings.")
+    else:
+        form = ActivitySettingsForm(instance=activity)
+
+    context = {"activity": activity, "form": form}
+
+    return render(request, "eighth/activity_settings.html", context)
+
+
 def chunks(items, n):
     for i in range(0, len(items), n):
         yield items[i : i + n]
diff --git a/intranet/templates/eighth/activity_settings.html b/intranet/templates/eighth/activity_settings.html
new file mode 100644
index 00000000000..662097c7c3d
--- /dev/null
+++ b/intranet/templates/eighth/activity_settings.html
@@ -0,0 +1,78 @@
+{% extends "page_with_nav.html" %}
+{% load static %}
+{% load pipeline %}
+
+{% block title %}
+    {{ block.super }}{% if request.user.is_eighth_admin %} - Eighth Admin{% endif %} - {% if scheduled_activity.block.locked %}Take Attendance{% else %}View Roster{% endif %}
+{% endblock %}
+
+{% block css %}
+    <link rel="stylesheet" href="{% static 'vendor/selectize.js-0.12.4/dist/css/selectize.default.css' %}">
+    {{ block.super }}
+    <style>
+        .selectize-control {
+            margin-bottom: -18px;
+        }
+        td {
+            padding-bottom: 25px;
+        }
+    </style>
+{% endblock %}
+
+{% block js %}
+    {{ block.super }}
+    <script src="{% static 'vendor/selectize.js-0.12.4/dist/js/standalone/selectize.min.js' %}"></script>
+    <script src="{% static 'js/vendor/jquery.timeago.js' %}"></script>
+    <!--[if lt IE 9]><script src="http://cdnjs.cloudflare.com/ajax/libs/es5-shim/2.0.8/es5-shim.min.js"></script><![endif]-->
+    <script src="{% static 'js/eighth/attendance.js' %}"></script>
+    <script src="{% static 'js/eighth/ui_init.js' %}"></script>
+    <script src="{% static 'js/eighth/user_link.js' %}"></script>
+{% endblock %}
+
+{% block head %}
+    {% if dark_mode_enabled %}
+        {% stylesheet 'dark/base' %}
+        {% stylesheet 'dark/nav' %}
+    {% endif %}
+{% endblock %}
+
+{% block main %}
+<div class="eighth primary-content">
+    <a class="button" href="{% url 'eighth_take_attendance' activity.id %}">
+        <i class="fa fa-arrow-left"></i>
+        Back to Attendance
+    </a>
+    <h2>Club Announcements Settings for {{ activity }}</h2>
+    <p>
+      Club announcements are a way for eighth period activities to reach the TJ community.
+      Students receive these announcements on Ion and by email when they subscribe to the club.
+    </p>
+    <br>
+    <p>
+      Club officers can post announcements without an approval requirement.
+      However, teacher moderators and Ion admins can always review these posts.
+    </p>
+    <br>
+    <p>
+      You can enable the feature for your club below, and choose who can send announcements on your club's behalf.
+    </p>
+    <br>
+    <table>
+        <form action="" method="post" autocomplete="off">
+        {% csrf_token %}
+            <tr>
+                <td colspan="2">
+                    <hr>
+                </td>
+            </tr>
+            {{ form.as_table }}
+            <tr>
+                <td></td>
+                <td>
+                    <button type="submit" style="width: 200px;">Save</button>
+                </td>
+            </tr>
+        </form>
+    </table>
+</div>
+{% endblock %}
diff --git a/intranet/templates/eighth/take_attendance.html b/intranet/templates/eighth/take_attendance.html
index e3be258d987..03af843df8b 100644
--- a/intranet/templates/eighth/take_attendance.html
+++ b/intranet/templates/eighth/take_attendance.html
@@ -497,6 +497,14 @@ <h3>Passes</h3>
                 &nbsp; &nbsp;
                 <a class="button print-hide" href="{% url 'eighth_email_students' scheduled_activity.id %}">Email Students</a>
                 {% endif %}
+                {% if request.user.sponsor_obj and request.user.sponsor_obj in scheduled_activity.activity.sponsors.all %}
+                    <br>
+                    <br>
+                    <a class="button print-hide" href="{% url 'eighth_activity_settings' scheduled_activity.activity.id %}">
+                      <i class="fa fa-users"></i>
+                      Club Announcement Settings
+                    </a>
+                {% endif %}
                 {% if user.is_eighth_admin %}
                     <br>
                     <br>

From 6b3147db93376f38bad4ea9c58680c3b3e37df27 Mon Sep 17 00:00:00 2001
From: Krishnan Shankar <krishnans2006@gmail.com>
Date: Sun, 7 Apr 2024 23:05:45 -0400
Subject: [PATCH 13/42] fix: format and build sources

---
 intranet/apps/announcements/views.py     | 9 ++-------
 intranet/apps/eighth/forms/activities.py | 6 +++---
 intranet/apps/eighth/views/activities.py | 6 +++---
 3 files changed, 8 insertions(+), 13 deletions(-)

diff --git a/intranet/apps/announcements/views.py b/intranet/apps/announcements/views.py
index dfb7419cb69..48989c5813b 100644
--- a/intranet/apps/announcements/views.py
+++ b/intranet/apps/announcements/views.py
@@ -15,13 +15,8 @@
 from ..groups.models import Group
 from .forms import AnnouncementAdminForm, AnnouncementEditForm, AnnouncementForm, AnnouncementRequestForm, ClubAnnouncementForm
 from .models import Announcement, AnnouncementRequest
-from .notifications import (
-    admin_request_announcement_email,
-    announcement_approved_email,
-    announcement_posted_email,
-    announcement_posted_twitter,
-    request_announcement_email,
-)
+from .notifications import (admin_request_announcement_email, announcement_approved_email, announcement_posted_email, announcement_posted_twitter,
+                            request_announcement_email)
 
 logger = logging.getLogger(__name__)
 
diff --git a/intranet/apps/eighth/forms/activities.py b/intranet/apps/eighth/forms/activities.py
index 91b9928ac78..631f37dc6e6 100644
--- a/intranet/apps/eighth/forms/activities.py
+++ b/intranet/apps/eighth/forms/activities.py
@@ -15,9 +15,9 @@ def __init__(self, *args, **kwargs):
         self.fields["subscriptions_enabled"].label = "Enable club announcements"
         self.fields["subscriptions_enabled"].help_text = "Allow students to subscribe to receive announcements for this activity through Ion."
         self.fields["club_sponsors"].label = "Teacher moderators"
-        self.fields[
-            "club_sponsors"
-        ].help_text = "Teacher moderators can post and manage this club's announcements. You should include club sponsors here."
+        self.fields["club_sponsors"].help_text = (
+            "Teacher moderators can post and manage this club's announcements. You should include club sponsors here."
+        )
         self.fields["officers"].label = "Student officers"
         self.fields["officers"].help_text = "Student officers can send club announcements to subscribers."
 
diff --git a/intranet/apps/eighth/views/activities.py b/intranet/apps/eighth/views/activities.py
index aaf52e25fdc..0e8d5d8f712 100644
--- a/intranet/apps/eighth/views/activities.py
+++ b/intranet/apps/eighth/views/activities.py
@@ -9,7 +9,7 @@
 from django.contrib.auth import get_user_model
 from django.contrib.auth.decorators import login_required
 from django.db.models import Count
-from django.http import HttpResponse, Http404
+from django.http import Http404, HttpResponse
 from django.shortcuts import get_object_or_404, render
 from django.utils import timezone
 from reportlab.lib.pagesizes import letter
@@ -17,11 +17,11 @@
 from reportlab.lib.units import inch
 from reportlab.platypus import PageBreak, Paragraph, SimpleDocTemplate, Spacer, Table
 
-from ..forms.activities import ActivitySettingsForm
 from ....utils.date import get_date_range_this_year, get_senior_graduation_year
 from ....utils.helpers import is_entirely_digit
 from ....utils.serialization import safe_json
 from ...auth.decorators import deny_restricted, eighth_sponsor_required
+from ..forms.activities import ActivitySettingsForm
 from ..forms.admin.activities import ActivityMultiSelectForm
 from ..models import EighthActivity, EighthBlock, EighthScheduledActivity, EighthSignup
 from ..utils import get_start_date
@@ -78,7 +78,7 @@ def settings_view(request, activity_id=None):
 
 def chunks(items, n):
     for i in range(0, len(items), n):
-        yield items[i : i + n]
+        yield items[i: i + n]
 
 
 def current_school_year():

From b7c7712e073ec34e6b7ca3942727d86a8a52762d Mon Sep 17 00:00:00 2001
From: Alan Zhu <2025azhu@tjhsst.edu>
Date: Mon, 8 Apr 2024 10:08:41 -0400
Subject: [PATCH 14/42] fix(eighth): fix club announcements settings back
 button

---
 intranet/templates/eighth/activity_settings.html | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/intranet/templates/eighth/activity_settings.html b/intranet/templates/eighth/activity_settings.html
index 662097c7c3d..571e7ea1f84 100644
--- a/intranet/templates/eighth/activity_settings.html
+++ b/intranet/templates/eighth/activity_settings.html
@@ -38,7 +38,7 @@
 
 {% block main %}
 <div class="eighth primary-content">
-    <a class="button" href="{% url 'eighth_take_attendance' activity.id %}">
+    <a class="button" onclick="javascript:history.back()">
         <i class="fa fa-arrow-left"></i>
         Back to Attendance
     </a>

From e2f9673b1c27990f2663024c48c22ed4f42ff58f Mon Sep 17 00:00:00 2001
From: Alan Zhu <2025azhu@tjhsst.edu>
Date: Wed, 1 May 2024 14:47:29 -0400
Subject: [PATCH 15/42] refactor(announcements): text changes

---
 intranet/templates/announcements/request.html | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/intranet/templates/announcements/request.html b/intranet/templates/announcements/request.html
index 5455659a098..d613290b863 100644
--- a/intranet/templates/announcements/request.html
+++ b/intranet/templates/announcements/request.html
@@ -59,7 +59,9 @@ <h2>
     {% endif %}
     <strong>Guidelines for News Posts:</strong><br>
     <ol>
-        <li>Use correct English grammar, punctuation, and spelling; do not use all caps; keep posts concise.</li>
+        <li>Use correct English grammar, punctuation, and spelling.</li>
+        <li>Use formal language and tone; avoid slang, all caps, texting-style abbreviations, excessive use of bolding, underlining, emojis, etc.</li>
+        <li>Keep posts short and concise. Viewable dashboard space is limited on Ion; be considerate of other announcements. Avoid unecessary linebreaks or whitespace.</li>
         <li>Do not submit repeat announcements; if an announcement about your topic has already been posted, do not request another.</li>
         <li>See detailed guidelines <a target="_blank" href="https://guides.tjhsst.edu/ion/ion-announcement-guidelines">here</a>.</li>
     </ol>

From d8a39b487eb9e7d66940bcb16ebf61240252c985 Mon Sep 17 00:00:00 2001
From: Krishnan Shankar <krishnans2006@gmail.com>
Date: Sun, 7 Apr 2024 23:05:45 -0400
Subject: [PATCH 16/42] fix: format code, build sources, build docs

---
 docs/sourcedoc/intranet.apps.eighth.forms.rst | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/docs/sourcedoc/intranet.apps.eighth.forms.rst b/docs/sourcedoc/intranet.apps.eighth.forms.rst
index af398219a61..a512b0cc269 100644
--- a/docs/sourcedoc/intranet.apps.eighth.forms.rst
+++ b/docs/sourcedoc/intranet.apps.eighth.forms.rst
@@ -9,6 +9,17 @@ Subpackages
 
    intranet.apps.eighth.forms.admin
 
+Submodules
+----------
+
+intranet.apps.eighth.forms.activities module
+--------------------------------------------
+
+.. automodule:: intranet.apps.eighth.forms.activities
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
 Module contents
 ---------------
 

From 07c6d70fc5f3a9809f4c19ad97534b0183d98699 Mon Sep 17 00:00:00 2001
From: Shreyas Jain <shrys.jain@gmail.com>
Date: Sat, 28 Sep 2024 23:14:20 -0400
Subject: [PATCH 17/42] fix: add club ann. text and remove hoco banner for ann.
 pages

---
 intranet/static/css/dashboard.scss            | 556 +++++++++---------
 intranet/templates/announcements/request.html |   2 +-
 intranet/templates/dashboard/dashboard.html   |  11 +-
 3 files changed, 304 insertions(+), 265 deletions(-)

diff --git a/intranet/static/css/dashboard.scss b/intranet/static/css/dashboard.scss
index 1d24ddf721d..cebe6c078ed 100644
--- a/intranet/static/css/dashboard.scss
+++ b/intranet/static/css/dashboard.scss
@@ -1,305 +1,335 @@
 @import "colors";
 
 .announcements {
-    padding-right: 432px;
-    min-width: 290px;
-    /* for 320x480 screens */
-    max-width: 1000px;
-    margin-bottom: 100px;
+  padding-right: 432px;
+  min-width: 290px;
+  /* for 320x480 screens */
+  max-width: 1000px;
+  margin-bottom: 100px;
+
+  &.no-widgets {
+    padding-right: 0;
+  }
+
+  h2 {
+    padding-left: 10px;
+    line-height: 38px;
+    float: left;
+  }
 
-    &.no-widgets {
-        padding-right: 0;
-    }
+  .announcement-banner {
+    background-color: #fff3cd;
+    color: #533f05;
+    border: 1px solid #fbe298;
+    padding: 10px;
+    margin-bottom: 15px;
+    margin-top: 10px;
+    align-items: center;
+    border-radius: 5px;
+    line-height: 1.5;
+  }
+
+  .announcement-link {
+    color: #856404;
+    font-weight: bold;
+    text-decoration: underline;
+  }
 
-    h2 {
-        padding-left: 10px;
-        line-height: 38px;
-        float: left;
-    }
+  .announcement-link:hover {
+    color: #251900;
+  }
 }
 
 .announcements-header {
-    height: 38px;
-    margin-bottom: 4px;
+  height: 38px;
+  margin-bottom: 4px;
 }
 
 .club-announcements {
-    padding: 10px;
-    border-radius: 4px;
-    transition: max-height 0.2s ease-in-out;
-    text-align: left;
-
-    &.collapsed {
-        max-height: 90px !important;
-        overflow: hidden;
-    }
-
-    &.collapsed::after {
-        content: "";
-        position: absolute;
-        z-index: 1;
-        bottom: 0;
-        left: 0;
-        pointer-events: none;
-        background-image: linear-gradient(to bottom, rgba(255, 255, 255, 0), #fce624 90%);
-        width: 100%;
-        height: 4em;
-    }
-
-    &::-webkit-scrollbar {
-        width: 7px;
-    }
-    &::-webkit-scrollbar-track {
-        background: #d6d6d6;
-    }
-    &::-webkit-scrollbar-thumb {
-        background: #888;
-    }
-    &::-webkit-scrollbar-thumb:hover {
-        background: #555;
-    }
+  padding: 10px;
+  border-radius: 4px;
+  transition: max-height 0.2s ease-in-out;
+  text-align: left;
+
+  &.collapsed {
+    max-height: 90px !important;
+    overflow: hidden;
+  }
+
+  &.collapsed::after {
+    content: "";
+    position: absolute;
+    z-index: 1;
+    bottom: 0;
+    left: 0;
+    pointer-events: none;
+    background-image: linear-gradient(
+      to bottom,
+      rgba(255, 255, 255, 0),
+      #fce624 90%
+    );
+    width: 100%;
+    height: 4em;
+  }
+
+  &::-webkit-scrollbar {
+    width: 7px;
+  }
+  &::-webkit-scrollbar-track {
+    background: #d6d6d6;
+  }
+  &::-webkit-scrollbar-thumb {
+    background: #888;
+  }
+  &::-webkit-scrollbar-thumb:hover {
+    background: #555;
+  }
 }
 
 .club-announcements-header {
-    text-align: center;
-    margin-bottom: 0;
+  text-align: center;
+  margin-bottom: 0;
 }
 
 .club-announcements-content {
-    display: none;
-    margin-top: 5px;
+  display: none;
+  margin-top: 5px;
 }
 
 .club-announcements-container {
-    .announcement, .announcement-meta {
-        display: none;
-    }
+  .announcement,
+  .announcement-meta {
+    display: none;
+  }
 }
 
-a.club-announcement-meta-link, a.club-announcement-meta-link:visited {
-    color: rgb(144, 144, 144);
-    text-decoration: underline;
+a.club-announcement-meta-link,
+a.club-announcement-meta-link:visited {
+  color: rgb(144, 144, 144);
+  text-decoration: underline;
 
-    &:hover {
-        color: rgb(66, 66, 66);
-    }
+  &:hover {
+    color: rgb(66, 66, 66);
+  }
 }
 
 .club-announcements-toggle-icon {
-    float: right;
-    margin-top: 4px;
+  float: right;
+  margin-top: 4px;
 }
 
 .announcements-icon-wrapper:has(> .club-announcements-button) {
-    @media (max-width: 800px) {
-        display: block !important;
-        width: 100%;
-    }
-
-    @media (max-width: 550px) {
-        margin-bottom: 6px;
-    }
-}
+  @media (max-width: 800px) {
+    display: block !important;
+    width: 100%;
+  }
 
-.announcement, .club-announcements, .announcement-meta {
-    background-color: white;
-    -webkit--radius: 5px;
-    -moz--radius: 5px;
-    -radius: 5px;
-    border: 1px solid rgb(216, 216, 216);
-    padding: 6px 10px;
+  @media (max-width: 550px) {
     margin-bottom: 6px;
-    overflow-x: auto;
-    position: relative;
-    behavior: url("/static/js/vendor/PIE/PIE.htc");
-
-    h3 {
-        cursor: pointer;
-
-        > a.announcement-link {
-            cursor: pointer;
-            color: $grey !important;
-        }
+  }
+}
 
-        &:hover .announcement-icon-wrapper .announcement-toggle,
-        .announcement-icon-wrapper .announcement-toggle:hover,
-        .announcement-icon-wrapper:hover .announcement-toggle:hover {
-            color: rgb(32, 66, 224);
-        }
+.announcement,
+.club-announcements,
+.announcement-meta {
+  background-color: white;
+  -webkit--radius: 5px;
+  -moz--radius: 5px;
+  -radius: 5px;
+  border: 1px solid rgb(216, 216, 216);
+  padding: 6px 10px;
+  margin-bottom: 6px;
+  overflow-x: auto;
+  position: relative;
+  behavior: url("/static/js/vendor/PIE/PIE.htc");
+
+  h3 {
+    cursor: pointer;
 
-        .announcement-icon-wrapper:hover .announcement-toggle {
-            color: $grey;
-        }
+    > a.announcement-link {
+      cursor: pointer;
+      color: $grey !important;
     }
 
-    &.announcement-meta h3 {
-        cursor: initial;
+    &:hover .announcement-icon-wrapper .announcement-toggle,
+    .announcement-icon-wrapper .announcement-toggle:hover,
+    .announcement-icon-wrapper:hover .announcement-toggle:hover {
+      color: rgb(32, 66, 224);
     }
 
-    &.pinned h3 {
-        color: rgb(181, 0, 0);
+    .announcement-icon-wrapper:hover .announcement-toggle {
+      color: $grey;
     }
+  }
 
-    .announcement-content {
-        b,
-        strong {
-            font-weight: bold;
-        }
-
-        i,
-        em {
-            font-style: italic;
-        }
-
-        u {
-            text-decoration: underline;
-        }
+  &.announcement-meta h3 {
+    cursor: initial;
+  }
 
-        ol {
-            list-style-type: decimal;
-            list-style-position: inside;
-        }
+  &.pinned h3 {
+    color: rgb(181, 0, 0);
+  }
 
-        p {
-            margin-bottom: 5px;
-        }
+  .announcement-content {
+    b,
+    strong {
+      font-weight: bold;
     }
 
-    &.partially-hidden {
-        .announcement-toggle-content {
-            max-height: 200px;
-            overflow-y: hidden;
+    i,
+    em {
+      font-style: italic;
+    }
 
-            &::after {
-                content: "";
-                position: absolute;
-                z-index: 1;
-                bottom: 0;
-                left: 0;
-                pointer-events: none;
-                background-image: linear-gradient(
-                                to bottom,
-                                rgba(255, 255, 255, 0),
-                                white 80%
-                );
-                width: 100%;
-                height: 5em;
-            }
-        }
+    u {
+      text-decoration: underline;
     }
 
-    &.club-announcements {
-        background-color: rgb(231, 231, 231);
+    ol {
+      list-style-type: decimal;
+      list-style-position: inside;
     }
 
-    &-icon {
-        cursor: pointer;
+    p {
+      margin-bottom: 5px;
     }
+  }
 
-    &.hidden .announcement-toggle-content {
-        display: none;
+  &.partially-hidden {
+    .announcement-toggle-content {
+      max-height: 200px;
+      overflow-y: hidden;
+
+      &::after {
+        content: "";
+        position: absolute;
+        z-index: 1;
+        bottom: 0;
+        left: 0;
+        pointer-events: none;
+        background-image: linear-gradient(
+          to bottom,
+          rgba(255, 255, 255, 0),
+          white 80%
+        );
+        width: 100%;
+        height: 5em;
+      }
     }
+  }
+
+  &.club-announcements {
+    background-color: rgb(231, 231, 231);
+  }
+
+  &-icon {
+    cursor: pointer;
+  }
+
+  &.hidden .announcement-toggle-content {
+    display: none;
+  }
 }
 
 .announcements-icon-wrapper {
-    float: right;
+  float: right;
 }
 
 .announcement-metadata {
-    color: rgb(144, 144, 144);
-    font-size: 12px;
-    line-height: 12px;
-    margin-bottom: 5px;
+  color: rgb(144, 144, 144);
+  font-size: 12px;
+  line-height: 12px;
+  margin-bottom: 5px;
 }
 
 .announcement-icon-wrapper {
-    float: right;
-    display: none;
+  float: right;
+  display: none;
 
-    .announcement:not(.club-announcements):hover & {
-        display: block;
-    }
+  .announcement:not(.club-announcements):hover & {
+    display: block;
+  }
 
-    > a {
-        color: $grey;
-        text-decoration: none !important;
-        padding-left: 2px;
+  > a {
+    color: $grey;
+    text-decoration: none !important;
+    padding-left: 2px;
 
-        &:hover {
-            color: rgb(32, 66, 224);
-        }
+    &:hover {
+      color: rgb(32, 66, 224);
     }
+  }
 }
 
 .club-announcement-filters {
-    display: flex;
-    justify-content: space-between;
-    flex-grow: 1;
-
-    > .club-announcement-filter {
-        background-color: white;
-        border: 1px solid rgb(216, 216, 216);
-        padding: 6px 10px;
-        margin-bottom: 6px;
-        position: relative;
-
-        text-align: center;
-        font-size: 14px;
-        width: 100%;
+  display: flex;
+  justify-content: space-between;
+  flex-grow: 1;
 
-        cursor: pointer;
-        font-weight: bolder;
+  > .club-announcement-filter {
+    background-color: white;
+    border: 1px solid rgb(216, 216, 216);
+    padding: 6px 10px;
+    margin-bottom: 6px;
+    position: relative;
 
-        &.active {
-            background-color: rgb(44, 103, 186);
-            color: white;
-        }
+    text-align: center;
+    font-size: 14px;
+    width: 100%;
 
-        &.subscribed-filter {
-            border-right: none;
-            border-top-left-radius: 5px;
-            border-bottom-left-radius: 5px;
-        }
+    cursor: pointer;
+    font-weight: bolder;
 
-        &.unsubscribed-filter {
-            border-top-right-radius: 5px;
-            border-bottom-right-radius: 5px;
-        }
+    &.active {
+      background-color: rgb(44, 103, 186);
+      color: white;
     }
-}
 
-a.button {
-    &.subscribe-button {
-        color: green;
-        float: right;
-        margin-left: 5px;
+    &.subscribed-filter {
+      border-right: none;
+      border-top-left-radius: 5px;
+      border-bottom-left-radius: 5px;
     }
 
-    &.unsubscribe-button {
-        color: red;
-        float: right;
-        margin-left: 5px;
+    &.unsubscribed-filter {
+      border-top-right-radius: 5px;
+      border-bottom-right-radius: 5px;
     }
+  }
+}
+
+a.button {
+  &.subscribe-button {
+    color: green;
+    float: right;
+    margin-left: 5px;
+  }
+
+  &.unsubscribe-button {
+    color: red;
+    float: right;
+    margin-left: 5px;
+  }
 }
 
 .event.hidden .event-toggle-content {
-    display: none;
+  display: none;
 }
 
 .content-center {
-    width: 100%;
-    text-align: center;
+  width: 100%;
+  text-align: center;
 }
 
 @media (max-width: 800px) {
-    /*
+  /*
      * widgets that fall underneath nav shouldn't float
      * all the way to the left in 800-500px tablet view.
      * mainly affects student admins
      */
-    ul.nav {
-        margin-bottom: 100%;
-    }
+  ul.nav {
+    margin-bottom: 100%;
+  }
 }
 
 /*
@@ -328,63 +358,63 @@ a.button {
 */
 
 @media print {
-    div.main div.announcements.primary-content {
-        position: absolute;
-        top: 0;
-        padding: 0;
-        min-width: initial;
-        max-width: initial;
-    }
-    div.announcements-header .announcements-icon-wrapper * {
-        visibility: hidden;
-    }
-    div.announcement {
-        &-icon-wrapper {
-            visibility: hidden !important;
-        }
-
-        &.announcement-meta {
-            display: none;
-        }
-    }
+  div.main div.announcements.primary-content {
+    position: absolute;
+    top: 0;
+    padding: 0;
+    min-width: initial;
+    max-width: initial;
+  }
+  div.announcements-header .announcements-icon-wrapper * {
+    visibility: hidden;
+  }
+  div.announcement {
+    &-icon-wrapper {
+      visibility: hidden !important;
+    }
+
+    &.announcement-meta {
+      display: none;
+    }
+  }
 }
 
 div[data-placeholder]:not(:focus):not([data-div-placeholder-content]):before {
-    content: attr(data-placeholder);
-    float: left;
-    margin-left: 5px;
-    color: $grey;
+  content: attr(data-placeholder);
+  float: left;
+  margin-left: 5px;
+  color: $grey;
 }
 
 .dashboard-item-icon {
-    float: left;
-    font-size: 32px;
-    opacity: 0.6;
-    margin: 0;
-    padding: 2px 8px 0 0;
-    width: 27px;
-    text-align: center;
-    cursor: pointer;
-
-    &:hover,
-    .announcement h3:hover &,
-    .event h3:hover & {
-        opacity: 1;
-    }
-
-    &.fa-users {
-        width: 36px;
-        font-size: 28px;
-        position: relative;
-        top: 2px;
-    }
+  float: left;
+  font-size: 32px;
+  opacity: 0.6;
+  margin: 0;
+  padding: 2px 8px 0 0;
+  width: 27px;
+  text-align: center;
+  cursor: pointer;
+
+  &:hover,
+  .announcement h3:hover &,
+  .event h3:hover & {
+    opacity: 1;
+  }
+
+  &.fa-users {
+    width: 36px;
+    font-size: 28px;
+    position: relative;
+    top: 2px;
+  }
 }
 
 .main div.primary-content {
-    @media (min-width: 801px) {
-        padding-right: 316px;
-    }
-    @media (min-width: 961px) {
-        padding-right: 432px;
-    }
+  @media (min-width: 801px) {
+    padding-right: 316px;
+  }
+  @media (min-width: 961px) {
+    padding-right: 432px;
+  }
 }
diff --git a/intranet/templates/announcements/request.html b/intranet/templates/announcements/request.html
index d613290b863..fb6c9dcc4b5 100644
--- a/intranet/templates/announcements/request.html
+++ b/intranet/templates/announcements/request.html
@@ -52,7 +52,7 @@ <h2>
     {% if not request.user.is_restricted %}
     <p>Want to make an announcement for a specific event or activity? <b><a href="{% url 'request_event' %}">Submit an event for approval instead!</a></b></p>
 
-    {% if request.user.is_club_officer %}
+    {% if request.user.is_club_officer or request.user.is_club_sponsor %}
     <p>Want to post an announcement for your club's members? <b><a href="{% url 'add_club_announcement' %}">Submit a club announcement instead!</a></b></p>
     {% endif %}
     <br />
diff --git a/intranet/templates/dashboard/dashboard.html b/intranet/templates/dashboard/dashboard.html
index 8a182869150..9cebdc6d143 100644
--- a/intranet/templates/dashboard/dashboard.html
+++ b/intranet/templates/dashboard/dashboard.html
@@ -170,7 +170,7 @@ <h2>{{ dashboard_header }}</h2>
     </div>
 
     <div class="announcements-container {% if view_announcements_url == "club_announcements" %}club-announcements-container{% endif %}">
-        {% if show_homecoming %}
+        {% if show_homecoming and not view_announcements_url == "club_announcements" and not view_announcements_url == "announcements_archive" %}
             {% include "special/hoco_ribbon.html" %}
         {% endif %}
         {% if show_tjstar %}
@@ -233,6 +233,15 @@ <h3 class="club-announcements-header">
         {% endif %}
 
         {% if view_announcements_url == "club_announcements" %}
+            {% if request.user.is_club_officer or request.user.is_club_sponsor %}
+                <div class="announcement-banner">
+                    Club announcements is a new Ion feature that enables clubs to better communicate with their members.
+                    It is currently in beta release and is only available to select clubs.
+                    If your club is interested in participating in the rollout, contact
+                    <a href="mailto:intranet@tjhsst.edu" class="announcement-link">intranet@tjhsst.edu</a>
+                    . The feature will be released to the entire TJ community in the near future. 
+                </div>
+            {% endif %}
             <div class="club-announcement-filters">
                 <div class="club-announcement-filter subscribed-filter active">Your Subscriptions</div>
                 <div class="club-announcement-filter unsubscribed-filter">Other Club Announcements</div>

From 60872bb041609bb223c13b2ae62fb9950a60bfb2 Mon Sep 17 00:00:00 2001
From: JasonGrace2282 <aarush.deshpande@gmail.com>
Date: Tue, 1 Oct 2024 20:42:40 -0400
Subject: [PATCH 18/42] fix: invalid button logic for dashboard

---
 intranet/templates/dashboard/dashboard.html | 16 ++++++----------
 1 file changed, 6 insertions(+), 10 deletions(-)

diff --git a/intranet/templates/dashboard/dashboard.html b/intranet/templates/dashboard/dashboard.html
index 9cebdc6d143..a18fd0dfb05 100644
--- a/intranet/templates/dashboard/dashboard.html
+++ b/intranet/templates/dashboard/dashboard.html
@@ -129,19 +129,15 @@ <h2>{{ dashboard_header }}</h2>
                         <i class="fas fa-plus"></i>
                         Post
                     </a>
-                {% elif "show_all" not in request.GET %}
-                    <a class="button" href="{% url view_announcements_url %}?{% query_transform request show_all=1 %}">
-                        Show All
-                    </a>
                 {% else %}
-                    {% if "show_all" not in request.GET %}
-                        <a class="button" href="{% url view_announcements_url %}?show_all=1">
-                            Show All
-                        </a>
-                    {% else %}
+                    {% if request.GET.show_all is not None and request.GET.show_all != "0" %}
                         <a class="button" href="{% url view_announcements_url %}">
                             <i class="fas fa-times"></i> Don't Show All
                         </a>
+                    {% else %}
+                        <a class="button" href="{% url view_announcements_url %}?{% query_transform request show_all=1 %}">
+                            Show All
+                        </a>
                     {% endif %}
                     <a class="button announcement-add" href="{% url 'add_announcement' %}">
                         <i class="fas fa-plus"></i>
@@ -239,7 +235,7 @@ <h3 class="club-announcements-header">
                     It is currently in beta release and is only available to select clubs.
                     If your club is interested in participating in the rollout, contact
                     <a href="mailto:intranet@tjhsst.edu" class="announcement-link">intranet@tjhsst.edu</a>
-                    . The feature will be released to the entire TJ community in the near future. 
+                    . The feature will be released to the entire TJ community in the near future.
                 </div>
             {% endif %}
             <div class="club-announcement-filters">

From 112c042ba4758d0fc0a0bd30f9c01d8b371f8b81 Mon Sep 17 00:00:00 2001
From: Shreyas Jain <shrys.jain@gmail.com>
Date: Wed, 2 Oct 2024 14:53:05 -0400
Subject: [PATCH 19/42] fix: show banner for everyone

---
 intranet/templates/dashboard/dashboard.html | 16 +++++++---------
 1 file changed, 7 insertions(+), 9 deletions(-)

diff --git a/intranet/templates/dashboard/dashboard.html b/intranet/templates/dashboard/dashboard.html
index a18fd0dfb05..70b086f8061 100644
--- a/intranet/templates/dashboard/dashboard.html
+++ b/intranet/templates/dashboard/dashboard.html
@@ -229,15 +229,13 @@ <h3 class="club-announcements-header">
         {% endif %}
 
         {% if view_announcements_url == "club_announcements" %}
-            {% if request.user.is_club_officer or request.user.is_club_sponsor %}
-                <div class="announcement-banner">
-                    Club announcements is a new Ion feature that enables clubs to better communicate with their members.
-                    It is currently in beta release and is only available to select clubs.
-                    If your club is interested in participating in the rollout, contact
-                    <a href="mailto:intranet@tjhsst.edu" class="announcement-link">intranet@tjhsst.edu</a>
-                    . The feature will be released to the entire TJ community in the near future.
-                </div>
-            {% endif %}
+              <div class="announcement-banner">
+                  Club announcements is a new Ion feature that enables clubs to better communicate with their members.
+                  It is currently in beta release and is only available to select clubs.
+                  If your club is interested in participating in the rollout, contact
+                  <a href="mailto:intranet@tjhsst.edu" class="announcement-link">intranet@tjhsst.edu</a>
+                  . The feature will be released to the entire TJ community in the near future.
+              </div>
             <div class="club-announcement-filters">
                 <div class="club-announcement-filter subscribed-filter active">Your Subscriptions</div>
                 <div class="club-announcement-filter unsubscribed-filter">Other Club Announcements</div>

From 66cf9ff57de14d454b6bfcfd95ceba9cc9a8ad35 Mon Sep 17 00:00:00 2001
From: Shreyas Jain <shrys.jain@gmail.com>
Date: Wed, 2 Oct 2024 15:09:37 -0400
Subject: [PATCH 20/42] format: remove extraneous space

---
 intranet/templates/dashboard/dashboard.html | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/intranet/templates/dashboard/dashboard.html b/intranet/templates/dashboard/dashboard.html
index 70b086f8061..66d9928068b 100644
--- a/intranet/templates/dashboard/dashboard.html
+++ b/intranet/templates/dashboard/dashboard.html
@@ -233,8 +233,8 @@ <h3 class="club-announcements-header">
                   Club announcements is a new Ion feature that enables clubs to better communicate with their members.
                   It is currently in beta release and is only available to select clubs.
                   If your club is interested in participating in the rollout, contact
-                  <a href="mailto:intranet@tjhsst.edu" class="announcement-link">intranet@tjhsst.edu</a>
-                  . The feature will be released to the entire TJ community in the near future.
+                  <a href="mailto:intranet@tjhsst.edu" class="announcement-link">intranet@tjhsst.edu</a>.
+                  The feature will be released to the entire TJ community in the near future.
               </div>
             <div class="club-announcement-filters">
                 <div class="club-announcement-filter subscribed-filter active">Your Subscriptions</div>

From e4420ba3ce21de581554d4d5303acf331a52430f Mon Sep 17 00:00:00 2001
From: JasonGrace2282 <aarush.deshpande@gmail.com>
Date: Wed, 2 Oct 2024 16:30:23 -0400
Subject: [PATCH 21/42] feat: add club announcement archive

---
 intranet/apps/dashboard/views.py              |  20 +--
 intranet/static/js/dashboard/announcements.js |   4 +-
 intranet/templates/dashboard/dashboard.html   | 141 +++++++++---------
 3 files changed, 85 insertions(+), 80 deletions(-)

diff --git a/intranet/apps/dashboard/views.py b/intranet/apps/dashboard/views.py
index c1faae131e5..75491698171 100644
--- a/intranet/apps/dashboard/views.py
+++ b/intranet/apps/dashboard/views.py
@@ -231,7 +231,7 @@ def get_announcements_list(request, context):
         # Show all announcements if user has admin permissions and the
         # show_all GET argument is given.
         announcements = Announcement.objects.all()
-    elif context["show_expired"]:
+    elif context["show_expired"] or context["show_all"]:
         announcements = Announcement.objects.visible_to_user(user)
     else:
         announcements = Announcement.objects.visible_to_user(user).filter(expiration_date__gt=timezone.now())
@@ -293,7 +293,7 @@ def filter_club_announcements(user, user_hidden_announcements, club_items):
     return visible, hidden, unsubscribed
 
 
-def paginate_announcements_list(request, context, items, visible_club_items, more_club_items):
+def paginate_announcements_list(request, context, items, visible_club_items):
     """
     Paginate ``items`` in groups of 15
 
@@ -315,14 +315,16 @@ def paginate_announcements_list(request, context, items, visible_club_items, mor
     prev_page = items.previous_page_number() if items.has_previous() else 0
     next_page = items.next_page_number() if more_items else 0
 
-    context.update(
-        {"items": items, "page_num": page_num, "prev_page": prev_page, "next_page": next_page, "more_items": more_items, "page_obj": paginator}
-    )
     club_items = visible_club_items[:15]
-
     context.update(
         {
             "club_items": club_items,
+            "items": items,
+            "page_num": page_num,
+            "prev_page": prev_page,
+            "next_page": next_page,
+            "more_items": more_items,
+            "page_obj": paginator,
         }
     )
 
@@ -495,11 +497,11 @@ def dashboard_view(request, show_widgets=True, show_expired=False, show_hidden_c
 
     if not show_hidden_club:
         # Dashboard
-        visible_club_items, hidden_club_items, other_club_items = filter_club_announcements(user, user_hidden_announcements, club_items)
-        context, items = paginate_announcements_list(request, context, items, visible_club_items, hidden_club_items or other_club_items)
+        visible_club_items, _hidden_club_items, _other_club_items = filter_club_announcements(user, user_hidden_announcements, club_items)
+        context, items = paginate_announcements_list(request, context, items, visible_club_items)
     else:
         # Club announcements only
-        context, items = paginate_announcements_list(request, context, club_items, [], [])
+        context, items = paginate_announcements_list(request, context, club_items, visible_club_items=[])
 
     if ignore_dashboard_types is None:
         ignore_dashboard_types = []
diff --git a/intranet/static/js/dashboard/announcements.js b/intranet/static/js/dashboard/announcements.js
index 9b5e8b7a438..98a10c34f4b 100644
--- a/intranet/static/js/dashboard/announcements.js
+++ b/intranet/static/js/dashboard/announcements.js
@@ -44,12 +44,14 @@ $(document).ready(function() {
 
     $(".subscribed-filter").click(function () {
         $(".unsubscribed-filter").removeClass("active");
+        $("#subscriptions-pagination").css("display", "");
         $(this).addClass("active");
         filterClubAnnouncements();
     });
 
     $(".unsubscribed-filter").click(function () {
         $(".subscribed-filter").removeClass("active");
+        $("#subscriptions-pagination").css("display", "none");
         $(this).addClass("active");
         filterClubAnnouncements();
     });
@@ -182,4 +184,4 @@ function filterClubAnnouncements() {
         });
     }
     updatePartiallyHidden();
-}
\ No newline at end of file
+}
diff --git a/intranet/templates/dashboard/dashboard.html b/intranet/templates/dashboard/dashboard.html
index 66d9928068b..7c05ffd4255 100644
--- a/intranet/templates/dashboard/dashboard.html
+++ b/intranet/templates/dashboard/dashboard.html
@@ -117,19 +117,19 @@ <h2>{{ dashboard_header }}</h2>
                     Request Post
                 </a>
             {% else %}
-            {% if view_announcements_url != "club_announcements" %}
-                <a class="button club-announcements-button" href="{% url 'club_announcements' %}{% if "show_all" in request.GET %}?show_all=1{% endif %}">
-                    <i class="fas fa-users"></i>
-                    Club Announcements
-                </a>
-            {% endif %}
-            {% if announcements_admin %}
-                {% if view_announcements_url == "club_announcements" %}
-                    <a class="button announcement-request" href="{% url 'add_club_announcement' %}">
-                        <i class="fas fa-plus"></i>
-                        Post
+                {% if view_announcements_url != "club_announcements" %}
+                    <a class="button club-announcements-button" href="{% url 'club_announcements' %}{% if "show_all" in request.GET %}?show_all=1{% endif %}">
+                        <i class="fas fa-users"></i>
+                        Club Announcements
                     </a>
-                {% else %}
+                {% endif %}
+                {% if announcements_admin %}
+                    {% if view_announcements_url == "club_announcements" %}
+                        <a class="button announcement-request" href="{% url 'add_club_announcement' %}">
+                            <i class="fas fa-plus"></i>
+                            Post
+                        </a>
+                    {% endif %}
                     {% if request.GET.show_all is not None and request.GET.show_all != "0" %}
                         <a class="button" href="{% url view_announcements_url %}">
                             <i class="fas fa-times"></i> Don't Show All
@@ -139,28 +139,28 @@ <h2>{{ dashboard_header }}</h2>
                             Show All
                         </a>
                     {% endif %}
-                    <a class="button announcement-add" href="{% url 'add_announcement' %}">
-                        <i class="fas fa-plus"></i>
-                        Add
-                    </a>
-                {% endif %}
-            {% else %}
+                    {% if view_announcements_url != "club_announcements" %}
+                        <a class="button announcement-add" href="{% url 'add_announcement' %}">
+                            <i class="fas fa-plus"></i>
+                            Add
+                        </a>
+                    {% endif %}
+                {% else %}
+                    {% if view_announcements_url == "club_announcements" %}
+                        {% if request.user.is_club_officer or request.user.is_club_sponsor %}
+                            <a class="button announcement-request" href="{% url 'add_club_announcement' %}">
+                                <i class="fas fa-plus"></i>
+                                Post
+                            </a>
+                        {% endif %}
+                    {% else %}
+                        <a class="button announcement-request" href="{% url 'request_announcement' %}">
+                            <i class="far fa-file-alt"></i>
+                            Request Post
+                        </a>
+                    {% endif %}
 
-            {% if view_announcements_url == "club_announcements" %}
-                {% if request.user.is_club_officer or request.user.is_club_sponsor %}
-                    <a class="button announcement-request" href="{% url 'add_club_announcement' %}">
-                        <i class="fas fa-plus"></i>
-                        Post
-                    </a>
                 {% endif %}
-            {% else %}
-                <a class="button announcement-request" href="{% url 'request_announcement' %}">
-                    <i class="far fa-file-alt"></i>
-                    Request Post
-                </a>
-            {% endif %}
-
-            {% endif %}
             {% endif %}
             </span>
     </div>
@@ -264,46 +264,47 @@ <h3 class="club-announcements-header">
             </div>
         {% endfor %}
         {% if not request.user.is_restricted %}
-
-        {% if page_num == 1 and view_announcements_url != "announcements_archive" and view_announcements_url != "club_announcements" %}
-            <a href="{% url 'announcements_archive' %}" class="button" style="float:left"><i class="fas fa-archive" style="width: 13px"></i> View Archive</a>
-        {% endif %}
-        {% if page_obj.num_pages > 1 %}
-            <div style="display:grid;grid-template-columns:1fr max-content 1fr">
-                <div style="text-align:center;grid-column-start:2">
-                  <a {% if prev_page > 0 %}
-                    href="{% url view_announcements_url %}?{% query_transform request page=prev_page %}"
-                    {% else %}
-                    disabled
-                    {% endif %}
-                    class="button"
-                  >&lt;</a>
-
-                {% for page in page_obj|page_list:items %}
-                <a {% if page %} class="button" {% else %} class="ellipses" {% endif %}
-                  {% if page == items.number %} style="background-image: linear-gradient(to bottom, #858585 0%, #5f5f5f 100%); color: white;" {% endif %}
-                  href="{% url view_announcements_url %}{% if page %}?{% query_transform request page=page %}{% else %}#{% endif %}">{{ page|default:"..." }}</a>
-                {% endfor %}
-
-                <a {% if more_items %}
-                  href="{% url view_announcements_url %}?{% query_transform request page=next_page %}"
-                  {% else %}
-                  disabled
-                  {% endif %}
-                  class="button"
-                >&gt;</a>
-            </div>
-            <div style="text-align:right">
-                <form action="{% url view_announcements_url %}" method="get" style="display:inline;float:right">
-                    <input name="page" type="number"
-                    min="1" max={{ page_obj.num_pages }} class="dashboard-textinput" style="width: 75px"
-                    placeholder={{ items.number }}> of {{ page_obj.num_pages }}
-                    <input type="submit" value="Go"/>
-                </form>
-                </div>
+            <div id="subscriptions-pagination">
+                {% if page_num == 1 and view_announcements_url != "announcements_archive" and view_announcements_url != "club_announcements" %}
+                    <a href="{% url 'announcements_archive' %}" class="button" style="float:left"><i class="fas fa-archive" style="width: 13px"></i> View Archive</a>
+                {% endif %}
+                {% if page_obj.num_pages > 1 %}
+                    <div style="display:grid;grid-template-columns:1fr max-content 1fr">
+                        <div style="text-align:center;grid-column-start:2">
+                          <a {% if prev_page > 0 %}
+                            href="{% url view_announcements_url %}?{% query_transform request page=prev_page %}"
+                            {% else %}
+                            disabled
+                            {% endif %}
+                            class="button"
+                          >&lt;</a>
+
+                        {% for page in page_obj|page_list:items %}
+                        <a {% if page %} class="button" {% else %} class="ellipses" {% endif %}
+                          {% if page == items.number %} style="background-image: linear-gradient(to bottom, #858585 0%, #5f5f5f 100%); color: white;" {% endif %}
+                          href="{% url view_announcements_url %}{% if page %}?{% query_transform request page=page %}{% else %}#{% endif %}">{{ page|default:"..." }}</a>
+                        {% endfor %}
+
+                        <a {% if more_items %}
+                          href="{% url view_announcements_url %}?{% query_transform request page=next_page %}"
+                          {% else %}
+                          disabled
+                          {% endif %}
+                          class="button"
+                        >&gt;</a>
+                    </div>
+                    <div style="text-align:right">
+                        <form action="{% url view_announcements_url %}" method="get" style="display:inline;float:right">
+                            <input name="page" type="number"
+                            min="1" max={{ page_obj.num_pages }} class="dashboard-textinput" style="width: 75px"
+                            placeholder={{ items.number }}> of {{ page_obj.num_pages }}
+                            <input type="submit" value="Go"/>
+                        </form>
+                        </div>
+                    </div>
+                {% endif %}
             </div>
         {% endif %}
-        {% endif %}
     </div>
 </div>
 

From 51c00aca9c1fe342c45c7822891f48eb4137a35b Mon Sep 17 00:00:00 2001
From: JasonGrace2282 <aarush.deshpande@gmail.com>
Date: Wed, 2 Oct 2024 16:38:46 -0400
Subject: [PATCH 22/42] chore: fix linting errors

---
 intranet/apps/announcements/views.py          |  9 +++++--
 intranet/apps/eighth/forms/activities.py      |  6 ++---
 .../apps/eighth/forms/admin/activities.py     | 25 +++++++++----------
 intranet/apps/eighth/views/activities.py      |  2 +-
 intranet/apps/users/models.py                 |  2 +-
 5 files changed, 24 insertions(+), 20 deletions(-)

diff --git a/intranet/apps/announcements/views.py b/intranet/apps/announcements/views.py
index 48989c5813b..dfb7419cb69 100644
--- a/intranet/apps/announcements/views.py
+++ b/intranet/apps/announcements/views.py
@@ -15,8 +15,13 @@
 from ..groups.models import Group
 from .forms import AnnouncementAdminForm, AnnouncementEditForm, AnnouncementForm, AnnouncementRequestForm, ClubAnnouncementForm
 from .models import Announcement, AnnouncementRequest
-from .notifications import (admin_request_announcement_email, announcement_approved_email, announcement_posted_email, announcement_posted_twitter,
-                            request_announcement_email)
+from .notifications import (
+    admin_request_announcement_email,
+    announcement_approved_email,
+    announcement_posted_email,
+    announcement_posted_twitter,
+    request_announcement_email,
+)
 
 logger = logging.getLogger(__name__)
 
diff --git a/intranet/apps/eighth/forms/activities.py b/intranet/apps/eighth/forms/activities.py
index 631f37dc6e6..91b9928ac78 100644
--- a/intranet/apps/eighth/forms/activities.py
+++ b/intranet/apps/eighth/forms/activities.py
@@ -15,9 +15,9 @@ def __init__(self, *args, **kwargs):
         self.fields["subscriptions_enabled"].label = "Enable club announcements"
         self.fields["subscriptions_enabled"].help_text = "Allow students to subscribe to receive announcements for this activity through Ion."
         self.fields["club_sponsors"].label = "Teacher moderators"
-        self.fields["club_sponsors"].help_text = (
-            "Teacher moderators can post and manage this club's announcements. You should include club sponsors here."
-        )
+        self.fields[
+            "club_sponsors"
+        ].help_text = "Teacher moderators can post and manage this club's announcements. You should include club sponsors here."
         self.fields["officers"].label = "Student officers"
         self.fields["officers"].help_text = "Student officers can send club announcements to subscribers."
 
diff --git a/intranet/apps/eighth/forms/admin/activities.py b/intranet/apps/eighth/forms/admin/activities.py
index 3d0a6a71edc..b2f82769357 100644
--- a/intranet/apps/eighth/forms/admin/activities.py
+++ b/intranet/apps/eighth/forms/admin/activities.py
@@ -1,5 +1,6 @@
+from __future__ import annotations
+
 import logging
-from typing import List  # noqa
 
 from django import forms, http
 from django.contrib.auth import get_user_model
@@ -11,7 +12,7 @@
 
 
 class ActivityDisplayField(forms.ModelChoiceField):
-    cancelled_acts = None  # type: List[EighthActivity]
+    cancelled_acts: list[EighthActivity] | None = None
 
     def __init__(self, *args, **kwargs):
         if "block" in kwargs:
@@ -144,9 +145,9 @@ def __init__(self, *args, **kwargs):
         self.fields["subscriptions_enabled"].label = "Enable club announcements"
         self.fields["subscriptions_enabled"].help_text = "Allow students to subscribe to receive announcements for this activity through Ion."
         self.fields["officers"].help_text = "Student officers can send club announcements to subscribers."
-        self.fields["club_sponsors"].help_text = (
-            "Club sponsors can manage this club's announcements. May be different from the activity's scheduled sponsors."
-        )
+        self.fields[
+            "club_sponsors"
+        ].help_text = "Club sponsors can manage this club's announcements. May be different from the activity's scheduled sponsors."
         self.fields["subscribers"].help_text = "Students who subscribe to this activity will receive club announcements."
 
         # These fields are rendered on the right of the page on the edit activity page.
@@ -161,14 +162,12 @@ def __init__(self, *args, **kwargs):
             "seniors_allowed",
         }
 
-        self.club_announcements_fields = set(
-            [
-                "subscriptions_enabled",
-                "club_sponsors",
-                "officers",
-                "subscribers",
-            ]
-        )
+        self.club_announcements_fields = {
+            "subscriptions_enabled",
+            "club_sponsors",
+            "officers",
+            "subscribers",
+        }
 
     class Meta:
         model = EighthActivity
diff --git a/intranet/apps/eighth/views/activities.py b/intranet/apps/eighth/views/activities.py
index 0e8d5d8f712..402ffba9f94 100644
--- a/intranet/apps/eighth/views/activities.py
+++ b/intranet/apps/eighth/views/activities.py
@@ -78,7 +78,7 @@ def settings_view(request, activity_id=None):
 
 def chunks(items, n):
     for i in range(0, len(items), n):
-        yield items[i: i + n]
+        yield items[i : i + n]
 
 
 def current_school_year():
diff --git a/intranet/apps/users/models.py b/intranet/apps/users/models.py
index 040a9506d7e..4ff84fe0bde 100644
--- a/intranet/apps/users/models.py
+++ b/intranet/apps/users/models.py
@@ -1272,7 +1272,7 @@ def attribute_is_public(self, permission: str) -> bool:
 
 
 PERMISSIONS_NAMES = {
-    prefix: [name[len(prefix) + 1:] for name in dir(UserProperties) if name.startswith(prefix + "_")] for prefix in ["self", "parent"]
+    prefix: [name[len(prefix) + 1 :] for name in dir(UserProperties) if name.startswith(prefix + "_")] for prefix in ["self", "parent"]
 }
 
 

From 8d4abda721f2cac6ce77786a8a8074ca4c314da9 Mon Sep 17 00:00:00 2001
From: JasonGrace2282 <aarush.deshpande@gmail.com>
Date: Thu, 3 Oct 2024 11:53:42 -0400
Subject: [PATCH 23/42] feat: implement "other club announcements" filter

Also adds typehints/docstrings to helper functions.
---
 intranet/apps/dashboard/views.py              | 137 ++++++++++++++----
 intranet/static/js/dashboard/announcements.js |  10 +-
 intranet/templates/dashboard/dashboard.html   |  50 ++++++-
 3 files changed, 162 insertions(+), 35 deletions(-)

diff --git a/intranet/apps/dashboard/views.py b/intranet/apps/dashboard/views.py
index 75491698171..6b78768ea6d 100644
--- a/intranet/apps/dashboard/views.py
+++ b/intranet/apps/dashboard/views.py
@@ -1,14 +1,20 @@
+from __future__ import annotations
+
 import logging
 from datetime import datetime, time, timedelta
 from itertools import chain
+from typing import Any, Generic, Iterable, Sequence, TypeVar
 
 from django.conf import settings
 from django.contrib.auth.decorators import login_required
-from django.core.paginator import Paginator
+from django.core.paginator import Page, Paginator
+from django.db.models import QuerySet
+from django.http import HttpRequest
 from django.shortcuts import redirect, render
 from django.urls import reverse
 from django.utils import timezone
 from django.utils.timezone import make_aware
+from typing_extensions import TypedDict, TypeGuard
 
 from ...utils.date import get_senior_graduation_date, get_senior_graduation_year
 from ...utils.helpers import get_ap_week_warning, get_fcps_emerg, get_warning_html
@@ -21,9 +27,10 @@
 from ..seniors.models import Senior
 
 logger = logging.getLogger(__name__)
+T = TypeVar("T")
 
 
-def gen_schedule(user, num_blocks=6, surrounding_blocks=None):
+def gen_schedule(user, num_blocks: int = 6, surrounding_blocks: Iterable[EighthBlock] | None = None):
     """Generate a list of information about a block and a student's current activity signup.
 
     Returns:
@@ -109,7 +116,7 @@ def gen_schedule(user, num_blocks=6, surrounding_blocks=None):
     return schedule, no_signup_today
 
 
-def gen_sponsor_schedule(user, sponsor=None, num_blocks=6, surrounding_blocks=None, given_date=None):
+def gen_sponsor_schedule(user, sponsor=None, num_blocks: int = 6, surrounding_blocks=None, given_date=None):
     r"""Return a list of :class:`EighthScheduledActivity`\s in which the
     given user is sponsoring.
 
@@ -193,7 +200,7 @@ def get_prerender_url(request):
     return request.build_absolute_uri(reverse(view))
 
 
-def get_announcements_list(request, context):
+def get_announcements_list(request, context) -> list[Announcement | Event]:
     """
     An announcement will be shown if:
     * It is not expired
@@ -265,11 +272,25 @@ def announcements_sorting_key(item):
     return items
 
 
-def split_club_announcements(items):
+def split_club_announcements(items: Iterable[Announcement | Event]) -> tuple[list[Announcement], list[Announcement]]:
+    """Split items into standard and club announcements.
+
+    .. warning::
+
+        This will discard any club announcements with subscriptions disabled
+        from the resulting list.
+
+    Returns:
+        a tuple of standard and club announcements.
+    """
+
+    def is_announcement(item: Announcement | Event) -> TypeGuard[Announcement]:
+        return item.dashboard_type == "announcement"
+
     standard, club = [], []
 
     for item in items:
-        if item.dashboard_type == "announcement" and item.is_club_announcement:
+        if is_announcement(item) and item.is_club_announcement:
             if item.activity.subscriptions_enabled:
                 club.append(item)
         else:
@@ -278,30 +299,62 @@ def split_club_announcements(items):
     return standard, club
 
 
-def filter_club_announcements(user, user_hidden_announcements, club_items):
+def filter_club_announcements(
+    user, user_hidden_announcements: QuerySet[Announcement], club_items: Iterable[Announcement]
+) -> tuple[list[Announcement], list[Announcement], list[Announcement]]:
+    """Filter club announcements into categories
+
+    Returns:
+        a tuple of visible, hidden, and unsubscribed club announcements for the user.
+    """
     visible, hidden, unsubscribed = [], [], []
 
     for item in club_items:
         if item.activity.subscriptions_enabled:
-            if user not in item.activity.subscribers.all():
-                unsubscribed.append(item)
-            elif item.id in user_hidden_announcements:
+            if item.id in user_hidden_announcements:
                 hidden.append(item)
-            else:
+            elif user.subscribed_activity_set.filter(announcement=item).exists():
                 visible.append(item)
+            else:
+                unsubscribed.append(item)
 
     return visible, hidden, unsubscribed
 
 
-def paginate_announcements_list(request, context, items, visible_club_items):
-    """
-    Paginate ``items`` in groups of 15
+class RawPaginationData(TypedDict, Generic[T]):
+    club_items: Sequence[Announcement]
+    items: Page[T]
+    page_num: int
+    prev_page: int
+    next_page: int
+    more_items: bool
+    page_obj: Paginator[T]
+
 
+def paginate_announcements_list_raw(
+    request: HttpRequest,
+    items: Sequence[T],
+    visible_club_items: Sequence[Announcement] = (),
+    *,
+    query_param: str = "page",
+) -> RawPaginationData[T]:
+    """Return the raw data for paginating announcements.
+
+    Args:
+        request: The :class:`django.http.HttpRequest` object.
+        items: The list of items to paginate.
+        visible_club_items: The list of club announcements to paginate and add to the context.
+        query_param: The ``request.GET`` parameter to use for the page number.
+
+    Returns:
+        A dictionary intended to be merged into the context.
     """
+
     DEFAULT_PAGE_NUM = 1
 
-    if request.GET.get("page", "INVALID").isdigit():
-        page_num = int(request.GET["page"])
+    num = request.GET.get(query_param, "")
+    if num.isdigit():
+        page_num = int(num)
     else:
         page_num = DEFAULT_PAGE_NUM
 
@@ -315,20 +368,34 @@ def paginate_announcements_list(request, context, items, visible_club_items):
     prev_page = items.previous_page_number() if items.has_previous() else 0
     next_page = items.next_page_number() if more_items else 0
 
+    # limit to 15 to prevent extreme slowdowns for large amounts
+    # of club announcements
     club_items = visible_club_items[:15]
-    context.update(
-        {
-            "club_items": club_items,
-            "items": items,
-            "page_num": page_num,
-            "prev_page": prev_page,
-            "next_page": next_page,
-            "more_items": more_items,
-            "page_obj": paginator,
-        }
+
+    return RawPaginationData(
+        club_items=club_items,
+        items=items,
+        page_num=page_num,
+        prev_page=prev_page,
+        next_page=next_page,
+        more_items=more_items,
+        page_obj=paginator,
     )
 
-    return context, items
+
+def paginate_announcements_list(
+    request, context: dict[str, Any], items: Sequence[T], visible_club_items: Sequence[Announcement] = ()
+) -> tuple[dict[str, Any], Page[T]]:
+    """Paginate ``items`` in groups of 15
+
+    Returns:
+        A tuple of the updated context and the page.
+    """
+    new_ctx = paginate_announcements_list_raw(request, items, visible_club_items)
+    context.update(new_ctx)
+    context["all_items"] = context["items"]
+
+    return context, new_ctx["items"]
 
 
 def get_tjstar_mapping(user):
@@ -495,13 +562,25 @@ def dashboard_view(request, show_widgets=True, show_expired=False, show_hidden_c
 
     items, club_items = split_club_announcements(items)
 
+    visible_club_items, _, unsubscribed_club_announcements = filter_club_announcements(user, user_hidden_announcements, club_items)
+
     if not show_hidden_club:
         # Dashboard
-        visible_club_items, _hidden_club_items, _other_club_items = filter_club_announcements(user, user_hidden_announcements, club_items)
         context, items = paginate_announcements_list(request, context, items, visible_club_items)
     else:
         # Club announcements only
-        context, items = paginate_announcements_list(request, context, club_items, visible_club_items=[])
+        context, items = paginate_announcements_list(request, context, visible_club_items, visible_club_items=[])
+
+        # add club announcement pagination for non-subscribed
+        raw_pagination_data = paginate_announcements_list_raw(
+            request,
+            unsubscribed_club_announcements,
+            query_param="unsubscribed_page",
+        )
+        # namespace the pagination data for unsubscribed club announcements so it doesn't
+        # conflict with other pagination data
+        context["unsubscribed"] = raw_pagination_data
+        context["all_items"] = (*context["items"], *context["unsubscribed"]["items"])
 
     if ignore_dashboard_types is None:
         ignore_dashboard_types = []
diff --git a/intranet/static/js/dashboard/announcements.js b/intranet/static/js/dashboard/announcements.js
index 98a10c34f4b..3fb827865ea 100644
--- a/intranet/static/js/dashboard/announcements.js
+++ b/intranet/static/js/dashboard/announcements.js
@@ -44,15 +44,21 @@ $(document).ready(function() {
 
     $(".subscribed-filter").click(function () {
         $(".unsubscribed-filter").removeClass("active");
-        $("#subscriptions-pagination").css("display", "");
+        $("#non-subscriptions-pagination").hide();
+
+        $("#subscriptions-pagination").show();
         $(this).addClass("active");
+
         filterClubAnnouncements();
     });
 
     $(".unsubscribed-filter").click(function () {
         $(".subscribed-filter").removeClass("active");
-        $("#subscriptions-pagination").css("display", "none");
+        $("#subscriptions-pagination").hide();
+
+        $("#non-subscriptions-pagination").show();
         $(this).addClass("active");
+
         filterClubAnnouncements();
     });
 
diff --git a/intranet/templates/dashboard/dashboard.html b/intranet/templates/dashboard/dashboard.html
index 7c05ffd4255..83bbeaf496b 100644
--- a/intranet/templates/dashboard/dashboard.html
+++ b/intranet/templates/dashboard/dashboard.html
@@ -209,7 +209,12 @@ <h3>
             <div class="club-announcements">
                 <h3 class="club-announcements-header">
                     <i class="fas fa-users"></i>&nbsp;
-                    You have <span class="num-club-announcements">{{ club_items|length }}</span> new club announcement{{ club_items|length|pluralize }}
+                    <!-- This only goes up to 15 items. Removing this restriction causes slowdowns. -->
+                    {% if club_items|length < 15 %}
+                        You have <span class="num-club-announcements">{{ club_items|length }}</span> new club announcement{{ club_items|length|pluralize }}
+                    {% else %}
+                        You have <span class="num-club-announcements">15+</span> new club announcements
+                    {% endif %}
                     <i class="fas fa-chevron-down club-announcements-toggle-icon"></i>
                 </h3>
                 <div class="club-announcements-content">
@@ -242,7 +247,7 @@ <h3 class="club-announcements-header">
             </div>
         {% endif %}
 
-        {% for item in items %}
+        {% for item in all_items %}
             {% if item.dashboard_type in ignore_dashboard_types %}
                 <!-- {{ item.dashboard_type }} hidden -->
             {% elif item.dashboard_type == "announcement" %}
@@ -280,8 +285,8 @@ <h3 class="club-announcements-header">
                           >&lt;</a>
 
                         {% for page in page_obj|page_list:items %}
-                        <a {% if page %} class="button" {% else %} class="ellipses" {% endif %}
-                          {% if page == items.number %} style="background-image: linear-gradient(to bottom, #858585 0%, #5f5f5f 100%); color: white;" {% endif %}
+                        <a class="{% if page %}button {% else %}ellipses{% endif %}"
+                          style="{% if page == items.number %}background-image: linear-gradient(to bottom, #858585 0%, #5f5f5f 100%); color: white;{% endif %}"
                           href="{% url view_announcements_url %}{% if page %}?{% query_transform request page=page %}{% else %}#{% endif %}">{{ page|default:"..." }}</a>
                         {% endfor %}
 
@@ -304,6 +309,43 @@ <h3 class="club-announcements-header">
                     </div>
                 {% endif %}
             </div>
+            <div id="non-subscriptions-pagination" style="display:none">
+                {% if unsubscribed.page_obj.num_pages > 1 %}
+                    <div style="display:grid;grid-template-columns:1fr max-content 1fr">
+                        <div style="text-align:center;grid-column-start:2">
+                          <a {% if unsubscribed.prev_page > 0 %}
+                            href="{% url view_announcements_url %}?{% query_transform request unsubscribed_page=unsubscribed.prev_page %}"
+                            {% else %}
+                            disabled
+                            {% endif %}
+                            class="button"
+                          >&lt;</a>
+
+                        {% for page in unsubscribed.page_obj|page_list:unsubscribed.items %}
+                        <a class="{% if page %}button {% else %}ellipses{% endif %}"
+                          style="{% if page == unsubscribed.items.number %}background-image: linear-gradient(to bottom, #858585 0%, #5f5f5f 100%); color: white;{% endif %}"
+                          href="{% url view_announcements_url %}{% if page %}?{% query_transform request unsubscribed_page=page %}{% else %}#{% endif %}">{{ page|default:"..." }}</a>
+                        {% endfor %}
+
+                        <a {% if unsubscribed.more_items %}
+                          href="{% url view_announcements_url %}?{% query_transform request unsubscribed_page=unsubscribed.next_page %}"
+                          {% else %}
+                          disabled
+                          {% endif %}
+                          class="button"
+                        >&gt;</a>
+                    </div>
+                    <div style="text-align:right">
+                        <form action="{% url view_announcements_url %}" method="get" style="display:inline;float:right">
+                            <input name="page" type="number"
+                            min="1" max={{ unsubscribed.page_obj.num_pages }} class="dashboard-textinput" style="width: 75px"
+                            placeholder={{ unsubscribed.items.number }}> of {{ unsubscribed.page_obj.num_pages }}
+                            <input type="submit" value="Go"/>
+                        </form>
+                        </div>
+                    </div>
+                {% endif %}
+            </div>
         {% endif %}
     </div>
 </div>

From fa826b9505ff160c823de1ae98f5e724990dafe1 Mon Sep 17 00:00:00 2001
From: JasonGrace2282 <aarush.deshpande@gmail.com>
Date: Thu, 3 Oct 2024 22:43:59 -0400
Subject: [PATCH 24/42] feat: search for club announcements

---
 intranet/apps/search/views.py                 | 27 ++++++++++++++-----
 intranet/templates/search/search_results.html | 21 +++++++++++++++
 2 files changed, 41 insertions(+), 7 deletions(-)

diff --git a/intranet/apps/search/views.py b/intranet/apps/search/views.py
index 3c0468d48c8..66b5df3f209 100644
--- a/intranet/apps/search/views.py
+++ b/intranet/apps/search/views.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 import logging
 
 from django.conf import settings
@@ -7,7 +9,7 @@
 from django.shortcuts import redirect, render
 
 from ...utils.helpers import is_entirely_digit
-from ..announcements.models import AnnouncementManager
+from ..announcements.models import Announcement, AnnouncementManager
 from ..auth.decorators import deny_restricted
 from ..eighth.models import EighthActivity
 from ..enrichment.models import EnrichmentActivity
@@ -214,14 +216,24 @@ def do_courses_search(q):
     return Course.objects.filter(filter_query).order_by("name")
 
 
-def do_announcements_search(q, user):
+def do_announcements_search(q, user) -> tuple[list[Announcement], list[Announcement]]:
+    """Search for announcements.
+
+    Returns:
+        A tuple of the announcements and club announcements
+    """
     filter_query = get_query(q, ["title", "content"])
     entries = AnnouncementManager().visible_to_user(user).filter(filter_query).order_by("title")
-    final_entries = []
+    club_announcements = []
+    announcements = []
     for e in entries:
-        if e.is_this_year:
-            final_entries.append(e)
-    return final_entries
+        if not e.is_this_year:
+            continue
+        if e.activity is None:
+            announcements.append(e)
+        else:
+            club_announcements.append(e)
+    return (announcements, club_announcements)
 
 
 def do_events_search(q):
@@ -266,7 +278,7 @@ def search_view(request):
             users = sorted(users, key=lambda u: (u.last_name, u.first_name))
 
         activities = do_activities_search(q)
-        announcements = do_announcements_search(q, request.user)
+        announcements, club_announcements = do_announcements_search(q, request.user)
         events = do_events_search(q)
         enrichments = do_enrichment_search(q) if settings.ENABLE_ENRICHMENT_APP else []
         classes = do_courses_search(q)
@@ -282,6 +294,7 @@ def search_view(request):
             "search_query": q,
             "search_results": users,  # User objects
             "announcements": announcements,  # Announcement objects
+            "club_announcements": club_announcements,  # Club Announcement objects
             "events": events,  # Event objects
             "enrichments": enrichments,  # EnrichmentActivity objects
             "activities": activities,  # EighthActivity objects
diff --git a/intranet/templates/search/search_results.html b/intranet/templates/search/search_results.html
index 1da1abef011..e76ae7f7a06 100644
--- a/intranet/templates/search/search_results.html
+++ b/intranet/templates/search/search_results.html
@@ -140,6 +140,27 @@ <h3>Announcements</h3>
         {% else %}
             <b>No results.</b>
         {% endif %}
+
+        <br><br>
+        <h3>Club Announcements</h3>
+        {% if club_announcements %}
+            {{ club_announcements|length }} result{{ club_announcements|pluralize }}
+            <table class="list-table announcements-table">
+                {% for a in club_announcements %}
+                    <tr>
+                        <td class="list-item">
+                            <a href="{% url 'view_announcement' a.id %}">
+                                {{ a.title|safe }}
+                            </a><br>
+                            &nbsp; &nbsp; {{ a.content|truncatechars_html:"50"|striptags }}
+                        </td>
+                    </tr>
+                {% endfor %}
+            </table>
+        {% else %}
+            <b>No results.</b>
+        {% endif %}
+
     {% endif %}
 
     {% if search_query and DJANGO_SETTINGS.ENABLE_ENRICHMENT_APP %}

From 6823c437b2271e6e5d204ce32cbd3330a0f2efd7 Mon Sep 17 00:00:00 2001
From: JasonGrace2282 <aarush.deshpande@gmail.com>
Date: Fri, 4 Oct 2024 23:10:55 -0400
Subject: [PATCH 25/42] feat: force club sponsors to be subscribed

---
 intranet/apps/eighth/views/admin/activities.py   |  5 ++++-
 intranet/apps/eighth/views/signup.py             |  2 +-
 .../templates/announcements/announcement.html    | 16 +++++++++-------
 3 files changed, 14 insertions(+), 9 deletions(-)

diff --git a/intranet/apps/eighth/views/admin/activities.py b/intranet/apps/eighth/views/admin/activities.py
index 919140edd6d..f9d2a1ca4ea 100644
--- a/intranet/apps/eighth/views/admin/activities.py
+++ b/intranet/apps/eighth/views/admin/activities.py
@@ -148,7 +148,10 @@ def edit_activity_view(request, activity_id):
                     messages.success(request, "Notifying students of this room change.")
                     room_changed_activity_email.delay(activity, old_rooms, EighthRoom.objects.filter(id__in=new_room_ids))
 
-                form.save()
+                activity = form.save()
+                activity.subscribers.add(*[sponsor.user for sponsor in form.cleaned_data["sponsors"]])
+                activity.save()
+
             except forms.ValidationError as error:
                 error = str(error)
                 messages.error(request, error)
diff --git a/intranet/apps/eighth/views/signup.py b/intranet/apps/eighth/views/signup.py
index 1d4c434db92..ecb1a62b8a9 100644
--- a/intranet/apps/eighth/views/signup.py
+++ b/intranet/apps/eighth/views/signup.py
@@ -404,7 +404,7 @@ def subscribe_to_club(request, activity_id):
 def unsubscribe_from_club(request, activity_id):
     activity = get_object_or_404(EighthActivity, id=activity_id)
 
-    if request.user in activity.subscribers.all():
+    if request.user in activity.subscribers.all() and not activity.sponsors.filter(user=request.user).exists():
         activity.subscribers.remove(request.user)
 
     return redirect(request.META.get("HTTP_REFERER", "/"))
diff --git a/intranet/templates/announcements/announcement.html b/intranet/templates/announcements/announcement.html
index 1af23e6d9c7..c7fb3e7c676 100644
--- a/intranet/templates/announcements/announcement.html
+++ b/intranet/templates/announcements/announcement.html
@@ -50,13 +50,15 @@ <h3>
         <div class="announcement-icon-wrapper">
             {% if announcement.is_club_announcement %}
                 {% if request.user in announcement.activity.subscribers.all %}
-                    <a class="button small-button unsubscribe-button"
-                        id="unsubscribe-button"
-                        href="{% url 'unsubscribe_from_club' announcement.activity.id %}"
-                        title="Unsubscribe from {{ announcement.activity.name }} club announcements">
-                        <i class="fas fa-times"></i>
-                        Unsubscribe
-                    </a>
+                    {% if request.user.sponsor_obj not in announcement.activity.sponsors.all %}
+                      <a class="button small-button unsubscribe-button"
+                          id="unsubscribe-button"
+                          href="{% url 'unsubscribe_from_club' announcement.activity.id %}"
+                          title="Unsubscribe from {{ announcement.activity.name }} club announcements">
+                          <i class="fas fa-times"></i>
+                          Unsubscribe
+                      </a>
+                    {% endif %}
                 {% else %}
                     <a class="button small-button subscribe-button"
                         id="subscribe-button"

From 5b0b82211bb0e134271fbafa3dc318ad7d838d38 Mon Sep 17 00:00:00 2001
From: Alan Zhu <2025azhu@tjhsst.edu>
Date: Sat, 5 Oct 2024 00:34:31 -0400
Subject: [PATCH 26/42] fix: don't create normal announcement on club
 announcement edit

---
 intranet/apps/announcements/forms.py | 18 +++++++++++++++++-
 intranet/apps/announcements/views.py | 20 ++++++++++++--------
 2 files changed, 29 insertions(+), 9 deletions(-)

diff --git a/intranet/apps/announcements/forms.py b/intranet/apps/announcements/forms.py
index 37fa37ac515..0a4f1d370d3 100644
--- a/intranet/apps/announcements/forms.py
+++ b/intranet/apps/announcements/forms.py
@@ -47,7 +47,7 @@ def __init__(self, user, *args, **kwargs):
 
         if "instance" in kwargs:  # Don't allow changing the activity once the announcement has been created
             self.fields["activity"].widget.attrs["disabled"] = True
-            self.fields["activity"].required = False
+            self.fields["activity"].initial = kwargs["instance"].activity
 
     expiration_date = forms.DateTimeInput()
 
@@ -59,6 +59,22 @@ class Meta:
         }
 
 
+class ClubAnnouncementEditForm(forms.ModelForm):
+    """A form for editing a club announcement."""
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+    expiration_date = forms.DateTimeInput()
+
+    class Meta:
+        model = Announcement
+        fields = ["title", "content", "expiration_date"]
+        help_texts = {
+            "expiration_date": "By default, announcements expire after two weeks. To change this, click in the box above.",
+        }
+
+
 class AnnouncementEditForm(forms.ModelForm):
     """A form for generating an announcement."""
 
diff --git a/intranet/apps/announcements/views.py b/intranet/apps/announcements/views.py
index dfb7419cb69..71659433455 100644
--- a/intranet/apps/announcements/views.py
+++ b/intranet/apps/announcements/views.py
@@ -13,7 +13,14 @@
 from ..auth.decorators import announcements_admin_required, deny_restricted
 from ..dashboard.views import dashboard_view
 from ..groups.models import Group
-from .forms import AnnouncementAdminForm, AnnouncementEditForm, AnnouncementForm, AnnouncementRequestForm, ClubAnnouncementForm
+from .forms import (
+    AnnouncementAdminForm,
+    AnnouncementEditForm,
+    AnnouncementForm,
+    AnnouncementRequestForm,
+    ClubAnnouncementEditForm,
+    ClubAnnouncementForm,
+)
 from .models import Announcement, AnnouncementRequest
 from .notifications import (
     admin_request_announcement_email,
@@ -146,7 +153,7 @@ def add_club_announcement_view(request):
         form = ClubAnnouncementForm(request.user, request.POST)
 
         if form.is_valid():
-            obj = form.save(commit=True)
+            obj = form.save(commit=False)
             obj.user = request.user
             # SAFE HTML
             obj.content = safe_html(obj.content)
@@ -186,15 +193,12 @@ def modify_club_announcement_view(request, announcement_id):
         return redirect("club_announcements")
 
     if request.method == "POST":
-        form = ClubAnnouncementForm(request.user, request.POST, instance=announcement)
+        form = ClubAnnouncementEditForm(request.POST, instance=announcement)
 
         if form.is_valid():
-            obj = form.save(commit=True)
-            obj.user = request.user
-            obj.activity = announcement.activity
+            obj = form.save(commit=False)
             # SAFE HTML
             obj.content = safe_html(obj.content)
-
             obj.save()
 
             messages.success(request, "Successfully modified club announcement.")
@@ -202,7 +206,7 @@ def modify_club_announcement_view(request, announcement_id):
         else:
             messages.error(request, "Error modifying club announcement")
     else:
-        form = ClubAnnouncementForm(request.user, instance=announcement)
+        form = ClubAnnouncementEditForm(instance=announcement)
     return render(request, "announcements/club-request.html", {"form": form, "action": "modify"})
 
 

From 226a2a016f260f4ebda12eeb4f501087bedf4362 Mon Sep 17 00:00:00 2001
From: JasonGrace2282 <aarush.deshpande@gmail.com>
Date: Sat, 5 Oct 2024 00:36:29 -0400
Subject: [PATCH 27/42] feat: add show expired button

---
 intranet/apps/dashboard/views.py            |  6 +++---
 intranet/templates/dashboard/dashboard.html | 10 +++++++++-
 2 files changed, 12 insertions(+), 4 deletions(-)

diff --git a/intranet/apps/dashboard/views.py b/intranet/apps/dashboard/views.py
index 6b78768ea6d..ffb373171f2 100644
--- a/intranet/apps/dashboard/views.py
+++ b/intranet/apps/dashboard/views.py
@@ -238,7 +238,7 @@ def get_announcements_list(request, context) -> list[Announcement | Event]:
         # Show all announcements if user has admin permissions and the
         # show_all GET argument is given.
         announcements = Announcement.objects.all()
-    elif context["show_expired"] or context["show_all"]:
+    elif context["show_expired"]:
         announcements = Announcement.objects.visible_to_user(user)
     else:
         announcements = Announcement.objects.visible_to_user(user).filter(expiration_date__gt=timezone.now())
@@ -530,7 +530,7 @@ def dashboard_view(request, show_widgets=True, show_expired=False, show_hidden_c
     events_admin = user.has_admin_permission("events")
 
     if not show_expired:
-        show_expired = "show_expired" in request.GET
+        show_expired = request.GET.get("show_expired") == "1"
 
     show_all = request.GET.get("show_all", "0") != "0"
     if "show_all" not in request.GET and request.user.is_eighthoffice:
@@ -610,7 +610,7 @@ def dashboard_view(request, show_widgets=True, show_expired=False, show_hidden_c
     elif show_expired:
         dashboard_title = dashboard_header = "Announcement Archive"
         view_announcements_url = "announcements_archive"
-    elif show_hidden_club:
+    if show_hidden_club:
         dashboard_title = dashboard_header = "Club Announcements"
         view_announcements_url = "club_announcements"
     else:
diff --git a/intranet/templates/dashboard/dashboard.html b/intranet/templates/dashboard/dashboard.html
index 83bbeaf496b..e73faf830c0 100644
--- a/intranet/templates/dashboard/dashboard.html
+++ b/intranet/templates/dashboard/dashboard.html
@@ -153,13 +153,21 @@ <h2>{{ dashboard_header }}</h2>
                                 Post
                             </a>
                         {% endif %}
+                        {% if request.GET.show_expired == "1" %}
+                            <a class="button" href="{% url view_announcements_url %}?{% query_transform request show_expired=0 %}">
+                                <i class="fas fa-times"></i> Don't Show Expired
+                            </a>
+                        {% else %}
+                            <a class="button" href="{% url view_announcements_url %}?{% query_transform request show_expired=1 %}">
+                                Show Expired
+                            </a>
+                        {% endif %}
                     {% else %}
                         <a class="button announcement-request" href="{% url 'request_announcement' %}">
                             <i class="far fa-file-alt"></i>
                             Request Post
                         </a>
                     {% endif %}
-
                 {% endif %}
             {% endif %}
             </span>

From c10a7b3ff7900d265ca3152db7a8deadb3a3ffeb Mon Sep 17 00:00:00 2001
From: Alan Zhu <2025azhu@tjhsst.edu>
Date: Sat, 5 Oct 2024 01:02:29 -0400
Subject: [PATCH 28/42] fix: show hidden announcements on club announcements
 view

---
 intranet/apps/dashboard/views.py | 13 +++++++------
 1 file changed, 7 insertions(+), 6 deletions(-)

diff --git a/intranet/apps/dashboard/views.py b/intranet/apps/dashboard/views.py
index ffb373171f2..1af23177d84 100644
--- a/intranet/apps/dashboard/views.py
+++ b/intranet/apps/dashboard/views.py
@@ -311,10 +311,11 @@ def filter_club_announcements(
 
     for item in club_items:
         if item.activity.subscriptions_enabled:
-            if item.id in user_hidden_announcements:
-                hidden.append(item)
-            elif user.subscribed_activity_set.filter(announcement=item).exists():
-                visible.append(item)
+            if user.subscribed_activity_set.filter(announcement=item).exists():
+                if item.id in user_hidden_announcements:
+                    hidden.append(item)
+                else:
+                    visible.append(item)
             else:
                 unsubscribed.append(item)
 
@@ -562,14 +563,14 @@ def dashboard_view(request, show_widgets=True, show_expired=False, show_hidden_c
 
     items, club_items = split_club_announcements(items)
 
-    visible_club_items, _, unsubscribed_club_announcements = filter_club_announcements(user, user_hidden_announcements, club_items)
+    visible_club_items, hidden_club_items, unsubscribed_club_announcements = filter_club_announcements(user, user_hidden_announcements, club_items)
 
     if not show_hidden_club:
         # Dashboard
         context, items = paginate_announcements_list(request, context, items, visible_club_items)
     else:
         # Club announcements only
-        context, items = paginate_announcements_list(request, context, visible_club_items, visible_club_items=[])
+        context, items = paginate_announcements_list(request, context, visible_club_items + hidden_club_items, visible_club_items=[])
 
         # add club announcement pagination for non-subscribed
         raw_pagination_data = paginate_announcements_list_raw(

From dfab2879e42d6b644cdd7461eb4b3d41046c4730 Mon Sep 17 00:00:00 2001
From: Alan Zhu <2025azhu@tjhsst.edu>
Date: Sat, 5 Oct 2024 01:09:27 -0400
Subject: [PATCH 29/42] refactor: announcement banner color change

---
 intranet/static/css/dark/dashboard.scss | 9 +++++++++
 intranet/static/css/dashboard.scss      | 8 +++-----
 2 files changed, 12 insertions(+), 5 deletions(-)

diff --git a/intranet/static/css/dark/dashboard.scss b/intranet/static/css/dark/dashboard.scss
index eaed2c8adff..c6d01947b44 100644
--- a/intranet/static/css/dark/dashboard.scss
+++ b/intranet/static/css/dark/dashboard.scss
@@ -51,3 +51,12 @@ a.button {
 a.club-announcement-meta-link:hover {
     color: rgb(196, 196, 196) !important;
 }
+
+.announcements {
+    .announcement-banner {
+        background-color: #000000;
+    }
+    .announcement-link {
+        color: #a3a3a3;
+    }
+}
diff --git a/intranet/static/css/dashboard.scss b/intranet/static/css/dashboard.scss
index cebe6c078ed..089eec6917d 100644
--- a/intranet/static/css/dashboard.scss
+++ b/intranet/static/css/dashboard.scss
@@ -18,9 +18,7 @@
   }
 
   .announcement-banner {
-    background-color: #fff3cd;
-    color: #533f05;
-    border: 1px solid #fbe298;
+    background-color: #e0e0e0;
     padding: 10px;
     margin-bottom: 15px;
     margin-top: 10px;
@@ -30,13 +28,13 @@
   }
 
   .announcement-link {
-    color: #856404;
+    color: #6c6c6c;
     font-weight: bold;
     text-decoration: underline;
   }
 
   .announcement-link:hover {
-    color: #251900;
+    color: #464646;
   }
 }
 

From bc8fa41d7e9a95d7cfa512758dbe5e31970e0ac3 Mon Sep 17 00:00:00 2001
From: Shreyas Jain <shrys.jain@gmail.com>
Date: Sat, 5 Oct 2024 01:09:42 -0400
Subject: [PATCH 30/42] fix: allow club sponsors to edit settings

---
 intranet/apps/eighth/forms/activities.py       | 13 +++++++++----
 intranet/apps/eighth/forms/admin/activities.py | 14 ++++++++++----
 intranet/apps/eighth/models.py                 |  4 +++-
 intranet/apps/eighth/views/activities.py       | 15 +++++++--------
 intranet/apps/eighth/views/admin/activities.py |  4 ++--
 intranet/templates/eighth/activity.html        |  7 +++++++
 intranet/templates/eighth/take_attendance.html |  2 +-
 7 files changed, 39 insertions(+), 20 deletions(-)

diff --git a/intranet/apps/eighth/forms/activities.py b/intranet/apps/eighth/forms/activities.py
index 91b9928ac78..ecae85c14ca 100644
--- a/intranet/apps/eighth/forms/activities.py
+++ b/intranet/apps/eighth/forms/activities.py
@@ -7,7 +7,7 @@
 
 
 class ActivitySettingsForm(forms.ModelForm):
-    def __init__(self, *args, **kwargs):
+    def __init__(self, *args, sponsors=None, **kwargs):
         super().__init__(*args, **kwargs)
         self.fields["officers"].queryset = get_user_model().objects.get_students()
         self.fields["club_sponsors"].queryset = get_user_model().objects.filter(user_type__in=["teacher", "counselor"])
@@ -15,9 +15,14 @@ def __init__(self, *args, **kwargs):
         self.fields["subscriptions_enabled"].label = "Enable club announcements"
         self.fields["subscriptions_enabled"].help_text = "Allow students to subscribe to receive announcements for this activity through Ion."
         self.fields["club_sponsors"].label = "Teacher moderators"
-        self.fields[
-            "club_sponsors"
-        ].help_text = "Teacher moderators can post and manage this club's announcements. You should include club sponsors here."
+        
+        sponsors_list = "; ".join([str(sponsor) for sponsor in sponsors]) if sponsors else "no sponsors"
+                
+        self.fields["club_sponsors"].help_text = (
+            f"Teacher moderators can post and manage this club's announcements. "
+            f"These are in addition to the activity's eighth period sponsors ({sponsors_list})."
+        )
+
         self.fields["officers"].label = "Student officers"
         self.fields["officers"].help_text = "Student officers can send club announcements to subscribers."
 
diff --git a/intranet/apps/eighth/forms/admin/activities.py b/intranet/apps/eighth/forms/admin/activities.py
index b2f82769357..68ec88b3084 100644
--- a/intranet/apps/eighth/forms/admin/activities.py
+++ b/intranet/apps/eighth/forms/admin/activities.py
@@ -119,7 +119,7 @@ def __init__(self, *args, label="Activities", block=None, **kwargs):  # pylint:
 
 
 class ActivityForm(forms.ModelForm):
-    def __init__(self, *args, **kwargs):
+    def __init__(self, *args, sponsors=None, **kwargs):
         super().__init__(*args, **kwargs)
 
         for fieldname in ["sponsors", "rooms", "users_allowed", "groups_allowed", "users_blacklisted"]:
@@ -145,9 +145,15 @@ def __init__(self, *args, **kwargs):
         self.fields["subscriptions_enabled"].label = "Enable club announcements"
         self.fields["subscriptions_enabled"].help_text = "Allow students to subscribe to receive announcements for this activity through Ion."
         self.fields["officers"].help_text = "Student officers can send club announcements to subscribers."
-        self.fields[
-            "club_sponsors"
-        ].help_text = "Club sponsors can manage this club's announcements. May be different from the activity's scheduled sponsors."
+             
+        sponsors_list = "; ".join([str(sponsor) for sponsor in sponsors]) if sponsors else "no sponsors"
+                
+        self.fields["club_sponsors"].help_text = (
+            f"Teacher moderators can post and manage this club's announcements. "
+            f"These are in addition to the activity's eighth period sponsors ({sponsors_list})."
+        )
+        
+        self.fields["club_sponsors"].label = "Teacher moderators"
         self.fields["subscribers"].help_text = "Students who subscribe to this activity will receive club announcements."
 
         # These fields are rendered on the right of the page on the edit activity page.
diff --git a/intranet/apps/eighth/models.py b/intranet/apps/eighth/models.py
index d759cecab44..2fa093dbad4 100644
--- a/intranet/apps/eighth/models.py
+++ b/intranet/apps/eighth/models.py
@@ -82,7 +82,9 @@ class EighthSponsor(AbstractBaseEighthModel):
     show_full_name = models.BooleanField(default=False)
 
     history = HistoricalRecords()
-
+    
+    def __str__(self):
+        return self.name
     class Meta:
         unique_together = (("first_name", "last_name", "user", "online_attendance", "full_time", "department"),)
         ordering = ("last_name", "first_name")
diff --git a/intranet/apps/eighth/views/activities.py b/intranet/apps/eighth/views/activities.py
index 402ffba9f94..6a002bf548e 100644
--- a/intranet/apps/eighth/views/activities.py
+++ b/intranet/apps/eighth/views/activities.py
@@ -23,7 +23,7 @@
 from ...auth.decorators import deny_restricted, eighth_sponsor_required
 from ..forms.activities import ActivitySettingsForm
 from ..forms.admin.activities import ActivityMultiSelectForm
-from ..models import EighthActivity, EighthBlock, EighthScheduledActivity, EighthSignup
+from ..models import EighthActivity, EighthBlock, EighthScheduledActivity, EighthSignup, EighthSponsor
 from ..utils import get_start_date
 
 logger = logging.getLogger(__name__)
@@ -52,24 +52,23 @@ def activity_view(request, activity_id=None):
     return render(request, "eighth/activity.html", context)
 
 
-@eighth_sponsor_required
+@login_required
 def settings_view(request, activity_id=None):
     activity = get_object_or_404(EighthActivity, id=activity_id)
-
-    if not request.user.sponsor_obj:
+      
+    if not (EighthSponsor.objects.filter(user=request.user).exists() or request.user in activity.club_sponsors.all()):
         raise Http404
 
-    if request.user.sponsor_obj not in activity.sponsors.all():
-        raise Http404
+    print(activity.sponsors.all())
 
     if request.method == "POST":
-        form = ActivitySettingsForm(request.POST, instance=activity)
+        form = ActivitySettingsForm(request.POST, instance=activity, sponsors=activity.sponsors.all())
         if form.is_valid():
             form.save()
         else:
             messages.error(request, "There was an error saving the activity settings.")
     else:
-        form = ActivitySettingsForm(instance=activity)
+        form = ActivitySettingsForm(instance=activity, sponsors=activity.sponsors.all())
 
     context = {"activity": activity, "form": form}
 
diff --git a/intranet/apps/eighth/views/admin/activities.py b/intranet/apps/eighth/views/admin/activities.py
index f9d2a1ca4ea..d1fe7a6cf25 100644
--- a/intranet/apps/eighth/views/admin/activities.py
+++ b/intranet/apps/eighth/views/admin/activities.py
@@ -48,7 +48,7 @@ def edit_activity_view(request, activity_id):
         raise http.Http404 from e
 
     if request.method == "POST":
-        form = ActivityForm(request.POST, instance=activity)
+        form = ActivityForm(request.POST, instance=activity, sponsors=activity.sponsors.all())
         if form.is_valid():
             try:
                 # Check if sponsor change
@@ -182,7 +182,7 @@ def edit_activity_view(request, activity_id):
         else:
             messages.error(request, "Error adding activity.")
     else:
-        form = ActivityForm(instance=activity)
+        form = ActivityForm(instance=activity, sponsors=activity.sponsors.all())
 
     activities = EighthActivity.undeleted_objects.order_by("name")
 
diff --git a/intranet/templates/eighth/activity.html b/intranet/templates/eighth/activity.html
index c28f2ab7b5c..37b73f407c3 100644
--- a/intranet/templates/eighth/activity.html
+++ b/intranet/templates/eighth/activity.html
@@ -78,6 +78,13 @@
         {% endif %}
     {% endif %}
 
+    {% if request.user.sponsor_obj and request.user.sponsor_obj in scheduled_activity.activity.sponsors.all or request.user in activity.club_sponsors.all %}
+        <a class="button print-hide" href="{% url 'eighth_activity_settings' activity.id %}">
+            <i class="fa fa-users"></i>
+            Club Announcement Settings
+        </a>
+    {% endif %}
+
     <h2 style="padding-bottom: 0">Activity: {{ activity }}</h2>
     {% if activity.special %}
         <span class="badge green" title="This is a special activity.">Special</span>
diff --git a/intranet/templates/eighth/take_attendance.html b/intranet/templates/eighth/take_attendance.html
index 03af843df8b..14efc00ddeb 100644
--- a/intranet/templates/eighth/take_attendance.html
+++ b/intranet/templates/eighth/take_attendance.html
@@ -497,7 +497,7 @@ <h3>Passes</h3>
                 &nbsp; &nbsp;
                 <a class="button print-hide" href="{% url 'eighth_email_students' scheduled_activity.id %}">Email Students</a>
                 {% endif %}
-                {% if request.user.sponsor_obj and request.user.sponsor_obj in scheduled_activity.activity.sponsors.all %}
+                {% if request.user.sponsor_obj and request.user.sponsor_obj in scheduled_activity.activity.sponsors.all or request.user in activity.club_sponsors.all %}
                     <br>
                     <br>
                     <a class="button print-hide" href="{% url 'eighth_activity_settings' scheduled_activity.activity.id %}">

From b5cce88a742e58cf249c844fba272cdc3ea330a5 Mon Sep 17 00:00:00 2001
From: JasonGrace2282 <aarush.deshpande@gmail.com>
Date: Sat, 5 Oct 2024 01:20:19 -0400
Subject: [PATCH 31/42] fix: unsubscribed button not showing

---
 intranet/apps/dashboard/views.py                   | 3 ++-
 intranet/apps/eighth/views/signup.py               | 5 ++++-
 intranet/templates/announcements/announcement.html | 2 +-
 3 files changed, 7 insertions(+), 3 deletions(-)

diff --git a/intranet/apps/dashboard/views.py b/intranet/apps/dashboard/views.py
index 1af23177d84..e8550d99383 100644
--- a/intranet/apps/dashboard/views.py
+++ b/intranet/apps/dashboard/views.py
@@ -19,7 +19,7 @@
 from ...utils.date import get_senior_graduation_date, get_senior_graduation_year
 from ...utils.helpers import get_ap_week_warning, get_fcps_emerg, get_warning_html
 from ..announcements.models import Announcement, AnnouncementRequest, WarningAnnouncement
-from ..eighth.models import EighthBlock, EighthScheduledActivity, EighthSignup
+from ..eighth.models import EighthBlock, EighthScheduledActivity, EighthSignup, EighthSponsor
 from ..enrichment.models import EnrichmentActivity
 from ..events.models import Event, TJStarUUIDMap
 from ..schedule.models import Day
@@ -553,6 +553,7 @@ def dashboard_view(request, show_widgets=True, show_expired=False, show_hidden_c
         "show_hidden_club": show_hidden_club,
         "show_expired": show_expired,
         "show_tjstar": settings.TJSTAR_BANNER_START_DATE <= now.date() <= settings.TJSTAR_DATE,
+        "user_sponsor_obj": EighthSponsor.objects.filter(user=request.user).first(),
     }
 
     user_hidden_announcements = Announcement.objects.hidden_announcements(user).values_list("id", flat=True)
diff --git a/intranet/apps/eighth/views/signup.py b/intranet/apps/eighth/views/signup.py
index ecb1a62b8a9..7b68adf8543 100644
--- a/intranet/apps/eighth/views/signup.py
+++ b/intranet/apps/eighth/views/signup.py
@@ -404,7 +404,10 @@ def subscribe_to_club(request, activity_id):
 def unsubscribe_from_club(request, activity_id):
     activity = get_object_or_404(EighthActivity, id=activity_id)
 
-    if request.user in activity.subscribers.all() and not activity.sponsors.filter(user=request.user).exists():
+    if activity.sponsors.filter(user=request.user).exists() or request.user in activity.club_sponsors.all():
+        raise http.Http404
+
+    if request.user in activity.subscribers.all():
         activity.subscribers.remove(request.user)
 
     return redirect(request.META.get("HTTP_REFERER", "/"))
diff --git a/intranet/templates/announcements/announcement.html b/intranet/templates/announcements/announcement.html
index c7fb3e7c676..b69b7141970 100644
--- a/intranet/templates/announcements/announcement.html
+++ b/intranet/templates/announcements/announcement.html
@@ -50,7 +50,7 @@ <h3>
         <div class="announcement-icon-wrapper">
             {% if announcement.is_club_announcement %}
                 {% if request.user in announcement.activity.subscribers.all %}
-                    {% if request.user.sponsor_obj not in announcement.activity.sponsors.all %}
+                    {% if user_sponsor_obj not in announcement.activity.sponsors.all and request.user not in announcement.activity.club_sponsors.all %}
                       <a class="button small-button unsubscribe-button"
                           id="unsubscribe-button"
                           href="{% url 'unsubscribe_from_club' announcement.activity.id %}"

From 7de2cb90da69e730c79d000ddf0a77e6218494ea Mon Sep 17 00:00:00 2001
From: JasonGrace2282 <aarush.deshpande@gmail.com>
Date: Sat, 5 Oct 2024 01:26:02 -0400
Subject: [PATCH 32/42] chore: fix pre-commit lints

---
 intranet/apps/auth/decorators.py               | 3 ---
 intranet/apps/eighth/forms/activities.py       | 4 ++--
 intranet/apps/eighth/forms/admin/activities.py | 6 +++---
 intranet/apps/eighth/models.py                 | 6 ++----
 intranet/apps/eighth/views/activities.py       | 4 ++--
 5 files changed, 9 insertions(+), 14 deletions(-)

diff --git a/intranet/apps/auth/decorators.py b/intranet/apps/auth/decorators.py
index c1005d5b3d4..c99db081cac 100644
--- a/intranet/apps/auth/decorators.py
+++ b/intranet/apps/auth/decorators.py
@@ -26,9 +26,6 @@ def in_admin_group(user):
 #: Restrict the wrapped view to eighth admins
 eighth_admin_required = admin_required("eighth")
 
-# Restrict the wrapped view to eighth sponsors
-eighth_sponsor_required = user_passes_test(lambda u: not u.is_anonymous and u.is_eighth_sponsor)
-
 #: Restrict the wrapped view to announcements admins
 announcements_admin_required = admin_required("announcements")
 
diff --git a/intranet/apps/eighth/forms/activities.py b/intranet/apps/eighth/forms/activities.py
index ecae85c14ca..b4fcf396981 100644
--- a/intranet/apps/eighth/forms/activities.py
+++ b/intranet/apps/eighth/forms/activities.py
@@ -15,9 +15,9 @@ def __init__(self, *args, sponsors=None, **kwargs):
         self.fields["subscriptions_enabled"].label = "Enable club announcements"
         self.fields["subscriptions_enabled"].help_text = "Allow students to subscribe to receive announcements for this activity through Ion."
         self.fields["club_sponsors"].label = "Teacher moderators"
-        
+
         sponsors_list = "; ".join([str(sponsor) for sponsor in sponsors]) if sponsors else "no sponsors"
-                
+
         self.fields["club_sponsors"].help_text = (
             f"Teacher moderators can post and manage this club's announcements. "
             f"These are in addition to the activity's eighth period sponsors ({sponsors_list})."
diff --git a/intranet/apps/eighth/forms/admin/activities.py b/intranet/apps/eighth/forms/admin/activities.py
index 68ec88b3084..c23ae83fa90 100644
--- a/intranet/apps/eighth/forms/admin/activities.py
+++ b/intranet/apps/eighth/forms/admin/activities.py
@@ -145,14 +145,14 @@ def __init__(self, *args, sponsors=None, **kwargs):
         self.fields["subscriptions_enabled"].label = "Enable club announcements"
         self.fields["subscriptions_enabled"].help_text = "Allow students to subscribe to receive announcements for this activity through Ion."
         self.fields["officers"].help_text = "Student officers can send club announcements to subscribers."
-             
+
         sponsors_list = "; ".join([str(sponsor) for sponsor in sponsors]) if sponsors else "no sponsors"
-                
+
         self.fields["club_sponsors"].help_text = (
             f"Teacher moderators can post and manage this club's announcements. "
             f"These are in addition to the activity's eighth period sponsors ({sponsors_list})."
         )
-        
+
         self.fields["club_sponsors"].label = "Teacher moderators"
         self.fields["subscribers"].help_text = "Students who subscribe to this activity will receive club announcements."
 
diff --git a/intranet/apps/eighth/models.py b/intranet/apps/eighth/models.py
index 2fa093dbad4..264aba5386e 100644
--- a/intranet/apps/eighth/models.py
+++ b/intranet/apps/eighth/models.py
@@ -82,9 +82,10 @@ class EighthSponsor(AbstractBaseEighthModel):
     show_full_name = models.BooleanField(default=False)
 
     history = HistoricalRecords()
-    
+
     def __str__(self):
         return self.name
+
     class Meta:
         unique_together = (("first_name", "last_name", "user", "online_attendance", "full_time", "department"),)
         ordering = ("last_name", "first_name")
@@ -108,9 +109,6 @@ def to_be_assigned(self) -> bool:
         """
         return any(x in self.name.lower() for x in ["to be assigned", "to be determined", "to be announced"])
 
-    def __str__(self):
-        return self.name
-
 
 class EighthRoom(AbstractBaseEighthModel):
     """Represents a room in which an eighth period activity can be held.
diff --git a/intranet/apps/eighth/views/activities.py b/intranet/apps/eighth/views/activities.py
index 6a002bf548e..697ce36c359 100644
--- a/intranet/apps/eighth/views/activities.py
+++ b/intranet/apps/eighth/views/activities.py
@@ -20,7 +20,7 @@
 from ....utils.date import get_date_range_this_year, get_senior_graduation_year
 from ....utils.helpers import is_entirely_digit
 from ....utils.serialization import safe_json
-from ...auth.decorators import deny_restricted, eighth_sponsor_required
+from ...auth.decorators import deny_restricted
 from ..forms.activities import ActivitySettingsForm
 from ..forms.admin.activities import ActivityMultiSelectForm
 from ..models import EighthActivity, EighthBlock, EighthScheduledActivity, EighthSignup, EighthSponsor
@@ -55,7 +55,7 @@ def activity_view(request, activity_id=None):
 @login_required
 def settings_view(request, activity_id=None):
     activity = get_object_or_404(EighthActivity, id=activity_id)
-      
+
     if not (EighthSponsor.objects.filter(user=request.user).exists() or request.user in activity.club_sponsors.all()):
         raise Http404
 

From fbab690f2ffcad016a9de62339d15e820fefa63c Mon Sep 17 00:00:00 2001
From: Alan Zhu <2025azhu@tjhsst.edu>
Date: Sat, 5 Oct 2024 01:29:54 -0400
Subject: [PATCH 33/42] fix: actually allow club announcements sponsors to
 modify

---
 intranet/apps/announcements/models.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/intranet/apps/announcements/models.py b/intranet/apps/announcements/models.py
index 390ed796086..814a95c62ac 100644
--- a/intranet/apps/announcements/models.py
+++ b/intranet/apps/announcements/models.py
@@ -155,7 +155,7 @@ def can_modify(self, user):
         return (
             user.is_announcements_admin
             or self.is_club_announcement
-            and (self.is_visible_submitter(user) or user.club_sponsor_for_set.filter(id=self.activity.id).exists())
+            and (user in self.activity.officers.all() or user in self.activity.sponsors.all() or user in self.activity.club_sponsors.all())
         )
 
     # False, not None. This can be None if no AnnouncementRequest exists for this Announcement,

From 51721c0b530c2013a691fd1e91a0c90a66d25b59 Mon Sep 17 00:00:00 2001
From: Alan Zhu <2025azhu@tjhsst.edu>
Date: Sat, 5 Oct 2024 01:43:42 -0400
Subject: [PATCH 34/42] fix: allow club officers to expire announcements

---
 intranet/apps/announcements/forms.py          | 10 ++---
 intranet/apps/announcements/views.py          | 41 +++++++++----------
 .../templates/announcements/club-request.html |  1 +
 intranet/templates/announcements/delete.html  |  9 ++++
 4 files changed, 34 insertions(+), 27 deletions(-)

diff --git a/intranet/apps/announcements/forms.py b/intranet/apps/announcements/forms.py
index 0a4f1d370d3..48a222e4749 100644
--- a/intranet/apps/announcements/forms.py
+++ b/intranet/apps/announcements/forms.py
@@ -22,7 +22,7 @@ class Meta:
         model = Announcement
         fields = ["title", "author", "content", "groups", "expiration_date", "notify_post", "notify_email_all", "update_added_date", "pinned"]
         help_texts = {
-            "expiration_date": "By default, announcements expire after two weeks. To change this, click in the box above.",
+            "expiration_date": "By default, announcements expire after two weeks. Choose the shortest time necessary.",
             "notify_post": "If this box is checked, students who have signed up for notifications will receive an email.",
             "notify_email_all": "This will send an email notification to all of the users who can see this post. This option does NOT take users' email notification preferences into account, so please use with care.",
             "update_added_date": "If this announcement has already been added, update the added date to now so that the announcement is pushed to the top. If this option is not selected, the announcement will stay in its current position.",
@@ -55,7 +55,7 @@ class Meta:
         model = Announcement
         fields = ["activity", "title", "content", "expiration_date"]
         help_texts = {
-            "expiration_date": "By default, announcements expire after two weeks. To change this, click in the box above.",
+            "expiration_date": "By default, announcements expire after two weeks. Choose the shortest time necessary.",
         }
 
 
@@ -71,7 +71,7 @@ class Meta:
         model = Announcement
         fields = ["title", "content", "expiration_date"]
         help_texts = {
-            "expiration_date": "By default, announcements expire after two weeks. To change this, click in the box above.",
+            "expiration_date": "By default, announcements expire after two weeks. Choose the shortest time necessary.",
         }
 
 
@@ -80,7 +80,7 @@ class AnnouncementEditForm(forms.ModelForm):
 
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
-        self.fields["expiration_date"].help_text = "By default, announcements expire after two weeks. To change this, click in the box above."
+        self.fields["expiration_date"].help_text = "By default, announcements expire after two weeks. Choose the shortest time necessary."
 
         self.fields["notify_post_resend"].help_text = "If this box is checked, students who have signed up for notifications will receive an email."
 
@@ -117,7 +117,7 @@ def __init__(self, *args, **kwargs):
             "Otherwise, your name will appear in this field automatically."
         )
         self.fields["content"].help_text = "The contents of the news post which will appear on Intranet."
-        self.fields["expiration_date"].help_text = "By default, announcements expire after two weeks. To change this, click in the box above."
+        self.fields["expiration_date"].help_text = "By default, announcements expire after two weeks. Choose the shortest time necessary."
         self.fields["notes"].help_text = (
             "Any information about this announcement you wish to share with the Intranet "
             "administrators and teachers selected above. If you want to restrict this posting "
diff --git a/intranet/apps/announcements/views.py b/intranet/apps/announcements/views.py
index 71659433455..a2754e27d17 100644
--- a/intranet/apps/announcements/views.py
+++ b/intranet/apps/announcements/views.py
@@ -426,7 +426,6 @@ def modify_announcement_view(request, announcement_id=None):
     return render(request, "announcements/add_modify.html", context)
 
 
-@announcements_admin_required
 @deny_restricted
 def delete_announcement_view(request, announcement_id):
     """Delete an announcement.
@@ -434,30 +433,28 @@ def delete_announcement_view(request, announcement_id):
     announcement_id: announcement id
 
     """
+    announcement = get_object_or_404(Announcement, id=announcement_id)
+
+    if not (request.user.is_announcements_admin or announcement.is_club_announcement and announcement.can_modify(request.user)):
+        messages.error(request, "You do not have permission to delete this announcement.")
+        return redirect("index")
+
     if request.method == "POST":
-        post_id = None
-        try:
-            post_id = request.POST["id"]
-        except AttributeError:
-            post_id = None
-        try:
-            a = Announcement.objects.get(id=post_id)
-            if request.POST.get("full_delete", False):
-                a.delete()
-                messages.success(request, "Successfully deleted announcement.")
-                logger.info("Admin %s deleted announcement: %s (%s)", request.user, a, a.id)
-            else:
-                a.expiration_date = timezone.localtime()
-                a.save()
-                messages.success(request, "Successfully expired announcement.")
-                logger.info("Admin %s expired announcement: %s (%s)", request.user, a, a.id)
-        except Announcement.DoesNotExist:
-            pass
+        if request.POST.get("full_delete", False) and request.user.is_announcements_admin:
+            announcement.delete()
+            messages.success(request, "Successfully deleted announcement.")
+            logger.info("Admin %s deleted announcement: %s (%s)", request.user, announcement, announcement.id)
+        else:
+            announcement.expiration_date = timezone.localtime()
+            announcement.save()
+            messages.success(request, "Successfully expired announcement.")
+            logger.info("%s expired announcement: %s (%s)", request.user, announcement, announcement.id)
 
+        if announcement.is_club_announcement:
+            return redirect("club_announcements")
         return redirect("index")
-    else:
-        announcement = get_object_or_404(Announcement, id=announcement_id)
-        return render(request, "announcements/delete.html", {"announcement": announcement})
+
+    return render(request, "announcements/delete.html", {"announcement": announcement})
 
 
 @login_required
diff --git a/intranet/templates/announcements/club-request.html b/intranet/templates/announcements/club-request.html
index 28379609c19..c70c82c6285 100644
--- a/intranet/templates/announcements/club-request.html
+++ b/intranet/templates/announcements/club-request.html
@@ -19,6 +19,7 @@
             {% if request.user.is_teacher %}
                 $("#id_teachers_requested")[0].selectize.setValue({{ request.user.id }});
             {% endif %}
+            $("#no-expire-btn").hide();
         });
     </script>
 {% endblock %}
diff --git a/intranet/templates/announcements/delete.html b/intranet/templates/announcements/delete.html
index 69be2a27bda..94203622e85 100644
--- a/intranet/templates/announcements/delete.html
+++ b/intranet/templates/announcements/delete.html
@@ -34,8 +34,15 @@
 
 {% block main %}
 <div class="announcements primary-content">
+    {% if request.user.is_announcements_admin %}
     <h3>Are you sure you want to delete this announcement?</h3>
     <p>Please <b>EXPIRE</b> this announcement instead of deleting it, unless the announcement was created by accident.</p>
+    {% else %}
+    <h3>Are you sure you want to expire this club announcement?</h3>
+    <p>Expiring an announcement will remove it from the dashboard, but it will still be accessible in the archive.
+        To request full deletion of a club announcement, email <a href="mailto:intranet@tjhsst.edu">intranet@tjhsst.edu</a>.</p>
+    {% endif %}
+    <br>
     <b>{{ announcement.title }}</b>
     <br>
     <br>
@@ -49,7 +56,9 @@ <h3>Are you sure you want to delete this announcement?</h3>
         <input type="submit" value="Expire">
         <br>
         <br>
+        {% if request.user.is_announcements_admin %}
         <a href="#" id="delete" class="button"><i class='fas fa-exclamation-triangle'></i> Full Delete</a> &nbsp;
+        {% endif %}
     </form>
 </div>
 <script>

From 2f65a620c6af5faae859071eb2e1d2ee177bd5e5 Mon Sep 17 00:00:00 2001
From: Alan Zhu <2025azhu@tjhsst.edu>
Date: Sat, 5 Oct 2024 01:44:06 -0400
Subject: [PATCH 35/42] feat: add announcement edit history

---
 intranet/apps/announcements/admin.py          |  5 +-
 ...ouncement_historicalwarningannouncement.py | 72 +++++++++++++++++++
 intranet/apps/announcements/models.py         |  5 ++
 3 files changed, 80 insertions(+), 2 deletions(-)
 create mode 100644 intranet/apps/announcements/migrations/0034_historicalannouncement_historicalwarningannouncement.py

diff --git a/intranet/apps/announcements/admin.py b/intranet/apps/announcements/admin.py
index a80e2268696..1232daaeaf6 100644
--- a/intranet/apps/announcements/admin.py
+++ b/intranet/apps/announcements/admin.py
@@ -1,9 +1,10 @@
 from django.contrib import admin
+from simple_history.admin import SimpleHistoryAdmin
 
 from .models import Announcement, WarningAnnouncement
 
 
-class AnnouncementAdmin(admin.ModelAdmin):
+class AnnouncementAdmin(SimpleHistoryAdmin):
     list_display = ("title", "user", "author", "activity", "added")
     list_filter = ("added", "updated", "activity")
     ordering = ("-added",)
@@ -11,7 +12,7 @@ class AnnouncementAdmin(admin.ModelAdmin):
     search_fields = ("title", "content", "user__first_name", "user__last_name", "user__username")
 
 
-class WarningAnnouncementAdmin(admin.ModelAdmin):
+class WarningAnnouncementAdmin(SimpleHistoryAdmin):
     list_display = ("title", "content", "active")
     list_filter = ("active",)
     search_fields = ("title", "content")
diff --git a/intranet/apps/announcements/migrations/0034_historicalannouncement_historicalwarningannouncement.py b/intranet/apps/announcements/migrations/0034_historicalannouncement_historicalwarningannouncement.py
new file mode 100644
index 00000000000..30a9005aae9
--- /dev/null
+++ b/intranet/apps/announcements/migrations/0034_historicalannouncement_historicalwarningannouncement.py
@@ -0,0 +1,72 @@
+# Generated by Django 3.2.25 on 2024-10-05 05:34
+
+import datetime
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+from django.utils.timezone import utc
+import simple_history.models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('eighth', '0070_eighthactivity_club_sponsors'),
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('announcements', '0033_announcement_activity'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='HistoricalWarningAnnouncement',
+            fields=[
+                ('id', models.IntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
+                ('title', models.CharField(max_length=127)),
+                ('content', models.TextField(help_text='Content of the warning. You can use HTML here.')),
+                ('type', models.CharField(choices=[('dashboard', 'Dashboard Warning (displays on dashboard)'), ('login', 'Login Warning (displays on login page)'), ('dashboard_login', 'Dashboard and Login Warning (displays on dashboard and login pages)'), ('global', 'Global Warning (displays on all pages)')], default='dashboard', max_length=127)),
+                ('active', models.BooleanField(default=True, help_text='Whether or not to show the warning.')),
+                ('added', models.DateTimeField(blank=True, editable=False)),
+                ('history_id', models.AutoField(primary_key=True, serialize=False)),
+                ('history_date', models.DateTimeField(db_index=True)),
+                ('history_change_reason', models.CharField(max_length=100, null=True)),
+                ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
+                ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
+            ],
+            options={
+                'verbose_name': 'historical warning announcement',
+                'verbose_name_plural': 'historical warning announcements',
+                'ordering': ('-history_date', '-history_id'),
+                'get_latest_by': ('history_date', 'history_id'),
+            },
+            bases=(simple_history.models.HistoricalChanges, models.Model),
+        ),
+        migrations.CreateModel(
+            name='HistoricalAnnouncement',
+            fields=[
+                ('id', models.IntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
+                ('title', models.CharField(max_length=127)),
+                ('content', models.TextField()),
+                ('author', models.CharField(blank=True, max_length=63)),
+                ('added', models.DateTimeField(blank=True, editable=False)),
+                ('updated', models.DateTimeField(blank=True, editable=False)),
+                ('expiration_date', models.DateTimeField(default=datetime.datetime(3000, 1, 1, 5, 0, tzinfo=utc))),
+                ('notify_post', models.BooleanField(default=True)),
+                ('notify_email_all', models.BooleanField(default=False)),
+                ('pinned', models.BooleanField(default=False)),
+                ('history_id', models.AutoField(primary_key=True, serialize=False)),
+                ('history_date', models.DateTimeField(db_index=True)),
+                ('history_change_reason', models.CharField(max_length=100, null=True)),
+                ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
+                ('activity', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='eighth.eighthactivity')),
+                ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
+                ('user', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)),
+            ],
+            options={
+                'verbose_name': 'historical announcement',
+                'verbose_name_plural': 'historical announcements',
+                'ordering': ('-history_date', '-history_id'),
+                'get_latest_by': ('history_date', 'history_id'),
+            },
+            bases=(simple_history.models.HistoricalChanges, models.Model),
+        ),
+    ]
diff --git a/intranet/apps/announcements/models.py b/intranet/apps/announcements/models.py
index 814a95c62ac..3a4cd45f155 100644
--- a/intranet/apps/announcements/models.py
+++ b/intranet/apps/announcements/models.py
@@ -6,6 +6,7 @@
 from django.db import models
 from django.db.models import Manager, Q
 from django.utils import timezone
+from simple_history.models import HistoricalRecords
 
 from ...utils.date import get_date_range_this_year, is_current_year
 from ...utils.deletion import set_historical_user
@@ -120,6 +121,8 @@ class Announcement(models.Model):
 
     pinned = models.BooleanField(default=False)
 
+    history = HistoricalRecords()
+
     def get_author(self) -> str:
         """Returns 'author' if it is set. Otherwise, returns the name of the user who created the announcement.
 
@@ -294,6 +297,8 @@ class WarningAnnouncement(models.Model):
     active = models.BooleanField(default=True, help_text="Whether or not to show the warning.")
     added = models.DateTimeField(auto_now_add=True)
 
+    history = HistoricalRecords()
+
     @property
     def show_on_dashboard(self):
         return self.type in ("dashboard", "dashboard_login")  # global is not included. It will show on all pages and this logic isn't needed.

From 19d9c25cb4f2ddce8e1029b4462eb3b64a0d1a99 Mon Sep 17 00:00:00 2001
From: JasonGrace2282 <aarush.deshpande@gmail.com>
Date: Sat, 5 Oct 2024 01:56:57 -0400
Subject: [PATCH 36/42] refactor: better errors and subscriber sticking

---
 intranet/apps/announcements/tests.py           | 12 ++++++++++++
 intranet/apps/eighth/views/admin/activities.py |  1 +
 intranet/apps/eighth/views/signup.py           |  3 ++-
 3 files changed, 15 insertions(+), 1 deletion(-)

diff --git a/intranet/apps/announcements/tests.py b/intranet/apps/announcements/tests.py
index f49d5871d05..64e81702c31 100644
--- a/intranet/apps/announcements/tests.py
+++ b/intranet/apps/announcements/tests.py
@@ -6,6 +6,7 @@
 
 from ...test.ion_test import IonTestCase
 from ...utils.date import get_senior_graduation_year
+from ..eighth.models import EighthActivity
 from ..users.models import Group
 from .models import Announcement, AnnouncementRequest
 
@@ -443,6 +444,17 @@ def test_hide_announcement_view(self):
         response = self.client.get(reverse("hide_announcement"))
         self.assertEqual(405, response.status_code)
 
+    def test_modify_club_announcement(self):
+        self.make_admin()
+        act = EighthActivity.objects.get_or_create(name="test")[0]
+        announce = Announcement.objects.get_or_create(title="test9", content="test9", activity=act)[0]
+        self.client.post(
+            reverse("modify_club_announcement", args=[announce.id]),
+            {"title": "hi", "content": "bye", "expiration_date": "3000-01-01"},
+        )
+        announce.refresh_from_db()
+        self.assertEqual(act, announce.activity)
+
 
 class ApiTest(IonTestCase):
     def test_api_announcements_list(self):
diff --git a/intranet/apps/eighth/views/admin/activities.py b/intranet/apps/eighth/views/admin/activities.py
index d1fe7a6cf25..704bbcf5e39 100644
--- a/intranet/apps/eighth/views/admin/activities.py
+++ b/intranet/apps/eighth/views/admin/activities.py
@@ -150,6 +150,7 @@ def edit_activity_view(request, activity_id):
 
                 activity = form.save()
                 activity.subscribers.add(*[sponsor.user for sponsor in form.cleaned_data["sponsors"]])
+                activity.subscribers.add(*form.cleaned_data["club_sponsors"], *form.cleaned_data["officers"])
                 activity.save()
 
             except forms.ValidationError as error:
diff --git a/intranet/apps/eighth/views/signup.py b/intranet/apps/eighth/views/signup.py
index 7b68adf8543..00e9cd35846 100644
--- a/intranet/apps/eighth/views/signup.py
+++ b/intranet/apps/eighth/views/signup.py
@@ -405,7 +405,8 @@ def unsubscribe_from_club(request, activity_id):
     activity = get_object_or_404(EighthActivity, id=activity_id)
 
     if activity.sponsors.filter(user=request.user).exists() or request.user in activity.club_sponsors.all():
-        raise http.Http404
+        messages.error(request, "You cannot unsubscribe from an activity you sponsor.")
+        return redirect("club_announcements")
 
     if request.user in activity.subscribers.all():
         activity.subscribers.remove(request.user)

From df3292611a8179f30783256b1fdf7c6e6eb772da Mon Sep 17 00:00:00 2001
From: JasonGrace2282 <aarush.deshpande@gmail.com>
Date: Sat, 5 Oct 2024 02:11:39 -0400
Subject: [PATCH 37/42] feat: improve pagination

---
 intranet/static/js/dashboard/announcements.js |  3 +++
 intranet/templates/dashboard/dashboard.html   | 15 +++++++++------
 2 files changed, 12 insertions(+), 6 deletions(-)

diff --git a/intranet/static/js/dashboard/announcements.js b/intranet/static/js/dashboard/announcements.js
index 3fb827865ea..27f31fe56a7 100644
--- a/intranet/static/js/dashboard/announcements.js
+++ b/intranet/static/js/dashboard/announcements.js
@@ -62,6 +62,9 @@ $(document).ready(function() {
         filterClubAnnouncements();
     });
 
+    if (flipToUnsubscribed) {
+      $(".unsubscribed-filter").click();
+    }
 });
 
 function updatePartiallyHidden() {
diff --git a/intranet/templates/dashboard/dashboard.html b/intranet/templates/dashboard/dashboard.html
index e73faf830c0..446ac8c4776 100644
--- a/intranet/templates/dashboard/dashboard.html
+++ b/intranet/templates/dashboard/dashboard.html
@@ -30,6 +30,9 @@
 {% block js %}
     {{ block.super }}
 
+    <script>
+        const flipToUnsubscribed = "{{ request.GET.flip_to }}" == "unsubscribed";
+    </script>
     <script src="{% static 'js/dashboard/eighth-widget.js' %}"></script>
     <script src="{% static 'js/schedule.js' %}"></script>
     <script src="{% static 'js/events.js' %}"></script>
@@ -285,7 +288,7 @@ <h3 class="club-announcements-header">
                     <div style="display:grid;grid-template-columns:1fr max-content 1fr">
                         <div style="text-align:center;grid-column-start:2">
                           <a {% if prev_page > 0 %}
-                            href="{% url view_announcements_url %}?{% query_transform request page=prev_page %}"
+                            href="{% url view_announcements_url %}?{% query_transform request page=prev_page flip_to="subscribed" %}"
                             {% else %}
                             disabled
                             {% endif %}
@@ -295,11 +298,11 @@ <h3 class="club-announcements-header">
                         {% for page in page_obj|page_list:items %}
                         <a class="{% if page %}button {% else %}ellipses{% endif %}"
                           style="{% if page == items.number %}background-image: linear-gradient(to bottom, #858585 0%, #5f5f5f 100%); color: white;{% endif %}"
-                          href="{% url view_announcements_url %}{% if page %}?{% query_transform request page=page %}{% else %}#{% endif %}">{{ page|default:"..." }}</a>
+                          href="{% url view_announcements_url %}{% if page %}?{% query_transform request page=page flip_to="subscribed" %}{% else %}#{% endif %}">{{ page|default:"..." }}</a>
                         {% endfor %}
 
                         <a {% if more_items %}
-                          href="{% url view_announcements_url %}?{% query_transform request page=next_page %}"
+                          href="{% url view_announcements_url %}?{% query_transform request page=next_page flip_to="subscribed" %}"
                           {% else %}
                           disabled
                           {% endif %}
@@ -322,7 +325,7 @@ <h3 class="club-announcements-header">
                     <div style="display:grid;grid-template-columns:1fr max-content 1fr">
                         <div style="text-align:center;grid-column-start:2">
                           <a {% if unsubscribed.prev_page > 0 %}
-                            href="{% url view_announcements_url %}?{% query_transform request unsubscribed_page=unsubscribed.prev_page %}"
+                            href="{% url view_announcements_url %}?{% query_transform request unsubscribed_page=unsubscribed.prev_page flip_to="unsubscribed" %}"
                             {% else %}
                             disabled
                             {% endif %}
@@ -332,11 +335,11 @@ <h3 class="club-announcements-header">
                         {% for page in unsubscribed.page_obj|page_list:unsubscribed.items %}
                         <a class="{% if page %}button {% else %}ellipses{% endif %}"
                           style="{% if page == unsubscribed.items.number %}background-image: linear-gradient(to bottom, #858585 0%, #5f5f5f 100%); color: white;{% endif %}"
-                          href="{% url view_announcements_url %}{% if page %}?{% query_transform request unsubscribed_page=page %}{% else %}#{% endif %}">{{ page|default:"..." }}</a>
+                          href="{% url view_announcements_url %}{% if page %}?{% query_transform request unsubscribed_page=page flip_to="unsubscribed" %}{% else %}#{% endif %}">{{ page|default:"..." }}</a>
                         {% endfor %}
 
                         <a {% if unsubscribed.more_items %}
-                          href="{% url view_announcements_url %}?{% query_transform request unsubscribed_page=unsubscribed.next_page %}"
+                          href="{% url view_announcements_url %}?{% query_transform request unsubscribed_page=unsubscribed.next_page flip_to="unsubscribed" %}"
                           {% else %}
                           disabled
                           {% endif %}

From f64049b4160c61bdf24240d9f5edeba42585eaf8 Mon Sep 17 00:00:00 2001
From: Alan Zhu <2025azhu@tjhsst.edu>
Date: Sat, 5 Oct 2024 02:15:34 -0400
Subject: [PATCH 38/42] feat: send club announcements emails

---
 intranet/apps/announcements/forms.py               |  6 +++++-
 intranet/apps/announcements/notifications.py       | 14 +++++++++++---
 intranet/apps/announcements/views.py               |  3 ++-
 .../announcements/emails/announcement_posted.html  |  2 +-
 .../announcements/emails/announcement_posted.txt   |  2 +-
 5 files changed, 20 insertions(+), 7 deletions(-)

diff --git a/intranet/apps/announcements/forms.py b/intranet/apps/announcements/forms.py
index 48a222e4749..67460d82ecb 100644
--- a/intranet/apps/announcements/forms.py
+++ b/intranet/apps/announcements/forms.py
@@ -44,16 +44,20 @@ def __init__(self, user, *args, **kwargs):
         else:
             self.fields["activity"].queryset = []
         self.fields["activity"].required = True
+        self.fields[
+            "notify_post"
+        ].help_text = "If this box is checked, students who have subscribed to your club's announcements will receive an email."
 
         if "instance" in kwargs:  # Don't allow changing the activity once the announcement has been created
             self.fields["activity"].widget.attrs["disabled"] = True
             self.fields["activity"].initial = kwargs["instance"].activity
 
     expiration_date = forms.DateTimeInput()
+    notify_post = forms.BooleanField(required=False, initial=True)
 
     class Meta:
         model = Announcement
-        fields = ["activity", "title", "content", "expiration_date"]
+        fields = ["activity", "title", "content", "expiration_date", "notify_post"]
         help_texts = {
             "expiration_date": "By default, announcements expire after two weeks. Choose the shortest time necessary.",
         }
diff --git a/intranet/apps/announcements/notifications.py b/intranet/apps/announcements/notifications.py
index 4b7c66cf4d8..05a8da436bf 100644
--- a/intranet/apps/announcements/notifications.py
+++ b/intranet/apps/announcements/notifications.py
@@ -122,8 +122,13 @@ def announcement_posted_email(request, obj, send_all=False):
             subject = f"Club Announcement for {obj.activity.name}: {obj.title}"
             users = (
                 get_user_model()
-                .objects.filter(user_type="student", graduation_year__gte=get_senior_graduation_year(), subscribed_to_set__contains=obj.activity)
-                .union(get_user_model().objects.filter(user_type__in=["teacher", "counselor"], subscribed_to_set__contains=obj.activity))
+                .objects.filter(
+                    user_type="student",
+                    graduation_year__gte=get_senior_graduation_year(),
+                    receive_news_emails=True,
+                    subscribed_activity_set=obj.activity,
+                )
+                .union(get_user_model().objects.filter(user_type__in=["teacher", "counselor"], subscribed_activity_set=obj.activity))
             )
 
         else:
@@ -152,7 +157,10 @@ def announcement_posted_email(request, obj, send_all=False):
         email_send_task.delay(
             "announcements/emails/announcement_posted.txt", "announcements/emails/announcement_posted.html", data, subject, emails, bcc=True
         )
-        messages.success(request, f"Sent email to {len(users_send)} users")
+        if request.user.is_announcements_admin:
+            messages.success(request, f"Sent email to {len(users_send)} users")
+        else:
+            messages.success(request, "Sent notification emails.")
     else:
         logger.info("Emailing announcements disabled")
 
diff --git a/intranet/apps/announcements/views.py b/intranet/apps/announcements/views.py
index a2754e27d17..4412ac0299b 100644
--- a/intranet/apps/announcements/views.py
+++ b/intranet/apps/announcements/views.py
@@ -157,9 +157,10 @@ def add_club_announcement_view(request):
             obj.user = request.user
             # SAFE HTML
             obj.content = safe_html(obj.content)
-
             obj.save()
 
+            announcement_posted_hook(request, obj)
+
             messages.success(request, "Successfully posted club announcement.")
             return redirect("club_announcements")
         else:
diff --git a/intranet/templates/announcements/emails/announcement_posted.html b/intranet/templates/announcements/emails/announcement_posted.html
index 2a78f79fd6e..f791864feef 100644
--- a/intranet/templates/announcements/emails/announcement_posted.html
+++ b/intranet/templates/announcements/emails/announcement_posted.html
@@ -6,7 +6,7 @@
 {{ announcement.content }}
 {% endautoescape %}
 
-<p>Posted by {{ announcement.get_author }} on {{ announcement.added|date:"l, F j, Y"}} at {{ announcement.added|date:"P"}} to {% if announcement.groups.count == 0 %}everyone{% else %}{{ announcement.groups.all|join:", " }}{% endif %}
+<p>Posted by {{ announcement.get_author }} on {{ announcement.added|date:"l, F j, Y"}} at {{ announcement.added|date:"P"}} to {% if announcement.is_club_announcement %}{{ announcement.activity.name }}{% else %}{% if announcement.groups.count == 0 %}everyone{% else %}{{ announcement.groups.all|join:", " }}{% endif %}{% endif %}
 </p>
 
 <p><a href="{{ info_link }}">View this announcement on Intranet</a></p>
diff --git a/intranet/templates/announcements/emails/announcement_posted.txt b/intranet/templates/announcements/emails/announcement_posted.txt
index 6591b80bf61..498e81ec965 100644
--- a/intranet/templates/announcements/emails/announcement_posted.txt
+++ b/intranet/templates/announcements/emails/announcement_posted.txt
@@ -2,7 +2,7 @@
 {{ announcement.content }}
 {% endautoescape %}
 
-Posted by {{ announcement.get_author }} on {{ announcement.added|date:"l, F j, Y"}} at {{ announcement.added|date:"P"}} to {% if announcement.groups.count == 0 %}everyone{% else %}{{ announcement.groups.all|join:", " }}{% endif %}
+Posted by {{ announcement.get_author }} on {{ announcement.added|date:"l, F j, Y"}} at {{ announcement.added|date:"P"}} to {% if announcement.is_club_announcement %}{{ announcement.activity.name }}{% else %}{% if announcement.groups.count == 0 %}everyone{% else %}{{ announcement.groups.all|join:", " }}{% endif %}{% endif %}
 
 View this announcement on Intranet: {{ info_link }}
 

From 3efd6f61a8d758430bf839c0ca3d7a31dd19118f Mon Sep 17 00:00:00 2001
From: Alan Zhu <2025azhu@tjhsst.edu>
Date: Sat, 5 Oct 2024 14:21:52 -0400
Subject: [PATCH 39/42] fix: allow both sponsors and moderators to edit club
 settings

---
 intranet/templates/eighth/activity.html          | 2 +-
 intranet/templates/eighth/activity_settings.html | 2 +-
 intranet/templates/eighth/take_attendance.html   | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/intranet/templates/eighth/activity.html b/intranet/templates/eighth/activity.html
index 37b73f407c3..d3d40214c8b 100644
--- a/intranet/templates/eighth/activity.html
+++ b/intranet/templates/eighth/activity.html
@@ -78,7 +78,7 @@
         {% endif %}
     {% endif %}
 
-    {% if request.user.sponsor_obj and request.user.sponsor_obj in scheduled_activity.activity.sponsors.all or request.user in activity.club_sponsors.all %}
+    {% if request.user.sponsor_obj and request.user.sponsor_obj in activity.sponsors.all or request.user in activity.club_sponsors.all %}
         <a class="button print-hide" href="{% url 'eighth_activity_settings' activity.id %}">
             <i class="fa fa-users"></i>
             Club Announcement Settings
diff --git a/intranet/templates/eighth/activity_settings.html b/intranet/templates/eighth/activity_settings.html
index 571e7ea1f84..fb1cbd31266 100644
--- a/intranet/templates/eighth/activity_settings.html
+++ b/intranet/templates/eighth/activity_settings.html
@@ -40,7 +40,7 @@
 <div class="eighth primary-content">
     <a class="button" onclick="javascript:history.back()">
         <i class="fa fa-arrow-left"></i>
-        Back to Attendance
+        Back
     </a>
     <h2>Club Announcements Settings for {{ activity }}</h2>
     <p>
diff --git a/intranet/templates/eighth/take_attendance.html b/intranet/templates/eighth/take_attendance.html
index 14efc00ddeb..63f1645c02b 100644
--- a/intranet/templates/eighth/take_attendance.html
+++ b/intranet/templates/eighth/take_attendance.html
@@ -497,7 +497,7 @@ <h3>Passes</h3>
                 &nbsp; &nbsp;
                 <a class="button print-hide" href="{% url 'eighth_email_students' scheduled_activity.id %}">Email Students</a>
                 {% endif %}
-                {% if request.user.sponsor_obj and request.user.sponsor_obj in scheduled_activity.activity.sponsors.all or request.user in activity.club_sponsors.all %}
+                {% if request.user.sponsor_obj and request.user.sponsor_obj in scheduled_activity.activity.sponsors.all or request.user in scheduled_activity.activity.club_sponsors.all %}
                     <br>
                     <br>
                     <a class="button print-hide" href="{% url 'eighth_activity_settings' scheduled_activity.activity.id %}">

From 81fb8561be8da96429f5e9a397d629d3b772bba0 Mon Sep 17 00:00:00 2001
From: Alan Zhu <2025azhu@tjhsst.edu>
Date: Sat, 5 Oct 2024 14:22:15 -0400
Subject: [PATCH 40/42] fix: get expiration date to show correctly on edit

---
 intranet/static/js/announcement.form.js       | 10 +++++++---
 intranet/templates/announcements/request.html |  2 +-
 2 files changed, 8 insertions(+), 4 deletions(-)

diff --git a/intranet/static/js/announcement.form.js b/intranet/static/js/announcement.form.js
index 69e668caf14..98c90f79688 100644
--- a/intranet/static/js/announcement.form.js
+++ b/intranet/static/js/announcement.form.js
@@ -71,7 +71,8 @@ $(function() {
             $(".exp-header").css("display", "none");
         for (var i = 0; i < dates.length; i++) {
             var use_date = dates[i].end ? dates[i].end.date() : dates[i].start.date();
-            $(".exp-list").append(`<li><a class='exp-suggest-item' data-date='${use_date}'>"${dates[i].text}" - ${use_date}</a></li>`);
+            var display_date = use_date.toDateString();
+            $(".exp-list").append(`<li><a class='exp-suggest-item' data-date='${use_date}'>"${dates[i].text}" - ${display_date}</a></li>`);
         }
     });
 
@@ -100,13 +101,16 @@ $(function() {
             $(".exp-header").css("display", "none");
         for (var i = 0; i < dates.length; i++) {
             var use_date = dates[i].end ? dates[i].end.date() : dates[i].start.date();
-            $(".exp-list").append(`<li><a class='exp-suggest-item' data-date='${use_date}'>"${dates[i].text}" - ${use_date}</a></li>`);
+            var display_date = use_date.toDateString();
+            $(".exp-list").append(`<li><a class='exp-suggest-item' data-date='${use_date}'>"${dates[i].text}" - ${display_date}</a></li>`);
         }
     });
 
     var exp = $("#id_expiration_date");
 
-    dateReset(exp);
+    if (exp.val() === "" || exp.val() === "3000-01-01 00:00:00") {
+        dateReset(exp);
+    }
 
     $(".helptext", exp.parent()).before("<h5 style='display: none' class='exp-header'><b>Suggested Expiration Dates</b></h4><ul class='exp-list'></ul>");
     $(".helptext", exp.parent()).before("<span class='exp-buttons'>" +
diff --git a/intranet/templates/announcements/request.html b/intranet/templates/announcements/request.html
index fb6c9dcc4b5..d3c8634b397 100644
--- a/intranet/templates/announcements/request.html
+++ b/intranet/templates/announcements/request.html
@@ -61,7 +61,7 @@ <h2>
     <ol>
         <li>Use correct English grammar, punctuation, and spelling.</li>
         <li>Use formal language and tone; avoid slang, all caps, texting-style abbreviations, excessive use of bolding, underlining, emojis, etc.</li>
-        <li>Keep posts short and concise. Viewable dashboard space is limited on Ion; be considerate of other announcements. Avoid unecessary linebreaks or whitespace.</li>
+        <li>Keep posts short and concise. Viewable dashboard space is limited on Ion; be considerate of other announcements. Avoid unnecessary linebreaks or whitespace.</li>
         <li>Do not submit repeat announcements; if an announcement about your topic has already been posted, do not request another.</li>
         <li>See detailed guidelines <a target="_blank" href="https://guides.tjhsst.edu/ion/ion-announcement-guidelines">here</a>.</li>
     </ol>

From a2f35f361280cb36d8c39100a77fc2dfd85c4426 Mon Sep 17 00:00:00 2001
From: JasonGrace2282 <aarush.deshpande@gmail.com>
Date: Sat, 5 Oct 2024 14:34:25 -0400
Subject: [PATCH 41/42] chore: resolve some linting errors

---
 intranet/apps/announcements/forms.py          | 10 ++-
 intranet/apps/eighth/forms/activities.py      |  2 -
 intranet/apps/eighth/views/activities.py      |  2 -
 intranet/static/css/dark/dashboard.scss       |  3 +-
 intranet/static/css/dashboard.scss            |  3 +
 intranet/static/js/announcement.form.js       | 80 +++++++++----------
 intranet/static/js/dashboard/announcements.js |  3 +-
 intranet/templates/dashboard/dashboard.html   |  3 -
 8 files changed, 55 insertions(+), 51 deletions(-)

diff --git a/intranet/apps/announcements/forms.py b/intranet/apps/announcements/forms.py
index 67460d82ecb..43684b8b1fc 100644
--- a/intranet/apps/announcements/forms.py
+++ b/intranet/apps/announcements/forms.py
@@ -24,8 +24,14 @@ class Meta:
         help_texts = {
             "expiration_date": "By default, announcements expire after two weeks. Choose the shortest time necessary.",
             "notify_post": "If this box is checked, students who have signed up for notifications will receive an email.",
-            "notify_email_all": "This will send an email notification to all of the users who can see this post. This option does NOT take users' email notification preferences into account, so please use with care.",
-            "update_added_date": "If this announcement has already been added, update the added date to now so that the announcement is pushed to the top. If this option is not selected, the announcement will stay in its current position.",
+            "notify_email_all": (
+                "This will send an email notification to all of the users who can see this post. "
+                "This option does NOT take users' email notification preferences into account, so please use with care."
+            ),
+            "update_added_date": (
+                "If this announcement has already been added, update the added date to now so that the announcement is pushed to the top. "
+                "If this option is not selected, the announcement will stay in its current position."
+            ),
         }
 
 
diff --git a/intranet/apps/eighth/forms/activities.py b/intranet/apps/eighth/forms/activities.py
index b4fcf396981..9a9be1aeb97 100644
--- a/intranet/apps/eighth/forms/activities.py
+++ b/intranet/apps/eighth/forms/activities.py
@@ -1,5 +1,3 @@
-from typing import List  # noqa
-
 from django import forms
 from django.contrib.auth import get_user_model
 
diff --git a/intranet/apps/eighth/views/activities.py b/intranet/apps/eighth/views/activities.py
index 697ce36c359..32017a3d84c 100644
--- a/intranet/apps/eighth/views/activities.py
+++ b/intranet/apps/eighth/views/activities.py
@@ -59,8 +59,6 @@ def settings_view(request, activity_id=None):
     if not (EighthSponsor.objects.filter(user=request.user).exists() or request.user in activity.club_sponsors.all()):
         raise Http404
 
-    print(activity.sponsors.all())
-
     if request.method == "POST":
         form = ActivitySettingsForm(request.POST, instance=activity, sponsors=activity.sponsors.all())
         if form.is_valid():
diff --git a/intranet/static/css/dark/dashboard.scss b/intranet/static/css/dark/dashboard.scss
index c6d01947b44..f63c2dd6486 100644
--- a/intranet/static/css/dark/dashboard.scss
+++ b/intranet/static/css/dark/dashboard.scss
@@ -54,8 +54,9 @@ a.club-announcement-meta-link:hover {
 
 .announcements {
     .announcement-banner {
-        background-color: #000000;
+        background-color: #000;
     }
+
     .announcement-link {
         color: #a3a3a3;
     }
diff --git a/intranet/static/css/dashboard.scss b/intranet/static/css/dashboard.scss
index 089eec6917d..0a0c7a98a09 100644
--- a/intranet/static/css/dashboard.scss
+++ b/intranet/static/css/dashboard.scss
@@ -73,12 +73,15 @@
   &::-webkit-scrollbar {
     width: 7px;
   }
+
   &::-webkit-scrollbar-track {
     background: #d6d6d6;
   }
+
   &::-webkit-scrollbar-thumb {
     background: #888;
   }
+
   &::-webkit-scrollbar-thumb:hover {
     background: #555;
   }
diff --git a/intranet/static/js/announcement.form.js b/intranet/static/js/announcement.form.js
index 98c90f79688..0003dbb1ee5 100644
--- a/intranet/static/js/announcement.form.js
+++ b/intranet/static/js/announcement.form.js
@@ -1,3 +1,24 @@
+function zero(v) {
+    return v < 10 ? "0" + v : v;
+}
+
+function dateFormat(date) {
+    return (date.getFullYear() + "-" +
+        zero(date.getMonth() + 1) + "-" +
+        zero(date.getDate()) + " 23:59:59");
+}
+
+function dateReset(exp) {
+    var date = new Date();
+    date.setDate(date.getDate() + 14);
+    exp.val(dateFormat(date));
+}
+
+function date3000(exp) {
+    var date = new Date("3000-01-01 00:00:00");
+    exp.val(dateFormat(date));
+}
+
 /* global $ */
 $(function() {
     $("select#id_groups").selectize({
@@ -51,15 +72,15 @@ $(function() {
         var text = editor.getData();
         dates = chrono.parse(text)
             .sort(function (a, b) {
-                var a_date = a.end ? a.end.date() : a.start.date();
-                var b_date = b.end ? b.end.date() : b.start.date();
-                return b_date.getTime() - a_date.getTime();
+                var aDate = a.end ? a.end.date() : a.start.date();
+                var bDate = b.end ? b.end.date() : b.start.date();
+                return bDate.getTime() - aDate.getTime();
             })
             .filter(function (val, ind, ary) {
                 if (ind) {
-                    var a_date = val.end ? val.end.date() : val.start.date();
-                    var b_date = ary[ind - 1].end ? ary[ind - 1].end.date() : ary[ind - 1].start.date();
-                    return !ind || a_date.getTime() != b_date.getTime();
+                    var aDate = val.end ? val.end.date() : val.start.date();
+                    var bDate = ary[ind - 1].end ? ary[ind - 1].end.date() : ary[ind - 1].start.date();
+                    return !ind || aDate.getTime() != bDate.getTime();
                 } else {
                     return true;
                 }
@@ -70,9 +91,9 @@ $(function() {
         else
             $(".exp-header").css("display", "none");
         for (var i = 0; i < dates.length; i++) {
-            var use_date = dates[i].end ? dates[i].end.date() : dates[i].start.date();
-            var display_date = use_date.toDateString();
-            $(".exp-list").append(`<li><a class='exp-suggest-item' data-date='${use_date}'>"${dates[i].text}" - ${display_date}</a></li>`);
+            var useDate = dates[i].end ? dates[i].end.date() : dates[i].start.date();
+            var displayDate = useDate.toDateString();
+            $(".exp-list").append(`<li><a class='exp-suggest-item' data-date='${useDate}'>"${dates[i].text}" - ${displayDate}</a></li>`);
         }
     });
 
@@ -81,15 +102,15 @@ $(function() {
         var text = editor.getData();
         dates = chrono.parse(text)
             .sort(function (a, b) {
-                var a_date = a.end ? a.end.date() : a.start.date();
-                var b_date = b.end ? b.end.date() : b.start.date();
-                return b_date.getTime() - a_date.getTime();
+                var aDate = a.end ? a.end.date() : a.start.date();
+                var bDate = b.end ? b.end.date() : b.start.date();
+                return bDate.getTime() - aDate.getTime();
             })
             .filter(function (val, ind, ary) {
                 if (ind) {
-                    var a_date = val.end ? val.end.date() : val.start.date();
-                    var b_date = ary[ind - 1].end ? ary[ind - 1].end.date() : ary[ind - 1].start.date();
-                    return !ind || a_date.getTime() != b_date.getTime();
+                    var aDate = val.end ? val.end.date() : val.start.date();
+                    var bDate = ary[ind - 1].end ? ary[ind - 1].end.date() : ary[ind - 1].start.date();
+                    return !ind || aDate.getTime() != bDate.getTime();
                 } else {
                     return true;
                 }
@@ -100,9 +121,9 @@ $(function() {
         else
             $(".exp-header").css("display", "none");
         for (var i = 0; i < dates.length; i++) {
-            var use_date = dates[i].end ? dates[i].end.date() : dates[i].start.date();
-            var display_date = use_date.toDateString();
-            $(".exp-list").append(`<li><a class='exp-suggest-item' data-date='${use_date}'>"${dates[i].text}" - ${display_date}</a></li>`);
+            var useDate = dates[i].end ? dates[i].end.date() : dates[i].start.date();
+            var displayDate = useDate.toDateString();
+            $(".exp-list").append(`<li><a class='exp-suggest-item' data-date='${useDate}'>"${dates[i].text}" - ${displayDate}</a></li>`);
         }
     });
 
@@ -120,7 +141,7 @@ $(function() {
 
     $(".exp-list").on("click", "a", function () {
         exp.val(dateFormat(new Date($(this).data("date"))));
-    })
+    });
 
     $("#date-reset-btn").click(function () {
         dateReset(exp);
@@ -130,24 +151,3 @@ $(function() {
         date3000(exp);
     });
 });
-
-function dateReset(exp) {
-    var date = new Date();
-    date.setDate(date.getDate() + 14);
-    exp.val(dateFormat(date));
-}
-
-function date3000(exp) {
-    var date = new Date("3000-01-01 00:00:00");
-    exp.val(dateFormat(date));
-}
-
-function dateFormat(date) {
-    return (date.getFullYear() + "-" +
-        zero(date.getMonth() + 1) + "-" +
-        zero(date.getDate()) + " 23:59:59");
-}
-
-function zero(v) {
-    return v < 10 ? "0" + v : v;
-}
\ No newline at end of file
diff --git a/intranet/static/js/dashboard/announcements.js b/intranet/static/js/dashboard/announcements.js
index 27f31fe56a7..854a309268f 100644
--- a/intranet/static/js/dashboard/announcements.js
+++ b/intranet/static/js/dashboard/announcements.js
@@ -62,7 +62,8 @@ $(document).ready(function() {
         filterClubAnnouncements();
     });
 
-    if (flipToUnsubscribed) {
+    const params = new URLSearchParams(window.location.search);
+    if (params.get("flip_to") == "unsubscribed") {
       $(".unsubscribed-filter").click();
     }
 });
diff --git a/intranet/templates/dashboard/dashboard.html b/intranet/templates/dashboard/dashboard.html
index 446ac8c4776..5f34b45b4a2 100644
--- a/intranet/templates/dashboard/dashboard.html
+++ b/intranet/templates/dashboard/dashboard.html
@@ -30,9 +30,6 @@
 {% block js %}
     {{ block.super }}
 
-    <script>
-        const flipToUnsubscribed = "{{ request.GET.flip_to }}" == "unsubscribed";
-    </script>
     <script src="{% static 'js/dashboard/eighth-widget.js' %}"></script>
     <script src="{% static 'js/schedule.js' %}"></script>
     <script src="{% static 'js/events.js' %}"></script>

From bd2828e451ae84aaeab1ed19bff5a661c81c845c Mon Sep 17 00:00:00 2001
From: JasonGrace2282 <aarush.deshpande@gmail.com>
Date: Sat, 5 Oct 2024 21:32:17 -0400
Subject: [PATCH 42/42] fix: prevent jitterclicking causing invalid results

---
 intranet/static/js/dashboard/announcements.js | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/intranet/static/js/dashboard/announcements.js b/intranet/static/js/dashboard/announcements.js
index 854a309268f..25c34111952 100644
--- a/intranet/static/js/dashboard/announcements.js
+++ b/intranet/static/js/dashboard/announcements.js
@@ -154,7 +154,11 @@ function announcementToggle() {
                 announcement.remove();
                 const numAnnouncementsSpan = $(".num-club-announcements");
                 const numAnnouncements = numAnnouncementsSpan.text().match(/\d+/);
-                numAnnouncementsSpan.text(numAnnouncements - 1);
+                // 15 is the cap, and prevent clicking on the button too fast
+                if(numAnnouncements != 15 && !announcement.hasClass("announcement-read")) {
+                  numAnnouncementsSpan.text(numAnnouncements - 1);
+                  announcement.addClass("announcement-read");
+                }
                 $(".club-announcements:has(.club-announcements-content:not(:has(.announcement)))").slideUp(350);
             }, 450);
         } else {