Skip to content

Commit

Permalink
Merge branch 'ta-reviews' into ta-reviews_frontend
Browse files Browse the repository at this point in the history
  • Loading branch information
benjmnxu committed Mar 28, 2024
2 parents 98c52fb + 906e8dc commit 8aa3c2f
Show file tree
Hide file tree
Showing 11 changed files with 617 additions and 23 deletions.
5 changes: 4 additions & 1 deletion backend/Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ gunicorn = "*"
django-scheduler = "*"
typing-extensions = "*"
drf-excel = "*"
django-extensions = "*"
django-debug-toolbar = "*"
parameterized = "*"

[requires]
python_version = "3"
python_version = "3.10.8"
31 changes: 31 additions & 0 deletions backend/ohq/migrations/0020_auto_20221106_1919.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Generated by Django 3.1.7 on 2022-11-06 19:19

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


class Migration(migrations.Migration):

dependencies = [
('ohq', '0019_auto_20211114_1800'),
]

operations = [
migrations.CreateModel(
name='Review',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('content', models.TextField()),
('rating', models.IntegerField(choices=[(1, 'One star'), (2, 'Two stars'), (3, 'Three stars'), (4, 'Four stars'), (5, 'Five stars')])),
],
),
migrations.AddField(
model_name='question',
name='review',
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to='ohq.review'),
),
migrations.AddConstraint(
model_name='question',
constraint=models.UniqueConstraint(fields=('review',), name='unique_question'),
),
]
18 changes: 18 additions & 0 deletions backend/ohq/migrations/0021_auto_20221110_1803.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.1.7 on 2022-11-10 18:03

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('ohq', '0020_auto_20221106_1919'),
]

operations = [
migrations.AlterField(
model_name='review',
name='content',
field=models.TextField(blank=True),
),
]
19 changes: 19 additions & 0 deletions backend/ohq/migrations/0022_auto_20221111_0135.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 3.1.7 on 2022-11-11 01:35

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


class Migration(migrations.Migration):

dependencies = [
('ohq', '0021_auto_20221110_1803'),
]

operations = [
migrations.AlterField(
model_name='question',
name='review',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='ohq.review'),
),
]
29 changes: 29 additions & 0 deletions backend/ohq/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,26 @@ class Meta:
def __str__(self):
return f"{self.course}: {self.name}"

class Review(models.Model):
"""
TA reviews within a question
"""
RATING_ONE = 1
RATING_TWO = 2
RATING_THREE = 3
RATING_FOUR = 4
RATING_FIVE = 5
RATING_CHOICES = [
(RATING_ONE, "One star"),
(RATING_TWO, "Two stars"),
(RATING_THREE, "Three stars"),
(RATING_FOUR, "Four stars"),
(RATING_FIVE, "Five stars")
]

content = models.TextField(blank=True)
rating = models.IntegerField(choices=RATING_CHOICES)


