Skip to content

Section swapping #396

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 21 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
1027c24
Made model and starting views
kevinbu233 Feb 17, 2023
1442c47
Created views for swap
KartavyaSharma Feb 28, 2023
ad5f33e
Refactored Swap models
KartavyaSharma Feb 28, 2023
9867af1
Added basic structure for GET and PUT for Swap requests, added test f…
KartavyaSharma Apr 11, 2023
ade355d
refactor: fixed typo in Mentor model
KartavyaSharma Apr 11, 2023
624b34f
Added Logging for when a Swap is successfully created. Added boilerpl…
KartavyaSharma Apr 12, 2023
6e042ac
WIP: Working on atomic transactions after student accepts swap
KartavyaSharma Apr 18, 2023
7550961
Fixed getting student id from request based on method
KartavyaSharma Apr 18, 2023
7972d99
Added process for swapping sections after swap invite has been accepted
KartavyaSharma Apr 18, 2023
fdd5d32
Added concurrency checks for swap sequence, and added email validatio…
KartavyaSharma Apr 18, 2023
e4a6503
First attempt at delete swap method
edwardneoprime Apr 25, 2023
3ae2149
Updated docstring for swaps
KartavyaSharma Apr 25, 2023
ec9c6af
Added tests for requesting basic section swap between two students
KartavyaSharma May 15, 2023
7ddd3b8
Swap list GET endpoint now works, added test utils for Swapping, and …
KartavyaSharma May 16, 2023
a7cac70
Added test for checking request email validity, also added possible t…
KartavyaSharma May 16, 2023
5950a3c
Swap processing is now working, implemented new swap tests, updated s…
KartavyaSharma May 16, 2023
fb03a04
Fixed invalidation process for swaps and added more tests.
KartavyaSharma Jun 4, 2023
bd2df33
Fixed error where only the sender's expired swaps were being deleted …
KartavyaSharma Jun 4, 2023
03e34c9
Fixed locked rows for sender and receiver and removed a concurrency test
KartavyaSharma Jul 1, 2023
bab07a4
Reverted comment change
KartavyaSharma Jul 1, 2023
59f467d
Resolved requested changes, added specific exceptions, and split tests
KartavyaSharma Jul 4, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions csm_web/scheduler/migrations/0033_swap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 3.2.16 on 2023-04-11 01:40

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('scheduler', '0032_word_of_the_day'),
]

operations = [
migrations.CreateModel(
name='Swap',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('receiver', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
related_name='receiver', to='scheduler.student')),
('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
related_name='sender', to='scheduler.student')),
],
),
]
9 changes: 9 additions & 0 deletions csm_web/scheduler/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,15 @@ class Meta:
unique_together = ("user", "section")


class Swap(models.Model):
"""
Represents a given "instance" of a swap. Every time a swap is requested between two users,
a Swap instance is initialized containing references to the respective students.
"""
sender = models.ForeignKey(Student, related_name="sender", on_delete=models.CASCADE)
receiver = models.ForeignKey(Student, related_name="receiver", on_delete=models.CASCADE)


