Skip to content

Commit

Permalink
feat: implement individual students stickying
Browse files Browse the repository at this point in the history
  • Loading branch information
JasonGrace2282 committed Sep 25, 2024
1 parent 2b4794b commit b763594
Show file tree
Hide file tree
Showing 7 changed files with 92 additions and 15 deletions.
2 changes: 1 addition & 1 deletion intranet/apps/dashboard/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def gen_schedule(user, num_blocks=6, surrounding_blocks=None):
if current_sched_act:
current_signup = current_sched_act.title_with_flags
current_signup_cancelled = current_sched_act.cancelled
current_signup_sticky = current_sched_act.activity.sticky
current_signup_sticky = current_sched_act.activity.is_user_stickied(user)
rooms = current_sched_act.get_true_rooms()
else:
current_signup = None
Expand Down
20 changes: 20 additions & 0 deletions intranet/apps/eighth/forms/admin/scheduling.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from django import forms
from django.contrib.auth import get_user_model

from ...models import EighthScheduledActivity

Expand All @@ -11,6 +12,8 @@ class ScheduledActivityForm(forms.ModelForm):

unschedule = forms.BooleanField(required=False)

sticky_students = forms.CharField(required=False)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

Expand All @@ -20,6 +23,10 @@ def __init__(self, *args, **kwargs):
for fieldname in ["block", "activity"]:
self.fields[fieldname].widget = forms.HiddenInput()

activity = getattr(self, "instance", None)
if activity and activity.pk:
self.fields["sticky_students"].initial = ", ".join(activity.sticky_students.all())

def validate_unique(self):
# We'll handle this ourselves by updating if already exists
pass
Expand Down Expand Up @@ -51,6 +58,19 @@ class Meta:
"admin_comments": forms.Textarea(attrs={"rows": 2, "cols": 30}),
}

def clean_sticky_students(self):
# assume that usernames do not have spaces
raw_data = self.cleaned_data["sticky_students"].replace(" ", "")
usernames = [name for name in raw_data.split(",") if name]
users = get_user_model().objects.filter(username__in=usernames).all()
if len(users) != len(usernames):
found = {user.username for user in users}
_missing = [name for name in usernames if name not in found]
# THIS BREAKS ROOM NUMBERS FOR SOME REASON???
# So for now ignoring it
# raise ValidationError(f"Invalid usernames {', '.join(missing)}")
return users

def clean(self):
cleaned_data = super().clean()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 3.2.25 on 2024-09-19 20:30

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='eighthscheduledactivity',
name='sticky_students',
field=models.ManyToManyField(blank=True, related_name='sticky_scheduledactivity_set', to=settings.AUTH_USER_MODEL),
),
]
30 changes: 19 additions & 11 deletions intranet/apps/eighth/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from cacheops import invalidate_obj
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AbstractBaseUser
from django.contrib.auth.models import Group as DjangoGroup
from django.core import validators
from django.core.cache import cache
Expand Down Expand Up @@ -798,6 +799,11 @@ class EighthScheduledActivity(AbstractBaseEighthModel):
activity = models.ForeignKey(EighthActivity, on_delete=models.CASCADE)
members = models.ManyToManyField(settings.AUTH_USER_MODEL, through="EighthSignup", related_name="eighthscheduledactivity_set")
waitlist = models.ManyToManyField(settings.AUTH_USER_MODEL, through="EighthWaitlist", related_name="%(class)s_scheduledactivity_set")
sticky_students = models.ManyToManyField(
settings.AUTH_USER_MODEL,
related_name="sticky_scheduledactivity_set",
blank=True,
)

admin_comments = models.CharField(max_length=1000, blank=True)
title = models.CharField(max_length=1000, blank=True)
Expand Down Expand Up @@ -853,6 +859,14 @@ def title_with_flags(self) -> str:
name_with_flags = "Special: " + name_with_flags
return name_with_flags

def is_user_stickied(self, user: AbstractBaseUser) -> bool:
"""Check if the given user is stickied to this activity.
Args:
user: The user to check for stickiness.
"""
return self.sticky or self.activity.sticky or self.sticky_students.filter(pk=user.pk).exists()

def get_true_sponsors(self) -> Union[QuerySet, Collection[EighthSponsor]]: # pylint: disable=unsubscriptable-object
"""Retrieves the sponsors for the scheduled activity, taking into account activity defaults and
overrides.
Expand Down Expand Up @@ -911,13 +925,6 @@ def get_restricted(self) -> bool:
"""
return self.restricted or self.activity.restricted

def get_sticky(self) -> bool:
"""Gets whether this scheduled activity is sticky.
Returns:
Whether this scheduled activity is sticky.
"""
return self.sticky or self.activity.sticky