class Question(models.Model):
"""
Expand Down Expand Up @@ -282,6 +302,14 @@ class Question(models.Model):
should_send_up_soon_notification = models.BooleanField(default=False)
tags = models.ManyToManyField(Tag, blank=True)
student_descriptor = models.CharField(max_length=255, blank=True, null=True)
review = models.OneToOneField(Review, on_delete=models.CASCADE, blank=True, null=True)

class Meta:
constraints = [
models.UniqueConstraint(
fields=["review"], name="unique_question"
)
]


class CourseStatistic(models.Model):
Expand Down Expand Up @@ -425,3 +453,4 @@ class Review(models.Model):
question = models.OneToOneField(Question, related_name="reviews",
on_delete=models.CASCADE, primary_key=True, blank=True, null=False)
time_updated = models.DateTimeField(auto_now=True)

25 changes: 8 additions & 17 deletions backend/ohq/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -504,31 +504,22 @@ def has_permission(self, request, view):

return True


class ReviewPermission(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
membership = Membership.objects.get(course=view.kwargs["course_pk"], user=request.user)
question = Question.objects.get(pk=view.kwargs["question_pk"])

# Students can get or modify their own review
# Only Head TAs and Professors can get or modify any questions
if view.action in ["retrieve"]:
return question.asked_by == request.user or membership.is_leadership

if view.action in ["update", "partial_update"]:
return question.asked_by == request.user

def has_permission(self, request, view):
# Anonymous users can't do anything
if not request.user.is_authenticated:
return False

membership = Membership.objects.filter(
course=view.kwargs["course_pk"], user=request.user
).first()

# Non-Students can't do anything
if membership is None:
return False

if view.action == "create":

# Only students can create, modify and delete reviews
if view.action in ["create", "update", "partial_update", "destroy"]:
return (not membership.is_ta) and (not membership.is_leadership)

return True
58 changes: 58 additions & 0 deletions backend/ohq/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from django.core.exceptions import ObjectDoesNotExist
from django.utils import timezone
from django.utils.crypto import get_random_string
from django.http import JsonResponse
from phonenumber_field.serializerfields import PhoneNumberField
from rest_framework import serializers
from rest_live.signals import save_handler
Expand Down Expand Up @@ -51,6 +52,41 @@ def save(self):
self.validated_data["queue"] = Queue.objects.get(pk=self.context["view"].kwargs["queue_pk"])
return super().save()

class QuestionReviewRouteMixin(serializers.ModelSerializer):
"""
Mixin for serializers that overrides the save method to
properly handle the URL parameter for questions.
"""

def save(self):
self.validated_data["question"] = Question.objects.get(pk=self.context["view"].kwargs["question_pk"])

if self.validated_data["question"].status != "ANSWERED":
return JsonResponse({"detail": "This question has not been answered by a TA yet"})

if self.context["request"].method == "POST" and "rating" in self.validated_data:
if "rating" not in self.validated_data:
return JsonResponse({"detail": "A rating must be provided"})
if self.validated_data["question"].review != None:
return JsonResponse({"detail": "This question is already reviewed."})
review = Review(content="", rating=self.validated_data["rating"])
if "content" in self.validated_data:
review.content = self.validated_data["content"]
review.save()
self.validated_data["question"].review = review
self.validated_data["question"].save()
return JsonResponse({"detail": "Your review has been posted."})

if self.context["request"].method == "PATCH":
self.validated_data["review"] = Review.objects.get(pk=self.context["view"].kwargs["pk"])
if "rating" not in self.validated_data and "content" not in self.validated_data:
return JsonResponse({"detail": "Your review does not contain any content or rating."})
if "rating" in self.validated_data:
self.validated_data["review"].rating = self.validated_data["rating"]
if "content" in self.validated_data:
self.validated_data["review"].content = self.validated_data["content"]
self.validated_data["review"].save()
return JsonResponse({"detail": "Your review is updated"})

class SemesterSerializer(serializers.ModelSerializer):
pretty = serializers.SerializerMethodField()
Expand Down Expand Up @@ -210,11 +246,26 @@ class Meta:
fields = ("id", "name")


class ReviewSerializer(QuestionReviewRouteMixin):
"""
Serializer for review
"""
question = serializers.ReadOnlyField(source="question.text")
ta_first_name = serializers.ReadOnlyField(source="question.responded_to_by.first_name")
ta_last_name = serializers.ReadOnlyField(source="question.responded_to_by.last_name")

class Meta:
model = Review
fields = ("id", "content", "rating", "question", "ta_first_name", "ta_last_name")
read_only_fields = ("question", "ta_first_name", "ta_last_name")


class QuestionSerializer(QueueRouteMixin):
asked_by = UserSerializer(read_only=True)
responded_to_by = UserSerializer(read_only=True)
tags = TagSerializer(many=True)
position = serializers.IntegerField(default=-1, read_only=True)
review = ReviewSerializer(read_only=True)

class Meta:
model = Question
Expand All @@ -235,6 +286,7 @@ class Meta:
"resolved_note",
"position",
"student_descriptor",
"review",
)
read_only_fields = (
"time_asked",
Expand All @@ -245,6 +297,7 @@ class Meta:
"should_send_up_soon_notification",
"resolved_note",
"position",
"review",
)

def update(self, instance, validated_data):
Expand Down Expand Up @@ -343,6 +396,11 @@ def create(self, validated_data):
except ObjectDoesNotExist:
continue
return question

def get_review(self, obj):
review = Review.objects.get(question=obj)
serializer = ReviewSerializer(review)
return serializer.data


class MembershipPrivateSerializer(CourseRouteMixin):
Expand Down
57 changes: 54 additions & 3 deletions backend/ohq/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,27 @@ def get_queryset(self):
)
return prefetch(qs, self.get_serializer_class())

@action(detail=True)
def reviews(self, request, pk):
membership = Membership.objects.get(course=pk, user=self.request.user)

if membership.kind == "PROFESSOR" or membership.kind_to_pretty == "HEAD_TA" or self.request.user.is_superuser:
questions = Question.objects.select_related("review").filter(queue__in=Queue.objects.filter(course=pk))

elif membership.kind == "TA":
questions = Question.objects.select_related("review").filter(queue__in=Queue.objects.filter(course=pk), responded_to_by=self.request.user)

elif membership.kind == "STUDENT":
questions = Question.objects.select_related("review").filter(queue__in=Queue.objects.filter(course=pk), asked_by=self.request.user)

reviews = []
serializer = ReviewSerializer(many=True)
for question in questions:
reviews.append(question.review)
serializer = ReviewSerializer(reviews, many=True)
return JsonResponse(serializer.data, safe=False)



class QuestionViewSet(viewsets.ModelViewSet, RealtimeMixin):
"""
Expand Down Expand Up @@ -778,11 +799,41 @@ def list(self, request, *args, **kwargs):
def get_queryset(self):
return Occurrence.objects.filter(pk=self.kwargs["pk"])


class ReviewViewSet(viewsets.ModelViewSet):
# permission_classes = [ReviewPermission | IsSuperuser]
"""
retrieve:
Return a review based on type of user. All reviews are anonymous.
Students can retrieve review made by themselves.
TAs can retrieve reviews made for themselves.
Head TAs/Professor can retrieve any review.
list:
Return a list of reviews based on type of user. All reviews are anonymous.
Students can retrieve reviews made by themselves.
TAs can retrieve reviews made for themselves.
Head TAs/Professor can retrieve anyreview.
update:
Update all fields in a review.
You must specify all of the fields or use a patch request.
partial_update:
Update certain fields in a review.
Only specify the fields that you want to change.
destroy:
Delete a review.
reviewId is required.
"""
serializer_class = ReviewSerializer
queryset = Review.objects.none()
permission_classes = [ReviewPermission | IsSuperuser]

def get_queryset(self):
membership = Membership.objects.get(course=self.kwargs["course_pk"], user=self.request.user)
if membership.kind == "TA":
return Review.objects.filter(question=self.kwargs["question_pk"], question__responded_to_by=self.request.user)

if membership.kind == "STUDENT":
return Review.objects.filter(question=self.kwargs["question_pk"], question__asked_by=self.request.user)

return Review.objects.filter(question=self.kwargs["question_pk"])
Loading

0 comments on commit 8aa3c2f

Please sign in to comment.