From 117b408c0895568d9b8380cd482ebb05f5720791 Mon Sep 17 00:00:00 2001 From: nsandler1 Date: Fri, 4 Nov 2022 00:50:01 -0400 Subject: [PATCH 01/16] display distinct list of courses taught on /professor --- home/views/professor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/home/views/professor.py b/home/views/professor.py index ac4d5fd4..a35fff66 100644 --- a/home/views/professor.py +++ b/home/views/professor.py @@ -32,7 +32,7 @@ def get(self, request, slug): courses_taught = ( Course.recent .filter(professors__pk=professor.pk) - .order_by("name") + .order_by("name").distinct() ) courses_reviewed = [] From b25b91af00bec13269244ad896e5edf3f6187f92 Mon Sep 17 00:00:00 2001 From: nsandler1 Date: Fri, 4 Nov 2022 00:52:25 -0400 Subject: [PATCH 02/16] formatting --- home/views/professor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/home/views/professor.py b/home/views/professor.py index a35fff66..5797df00 100644 --- a/home/views/professor.py +++ b/home/views/professor.py @@ -32,7 +32,8 @@ def get(self, request, slug): courses_taught = ( Course.recent .filter(professors__pk=professor.pk) - .order_by("name").distinct() + .order_by("name") + .distinct() ) courses_reviewed = [] From 1ecdbf6848e3de7d9eddf5a29d5448dae7ff76c6 Mon Sep 17 00:00:00 2001 From: nsandler1 Date: Fri, 4 Nov 2022 01:25:15 -0400 Subject: [PATCH 03/16] make course dropdown distinct on /professor --- home/forms/professor_forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/home/forms/professor_forms.py b/home/forms/professor_forms.py index ce8546e5..6a3b1047 100644 --- a/home/forms/professor_forms.py +++ b/home/forms/professor_forms.py @@ -205,7 +205,7 @@ class ProfessorFormReview(ProfessorForm): def __init__(self, user, professor, **kwargs): super().__init__(user, Review.ReviewType.REVIEW, **kwargs) - courses = professor.course_set.order_by("name") + courses = professor.course_set.order_by("name").distinct() choices = [(course.name, course.name) for course in courses] choices.insert(0, ('', "Course")) choices.append(("other", "Other")) From 818a410c1f2ce2c1f80506681fffa2378996a6f7 Mon Sep 17 00:00:00 2001 From: nsandler1 Date: Sun, 6 Nov 2022 19:25:15 -0500 Subject: [PATCH 04/16] add review rating to embed --- home/management/commands/importgradedata.py | 7 +++++-- home/views/admin.py | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/home/management/commands/importgradedata.py b/home/management/commands/importgradedata.py index 0d5ebf04..0f00e0c8 100644 --- a/home/management/commands/importgradedata.py +++ b/home/management/commands/importgradedata.py @@ -43,8 +43,11 @@ def handle(self, *args, **options): for row in reader: self.add_grade(row) - grades = Grade.unfiltered.bulk_create(self.grades) - self.stdout.write(f"Done, added {len(grades)} grades") + try: + grades = Grade.unfiltered.bulk_create(self.grades) + self.stdout.write(f"Done, added {len(grades)} grades") + except Exception as e: + print(e) if self.reject_rows: print(f"Exporting {len(self.reject_rows)} rejected rows...") diff --git a/home/views/admin.py b/home/views/admin.py index d725842c..274fca5f 100644 --- a/home/views/admin.py +++ b/home/views/admin.py @@ -86,6 +86,7 @@ def post(self, request): username = "Anonymous" if (review.anonymous or not review.user) else review.user.username embed.add_embed_field(name="Reviewer", value=username, inline=True) + embed.add_embed_field(name="Rating", value=review.rating, inline=True) embed.add_embed_field(name="Course", value=course, inline=True) embed.add_embed_field(name="Grade", value=grade, inline=True) embed.add_embed_field(name="Review", value=review_text, inline=False) From 5b0ba599f8d921f3430aaf6388a198ce064b75d7 Mon Sep 17 00:00:00 2001 From: nsandler1 Date: Sun, 6 Nov 2022 19:54:31 -0500 Subject: [PATCH 05/16] revert accidental changes --- home/management/commands/importgradedata.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/home/management/commands/importgradedata.py b/home/management/commands/importgradedata.py index 0f00e0c8..5c402548 100644 --- a/home/management/commands/importgradedata.py +++ b/home/management/commands/importgradedata.py @@ -43,11 +43,9 @@ def handle(self, *args, **options): for row in reader: self.add_grade(row) - try: - grades = Grade.unfiltered.bulk_create(self.grades) - self.stdout.write(f"Done, added {len(grades)} grades") - except Exception as e: - print(e) + + grades = Grade.unfiltered.bulk_create(self.grades) + self.stdout.write(f"Done, added {len(grades)} grades") if self.reject_rows: print(f"Exporting {len(self.reject_rows)} rejected rows...") From 85418a496c4dc2019a126bbfb5bfba2ac328b4a8 Mon Sep 17 00:00:00 2001 From: nsandler1 Date: Sun, 6 Nov 2022 19:56:28 -0500 Subject: [PATCH 06/16] remove space --- home/management/commands/importgradedata.py | 1 - 1 file changed, 1 deletion(-) diff --git a/home/management/commands/importgradedata.py b/home/management/commands/importgradedata.py index 5c402548..0d5ebf04 100644 --- a/home/management/commands/importgradedata.py +++ b/home/management/commands/importgradedata.py @@ -43,7 +43,6 @@ def handle(self, *args, **options): for row in reader: self.add_grade(row) - grades = Grade.unfiltered.bulk_create(self.grades) self.stdout.write(f"Done, added {len(grades)} grades") From 190d7b98df61794dda2d532df357d45a2fa075c2 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 6 Nov 2022 20:33:56 -0500 Subject: [PATCH 07/16] fix invalid filtering after slice in /professors serializer --- api/serializers.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/api/serializers.py b/api/serializers.py index 110e4fc0..21fbad41 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -48,8 +48,17 @@ def to_representation(self, data): class ProfessorListSerializer(ListSerializer): def to_representation(self, data): - data = data.filter(status=Professor.Status.VERIFIED) - return super().to_representation() + # one would expect to be able to write + # `data.filter(status=Professor.Status.VERIFIED)` here, but filtering an + # already-sliced queryset is prohibited by django due to ambiguity + # concerns: https://docs.djangoproject.com/en/3.2/ref/models/querysets/. + # I can't see how it would be ambiguous unless you did something like + # slice-filter-slice, but oh well. + # We should be careful in the future that this doesn't cause performance + # issues; this relies on slicing already having occured before we + # receive `data`, limiting it to a reasonable 100 professors. + data = [p for p in data if p.status == Professor.Status.VERIFIED] + return super().to_representation(data) class ReviewsSerializer(ModelSerializer): course = CourseField() From d538c1052942849fac771995a2e0af790eda52a1 Mon Sep 17 00:00:00 2001 From: nsandler1 Date: Wed, 9 Nov 2022 16:52:01 -0500 Subject: [PATCH 08/16] Add fuzzywuzzy as a requirement --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 9cb864d7..8bb11ae7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ google-api-python-client google-auth-httplib2 mysqlclient==2.1.0 django==3.2.4 +fuzzywuzzy==0.18.0 # django addons django-tables2==2.4.0 From 8a0a3c20cfab646d06787fea97cc0104e979d7b2 Mon Sep 17 00:00:00 2001 From: nsandler1 Date: Wed, 9 Nov 2022 17:24:10 -0500 Subject: [PATCH 09/16] create Professor.find_similar() --- home/models.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/home/models.py b/home/models.py index 28db682a..0cd4c979 100644 --- a/home/models.py +++ b/home/models.py @@ -11,6 +11,8 @@ CASCADE, ManyToManyField, SlugField, TextChoices, FloatField, Manager, QuerySet, Sum, UniqueConstraint, Index, Count) +from fuzzywuzzy import fuzz + class GradeQuerySet(QuerySet): def exclude_pf(self): @@ -264,6 +266,15 @@ def average_rating(self): def get_absolute_url(self): return reverse("professor", kwargs={"slug": self.slug}) + @staticmethod + def find_similar(professor_name): + SIMILARITY_TOLERANCE = 80 + for professor in Professor.verified.all(): + if fuzz.ratio(professor_name, professor.name) > SIMILARITY_TOLERANCE: + return professor + + return None + def __str__(self): return f"{self.name} ({self.id})" From 0a685f1d53d86d271751b416252bfa58ab1b1d3e Mon Sep 17 00:00:00 2001 From: nsandler1 Date: Wed, 9 Nov 2022 17:24:41 -0500 Subject: [PATCH 10/16] use Professor.find_similar() --- home/views/admin.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/home/views/admin.py b/home/views/admin.py index 274fca5f..607e1438 100644 --- a/home/views/admin.py +++ b/home/views/admin.py @@ -3,11 +3,11 @@ from django.shortcuts import render from django.views import View -from django.db.models import Q from django.http import JsonResponse from django.template.context_processors import csrf from django.contrib.auth.mixins import UserPassesTestMixin from django.urls import reverse +from django.utils.safestring import mark_safe from crispy_forms.utils import render_crispy_form from discord_webhook import DiscordWebhook, DiscordEmbed @@ -294,34 +294,32 @@ def verify_professor(self, verified_status: Professor.Status, slug: Optional[str response["error_msg"] = self.not_found_err("Professor") return JsonResponse(response) if not professor.slug and slug is None: - # Attempt to create slug automatically - split_name = str(professor.name).strip().split(" ") - first_name = split_name[0].lower().strip() - last_name = split_name[-1].lower().strip() - - query = Professor.verified.filter( - ( - Q(name__istartswith=first_name) & - Q(name__iendswith=last_name) - ) | - Q(slug="_".join(reversed(split_name)).lower()) - ) ctx = {} ctx.update(csrf(request)) + similar_professor = Professor.find_similar(professor.name) verify_override = json.loads(request.POST["override"]) - if not verify_override and query.exists(): - form = ProfessorInfoModal(professor, query[0]) + + if not verify_override and similar_professor: + form = ProfessorInfoModal(professor, similar_professor) response["form"] = render_crispy_form(form, form.helper, context=ctx) response["success_msg"] = "#info-modal-container" return JsonResponse(response) + split_name = str(professor.name).strip().split(" ") + new_slug = "_".join(reversed(split_name)).lower() + modal_msg = None + + if Professor.verified.filter(slug=new_slug).exists(): + modal_msg = mark_safe(f"The slug {new_slug} already belongs to a professor. Please enter a slug below.") + if len(split_name) > 2: modal_msg = ( f"The name '{professor.name}' is too long and " "can't be slugged automatcially. Please enter a slug below." ) + if modal_msg: # Create the modal form to manualy enter a slug and add it # to the response. The form creates the modal, though it's # actually summoned from admin-action.js @@ -330,7 +328,7 @@ def verify_professor(self, verified_status: Professor.Status, slug: Optional[str response["success_msg"] = "#slug-modal-container" return JsonResponse(response) - professor.slug = "_".join(reversed(split_name)).lower() + professor.slug = new_slug else: professor.slug = None if verified_status is Professor.Status.REJECTED: From b3798fe5fc5d263f21689d47cce0acfe4f88508f Mon Sep 17 00:00:00 2001 From: nsandler1 Date: Wed, 9 Nov 2022 18:20:07 -0500 Subject: [PATCH 11/16] Add another dependency --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 8bb11ae7..fb139b40 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,7 @@ google-auth-httplib2 mysqlclient==2.1.0 django==3.2.4 fuzzywuzzy==0.18.0 +python-Levenshtein==0.20.8 # django addons django-tables2==2.4.0 From 527810a6441f45ca6a40c174ed5adec4f1121a44 Mon Sep 17 00:00:00 2001 From: nsandler1 Date: Wed, 9 Nov 2022 20:55:11 -0500 Subject: [PATCH 12/16] Professor.find_similar() returns list of similar professors --- home/models.py | 7 ++++--- home/views/admin.py | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/home/models.py b/home/models.py index 0cd4c979..9381708f 100644 --- a/home/models.py +++ b/home/models.py @@ -268,12 +268,13 @@ def get_absolute_url(self): @staticmethod def find_similar(professor_name): - SIMILARITY_TOLERANCE = 80 + SIMILARITY_TOLERANCE = 70 + similar_professors = [] for professor in Professor.verified.all(): if fuzz.ratio(professor_name, professor.name) > SIMILARITY_TOLERANCE: - return professor + similar_professors.append(professor) - return None + return similar_professors def __str__(self): return f"{self.name} ({self.id})" diff --git a/home/views/admin.py b/home/views/admin.py index 607e1438..5dcb46d1 100644 --- a/home/views/admin.py +++ b/home/views/admin.py @@ -297,11 +297,11 @@ def verify_professor(self, verified_status: Professor.Status, slug: Optional[str ctx = {} ctx.update(csrf(request)) - similar_professor = Professor.find_similar(professor.name) + similar_professors = Professor.find_similar(professor.name) verify_override = json.loads(request.POST["override"]) - if not verify_override and similar_professor: - form = ProfessorInfoModal(professor, similar_professor) + if not verify_override and len(similar_professors) > 0: + form = ProfessorInfoModal(professor, similar_professors) response["form"] = render_crispy_form(form, form.helper, context=ctx) response["success_msg"] = "#info-modal-container" return JsonResponse(response) From d70d1d65ef52785866708bf25feea003829b924c Mon Sep 17 00:00:00 2001 From: nsandler1 Date: Wed, 9 Nov 2022 20:55:28 -0500 Subject: [PATCH 13/16] Modify InfoModal to display list of professors --- home/forms/admin_forms.py | 76 +++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 39 deletions(-) diff --git a/home/forms/admin_forms.py b/home/forms/admin_forms.py index 22fe4d63..caab6259 100644 --- a/home/forms/admin_forms.py +++ b/home/forms/admin_forms.py @@ -531,10 +531,10 @@ def clean(self): # Used on /admin when verifying professors that might be duplicates of already # verified professors. class ProfessorInfoModal(Form): - def __init__(self, unverified_professor: Professor, verified_professor: Professor): + def __init__(self, unverified_professor, verified_professors): super().__init__() self.unverified_professor = unverified_professor - self.verified_professor = verified_professor + self.verified_professors = verified_professors self.helper = FormHelper() self.helper.form_tag = False self.helper.layout = self.generate_layout() @@ -557,44 +557,46 @@ def get_courses(professor: Professor): return "No Courses" if len(courses) == 0 else ', '.join(course.name for course in courses) + def create_row(professor: Professor): + row = ''' + + {verified_name} + {verified_courses} + + + ''' + + merge_data = { + "merge_subject": self.unverified_professor.name, + "subject_id": self.unverified_professor.pk, + "merge_target": professor.name, + "target_id": professor.pk + } + + kwargs = { + "absolute_url": professor.get_absolute_url(), + "verified_name": professor.name, + "verified_courses": get_courses(professor), + "merge_args": merge_data + } + return format_html(row, **kwargs) + table_str = ''' - - - - - - - +
NameCourses
- - - - - - - - + ''' + for professor in self.verified_professors: + table_str += create_row(professor) + + table_str += '''
{unverified_name} (this professor){unverified_courses}
{verified_name}{verified_courses}
''' - kwargs = { - "unverified_name": self.unverified_professor.name, - "unverified_courses": get_courses(self.unverified_professor), - "absolute_url": self.verified_professor.get_absolute_url(), - "verified_name": self.verified_professor.name, - "verified_courses": get_courses(self.verified_professor) - } modal_title = ( - f'This {self.unverified_professor.type} might be a duplicate of {self.verified_professor.name} ({self.verified_professor.pk}).
' - f'Given the information below, please decide if these {self.unverified_professor.type}s are the same.' + f'This {self.unverified_professor.type} might be a duplicate of one of the professors below.
' + f'{self.unverified_professor.name} has taught: {get_courses(self.unverified_professor)}' ) - merge_data = { - "merge_subject": self.unverified_professor.name, - "subject_id": self.unverified_professor.pk, - "merge_target": self.verified_professor.name, - "target_id": self.verified_professor.pk - } verify_data = { "professor_id": self.unverified_professor.pk, @@ -604,14 +606,10 @@ def get_courses(professor: Professor): return Layout( Modal( - HTML(format_html(table_str, **kwargs)), - Div( - Button("verify", "Verify", css_class="btn btn-success", onclick=format_html("verifyProfessor({args})", args=verify_data)), - Button("merge", "Merge", css_class="btn btn-primary", onclick=format_html("mergeProfessor({args})", args=merge_data)), - css_class="btn-group w-100" - ), + HTML(table_str), + Button("verify", "Verify", css_class="btn btn-success w-100", onclick=format_html("verifyProfessor({args})", args=verify_data)), css_id="info-modal", title=format_html(modal_title), - title_class="text-center" + title_class="text-center w-100" ) ) From d869d4e11196a9aefed650de4065c2a3cccc0060 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Thu, 10 Nov 2022 12:17:43 -0500 Subject: [PATCH 14/16] fix current semester calculation for months >9 --- home/utils.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/home/utils.py b/home/utils.py index 29a3c4de..bb169f08 100644 --- a/home/utils.py +++ b/home/utils.py @@ -69,14 +69,20 @@ def from_name(cls, name): @staticmethod def current(): now = datetime.now() + # spring (current year) + if now.month < 3: + semester = "01" + year = now.year # fall if 3 <= now.month <= 9: semester = "08" - # spring + year = now.year + # spring (of next year) else: semester = "01" + year = now.year + 1 - return Semester(f"{now.year}{semester}") + return Semester(f"{year}{semester}") def name(self, *, year_first=False, short=False): year = self.year From 44185921da3395a9af98f4280638bdd9833881fa Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Thu, 10 Nov 2022 12:25:58 -0500 Subject: [PATCH 15/16] fix nonexclusive if --- home/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/home/utils.py b/home/utils.py index bb169f08..ca187cc6 100644 --- a/home/utils.py +++ b/home/utils.py @@ -78,7 +78,7 @@ def current(): semester = "08" year = now.year # spring (of next year) - else: + if 10 < now.month: semester = "01" year = now.year + 1 From 175db693de7cea8b029440dfcf3d1e95f2e6347e Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Thu, 10 Nov 2022 14:48:26 -0500 Subject: [PATCH 16/16] fix range, again --- home/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/home/utils.py b/home/utils.py index ca187cc6..4f185c5a 100644 --- a/home/utils.py +++ b/home/utils.py @@ -78,7 +78,7 @@ def current(): semester = "08" year = now.year # spring (of next year) - if 10 < now.month: + if 9 < now.month: semester = "01" year = now.year + 1