Skip to content
This repository has been archived by the owner on Feb 5, 2024. It is now read-only.

Commit

Permalink
Merge pull request #450 from SELab-2/feature/endpoints-student-analysis
Browse files Browse the repository at this point in the history
Student Analysis Endpoints
  • Loading branch information
jonathancasters authored May 11, 2023
2 parents bd42a52 + b5d1c15 commit 0101835
Show file tree
Hide file tree
Showing 18 changed files with 6,367 additions and 4,531 deletions.
Empty file added backend/analysis/__init__.py
Empty file.
87 changes: 87 additions & 0 deletions backend/analysis/serializers.py
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 added backend/analysis/tests.py
Empty file.
8 changes: 8 additions & 0 deletions backend/analysis/urls.py
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"),
]
137 changes: 137 additions & 0 deletions backend/analysis/views.py
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)
3 changes: 2 additions & 1 deletion backend/base/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from django.db import models
from django.db.models import UniqueConstraint, Q
from django.db.models.functions import Lower
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from phonenumber_field.modelfields import PhoneNumberField

Expand Down Expand Up @@ -371,7 +372,7 @@ class RemarkAtBuilding(models.Model):
def clean(self):
super().clean()
if not self.timestamp:
self.timestamp = datetime.now()
self.timestamp = timezone.now()
if self.type == "AA" or self.type == "BI" or type == "VE":
remark_instances = RemarkAtBuilding.objects.filter(
building=self.building, student_on_tour=self.student_on_tour, type=self.type
Expand Down
24 changes: 23 additions & 1 deletion backend/base/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,24 @@
from django.db.models import Max
from django.db.models.signals import post_save, pre_save
from django.dispatch import receiver
from django.utils import timezone

from base.models import StudentOnTour, RemarkAtBuilding


@receiver(post_save, sender=RemarkAtBuilding)
def progress_current_building_index(sender, instance: RemarkAtBuilding, **kwargs):
student_on_tour = instance.student_on_tour

if instance.type == RemarkAtBuilding.AANKOMST:
student_on_tour = instance.student_on_tour
# since we start indexing our BuildingOnTour with index 1, this works (since current_building_index starts at 0)
student_on_tour.current_building_index += 1

# since we only start calculating worked time from the moment we arrive at the first building
# we recalculate the start time of the tour
if student_on_tour.current_building_index == 1:
student_on_tour.started_tour = timezone.now()

student_on_tour.save()

# Broadcast update to websocket
Expand All @@ -24,6 +32,20 @@ def progress_current_building_index(sender, instance: RemarkAtBuilding, **kwargs
"current_building_index": student_on_tour.current_building_index,
},
)
elif (instance.type == RemarkAtBuilding.VERTREK and
student_on_tour.current_building_index == student_on_tour.max_building_index):
student_on_tour.completed_tour = timezone.now()
student_on_tour.save()

# Broadcast update to websocket
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
"student_on_tour_updates",
{
"type": 'student.on.tour.completed',
"student_on_tour_id": student_on_tour.id
}
)


@receiver(pre_save, sender=StudentOnTour)
Expand Down
1 change: 1 addition & 0 deletions backend/config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@

urlpatterns = [
path("", RootDefault.as_view()),
path("analysis/", include("analysis.urls")),
path("admin/", admin.site.urls),
path("docs/", SpectacularAPIView.as_view(), name="schema"),
path("docs/ui/", SpectacularSwaggerView.as_view(url="/docs"), name="swagger-ui"),
Expand Down
Loading

0 comments on commit 0101835

Please sign in to comment.