diff --git a/intranet/apps/announcements/admin.py b/intranet/apps/announcements/admin.py index e09ceeb8179..771ae454563 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",) diff --git a/intranet/apps/announcements/forms.py b/intranet/apps/announcements/forms.py index 476de634133..54249ee04fd 100644 --- a/intranet/apps/announcements/forms.py +++ b/intranet/apps/announcements/forms.py @@ -4,7 +4,11 @@ from ..users.forms import SortedTeacherMultipleChoiceField from .models import Announcement, AnnouncementRequest +from ..eighth.models import EighthActivity +import logging + +logger = logging.getLogger(__name__) class AnnouncementForm(forms.ModelForm): """A form for generating an announcement.""" @@ -29,17 +33,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 6fc69786bcf..c1d33379430 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 @@ -151,6 +150,11 @@ 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. @@ -165,13 +169,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 22f11b55bd8..3292c289fba 100644 --- a/intranet/apps/announcements/notifications.py +++ b/intranet/apps/announcements/notifications.py @@ -120,14 +120,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\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\d+)$", views.approve_announcement_view, name="approve_announcement"), diff --git a/intranet/apps/announcements/views.py b/intranet/apps/announcements/views.py index ebdd357835f..2bada552951 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,46 @@ 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 @@ -355,7 +389,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 9ac6f16fee8..2d3f91964f6 100644 --- a/intranet/apps/dashboard/views.py +++ b/intranet/apps/dashboard/views.py @@ -239,11 +239,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() @@ -255,6 +255,8 @@ def get_announcements_list(request, context): 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") + items = sorted(chain(announcements, events), key=lambda item: (item.pinned, item.added)) items.reverse() @@ -266,7 +268,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) @@ -277,12 +280,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 d81a7744b19..cda5d81de3b 100644 --- a/intranet/apps/eighth/forms/admin/activities.py +++ b/intranet/apps/eighth/forms/admin/activities.py @@ -135,9 +135,16 @@ 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 = set( @@ -153,6 +160,15 @@ def __init__(self, *args, **kwargs): ] ) + self.club_announcements_fields = set( + [ + "subscriptions_enabled", + "club_sponsors", + "officers", + "subscribers", + ] + ) + class Meta: model = EighthActivity fields = [ @@ -182,6 +198,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 294b8bab66f..33e4f62fca0 100644 --- a/intranet/apps/eighth/models.py +++ b/intranet/apps/eighth/models.py @@ -75,7 +75,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) @@ -243,7 +243,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) @@ -272,15 +271,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() @@ -419,6 +422,7 @@ def is_popular(self) -> bool: """ return self.frequent_users.count() > (settings.SIMILAR_THRESHOLD * 2) + class Meta: verbose_name_plural = "eighth activities" diff --git a/intranet/apps/eighth/views/signup.py b/intranet/apps/eighth/views/signup.py index 6f565ab5e96..98bde97c284 100644 --- a/intranet/apps/eighth/views/signup.py +++ b/intranet/apps/eighth/views/signup.py @@ -394,7 +394,10 @@ 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", "/")) diff --git a/intranet/apps/users/models.py b/intranet/apps/users/models.py index b725dd99556..de2597f0fab 100644 --- a/intranet/apps/users/models.py +++ b/intranet/apps/users/models.py @@ -929,7 +929,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: @@ -937,6 +937,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): @@ -1270,7 +1276,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/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 @@ {% endblock %} -
+