def get_finance(self) -> str:
"""Retrieves the name of this activity's account with the
finance office, if any.
Expand Down Expand Up @@ -1091,7 +1098,7 @@ def notify_waitlist(self, waitlists: Iterable["EighthWaitlist"]):
@transaction.atomic # This MUST be run in a transaction. Do NOT remove this decorator.
def add_user(
self,
user: "get_user_model()",
user: AbstractBaseUser,
request: Optional[HttpRequest] = None,
force: bool = False,
no_after_deadline: bool = False,
Expand Down Expand Up @@ -1151,8 +1158,9 @@ def add_user(
if (
EighthSignup.objects.filter(user=user, scheduled_activity__block__in=all_blocks)
.filter(Q(scheduled_activity__activity__sticky=True) | Q(scheduled_activity__sticky=True))
.filter(Q(scheduled_activity__cancelled=False))
.filter(scheduled_activity__cancelled=False)
.exists()
or user.sticky_scheduledactivity_set.filter(block__in=all_blocks, cancelled=False).exists()
):
exception.Sticky = True

Expand Down Expand Up @@ -1214,7 +1222,7 @@ def add_user(
if self.activity.users_blacklisted.filter(username=user).exists():
exception.Blacklisted = True

if self.get_sticky():
if self.is_user_stickied(user):
EighthWaitlist.objects.filter(user_id=user.id, block_id=self.block.id).delete()

success_message = "Successfully added to waitlist for activity." if waitlist else "Successfully signed up for activity."
Expand Down Expand Up @@ -1688,7 +1696,7 @@ def remove_signup(self, user: "get_user_model()" = None, force: bool = False, do
exception.ActivityDeleted = True

# Check if the user is already stickied into an activity
if self.scheduled_activity.activity and self.scheduled_activity.activity.sticky and not self.scheduled_activity.cancelled:
if self.scheduled_activity.activity and self.scheduled_activity.is_user_stickied(user) and not self.scheduled_activity.cancelled:
exception.Sticky = True

if exception.messages() and not force:
Expand Down
7 changes: 5 additions & 2 deletions intranet/apps/eighth/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,10 @@ def process_scheduled_activity(
if scheduled_activity.title:
prefix += " - " + scheduled_activity.title
middle = " (R)" if restricted_for_user else ""
suffix = " (S)" if activity.sticky else ""
if user is not None and scheduled_activity.is_user_stickied(user):
suffix = " (S)"
else:
suffix = ""
suffix += " (BB)" if scheduled_activity.is_both_blocks() else ""
suffix += " (A)" if activity.administrative else ""
suffix += " (Deleted)" if activity.deleted else ""
Expand Down Expand Up @@ -142,7 +145,7 @@ def process_scheduled_activity(
"administrative": scheduled_activity.get_administrative(),
"presign": activity.presign,
"presign_time": scheduled_activity.is_too_early_to_signup()[1].strftime("%A, %B %-d at %-I:%M %p"),
"sticky": scheduled_activity.get_sticky(),
"sticky": scheduled_activity.is_user_stickied(user),
"finance": "", # TODO: refactor JS to remove this
"title": scheduled_activity.title,
"comments": scheduled_activity.comments, # TODO: refactor JS to remove this
Expand Down
25 changes: 25 additions & 0 deletions intranet/apps/eighth/tests/test_signup.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,31 @@ def test_signup_restricitons(self):
self.assertEqual(len(EighthScheduledActivity.objects.get(block=block1.id, activity=act1.id).members.all()), 1)
self.assertEqual(len(EighthScheduledActivity.objects.get(block=block1.id, activity=act2.id).members.all()), 0)

def test_user_stickied(self):
"""Test that stickying an individual user into an activity works."""
self.make_admin()
user = get_user_model().objects.create(username="user1", graduation_year=get_senior_graduation_year())

block = self.add_block(date="2024-09-09", block_letter="A")
room = self.add_room(name="room1", capacity=1)
act = self.add_activity(name="Test Activity 1", restricted=True, users_allowed=[user])
act.rooms.add(room)

schact = EighthScheduledActivity.objects.create(block=block, activity=act, capacity=5)
schact.sticky_students.add(user)
schact.save()

act2 = self.add_activity(name="Test Activity 2")
act2.rooms.add(room)
schact2 = EighthScheduledActivity.objects.create(block=block, activity=act2, capacity=5)

# ensure that the user can't sign up to something else
with self.assertRaisesMessage(SignupException, "Sticky"):
self.verify_signup(user, schact2)

self.client.post(reverse("eighth_signup", args=[block.id]), {"aid": act2.id})
self.assertFalse(schact2.members.exists())

def test_eighth_signup_view(self):
"""Tests :func:`~intranet.apps.eighth.views.signup.eighth_signup_view`."""

Expand Down
3 changes: 2 additions & 1 deletion intranet/templates/eighth/admin/schedule_activity.html
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ <h4>Select an Activity:</h4>
</th>
<th>Comments</th><th></th>
<th>Admin Comments</th><th></th>
<th><span>Sticky Students</span></th>
</tr>
</thead>
<tbody>
Expand Down Expand Up @@ -295,7 +296,7 @@ <h4>Select an Activity:</h4>
{% endif %}
</td>

{% if field.name in "rooms capacity sponsors title special administrative restricted sticky both_blocks comments admin_comments" %}
{% if field.name in "rooms capacity sponsors title special administrative restricted sticky sticky_students both_blocks comments admin_comments" %}
<td class="propagate" data-base-field="{{ field.name }}">
<a class="propagate {{ field.name }} button" title="Propagate" data-block="{{ form.block.value }}" data-field="{{ field.name }}" data-input="{{ field.id_for_label }}">
<i class="fas fa-arrows-alt-v"></i>
Expand Down

0 comments on commit b763594

Please sign in to comment.