Skip to content
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

PCR comments #672

Open
wants to merge 29 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
5b5b308
Add Initial Model
Jan 19, 2024
3dd1a0b
Create Initial Viewsets, Serializers, and Urls
Jan 20, 2024
c6f2b35
Add Authorization Checks
Jan 20, 2024
2b08816
Move Comment Model to Courses Folder
Jan 20, 2024
f9c5582
Add ViewSet Url
Jan 21, 2024
3b1a18e
comment UI progress
luke-rt Apr 22, 2024
bde4108
Add Test Cases
shiva-menta Apr 23, 2024
0fafac2
Intermediate Progress
shiva-menta Apr 25, 2024
3cbf128
Finish Initial Testing Suite
shiva-menta Apr 25, 2024
f5e1a51
Merge in Changes from Daniel's Branch
shiva-menta Apr 26, 2024
11e542b
Merge in Frontend and Fix Migrations
shiva-menta Apr 26, 2024
8967bcd
comment UI progress
luke-rt Apr 22, 2024
f213f3c
comments skeleton, sorting by semester
luke-rt Apr 25, 2024
78b459f
component refactoring for ratings/comment tab
luke-rt Apr 25, 2024
d6f966c
writing comment functionality
luke-rt Apr 26, 2024
fa27c9f
Merge branch 'pcr-comments-luke' into pcr-comments
luke-rt Apr 26, 2024
42611a3
progress and csrf tocken
luke-rt Apr 26, 2024
5cf8ff1
val tannen
luke-rt Apr 26, 2024
d793bdb
stuff
luke-rt Apr 26, 2024
8d38e00
like ui
luke-rt May 16, 2024
4605ae0
added new get section from instructor function
el-agua May 21, 2024
1cdf8fa
linting
el-agua Sep 1, 2024
8258892
modified frontend requests
el-agua Sep 8, 2024
e9a3c8b
Revert "modified frontend requests"
el-agua Sep 8, 2024
53dc027
Revert "Revert "modified frontend requests""
el-agua Sep 8, 2024
d32d45b
fixed yarn
luke-rt Sep 8, 2024
d44f92e
added semester-wide comments
el-agua Sep 8, 2024
3247310
comments instructor tab
el-agua Oct 19, 2024
7eac379
Instructor back many-many implementation
el-agua Nov 10, 2024
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
45 changes: 45 additions & 0 deletions backend/courses/migrations/0064_auto_20240120_1914.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Generated by Django 3.2.23 on 2024-01-21 00:14

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


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("courses", "0063_auto_20231212_1750"),
]

operations = [
migrations.AlterField(
model_name="section",
name="activity",
field=models.CharField(
choices=[
("", "Undefined"),
("CLN", "Clinic"),
("CRT", "Clinical Rotation"),
("DAB", "Dissertation Abroad"),
("DIS", "Dissertation"),
("DPC", "Doctoral Program Exchange"),
("FLD", "Field Work"),
("HYB", "Hybrid"),
("IND", "Independent Study"),
("LAB", "Lab"),
("LEC", "Lecture"),
("MST", "Masters Thesis"),
("ONL", "Online"),
("PRC", "Practicum"),
("REC", "Recitation"),
("SEM", "Seminar"),
("SRT", "Senior Thesis"),
("STU", "Studio"),
],
db_index=True,
help_text='The section activity, e.g. `LEC` for CIS-120-001 (2020A). Options and meanings: <table width=100%><tr><td>""</td><td>"Undefined"</td></tr><tr><td>"CLN"</td><td>"Clinic"</td></tr><tr><td>"CRT"</td><td>"Clinical Rotation"</td></tr><tr><td>"DAB"</td><td>"Dissertation Abroad"</td></tr><tr><td>"DIS"</td><td>"Dissertation"</td></tr><tr><td>"DPC"</td><td>"Doctoral Program Exchange"</td></tr><tr><td>"FLD"</td><td>"Field Work"</td></tr><tr><td>"HYB"</td><td>"Hybrid"</td></tr><tr><td>"IND"</td><td>"Independent Study"</td></tr><tr><td>"LAB"</td><td>"Lab"</td></tr><tr><td>"LEC"</td><td>"Lecture"</td></tr><tr><td>"MST"</td><td>"Masters Thesis"</td></tr><tr><td>"ONL"</td><td>"Online"</td></tr><tr><td>"PRC"</td><td>"Practicum"</td></tr><tr><td>"REC"</td><td>"Recitation"</td></tr><tr><td>"SEM"</td><td>"Seminar"</td></tr><tr><td>"SRT"</td><td>"Senior Thesis"</td></tr><tr><td>"STU"</td><td>"Studio"</td></tr></table>',
max_length=50,
),
)
]
13 changes: 13 additions & 0 deletions backend/courses/migrations/0066_merge_20240426_0627.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Generated by Django 3.2.23 on 2024-04-26 10:27

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("courses", "0064_auto_20240120_1914"),
("courses", "0065_topic_historical_probabilities_fall_and_more"),
]

operations = []
82 changes: 82 additions & 0 deletions backend/courses/migrations/0067_comment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Generated by Django 3.2.23 on 2024-04-26 10:27

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


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("courses", "0066_merge_20240426_0627"),
]

