Skip to content

Commit d921a09

Browse files
WIP Staging Resolving Merge Issues
1 parent 6944dbb commit d921a09

7 files changed

+196
-63
lines changed

backend/Pipfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,4 @@ ics = "*"
6565
drf-nested-routers = "*"
6666

6767
[requires]
68-
python_full_version = "3.11"
68+
python_full_version = "3.11"

backend/Pipfile.lock

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Generated by Django 5.0.2 on 2024-11-05 17:58
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("courses", "0065_auto_20240425_2031"),
10+
("courses", "0065_topic_historical_probabilities_fall_and_more"),
11+
]
12+
13+
operations = []
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Generated by Django 5.0.2 on 2024-11-05 18:16
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("courses", "0066_merge_20241105_1258"),
10+
]
11+
12+
operations = [
13+
migrations.RemoveField(
14+
model_name="course",
15+
name="credits",
16+
),
17+
migrations.RemoveField(
18+
model_name="topic",
19+
name="historical_probabilities_fall",
20+
),
21+
migrations.RemoveField(
22+
model_name="topic",
23+
name="historical_probabilities_spring",
24+
),
25+
migrations.RemoveField(
26+
model_name="topic",
27+
name="historical_probabilities_summer",
28+
),
29+
migrations.AlterField(
30+
model_name="course",
31+
name="num_activities",
32+
field=models.IntegerField(
33+
default=0,
34+
help_text="\nThe number of distinct activities belonging to this course (precomputed for efficiency).\nMaintained by the registrar import / recompute_soft_state script.\n",
35+
),
36+
),
37+
]

backend/courses/models.py

+78-61
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
from PennCourses.settings.base import FIRST_BANNER_SEM, PRE_NGSS_PERMIT_REQ_RESTRICTION_CODES
1616
from review.annotations import review_averages
1717

18-
1918
User = get_user_model()
2019

2120

@@ -154,9 +153,7 @@ class Course(models.Model):
154153
),
155154
)
156155
code = models.CharField(
157-
max_length=8,
158-
db_index=True,
159-
help_text="The course code, e.g. `120` for CIS-120.",
156+
max_length=8, db_index=True, help_text="The course code, e.g. `120` for CIS-120."
160157
)
161158
semester = models.CharField(
162159
max_length=5,
@@ -204,15 +201,6 @@ class Course(models.Model):
204201
help_text="The dash-joined department and code of the course, e.g. `CIS-120` for CIS-120.",
205202
)
206203

207-
credits = models.DecimalField(
208-
max_digits=4, # some course for 2019C is 14 CR...
209-
decimal_places=2,
210-
null=True,
211-
blank=True,
212-
db_index=True,
213-
help_text="The number of credits this course takes. This is precomputed for efficiency.",
214-
)
215-
216204
prerequisites = models.TextField(
217205
blank=True,
218206
help_text="Text describing the prereqs for a course, e.g. 'CIS 120, 160' for CIS-121.",
@@ -276,7 +264,7 @@ class Course(models.Model):
276264
help_text=dedent(
277265
"""
278266
The number of distinct activities belonging to this course (precomputed for efficiency).
279-
Maintained by the registrar import / recomputestats script.
267+
Maintained by the registrar import / recompute_soft_state script.
280268
"""
281269
),
282270
)
@@ -377,36 +365,6 @@ class Topic(models.Model):
377365
),
378366
)
379367

380-
historical_probabilities_spring = models.FloatField(
381-
default=0,
382-
help_text=dedent(
383-
"""
384-
The historical probability of a student taking a course in this topic in the spring
385-
semester, based on historical data. This field is recomputed nightly from the
386-
`parent_course` graph (in the recompute_soft_state cron job).
387-
"""
388-
),
389-
)
390-
historical_probabilities_summer = models.FloatField(
391-
default=0,
392-
help_text=dedent(
393-
"""
394-
The historical probability of a student taking a course in this topic in the summer
395-
semester, based on historical data. This field is recomputed nightly from the
396-
`parent_course` graph (in the recompute_soft_state cron job).
397-
"""
398-
),
399-
)
400-
historical_probabilities_fall = models.FloatField(
401-
default=0,
402-
help_text=dedent(
403-
"""
404-
The historical probability of a student taking a course in this topic in the fall
405-
semester, based on historical data. This field is recomputed nightly from the
406-
`parent_course` graph (in the recompute_soft_state cron job).
407-
"""
408-
),
409-
)
410368
branched_from = models.ForeignKey(
411369
"Topic",
412370
related_name="branched_to",
@@ -763,8 +721,7 @@ class Meta:
763721
)
764722

