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..8865925eb8e 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"^/request/club$", views.request_club_announcement_view, name="request_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..a3d4b71d19f 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,27 @@ def request_announcement_view(request): return render(request, "announcements/request.html", {"form": form, "action": "add"}) +def request_club_announcement_view(request): + """The request announcement page.""" + 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..18207293034 100644 --- a/intranet/apps/dashboard/views.py +++ b/intranet/apps/dashboard/views.py @@ -261,7 +261,31 @@ 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_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): """ ***TODO*** Migrate to django Paginator (see lostitems) @@ -287,7 +311,24 @@ 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] + + if hidden_club_items: + more_club_items = True + else: + more_club_items = False + + 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 +421,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 +470,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 +484,26 @@ 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) + # 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 = [] @@ -483,6 +534,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/views/signup.py b/intranet/apps/eighth/views/signup.py index 4671ce6efb4..8017b8276ca 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("eighth_signup_view") + + +def unsubscribe_to_club(request, activity_id): + activity = get_object_or_404(EighthActivity, id=activity_id) + if request.user in activity.subscribers: + activity.subscribers.remove(request.user) + + return redirect("eighth_signup_view") + + @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/dashboard.scss b/intranet/static/css/dashboard.scss index 0a4f5809ed8..106d5752368 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; 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..1884668ddd8 100644 --- a/intranet/templates/announcements/announcement.html +++ b/intranet/templates/announcements/announcement.html @@ -53,12 +53,16 @@

{% 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 %} + {% if more_club_items and view_announcements_url != "club_announcements" %} + + + Show All Club Announcements + + {% endif %} +
+
+ {% endif %} + {% if show_near_graduation_message %} {% include "dashboard/senior_forwarding.html" %} {% endif %} @@ -206,7 +237,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..c6022bb9feb 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 %}; @@ -314,11 +315,29 @@

+ <% if (subscribedTo) { %> + + <% } else { %> + + <% } %> <%} else {%> <% if (display_text.length > 0) {%> <%= display_text %>

<% }else{ %> + <% if (subscribedTo) { %> + + <% } else { %> + + <% } %> <% if (waitlisted && waitlistEnabled) { %> <% if (isEighthAdmin) { %>