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 b8d445aecea..6fc69786bcf 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 e2e61ecabf9..22f11b55bd8 100644 --- a/intranet/apps/announcements/notifications.py +++ b/intranet/apps/announcements/notifications.py @@ -10,6 +10,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 ...utils.date import get_senior_graduation_year @@ -119,6 +120,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\d+)$", views.approve_announcement_view, name="approve_announcement"), diff --git a/intranet/apps/announcements/views.py b/intranet/apps/announcements/views.py index 105ceb89c69..a6268bbe956 100644 --- a/intranet/apps/announcements/views.py +++ b/intranet/apps/announcements/views.py @@ -13,10 +13,15 @@ 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, 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__) @@ -35,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. @@ -118,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 b7b6f0101b8..f75c8ad2543 100644 --- a/intranet/apps/dashboard/views.py +++ b/intranet/apps/dashboard/views.py @@ -261,7 +261,33 @@ def get_announcements_list(request, context): 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_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) + + return visible, hidden, unsubscribed + + +def paginate_announcements_list(request, context, items, visible_club_items, more_club_items): """ ***TODO*** Migrate to django Paginator (see lostitems) @@ -287,7 +313,19 @@ def paginate_announcements_list(request, context, items): else: items = items_sorted - context.update({"items": items, "start_num": start_num, "end_num": end_num, "prev_page": prev_page, "more_items": more_items}) + club_items = visible_club_items[:display_num] + + context.update( + { + "club_items": club_items, + "more_club_items": more_club_items, + "items": items, + "start_num": start_num, + "end_num": end_num, + "prev_page": prev_page, + "more_items": more_items, + } + ) return context, items @@ -380,7 +418,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 @@ -429,6 +467,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 + # Include show_all postfix on next/prev links paginate_link_suffix = "&show_all=1" if show_all else "" is_index_page = request.path_info in ["/", ""] @@ -440,19 +481,27 @@ 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, "paginate_link_suffix": paginate_link_suffix, "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) + 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) + else: + # Club announcements only + context, items = paginate_announcements_list(request, context, club_items, [], []) if ignore_dashboard_types is None: ignore_dashboard_types = [] @@ -483,6 +532,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 93e7684cf20..294b8bab66f 100644 --- a/intranet/apps/eighth/models.py +++ b/intranet/apps/eighth/models.py @@ -186,6 +186,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() @@ -232,6 +233,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. """ @@ -241,6 +243,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) @@ -275,6 +278,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 d20a45e3d30..1c354ad87e2 100644 --- a/intranet/apps/eighth/serializers.py +++ b/intranet/apps/eighth/serializers.py @@ -93,7 +93,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: @@ -128,6 +135,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, @@ -162,12 +171,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): @@ -180,9 +203,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) @@ -203,7 +228,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 1035f215a5a..bf226b07b3a 100644 --- a/intranet/apps/eighth/urls.py +++ b/intranet/apps/eighth/urls.py @@ -15,6 +15,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\d+)$", signup.subscribe_to_club, name="subscribe_to_club"), + re_path(r"^/signup/unsubscribe/(?P\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\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 4671ce6efb4..6f565ab5e96 100644 --- a/intranet/apps/eighth/views/signup.py +++ b/intranet/apps/eighth/views/signup.py @@ -389,6 +389,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 1817311b47e..1e78b08edfa 100644 --- a/intranet/apps/users/models.py +++ b/intranet/apps/users/models.py @@ -918,6 +918,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 @@ -1250,7 +1260,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 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 0a4f5809ed8..0e319b8dfd7 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; @@ -116,7 +172,7 @@ float: right; display: none; - .announcement:hover & { + .announcement:not(.club-announcements):hover & { display: block; } @@ -131,6 +187,57 @@ } } +.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; + 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/static/js/dashboard/announcements.js b/intranet/static/js/dashboard/announcements.js index b22f2e26db2..7abe9b79e00 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); + } } }; @@ -117,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 43c90672608..f9a12f578eb 100644 --- a/intranet/templates/announcements/announcement.html +++ b/intranet/templates/announcements/announcement.html @@ -7,7 +7,7 @@ {% endblock %} -
+

{% if show_icon and not announcement.pinned %} @@ -25,7 +25,21 @@

{{ announcement.title }} {% endif %} -
+
+ {% if announcement.is_club_announcement %} + {% if request.user in announcement.activity.subscribers.all %} + + + Unsubscribe + + {% else %} + + {% endif %} + {% endif %} + {% if hide_announcements %} {% if announcement.id in user_hidden_announcements %} @@ -48,17 +62,21 @@

{% endif %} -

+

@@ -180,9 +198,34 @@

{% endif %} + {% if club_items %} +
+

+   + You have {{ club_items|length }} new club announcement{{ club_items|length|pluralize }} +

+
+ {% 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 %} +
+
+ {% endif %} + {% if show_near_graduation_message %} {% include "dashboard/senior_forwarding.html" %} {% endif %} + + {% if view_announcements_url == "club_announcements" %} +
+
Your Subscriptions
+
Other Club Announcements
+
+ {% endif %} {% for item in items %} {% if item.dashboard_type in ignore_dashboard_types %} @@ -206,7 +249,7 @@

{% endfor %} {% if not request.user.is_restricted %} - {% if start_num == 0 and view_announcements_url != 'announcements_archive' %} + {% if start_num == 0 and view_announcements_url != "announcements_archive" and view_announcements_url != "club_announcements" %} View Archive {% endif %} diff --git a/intranet/templates/eighth/signup.html b/intranet/templates/eighth/signup.html index 85a39ab42ec..94b8f72392f 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 @@

<% } %> <% } %> <%}%> +
+ <% if (subscriptions_enabled || true) { %> + + <% if (subscribed_to) { %> + + + Unsubscribe + + <% } else { %> + + + Subscribe + + <% } %> + <% } %> <%}%>