This repository has been archived by the owner on Feb 5, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #450 from SELab-2/feature/endpoints-student-analysis
Student Analysis Endpoints
- Loading branch information
Showing
18 changed files
with
6,367 additions
and
4,531 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
from datetime import timedelta, datetime | ||
from typing import List | ||
|
||
from rest_framework import serializers | ||
|
||
from base.models import StudentOnTour, RemarkAtBuilding | ||
|
||
|
||
def validate_student_on_tours(student_on_tours): | ||
# raise an error if the student_on_tours queryset is not provided | ||
if student_on_tours is None: | ||
raise serializers.ValidationError("student_on_tours must be provided to serialize data") | ||
|
||
return student_on_tours | ||
|
||
|
||
class WorkedHoursAnalysisSerializer(serializers.BaseSerializer): | ||
def to_representation(self, student_on_tours: List[StudentOnTour]): | ||
# create an empty dictionary to store worked hours data for each student | ||
student_data = {} | ||
|
||
# iterate over the list of student_on_tours objects | ||
for sot in student_on_tours: | ||
# get the student id for the current StudentOnTour object | ||
student_id = sot.student.id | ||
|
||
# calculate the worked hours for the current StudentOnTour object | ||
if sot.completed_tour and sot.started_tour: | ||
worked_time = sot.completed_tour - sot.started_tour | ||
else: | ||
worked_time = timedelta() | ||
|
||
# convert the worked hours to minutes | ||
worked_minutes = int(worked_time.total_seconds() // 60) | ||
|
||
# if we've seen this student before, update their worked hours and student_on_tour_ids | ||
if student_id in student_data: | ||
student_data[student_id]["worked_minutes"] += worked_minutes | ||
student_data[student_id]["student_on_tour_ids"].append(sot.id) | ||
# otherwise, add a new entry for this student | ||
else: | ||
student_data[student_id] = { | ||
"student_id": student_id, | ||
"worked_minutes": worked_minutes, | ||
"student_on_tour_ids": [sot.id], | ||
} | ||
|
||
# return the list of student data dictionaries | ||
return list(student_data.values()) | ||
|
||
|
||
class StudentOnTourAnalysisSerializer(serializers.BaseSerializer): | ||
def to_representation(self, remarks_at_buildings: List[RemarkAtBuilding]): | ||
building_data = {} | ||
|
||
for rab in remarks_at_buildings: | ||
# get the building id for the current RemarkAtBuilding object | ||
building_id = rab.building.id | ||
# add a dict if we haven't seen this building before | ||
if building_id not in building_data: | ||
# Convert the TimeField to a datetime object with today's date | ||
today = datetime.today() | ||
transformed_datetime = datetime.combine(today, rab.building.duration) | ||
expected_duration_in_seconds = ( | ||
transformed_datetime.time().second | ||
+ (transformed_datetime.time().minute * 60) | ||
+ (transformed_datetime.time().hour * 3600) | ||
) | ||
building_data[building_id] = { | ||
"building_id": building_id, | ||
"expected_duration_in_seconds": expected_duration_in_seconds, | ||
} | ||
|
||
if rab.type == "AA": | ||
building_data[building_id]["arrival_time"] = rab.timestamp | ||
elif rab.type == "VE": | ||
building_data[building_id]["departure_time"] = rab.timestamp | ||
|
||
for building_id, building_info in building_data.items(): | ||
# calculate the duration of the visit | ||
if "arrival_time" in building_info and "departure_time" in building_info: | ||
duration = building_info["departure_time"] - building_info["arrival_time"] | ||
# add the duration in seconds to the building info | ||
building_info["duration_in_seconds"] = round(duration.total_seconds()) | ||
|
||
# return the list of building data dictionaries | ||
return list(building_data.values()) |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
from django.urls import path | ||
|
||
from analysis.views import WorkedHoursAnalysis, StudentOnTourAnalysis | ||
|
||
urlpatterns = [ | ||
path("worked-hours/", WorkedHoursAnalysis.as_view(), name="worked-hours-analysis"), | ||
path("student-on-tour/<int:student_on_tour_id>/", StudentOnTourAnalysis.as_view(), name="student-on-tour-analysis"), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
from django.core.exceptions import BadRequest | ||
from drf_spectacular.types import OpenApiTypes | ||
from drf_spectacular.utils import extend_schema, OpenApiResponse, OpenApiExample, inline_serializer | ||
from rest_framework import status | ||
from rest_framework.permissions import IsAuthenticated | ||
from rest_framework.response import Response | ||
from rest_framework.views import APIView | ||
from rest_framework import serializers | ||
|
||
from analysis.serializers import WorkedHoursAnalysisSerializer, StudentOnTourAnalysisSerializer | ||
from base.models import StudentOnTour, RemarkAtBuilding | ||
from base.permissions import IsAdmin, IsSuperStudent | ||
from util.request_response_util import ( | ||
get_success, | ||
get_filter_object, | ||
filter_instances, | ||
bad_request_custom_error_message, | ||
not_found, | ||
param_docs, | ||
) | ||
|
||
|
||
class WorkedHoursAnalysis(APIView): | ||
permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent] | ||
serializer_class = WorkedHoursAnalysisSerializer | ||
|
||
@extend_schema( | ||
description="Get all worked hours for each student for a certain period", | ||
parameters=param_docs( | ||
{ | ||
"start-date": ("Filter by start-date", True, OpenApiTypes.DATE), | ||
"end-date": ("Filter by end-date", True, OpenApiTypes.DATE), | ||
"region_id": ("Filter by region id", False, OpenApiTypes.INT), | ||
} | ||
), | ||
responses={ | ||
200: OpenApiResponse( | ||
description="All worked hours for each student for a certain period", | ||
response=inline_serializer( | ||
name="WorkedHoursAnalysisResponse", | ||
fields={ | ||
"student_id": serializers.IntegerField(), | ||
"worked_minutes": serializers.IntegerField(), | ||
"student_on_tour_ids": serializers.ListField(child=serializers.IntegerField()), | ||
}, | ||
), | ||
examples=[ | ||
OpenApiExample( | ||
"Successful Response 1", | ||
value={"student_id": 6, "worked_minutes": 112, "student_on_tour_ids": [1, 6, 9, 56, 57]}, | ||
), | ||
OpenApiExample( | ||
"Successful Response 2", | ||
value={"student_id": 7, "worked_minutes": 70, "student_on_tour_ids": [2, 26]}, | ||
), | ||
], | ||
), | ||
}, | ||
) | ||
def get(self, request): | ||
""" | ||
Get all worked hours for each student for a certain period | ||
""" | ||
student_on_tour_instances = StudentOnTour.objects.all() | ||
filters = { | ||
"start_date": get_filter_object("date__gte", required=True), | ||
"end_date": get_filter_object("date__lte", required=True), | ||
"region_id": get_filter_object("tour__region__id"), | ||
} | ||
|
||
try: | ||
student_on_tour_instances = filter_instances(request, student_on_tour_instances, filters) | ||
except BadRequest as e: | ||
return bad_request_custom_error_message(str(e)) | ||
|
||
serializer = self.serializer_class() | ||
serialized_data = serializer.to_representation(student_on_tour_instances) | ||
return Response(serialized_data, status=status.HTTP_200_OK) | ||
|
||
|
||
class StudentOnTourAnalysis(APIView): | ||
permission_classes = [IsAuthenticated, IsAdmin | IsSuperStudent] | ||
serializer_class = StudentOnTourAnalysisSerializer | ||
|
||
@extend_schema( | ||
description="Get a detailed view on a student on tour's timings", | ||
responses={ | ||
200: OpenApiResponse( | ||
description="A list of buildings and their timings on this student on tour", | ||
response=inline_serializer( | ||
name="DetailedStudentOnTourTimings", | ||
fields={ | ||
"building_id": serializers.IntegerField(), | ||
"expected_duration_in_seconds": serializers.IntegerField(), | ||
"arrival_time": serializers.DateTimeField(), | ||
"departure_time": serializers.DateTimeField(), | ||
"duration_in_seconds": serializers.IntegerField(), | ||
}, | ||
), | ||
examples=[ | ||
OpenApiExample( | ||
"Successful Response 1", | ||
value={ | ||
"building_id": 2, | ||
"expected_duration_in_seconds": 2700, | ||
"arrival_time": "2023-05-08T08:01:52.264000Z", | ||
"departure_time": "2023-05-08T08:07:49.868000Z", | ||
"duration_in_seconds": 358, | ||
}, | ||
), | ||
OpenApiExample( | ||
"Successful Response 2", | ||
value={ | ||
"building_id": 11, | ||
"expected_duration_in_seconds": 3600, | ||
"arrival_time": "2023-05-08T08:08:04.693000Z", | ||
"departure_time": "2023-05-08T08:08:11.714000Z", | ||
"duration_in_seconds": 7, | ||
}, | ||
), | ||
], | ||
), | ||
}, | ||
) | ||
def get(self, request, student_on_tour_id): | ||
""" | ||
Get a detailed view on a student on tour's timings | ||
""" | ||
student_on_tour_instance = StudentOnTour.objects.get(id=student_on_tour_id) | ||
if not student_on_tour_instance: | ||
return not_found("StudentOnTour") | ||
|
||
remarks_at_buildings = RemarkAtBuilding.objects.filter(student_on_tour_id=student_on_tour_id) | ||
|
||
serializer = self.serializer_class() | ||
serialized_data = serializer.to_representation(remarks_at_buildings) | ||
return Response(serialized_data, status=status.HTTP_200_OK) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.