diff --git a/bb_back/bb_back/core/migrations/0013_remove_round_data_round_round_target_and_more.py b/bb_back/bb_back/core/migrations/0013_remove_round_data_round_round_target_and_more.py new file mode 100644 index 00000000..c95b2d45 --- /dev/null +++ b/bb_back/bb_back/core/migrations/0013_remove_round_data_round_round_target_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 4.1.3 on 2023-05-03 11:34 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0012_teams_and_teamapplications'), + ] + + operations = [ + migrations.AddField( + model_name='round', + name='round_target', + field=models.FileField(null=True, upload_to='round_target'), + ), + migrations.AddField( + model_name='submit', + name='created_at', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AddField( + model_name='submit', + name='score', + field=models.FloatField(null=True), + ), + ] diff --git a/bb_back/bb_back/core/models/round.py b/bb_back/bb_back/core/models/round.py index 178f7084..afb84c81 100644 --- a/bb_back/bb_back/core/models/round.py +++ b/bb_back/bb_back/core/models/round.py @@ -14,3 +14,4 @@ class Round(models.Model): is_active = models.BooleanField(default=True) data_of_round = models.FileField(upload_to="round_data", null=True) + round_target = models.FileField(upload_to="round_target", null=True) diff --git a/bb_back/bb_back/core/models/submit.py b/bb_back/bb_back/core/models/submit.py index c23ab278..b10411d2 100644 --- a/bb_back/bb_back/core/models/submit.py +++ b/bb_back/bb_back/core/models/submit.py @@ -1,11 +1,12 @@ from django.db import models -from datetime import datetime +from django.utils import timezone class Submit(models.Model): file = models.FileField(upload_to="submits", null=True) id_command = models.IntegerField(null=True) round_num = models.IntegerField(null=True) + score = models.FloatField(null=True) final = models.BooleanField(null=False, default=False) - score = models.FloatField(null=False, default=0) - create_at = models.DateTimeField(default=datetime.min) + + created_at = models.DateTimeField(default=timezone.now) diff --git a/bb_back/bb_back/core/views/__init__.py b/bb_back/bb_back/core/views/__init__.py index f606f944..5a84dfcd 100644 --- a/bb_back/bb_back/core/views/__init__.py +++ b/bb_back/bb_back/core/views/__init__.py @@ -5,6 +5,7 @@ RoundView, GetRoundDataView, CreateRoundView, UploudRoundData, TeamApplicationView, ReviewTeamApplicationView, TeamListView, CurrentTeamView, + UploadRoundTargetView MemberApplicationView, ReviewMemberApplicationView) __all__ = [ @@ -14,5 +15,5 @@ "GetGameLogoView", "UploadGameLogoView", "RoundView", "GetRoundDataView", "CreateRoundView", "UploudRoundData", "TeamApplicationView", "ReviewTeamApplicationView", "TeamListView", "CurrentTeamView", - "MemberApplicationView", "ReviewMemberApplicationView" + "UploadRoundTargetView", "MemberApplicationView", "ReviewMemberApplicationView" ] diff --git a/bb_back/bb_back/core/views/shared/__init__.py b/bb_back/bb_back/core/views/shared/__init__.py index 8671fef8..79af3e95 100644 --- a/bb_back/bb_back/core/views/shared/__init__.py +++ b/bb_back/bb_back/core/views/shared/__init__.py @@ -9,6 +9,8 @@ from .game import CreateGameView, GameView, GetGameLogoView, UploadGameLogoView from .submit_upload import SubmitView from .view_404 import view_404 +from .round import RoundView, UploadRoundTargetView, GetRoundDataView, CreateRoundView, UploudRoundData +from .teams import TeamApplicationView, ReviewTeamApplicationView, TeamListView, CurrentTeamView from .round import RoundView from .round import GetRoundDataView from .round import CreateRoundView @@ -24,5 +26,5 @@ "UploadGameLogoView", "SubmitView", "view_404", "RoundView", "GetRoundDataView", "CreateRoundView", "UploudRoundData", "TeamApplicationView", "ReviewTeamApplicationView", "TeamListView", - "CurrentTeamView", "MemberApplicationView", "ReviewMemberApplicationView" + "CurrentTeamView", "MemberApplicationView", "ReviewMemberApplicationView", "UploadRoundTargetView" ] diff --git a/bb_back/bb_back/core/views/shared/round.py b/bb_back/bb_back/core/views/shared/round.py index befa4103..b9a0a48f 100644 --- a/bb_back/bb_back/core/views/shared/round.py +++ b/bb_back/bb_back/core/views/shared/round.py @@ -16,6 +16,8 @@ NotFoundResponseSerializer, UserRolePermissionDeniedSerializer, ) +import codecs +import csv from bb_back.core.views.utils.decorators import is_staff_user @@ -204,6 +206,7 @@ class CreateRoundView(APIView): request_body=CreateRoundRequestSerializer, responses={status.HTTP_201_CREATED: CreateRoundResponseSerializer}, ) + @is_staff_user def post(self, request): request_data = CreateRoundRequestSerializer(data=request.data) @@ -293,3 +296,79 @@ def put(self, request, round_id): data={"response_data": {}}) response_data.is_valid() return Response(data=response_data.data, status=status.HTTP_200_OK) + + +class UploadRoundTargetView(APIView): + parser_classes = [MultiPartParser, FormParser, FileUploadParser] + + @swagger_auto_schema( + manual_parameters=[ + openapi.Parameter( + "file", + in_=openapi.IN_FORM, + description="file", + type=openapi.TYPE_FILE, + required=True, + ) + ], + responses={status.HTTP_200_OK: UploadRoundDataResponseSerializer}, + ) + @is_staff_user + def post(self, request, round_id): + round = Round.objects.filter(id=round_id).first() + if not round: + return response( + status_code=status.HTTP_404_NOT_FOUND, + data={}, + message=f"Round with id = {round_id} does not exist.") + + file = request.FILES.get("file") + try: + csv_file = csv.DictReader(codecs.iterdecode(file, 'utf-8')) + for line in csv_file: + line: dict + if list(line.keys()) != ['id', 'fact', 'amount']: + raise ValueError( + 'CSV file columns must be: "id", "fact", "amount"') + if not all([value.isnumeric() for value in line.values()]): + raise ValueError( + 'All provided values must be valid integers') + except ValueError as ex: + return response(status_code=status.HTTP_400_BAD_REQUEST, + data={}, + message=f"{str(ex)}") + if not file.name.endswith(".csv"): + return response(status_code=status.HTTP_400_BAD_REQUEST, + data={}, + message="Target file must have .csv extension") + round.round_target = file + round.save() + round.round_rates = file + round.save() + response_data = UploadRoundDataResponseSerializer( + data={"response_data": {}}) + response_data.is_valid() + return Response(data=response_data.data, status=status.HTTP_200_OK) + + @swagger_auto_schema( + responses={status.HTTP_200_OK: RoundDataResponseSerializer}, ) + def get(self, request, round_id): + round = Round.objects.filter(id=round_id).first() + if not round: + return response( + status_code=status.HTTP_404_NOT_FOUND, + data={}, + message=f"Round with id = {round_id} does not exist.") + if not round.round_target: + return response( + success=False, + status_code=status.HTTP_404_NOT_FOUND, + data={}, + message=f"No target found for round {round.name}", + ) + + response_data = HttpResponse(round.round_target, + content_type="application/vnd.ms-excel") + response_data[ + "Content-Disposition"] = "inline; filename=" + round.round_target.name + return response_data diff --git a/bb_back/bb_back/core/views/shared/submit_upload.py b/bb_back/bb_back/core/views/shared/submit_upload.py index c3d91937..1cdd9334 100644 --- a/bb_back/bb_back/core/views/shared/submit_upload.py +++ b/bb_back/bb_back/core/views/shared/submit_upload.py @@ -1,3 +1,7 @@ +import codecs +import csv +from math import exp + from drf_yasg import openapi from drf_yasg.utils import swagger_auto_schema from rest_framework import serializers, status @@ -6,15 +10,14 @@ from rest_framework.views import APIView from datetime import datetime -from bb_back.core.models import Submit -from bb_back.core.utils.view_utils import failed_validation_response +from bb_back.core.models import Submit, Round +from bb_back.core.utils.view_utils import failed_validation_response, response from bb_back.core.views.utils.base_serializers import BaseResponseSerializer from bb_back.settings import SUBMIT_MAX_SIZE class SubmitRequestSerializer(serializers.Serializer): file = serializers.FileField() - id_command = serializers.IntegerField() round_num = serializers.IntegerField() @@ -39,6 +42,7 @@ class SubmitView(APIView): responses={status.HTTP_200_OK: SubmitResponseSerializer}, ) def post(self, request): + team = request.user.team request_data = SubmitRequestSerializer(data=request.data) if not request_data.is_valid() or request.FILES.get("file") is None: @@ -50,24 +54,56 @@ def post(self, request): return failed_validation_response(serializer=request_data) submit_schema = request_data.data - submit = Submit.objects.filter( - id_command=submit_schema.get("id_command"), - round_num=submit_schema.get("round_num")).first() - - if submit: - submit.file = submit_file - submit.create_at = datetime.datetime.now() - submit.save() - else: - Submit.objects.create( - file=submit_file, - id_command=submit_schema.get("id_command"), - round_num=submit_schema.get("round_num"), - create_at=datetime.now(), - ) + round = Round.objects.filter(id=submit_schema.get("round_num")).first() + if not round: + return response( + status_code=status.HTTP_400_BAD_REQUEST, + data={}, + message= + f"Round with id={submit_schema.get('round_num')} not found") + try: + csv_file = csv.DictReader(codecs.iterdecode(submit_file, 'utf-8')) + for line in csv_file: + line: dict + if list(line.keys()) != ['id', 'rate']: + raise ValueError( + 'CSV file columns must be: "id", "fact", "amount"') + if not all([value.isnumeric() for value in line.values()]): + raise ValueError( + 'All provided values must be valid integers') + except ValueError as ex: + return response(status_code=status.HTTP_400_BAD_REQUEST, + data={}, + message=f"{str(ex)}") + score = score_submit(submit_file=submit_file, round_id=round.id) + + Submit.objects.create(file=submit_file, + id_command=team.id, + round_num=submit_schema.get("round_num"), + score=score) response_data = SubmitResponseSerializer( data={"response_data": submit_schema}) response_data.is_valid() return Response(data=response_data.data, status=status.HTTP_200_OK) + + +def score_submit(submit_file, round_id: int): + submit = list(csv.DictReader(codecs.iterdecode(submit_file, 'utf-8'))) + total_score = 0.0 + round = Round.objects.get(id=round_id) + target = list( + csv.DictReader(codecs.iterdecode(round.round_target, 'utf-8'))) + for result in submit: + target_line = [ + line for line in target if line.get('id') == result.get("id") + ][0] + rate: float = float(result.get("rate")) # type: ignore + fact = int(target_line.get("fact")) # type: ignore + amount = float(target_line.get("amount")) # type: ignore + p_tup = 1 / (1 + exp(-(0.1 - rate / 100) * 20)) + result_score = p_tup * ((1 - fact) * amount * rate - fact * amount) + total_score += result_score + + return total_score diff --git a/bb_back/bb_back/settings.py b/bb_back/bb_back/settings.py index 9f11d4d1..17547ffa 100644 --- a/bb_back/bb_back/settings.py +++ b/bb_back/bb_back/settings.py @@ -133,7 +133,16 @@ "DEFAULT_AUTHENTICATION_CLASSES": ("rest_framework_simplejwt.authentication.JWTAuthentication", ) } -SWAGGER_SETTINGS = {"USE_SESSION_AUTH": False} +SWAGGER_SETTINGS = { + 'SECURITY_DEFINITIONS': { + 'Bearer': { + 'type': 'apiKey', + 'name': 'Authorization', + 'in': 'header' + } + }, + "USE_SESSION_AUTH": False +} SIMPLE_JWT = { "ACCESS_TOKEN_LIFETIME": timedelta(minutes=60), "REFRESH_TOKEN_LIFETIME": timedelta(days=1), diff --git a/bb_back/bb_back/urls.py b/bb_back/bb_back/urls.py index aec92ce2..75c208ef 100644 --- a/bb_back/bb_back/urls.py +++ b/bb_back/bb_back/urls.py @@ -90,6 +90,10 @@ f"{API_PREFIX}/{API_VERSION}/round/uploud_data//", views.UploudRoundData.as_view(), ), + path( + f"{API_PREFIX}/{API_VERSION}/round/target//", + views.UploadRoundTargetView.as_view(), + ), path( f"{API_PREFIX}/{API_VERSION}/team/application/", views.TeamApplicationView.as_view(),