Skip to content

Feat/search bar/backend #472

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Open
wants to merge 6 commits into
base: feat/coord-interface/old
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions csm_web/scheduler/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@
path("matcher/<int:pk>/mentors/", views.matcher.mentors),
path("matcher/<int:pk>/configure/", views.matcher.configure),
path("matcher/<int:pk>/create/", views.matcher.create),
path("search/sections/", views.get_sections_of_user, name="get_sections_of_user"),
]
3 changes: 2 additions & 1 deletion csm_web/scheduler/views/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from .course import CourseViewSet
from . import matcher
from .course import CourseViewSet
from .profile import ProfileViewSet
from .resource import ResourceViewSet
from .search import get_sections_of_user
from .section import SectionViewSet
from .spacetime import SpacetimeViewSet
from .student import StudentViewSet
Expand Down
154 changes: 154 additions & 0 deletions csm_web/scheduler/views/search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
from django.db.models import Count, Q
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.exceptions import PermissionDenied
from rest_framework.response import Response
from scheduler.models import Section, Student
from scheduler.serializers import SectionSerializer, StudentSerializer


@api_view(["GET"])
def get_sections_of_user(request):
"""
Gets sections and associated students based on query parameters.

Query Parameters:
- `query`: (optional) Search query to filter sections and students by name or email.
- `student_absences`: (optional) Filter students by num absences (exact int or int range).

Returns:
- JSON response containing sections matching the query, along with associated student details.

Raises:
- PermissionDenied: If user lacks permission to search sections.
- HTTP 400 Bad Request: If neither query parameters provided,
or invalid value for `student_absences`.

Note:
- Only `query` provided: returns sections & students matching the query.
- Only `student_absences` provided: returns sections & students filtered by absences.
- Both `query` and `student_absences` provided: returns sections and students
filtered by both query and absences.
"""

is_coordinator = bool(
request.user.coordinator_set.filter(user=request.user).count()
)
if not is_coordinator:
raise PermissionDenied(
"You are not authorized to search through sections of this course."
)

query = request.query_params.get("query", "")
student_absences = request.query_params.get("student_absences", None)

if not query and student_absences is None:
return Response(
{"error": "Please provide a query"}, status=status.HTTP_400_BAD_REQUEST
)

sections = Section.objects.all()

# Fetch courses associated with the user's coordinator role
courses = request.user.coordinator_set.values_list("course", flat=True)

# Filter sections based on the courses associated with the user's coordinator role
sections = sections.filter(mentor__course__in=courses)

if query:
sections = sections.filter(
# pylint: disable-next=unsupported-binary-operation
Q(students__user__first_name__icontains=query)
| Q(students__user__last_name__icontains=query)
| Q(students__user__email__icontains=query)
| Q(mentor__user__first_name__icontains=query)
| Q(mentor__user__last_name__icontains=query)
| Q(mentor__user__email__icontains=query)
).distinct()

students = (
Student.objects.filter(
# pylint: disable-next=unsupported-binary-operation
Q(user__first_name__icontains=query)
| Q(user__last_name__icontains=query)
| Q(user__email__icontains=query),
section__in=sections,
)
.distinct()
.annotate(
num_absences=Count("attendance", filter=Q(attendance__presence="UN"))
)
)
student_query_results = students

if student_absences is not None:
try:
# Check if the query is a range or single number
if "-" in student_absences:
start, end = student_absences.split("-")
students = (
Student.objects.annotate(
num_absences=Count(
"attendance", filter=Q(attendance__presence="UN")
)
)
.filter(
num_absences__gte=start,
num_absences__lte=end,
section__in=sections,
)
.distinct()
)
else:
num_absences = int(student_absences)
students = (
Student.objects.annotate(
num_absences=Count(
"attendance", filter=Q(attendance__presence="UN")
)
)
.filter(num_absences=num_absences, section__in=sections)
.distinct()
)
sections = sections.filter(students__in=students).distinct()
except ValueError:
return Response(
{
"error": (
"Invalid value for student_absences. Please provide an integer"
" or an integer range of format start-end."
)
},
status=status.HTTP_400_BAD_REQUEST,
)
student_absences_results = students