class Mentor(Profile):
"""
Represents a given "instance" of a mentor. Every section a mentor teaches in every course should
Expand Down
6 changes: 5 additions & 1 deletion csm_web/scheduler/serializers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from rest_framework import serializers
from enum import Enum
from django.utils import timezone
from .models import Attendance, Course, Link, SectionOccurrence, User, Student, Section, Mentor, Override, Spacetime, Coordinator, DayOfWeekField, Resource, Worksheet, Matcher, MatcherSlot, MatcherPreference
from .models import Attendance, Swap, Course, Link, SectionOccurrence, User, Student, Section, Mentor, Override, Spacetime, Coordinator, DayOfWeekField, Resource, Worksheet, Matcher, MatcherSlot, MatcherPreference


class Role(Enum):
Expand Down Expand Up @@ -58,6 +58,10 @@ class Meta:
fields = ("spacetime", "date")
read_only_fields = ("spacetime", "date")

class SwapSerializer(serializers.ModelSerializer):
class Meta:
model = Swap
fields = ("id", "sender", "receiver")

class SpacetimeSerializer(serializers.ModelSerializer):

Expand Down
234 changes: 234 additions & 0 deletions csm_web/scheduler/tests/models/test_swap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
import pytest
import json

from django.urls import reverse
from scheduler.models import Swap
from scheduler.factories import (
UserFactory,
CourseFactory,
SectionFactory,
StudentFactory,
MentorFactory
)


@pytest.fixture
def setup_scheduler(db):
"""
Creates a pair of sections within the same course, each with a mentor and student.
"""
# Setup course
course = CourseFactory.create()
# Setup sections
section_one = create_section(course)
section_two = create_section(course)
# Setup students
section_one_students = create_students(course, section_one, 3)
section_two_students = create_students(course, section_two, 3)

return course, section_one, section_two, section_one_students, section_two_students


@pytest.mark.django_db
def test_basic_swap_request_success(client, setup_scheduler):
"""
Tests that a student can successfully request a swap.
"""
section_one_students, section_two_students = setup_scheduler[3:]
sender_student, reciever_student = section_one_students[0], section_two_students[0]
# Create a swap request
create_swap_request(client, sender_student, reciever_student)
# Make sure that the swap request was created
assert Swap.objects.filter(receiver=reciever_student).exists()


@pytest.mark.django_db
def test_empty_swap_list(client, setup_scheduler):
"""
Test getting an empty list of swaps for a student.
"""
section_one_students, section_two_students = setup_scheduler[3:]
sender_student = section_one_students[0]
# Get the list of swaps for the sender
swaps = get_swap_requests(client, sender_student)
# Decode get response to UTF-8
swaps = json.loads(swaps.content.decode("utf-8"))
# Make sure that the list of swaps is empty
assert len(swaps["sender"]) == 0


@pytest.mark.django_db
def test_non_empty_swap_list(client, setup_scheduler):
"""
Test getting a non-empty list of swaps for a student.
"""
section_one_students, section_two_students = setup_scheduler[3:]
sender_student, receiver_student = section_one_students[0], section_two_students[0]
# Create a swap request
create_swap_request(client, sender_student, receiver_student)
# Get the list of swaps for the sender
swaps = get_swap_requests(client, sender_student)
# Decode get response to UTF-8
swaps = json.loads(swaps.content.decode("utf-8"))
# Make sure that the swap request was created
assert Swap.objects.filter(receiver=receiver_student).exists()
# Make sure that the swap request is in the list of swaps
assert len(swaps["sender"]) != 0 and swaps["sender"][0]["receiver"] == receiver_student.id


@pytest.mark.django_db
@pytest.mark.parametrize(
["email"],
[
# empty email
(" ",),
# invalid email
("invalid",),
# non-existent email
("[email protected]",),
# valid email
(setup_scheduler,)
],
ids=["empty email", "invalid email", "non-existent email", "valid email"],
)
def test_request_swap_invalid_email(client, setup_scheduler, email, request):
"""
Tests that a student cannot request a swap with an invalid email.
"""
section_one_students, section_two_students = setup_scheduler[3:]
sender, receiver = section_one_students[0], section_two_students[0]
receiver_email = email
if (type(receiver_email) != str):
receiver_email = receiver.user.email
receiver.user.email = receiver_email
response = create_swap_request(client, sender, receiver)
if type(email) == str:
assert response.status_code == 404
else:
assert response.status_code == 201
# Check that the swap request was created
assert Swap.objects.filter(receiver=receiver).exists()


@pytest.mark.django_db
def test_accept_swap_request(client, setup_scheduler):
"""
Tests that a student can accept a swap request.
"""
section_one_students, section_two_students = setup_scheduler[3:]
sender, receiver = section_one_students[0], section_two_students[0]
create_swap_request(client, sender, receiver)
# Get the swap_id
swap_id = json.loads(get_swap_requests(client, sender).content.decode("utf-8"))["sender"][0]["id"]
accept_swap_request(client, receiver, swap_id)
# Record old section ids
old_sender_section_id, old_receiver_section_id = sender.section.id, receiver.section.id
# Get the new section ids
sender.refresh_from_db()
receiver.refresh_from_db()
# Make sure that the swap was accepted
assert sender.section.id == old_receiver_section_id
assert receiver.section.id == old_sender_section_id
# Make sure that all swaps for both students are cleared
assert len(json.loads(get_swap_requests(client, sender).content.decode("utf-8"))["sender"]) == 0
assert len(json.loads(get_swap_requests(client, receiver).content.decode("utf-8"))["sender"]) == 0


@pytest.mark.django_db
def test_reject_swap_request(client, setup_scheduler):
"""
Tests that a student can reject a swap request.
"""
section_one_students, section_two_students = setup_scheduler[3:]
sender, receiver = section_one_students[0], section_two_students[0]
create_swap_request(client, sender, receiver)
# Get the swap_id
swap_id = json.loads(get_swap_requests(client, sender).content.decode("utf-8"))["sender"][0]["id"]
reject_swap_request(client, receiver, swap_id)
# Make sure that the swap was rejected
assert len(json.loads(get_swap_requests(client, receiver).content.decode("utf-8"))["sender"]) == 0


@pytest.mark.django_db
def test_swap_conflict_clear(client, setup_scheduler):
"""
Test for when a student has multiple swap requests pending and one of them
is accepted. In this case, all other swap requests from that user should be
invalidated and cleared.
"""
section_one_students, section_two_students = setup_scheduler[3:]
sender = section_one_students[0]
receiver_one, receiver_two = section_two_students[0], section_two_students[1]
create_swap_request(client, sender, receiver_one)
create_swap_request(client, sender, receiver_two)
# Get first swap_id
swap_id_one = json.loads(get_swap_requests(client, sender).content.decode("utf-8"))["sender"][0]["id"]
# Accept first swap request
accept_swap_request(client, receiver_one, swap_id_one)
# Make sure that the second swap request was cleared
assert len(json.loads(get_swap_requests(client, receiver_two).content.decode("utf-8"))["receiver"]) == 0


def create_students(course, section, quantity):
"""
Creates a given number of students for a given section.
"""
student_users = UserFactory.create_batch(quantity)
students = []
for student_user in student_users:
student = StudentFactory.create(user=student_user, course=course, section=section)
students.append(student)
return students


def create_section(course):
"""
Creates a section for a given course.
"""
mentor_user = UserFactory.create()
mentor = MentorFactory.create(user=mentor_user, course=course)
section = SectionFactory.create(mentor=mentor)
return section


def create_swap_request(client, sender, reciever):
"""
Creates a swap request between two students.
"""
client.force_login(sender.user)
post_url = reverse("section-swap-no-id", args=[sender.section.id])
data = json.dumps({"receiver_email": reciever.user.email, "student_id": sender.id})
response = client.post(post_url, data=data, content_type="application/json")
return response


def get_swap_requests(client, sender):
"""
Gets a list of swap requests for a given student.
"""
client.force_login(sender.user)
get_url = reverse("section-swap-no-id", args=[sender.section.id])
body = {"student_id": sender.id}
response = client.get(get_url, body, content_type="application/json")
return response


def accept_swap_request(client, receiver, swap_id):
"""
Accepts a swap request between two students.
"""
client.force_login(receiver.user)
post_url = reverse("section-swap-with-id", kwargs={"pk": receiver.section.id, "swap_id": swap_id})
data = json.dumps({"student_id": receiver.id})
response = client.post(post_url, data, content_type="application/json")
return response


def reject_swap_request(client, receiver, swap_id):
"""
Receiver rejects a swap request from a sender.
"""
client.force_login(receiver.user)
delete_url = reverse("section-swap-with-id", kwargs={"pk": receiver.section.id, "swap_id": swap_id})
response = client.delete(delete_url)
return response
Loading