Skip to content

Commit

Permalink
Merge branch 'fuzzy-professors' into update-courses-no-descriptions
Browse files Browse the repository at this point in the history
  • Loading branch information
nsandler1 committed Nov 10, 2022
2 parents 6483460 + 4455081 commit 76b27b1
Show file tree
Hide file tree
Showing 8 changed files with 88 additions and 61 deletions.
13 changes: 11 additions & 2 deletions api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
76 changes: 37 additions & 39 deletions home/forms/admin_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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 = '''
<tr>
<td><a href="{absolute_url}" target="_blank">{verified_name}</a></td>
<td>{verified_courses}</td>
<td><button class="btn btn-primary" onclick="mergeProfessor({merge_args})">Merge</button></td>
</tr>
'''

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 = '''
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Courses</th>
</tr>
</thead>
<table class="table text-center w-100">
<tbody>
<tr>
<td>{unverified_name} (this professor)</td>
<td>{unverified_courses}</td>
</tr>
<tr>
<td><a href="{absolute_url}" target="_blank">{verified_name}</a></td>
<td>{verified_courses}</td>
</tr>
'''
for professor in self.verified_professors:
table_str += create_row(professor)

table_str += '''
</tbody>
</table>
'''
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 <b>{self.verified_professor.name} ({self.verified_professor.pk})</b>. <br>'
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. <br>'
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,
Expand All @@ -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"
)
)
2 changes: 1 addition & 1 deletion home/forms/professor_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
12 changes: 12 additions & 0 deletions home/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -264,6 +266,16 @@ def average_rating(self):
def get_absolute_url(self):
return reverse("professor", kwargs={"slug": self.slug})

@staticmethod
def find_similar(professor_name):
SIMILARITY_TOLERANCE = 70
similar_professors = []
for professor in Professor.verified.all():
if fuzz.ratio(professor_name, professor.name) > SIMILARITY_TOLERANCE:
similar_professors.append(professor)

return similar_professors

def __str__(self):
return f"{self.name} ({self.id})"

Expand Down
12 changes: 9 additions & 3 deletions home/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
else:
year = now.year
# spring (of next year)
if 9 < now.month:
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
Expand Down
31 changes: 15 additions & 16 deletions home/views/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -293,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_professors = 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 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)

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 <b>{new_slug}</b> 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
Expand All @@ -329,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:
Expand Down
1 change: 1 addition & 0 deletions home/views/professor.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def get(self, request, slug):
Course.recent
.filter(professors__pk=professor.pk)
.order_by("name")
.distinct()
)

courses_reviewed = []
Expand Down
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ google-api-python-client
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
Expand Down

0 comments on commit 76b27b1

Please sign in to comment.