operations = [
migrations.CreateModel(
name="Comment",
fields=[
(
"id",
models.AutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("text", models.TextField()),
("created_at", models.DateTimeField(auto_now_add=True)),
("modified_at", models.DateTimeField(auto_now=True)),
("path", models.TextField(db_index=True)),
(
"author",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="comments",
to=settings.AUTH_USER_MODEL,
),
),
(
"base",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="courses.comment",
),
),
(
"downvotes",
models.ManyToManyField(
help_text="The number of downvotes a comment gets.",
related_name="downvotes",
to=settings.AUTH_USER_MODEL,
),
),
(
"parent",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="children",
to="courses.comment",
),
),
(
"section",
models.ForeignKey(
help_text="\nThe section with which a comment is associated. Section was chosen instead of topics for\nhosting comments because topics are SOFT STATE and are recomputed regularly.\n",
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="courses.section",
),
),
(
"upvotes",
models.ManyToManyField(
help_text="The number of upvotes a comment gets.",
related_name="upvotes",
to=settings.AUTH_USER_MODEL,
),
),
],
),
]
24 changes: 24 additions & 0 deletions backend/courses/migrations/0068_comment_instructor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 5.0.2 on 2024-10-27 17:06

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


class Migration(migrations.Migration):

dependencies = [
("courses", "0067_comment"),
]

operations = [
migrations.AddField(
model_name="comment",
name="instructor",
field=models.ForeignKey(
help_text="\nThe instructor with which a comment is associated. Instructor was chosen instead of topics\nfor hosting comments because topics are SOFT STATE and are recomputed regularly.\n",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="courses.instructor",
),
),
]
76 changes: 76 additions & 0 deletions backend/courses/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1540,3 +1540,79 @@ def __str__(self):
return (
f"Friendship(Sender: {self.sender}, Recipient: {self.recipient}, Status: {self.status})"
)


class Comment(models.Model):
"""
A single comment associated with a topic to be displayed on PCR. Comments support replies
through the parent_id and path fields. The path field allows for efficient database querying
and can indicate levels of nesting and can make pagination simpler. Idea implemented based
on this guide: https://blog.miguelgrinberg.com/post/implementing-user-comments-with-sqlalchemy.
"""

# Log base 10 value of maximum adjacent comment length.
_N = 10

text = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
modified_at = models.DateTimeField(auto_now=True)
author = models.ForeignKey(
get_user_model(), on_delete=models.SET_NULL, null=True, related_name="comments"
)
upvotes = models.ManyToManyField(
get_user_model(), related_name="upvotes", help_text="The number of upvotes a comment gets."
)
downvotes = models.ManyToManyField(
get_user_model(),
related_name="downvotes",
help_text="The number of downvotes a comment gets.",
)
section = models.ForeignKey(
Section,
on_delete=models.CASCADE,
help_text=dedent(
"""
The section with which a comment is associated. Section was chosen instead of topics for
hosting comments because topics are SOFT STATE and are recomputed regularly.
"""
),
null=True,
)

instructor = models.ForeignKey(
Instructor,
on_delete=models.SET_NULL,
help_text=dedent(
"""
The instructor with which a comment is associated. Instructor was chosen instead of topics
for hosting comments because topics are SOFT STATE and are recomputed regularly.
"""
),
null=True,
)

base = models.ForeignKey(
"self",
on_delete=models.SET_NULL, # redundant due to special deletion conditions
null=True,
)
parent = models.ForeignKey(
"self", on_delete=models.SET_NULL, null=True, related_name="children" # similarly redundant
)
path = models.TextField(db_index=True)

def level(self):
return len(self.path.split("."))

def delete(self, **kwargs):
if Comment.objects.filter(parent_id=self).exists():
self.text = "This comment has been removed."
self.upvotes.clear()
self.downvotes.clear()
self.author = None
self.save()
else:
super().delete(**kwargs)

def __str__(self):
return f"{self.author}: {self.text}"
81 changes: 81 additions & 0 deletions backend/courses/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from courses.models import (
Attribute,
Comment,
Course,
Friendship,
Instructor,
Expand Down Expand Up @@ -475,3 +476,83 @@ class FriendshipRequestSerializer(serializers.Serializer):

def to_representation(self, instance):
return super().to_representation(instance)


class CommentSerializer(serializers.ModelSerializer):
author_name = serializers.CharField(source="author.username", read_only=True)
votes = serializers.SerializerMethodField()
section = serializers.CharField(source="section.full_code", read_only=True)
base = serializers.SerializerMethodField()
parent = serializers.SerializerMethodField()
semester = serializers.CharField(source="section.course.semester", read_only=True)

def get_votes(self, obj):
return len(obj.upvotes.values_list("id")) - len(obj.downvotes.values_list("id"))

def get_base(self, obj):
if obj.base is None:
return None
return obj.base.id

def get_parent(self, obj):
if obj.parent is None:
return None
return obj.parent.id

class Meta:
model = Comment
fields = [
"id",
"text",
"created_at",
"modified_at",
"author_name",
"votes",
"section",
"base",
"parent",
"path",
"semester"
]


class CommentListSerializer(serializers.ModelSerializer):
author_name = serializers.CharField(source="author.username", read_only=True)
votes = serializers.SerializerMethodField()
section = serializers.CharField(source="section.full_code", read_only=True)
base = serializers.SerializerMethodField()
parent = serializers.SerializerMethodField()
user_upvoted = serializers.BooleanField()
user_downvoted = serializers.BooleanField()
semester = serializers.CharField(source="section.course.semester", read_only=True)

def get_votes(self, obj):
return len(obj.upvotes.values_list("id")) - len(obj.downvotes.values_list("id"))

def get_base(self, obj):
if obj.base is None:
return None
return obj.base.id

def get_parent(self, obj):
if obj.parent is None:
return None
return obj.parent.id

class Meta:
model = Comment
fields = [
"id",
"text",
"created_at",
"modified_at",
"author_name",
"votes",
"section",
"base",
"parent",
"path",
"user_upvoted",
"user_downvoted",
"semester",
]
Loading
Loading