if query and student_absences:
# Filter students based on query
students = student_query_results.intersection(student_absences_results)

section_serializer = SectionSerializer(
sections, many=True, context={"request": request}
)

student_data = []
for student in students:
# attendance_data = student.attendance.filter(presence="UN")
student_serializer = StudentSerializer(student)
student_data.append(
{
"id": student_serializer.data["id"],
"name": student_serializer.data["name"],
"email": student_serializer.data["email"],
"section": student_serializer.data["section"],
"mentor": student.section.mentor.user.get_full_name(),
"num_absences": student.num_absences,
}
)

combined_data = {
"sections": section_serializer.data,
"students": student_data,
}

return Response(combined_data, status=status.HTTP_200_OK)
46 changes: 16 additions & 30 deletions csm_web/scheduler/views/section.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ def students(self, request, pk=None):
Error status format:
- 'OK': student is ready to be enrolled (but no action has been taken)
- 'CONFLICT': student is already enrolled in another section
- if there is a cnoflicting section:
- if there is a conflicting section:
detail = { 'section': serialized section }
- if user can't enroll:
detail = { 'reason': reason }
Expand Down Expand Up @@ -356,10 +356,8 @@ class RestrictedAction:
if student_queryset.count() > 1:
# something bad happened, return immediately with error
logger.error(
(
"<Enrollment:Critical> Multiple student objects exist in the"
" database (Students %s)!"
),
"<Enrollment:Critical> Multiple student objects exist in the"
" database (Students %s)!",
student_queryset.all(),
)
return Response(
Expand Down Expand Up @@ -536,10 +534,8 @@ class RestrictedAction:
)
student.save()
logger.info(
(
"<Enrollment:Success> User %s swapped into Section %s from"
" Section %s"
),
"<Enrollment:Success> User %s swapped into Section %s from"
" Section %s",
log_str(student.user),
log_str(section),
log_str(old_section),
Expand All @@ -564,26 +560,20 @@ def _student_add(self, request, section):
"""
if not request.user.can_enroll_in_course(section.mentor.course):
logger.warning(
(
"<Enrollment:Failure> User %s was unable to enroll in Section %s"
" because they are already involved in this course"
),
"<Enrollment:Failure> User %s was unable to enroll in Section %s"
" because they are already involved in this course",
log_str(request.user),
log_str(section),
)
raise PermissionDenied(
(
"You are already either mentoring for this course or enrolled in a"
" section, or the course is closed for enrollment"
),
"You are already either mentoring for this course or enrolled in a"
" section, or the course is closed for enrollment",
status.HTTP_422_UNPROCESSABLE_ENTITY,
)
if section.current_student_count >= section.capacity:
logger.warning(
(
"<Enrollment:Failure> User %s was unable to enroll in Section %s"
" because it was full"
),
"<Enrollment:Failure> User %s was unable to enroll in Section %s"
" because it was full",
log_str(request.user),
log_str(section),
)
Expand All @@ -596,18 +586,14 @@ def _student_add(self, request, section):
)
if student_queryset.count() > 1:
logger.error(
(
"<Enrollment:Critical> Multiple student objects exist in the"
" database (Students %s)!"
),
"<Enrollment:Critical> Multiple student objects exist in the"
" database (Students %s)!",
student_queryset.all(),
)
return PermissionDenied(
(
"An internal error occurred; email mentors@berkeley.edu"
" immediately. (Duplicate students exist in the database (Students"
f" {student_queryset.all()}))"
),
"An internal error occurred; email mentors@berkeley.edu"
" immediately. (Duplicate students exist in the database (Students"
f" {student_queryset.all()}))",
code=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
if student_queryset.count() == 1:
Expand Down
Loading