765723
instructors = models.ManyToManyField(
766-
Instructor,
767-
help_text="The Instructor object(s) of the instructor(s) teaching the section.",
724+
Instructor, help_text="The Instructor object(s) of the instructor(s) teaching the section."
768725
)
769726
associated_sections = models.ManyToManyField(
770727
"Section",
@@ -823,8 +780,7 @@ class Meta:
823780
)
824781

825782
registration_volume = models.PositiveIntegerField(
826-
default=0,
827-
help_text="The number of active PCA registrations watching this section.",
783+
default=0, help_text="The number of active PCA registrations watching this section."
828784
) # For the set of PCA registrations for this section, use the related field `registrations`.
829785

830786
def __str__(self):
@@ -881,9 +837,7 @@ def current_percent_open(self):
881837
return None
882838
try:
883839
last_status_update = StatusUpdate.objects.filter(
884-
section=self,
885-
created_at__gt=add_drop_start,
886-
created_at__lt=add_drop_end,
840+
section=self, created_at__gt=add_drop_start, created_at__lt=add_drop_end
887841
).latest("created_at")
888842
except StatusUpdate.DoesNotExist:
889843
last_status_update = None
@@ -928,12 +882,7 @@ class StatusUpdate(models.Model):
928882
A registration status update for a specific section (e.g. CIS-120-001 went from open to close)
929883
"""
930884

931-
STATUS_CHOICES = (
932-
("O", "Open"),
933-
("C", "Closed"),
934-
("X", "Cancelled"),
935-
("", "Unlisted"),
936-
)
885+
STATUS_CHOICES = (("O", "Open"), ("C", "Closed"), ("X", "Cancelled"), ("", "Unlisted"))
937886
section = models.ForeignKey(
938887
Section,
939888
related_name="status_updates",
@@ -968,8 +917,7 @@ class StatusUpdate(models.Model):
968917
# and the save() method of StatusUpdate
969918

970919
in_add_drop_period = models.BooleanField(
971-
default=False,
972-
help_text="Was this status update created during the add/drop period?",
920+
default=False, help_text="Was this status update created during the add/drop period?"
973921
) # This field is maintained in the save() method of alerts.models.AddDropPeriod,
974922
# and the save() method of StatusUpdate
975923

@@ -1100,8 +1048,7 @@ class Room(models.Model):
11001048
),
11011049
)
11021050
number = models.CharField(
1103-
max_length=8,
1104-
help_text="The room number, e.g. `101` for Wu and Chen Auditorium in Levine.",
1051+
max_length=8, help_text="The room number, e.g. `101` for Wu and Chen Auditorium in Levine."
11051052
)
11061053
name = models.CharField(
11071054
max_length=80,
@@ -1540,3 +1487,73 @@ def __str__(self):
15401487
return (
15411488
f"Friendship(Sender: {self.sender}, Recipient: {self.recipient}, Status: {self.status})"
15421489
)
1490+
1491+
class Comment(models.Model):
1492+
"""
1493+
A single comment associated with a topic to be displayed on PCR. Comments support replies
1494+
through the parent_id and path fields. The path field allows for efficient database querying
1495+
and can indicate levels of nesting and can make pagination simpler. Idea implemented based
1496+
on this guide: https://blog.miguelgrinberg.com/post/implementing-user-comments-with-sqlalchemy.
1497+
"""
1498+
1499+
# Log base 10 value of maximum adjacent comment length.
1500+
_N = 10
1501+
1502+
text = models.TextField()
1503+
created_at = models.DateTimeField(auto_now_add=True)
1504+
modified_at = models.DateTimeField(auto_now=True)
1505+
author = models.ForeignKey(
1506+
get_user_model(),
1507+
on_delete = models.SET_NULL,
1508+
null=True,
1509+
related_name="comments"
1510+
)
1511+
upvotes = models.ManyToManyField(
1512+
get_user_model(),
1513+
related_name="upvotes",
1514+
help_text="The number of upvotes a comment gets."
1515+
)
1516+
downvotes = models.ManyToManyField(
1517+
get_user_model(),
1518+
related_name="downvotes",
1519+
help_text="The number of downvotes a comment gets."
1520+
)
1521+
section = models.ForeignKey(
1522+
Section,
1523+
on_delete=models.CASCADE,
1524+
help_text=dedent(
1525+
"""
1526+
The section with which a comment is associated. Section was chosen instead of topics for
1527+
hosting comments because topics are SOFT STATE and are recomputed regularly.
1528+
"""
1529+
),
1530+
null=True
1531+
)
1532+
1533+
base = models.ForeignKey(
1534+
"self",
1535+
on_delete=models.SET_NULL, # redundant due to special deletion conditions
1536+
null=True,
1537+
)
1538+
parent = models.ForeignKey(
1539+
"self",
1540+
on_delete=models.SET_NULL, # similarly redundant
1541+
null=True,
1542+
related_name="children"
1543+
)
1544+
path = models.TextField(db_index=True)
1545+
1546+
def level(self):
1547+
return len(self.path.split('.'))
1548+
1549+
def delete(self, **kwargs):
1550+
if Comment.objects.filter(parent_id=self).exists():
1551+
self.text = "This comment has been removed."
1552+
self.upvotes.clear()
1553+
self.downvotes.clear()
1554+
self.author = None
1555+
self.save()
1556+
else:
1557+
super().delete(**kwargs)
1558+
def __str__(self):
1559+
return f"{self.author}: {self.text}"

backend/courses/serializers.py

+47
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
Section,
1515
StatusUpdate,
1616
UserProfile,
17+
Comment
1718
)
1819
from plan.management.commands.recommendcourses import cosine_similarity
1920

@@ -475,3 +476,49 @@ class FriendshipRequestSerializer(serializers.Serializer):
475476

476477
def to_representation(self, instance):
477478
return super().to_representation(instance)
479+
480+
class CommentSerializer(serializers.ModelSerializer):
481+
author_name = serializers.CharField(source="author.username", read_only=True)
482+
votes = serializers.SerializerMethodField()
483+
section = serializers.CharField(source="section.full_code", read_only=True)
484+
base = serializers.SerializerMethodField()
485+
parent = serializers.SerializerMethodField()
486+
487+
def get_votes(self, obj):
488+
return len(obj.upvotes.values_list('id')) - len(obj.downvotes.values_list('id'))
489+
def get_base(self, obj):
490+
if obj.base is None:
491+
return None
492+
return obj.base.id
493+
def get_parent(self, obj):
494+
if obj.parent is None:
495+
return None
496+
return obj.parent.id
497+
498+
class Meta:
499+
model = Comment
500+
fields = ['id', 'text', 'created_at', 'modified_at', 'author_name', 'votes', 'section', 'base', 'parent', 'path']
501+
502+
class CommentListSerializer(serializers.ModelSerializer):
503+
author_name = serializers.CharField(source="author.username", read_only=True)
504+
votes = serializers.SerializerMethodField()
505+
section = serializers.CharField(source="section.full_code", read_only=True)
506+
base = serializers.SerializerMethodField()
507+
parent = serializers.SerializerMethodField()
508+
user_upvoted = serializers.BooleanField()
509+
user_downvoted = serializers.BooleanField()
510+
511+
def get_votes(self, obj):
512+
return len(obj.upvotes.values_list('id')) - len(obj.downvotes.values_list('id'))
513+
def get_base(self, obj):
514+
if obj.base is None:
515+
return None
516+
return obj.base.id
517+
def get_parent(self, obj):
518+
if obj.parent is None:
519+
return None
520+
return obj.parent.id
521+
522+
class Meta:
523+
model = Comment
524+
fields = ['id', 'text', 'created_at', 'modified_at', 'author_name', 'votes', 'section', 'base', 'parent', 'path', 'user_upvoted', 'user_downvoted']

backend/courses/util.py

+19
Original file line numberDiff line numberDiff line change
@@ -771,3 +771,22 @@ def normalize_and_round(prob, i):
771771
[semester_probabilities["A"], semester_probabilities["B"], semester_probabilities["C"]],
772772
)
773773
)
774+
775+
def get_section_from_course_instructor_semester(course_code, professors, semester):
776+
"""
777+
Attempts to return a course section that matches the given parameters.
778+
ValueError is raised if the section does not exist.
779+
"""
780+
sections = Section.objects.prefetch_related('instructors').filter(
781+
course__full_code=course_code,
782+
course__semester=semester
783+
)
784+
785+
professors_query = Q(instructors__name=professors[0])
786+
for professor in professors[1:]:
787+
professors_query &= Q(instructors__name=professor)
788+
matching_sections = sections.filter(professors_query).distinct()
789+
790+
if matching_sections.count() == 1:
791+
return matching_sections.first()
792+
raise ValueError(f"No section exists with course code ({course_code}), professor ({professors[0]}), semester ({semester})")

0 commit comments

Comments
 (0)