diff --git a/backend/api/migrations/0008_userreport.py b/backend/api/migrations/0008_userreport.py new file mode 100644 index 0000000..d51c971 --- /dev/null +++ b/backend/api/migrations/0008_userreport.py @@ -0,0 +1,57 @@ +# Generated by Django 4.2.11 on 2024-04-20 23:10 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("api", "0007_remove_file_points_file_downvotes_file_upvotes"), + ] + + operations = [ + migrations.CreateModel( + name="UserReport", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("description", models.TextField(max_length=300)), + ( + "file", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="file_report", + to="api.file", + ), + ), + ( + "reporting_user", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="user_report_generated", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "user", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="user_report", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + ] diff --git a/backend/api/models.py b/backend/api/models.py index 95ce20c..e4208e6 100644 --- a/backend/api/models.py +++ b/backend/api/models.py @@ -61,10 +61,28 @@ class File(models.Model): upvotes = models.ManyToManyField(PeerUser, related_name="upvoted_file") downvotes = models.ManyToManyField(PeerUser, related_name="downvoted_file") - @property def points(self): return self.upvotes.count() - self.downvotes.count() def __str__(self): return self.filename + + +class UserReport(models.Model): + user = models.ForeignKey( + PeerUser, on_delete=models.SET_NULL, null=True, related_name="user_report" + ) + reporting_user = models.ForeignKey( + PeerUser, + on_delete=models.SET_NULL, + null=True, + related_name="user_report_generated", + ) + description = models.TextField(blank=False, null=False, max_length=300) + file = models.ForeignKey( + File, on_delete=models.CASCADE, null=True, related_name="file_report" + ) + + def __str__(self) -> str: + return f"{self.user}:{self.file}:{self.reporting_user}" diff --git a/backend/api/serializers.py b/backend/api/serializers.py index b95a237..c369484 100644 --- a/backend/api/serializers.py +++ b/backend/api/serializers.py @@ -46,4 +46,17 @@ class FileSerializer(serializers.ModelSerializer): class Meta: model = File - fields = "__all__" + fields = [ + "id", + "filename", + "points", + "original_author", + "peer_users", + "topic", + "professor", + "semester", + "course", + "created_at", + "upvotes", + "downvotes", + ] diff --git a/backend/api/urls.py b/backend/api/urls.py index 5f57f26..2ecfb97 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -65,6 +65,7 @@ urlpatterns = [ name="downvote-file", ), path("api/files//", views.FileDetailAPIView.as_view(), name="file-detail"), + path("api/report_user/", views.ReportUserView.as_view(), name="report-user"), path("api/register/", views.RegisterFile.as_view(), name="file-register"), path("api/files/filter/", views.FileFilterView.as_view(), name="file-filter-view"), ] diff --git a/backend/api/user_login.py b/backend/api/user_login.py index f5da98f..c88a84d 100644 --- a/backend/api/user_login.py +++ b/backend/api/user_login.py @@ -6,7 +6,7 @@ from rest_framework.response import Response from rest_framework.views import APIView from rest_framework.authentication import TokenAuthentication from rest_framework.permissions import IsAuthenticated -from api.utils.get_client_ip import get_client_ip +from api.utils.get_client_ip import get_client_ip, PollMiddleware from django.utils import timezone diff --git a/backend/api/utils/get_client_ip.py b/backend/api/utils/get_client_ip.py index 6d65d6a..739a1e7 100644 --- a/backend/api/utils/get_client_ip.py +++ b/backend/api/utils/get_client_ip.py @@ -1,3 +1,4 @@ +from typing import Any from django.utils import timezone @@ -16,3 +17,15 @@ def update_user_ip(request): user.ip_address = ip_address user.last_poll = timezone.now() user.save() + + +class PollMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + print("Updating user last online and IP address") + update_user_ip(request) + response = self.get_response(request) + + return response diff --git a/backend/api/views.py b/backend/api/views.py index 05c3f7d..2569605 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -1,4 +1,4 @@ -from api.models import Course, File, Professor, Semester, Topic, PeerUser +from api.models import Course, File, Professor, Semester, Topic, PeerUser, UserReport from api.serializers import ( CourseSerializer, FileSerializer, @@ -7,13 +7,13 @@ from api.serializers import ( TopicSerializer, UserSerializer, ) -from datetime import datetime, timedelta +from datetime import timedelta from django.contrib.auth import authenticate from django.db import transaction +from django.db.models import Count, Value, F, IntegerField from django.utils import timezone from rest_framework import generics, status from rest_framework.authentication import TokenAuthentication -from rest_framework.authtoken.models import Token from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView @@ -123,7 +123,7 @@ class UpvoteFile(APIView): file.save() return Response( {"msg": f"Upvoted {file.filename}", "file_points": file.points}, - status=status.HTTP_200_OK, + status=status.HTTP_200_OK, ) except Exception as e: print(f"Error: {str(e)}") @@ -235,15 +235,6 @@ class RegisterFile(APIView): ) -from django.utils import timezone -from datetime import timedelta -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status -from .models import File, PeerUser -from .serializers import FileSerializer - - class FileFilterView(APIView): authentication_classes = [TokenAuthentication] permission_classes = [IsAuthenticated] @@ -251,6 +242,7 @@ class FileFilterView(APIView): def get(self, request, format=None): try: # Extract filters from the query parameters + update_user_ip(request) topic_id = request.query_params.get("topic") professor_id = request.query_params.get("professor") course_id = request.query_params.get("course") @@ -291,19 +283,26 @@ class FileFilterView(APIView): if semester_id: queryset = queryset.filter(semester__id=semester_id) + queryset = queryset.annotate( + upvote_count=Count("upvotes"), downvote_count=Count("downvotes") + ).order_by(F("downvote_count") - F("upvote_count")) + # Filter files based on active peers in the past hour active_peer_ids = PeerUser.objects.filter( last_poll__gte=timezone.now() - timedelta(hours=1) ).values_list("id", flat=True) queryset = queryset.filter(peer_users__in=active_peer_ids).distinct() - # Sort files based on points column in descending order - queryset = queryset.order_by("-points") - - # Serialize files serializer = FileSerializer(queryset, many=True) # Loop through each serialized file to sort its peer_users - for file_data in serializer.data: + + try: + serialized_data = serializer.data + print(serialized_data) + except Exception as e: + print(str(e)) + for file_data in serialized_data: + print(file_data) file_obj = File.objects.get(id=file_data["id"]) sorted_peer_users = sorted( file_obj.peer_users.all(), key=lambda x: x.points, reverse=True @@ -312,9 +311,72 @@ class FileFilterView(APIView): sorted_peer_users, many=True ).data - return Response(serializer.data) + return Response(serialized_data) except Exception as e: return Response( {"error": f"Something went wrong: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) + + +class ReportUserView(APIView): + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] + + def post(self, request): + data = request.data + update_user_ip(request) + try: + assert "file_id" in data + assert "user_id" in data + assert "description" in data + except AssertionError as e: + return Response( + {"error": f"Missing Parameters. {str(e)}"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + try: + file = File.objects.get(id=data["file_id"]) + malicious_user = PeerUser.objects.get(id=data["user_id"]) + reporting_user = request.user + description = data["description"] + except Exception as e: + return Response( + {"error": f"Missing/Incorrect Parameters. {str(e)}"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + existing_user_report_queryset = UserReport.objects.filter( + user=malicious_user, file=file, reporting_user=reporting_user + ) + + if len(existing_user_report_queryset): + existing_report = existing_user_report_queryset.first() + existing_report.description = description + existing_report.save() + return Response( + { + "msg": "User report already exists for this file. Updated description" + }, + status=status.HTTP_208_ALREADY_REPORTED, + ) + + if ( + file not in malicious_user.owned_files.all() + or file not in malicious_user.shared_files.all() + ): + return Response( + {"error": "User does not own or host the file reported"}, + status=status.HTTP_404_NOT_FOUND, + ) + + UserReport.objects.create( + user=malicious_user, + reporting_user=reporting_user, + file=file, + description=description, + ) + malicious_user.points -= 1 + malicious_user.save() + return Response({"msg": "Report Created!"}, status=status.HTTP_201_CREATED)