diff --git a/backend/api/migrations/0006_file_points_peeruser_points.py b/backend/api/migrations/0006_file_points_peeruser_points.py new file mode 100644 index 0000000..27daea7 --- /dev/null +++ b/backend/api/migrations/0006_file_points_peeruser_points.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.11 on 2024-04-12 20:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("api", "0005_file_created_at"), + ] + + operations = [ + migrations.AddField( + model_name="file", + name="points", + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name="peeruser", + name="points", + field=models.IntegerField(default=0), + ), + ] diff --git a/backend/api/migrations/0007_remove_file_points_file_downvotes_file_upvotes.py b/backend/api/migrations/0007_remove_file_points_file_downvotes_file_upvotes.py new file mode 100644 index 0000000..cb9e9e0 --- /dev/null +++ b/backend/api/migrations/0007_remove_file_points_file_downvotes_file_upvotes.py @@ -0,0 +1,32 @@ +# Generated by Django 4.2.11 on 2024-04-12 21:47 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("api", "0006_file_points_peeruser_points"), + ] + + operations = [ + migrations.RemoveField( + model_name="file", + name="points", + ), + migrations.AddField( + model_name="file", + name="downvotes", + field=models.ManyToManyField( + related_name="downvoted_file", to=settings.AUTH_USER_MODEL + ), + ), + migrations.AddField( + model_name="file", + name="upvotes", + field=models.ManyToManyField( + related_name="upvoted_file", to=settings.AUTH_USER_MODEL + ), + ), + ] diff --git a/backend/api/models.py b/backend/api/models.py index ce8f50a..95ce20c 100644 --- a/backend/api/models.py +++ b/backend/api/models.py @@ -6,6 +6,7 @@ from django.db import models class PeerUser(AbstractUser): ip_address = models.GenericIPAddressField(blank=True, null=True) last_poll = models.DateTimeField(auto_now_add=True) + points = models.IntegerField(default=0, blank=False) class Topic(models.Model): @@ -57,6 +58,13 @@ class File(models.Model): Course, on_delete=models.SET_NULL, null=True, related_name="files" ) created_at = models.DateTimeField(auto_now_add=True) + 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 diff --git a/backend/api/serializers.py b/backend/api/serializers.py index 8b582fa..b95a237 100644 --- a/backend/api/serializers.py +++ b/backend/api/serializers.py @@ -10,7 +10,7 @@ class LoginSerializer(serializers.Serializer): class UserSerializer(serializers.ModelSerializer): class Meta: model = PeerUser - fields = ["id", "username", "email", "ip_address"] + fields = ["id", "username", "email", "ip_address", "points"] class TopicSerializer(serializers.ModelSerializer): diff --git a/backend/api/urls.py b/backend/api/urls.py index 9409a0b..5f57f26 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -54,7 +54,17 @@ urlpatterns = [ name="course-detail", ), path("api/files/", views.FileListCreateAPIView.as_view(), name="file-list"), + path( + "api/files//upvote/", + views.UpvoteFile.as_view(), + name="upvote-file", + ), + path( + "api/files//downvote/", + views.DownvoteFile.as_view(), + name="downvote-file", + ), path("api/files//", views.FileDetailAPIView.as_view(), name="file-detail"), - path("api/files/filter/", views.FileFilterView.as_view(), name="file-filter-view"), 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 4cd8ff5..f5da98f 100644 --- a/backend/api/user_login.py +++ b/backend/api/user_login.py @@ -7,6 +7,7 @@ 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 django.utils import timezone class LoginView(APIView): @@ -18,6 +19,7 @@ class LoginView(APIView): user = authenticate(username=username, password=password) if user: token, _ = Token.objects.get_or_create(user=user) + user.last_poll = timezone.now() response = Response({"token": token.key}, status=status.HTTP_200_OK) response.set_cookie("token", token.key) return response @@ -66,6 +68,7 @@ class PollOnlineView(APIView): user = request.user ip_address = get_client_ip(request) user.ip_address = ip_address + user.last_poll = timezone.now() user.save() return Response( { diff --git a/backend/api/utils/get_client_ip.py b/backend/api/utils/get_client_ip.py index 821928c..6d65d6a 100644 --- a/backend/api/utils/get_client_ip.py +++ b/backend/api/utils/get_client_ip.py @@ -1,3 +1,6 @@ +from django.utils import timezone + + def get_client_ip(request): x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR") if x_forwarded_for: @@ -11,4 +14,5 @@ def update_user_ip(request): user = request.user ip_address = get_client_ip(request) user.ip_address = ip_address + user.last_poll = timezone.now() user.save() diff --git a/backend/api/views.py b/backend/api/views.py index 5ef87b6..05c3f7d 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -5,10 +5,12 @@ from api.serializers import ( ProfessorSerializer, SemesterSerializer, TopicSerializer, + UserSerializer, ) from datetime import datetime, timedelta from django.contrib.auth import authenticate from django.db import transaction +from django.utils import timezone from rest_framework import generics, status from rest_framework.authentication import TokenAuthentication from rest_framework.authtoken.models import Token @@ -105,6 +107,58 @@ class FileDetailAPIView(generics.RetrieveUpdateDestroyAPIView): permission_classes = [IsAuthenticated] +class UpvoteFile(APIView): + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] + + def post(self, request, file_id): + user = request.user + update_user_ip(request) + + try: + file = File.objects.get(id=file_id) + if file.downvotes.contains(user): + file.downvotes.remove(user) + file.upvotes.add(user) + file.save() + return Response( + {"msg": f"Upvoted {file.filename}", "file_points": file.points}, + status=status.HTTP_200_OK, + ) + except Exception as e: + print(f"Error: {str(e)}") + return Response( + {"error": "Please enter valid file id"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + +class DownvoteFile(APIView): + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] + + def post(self, request, file_id): + user = request.user + update_user_ip(request) + + try: + file = File.objects.get(id=file_id) + if file.upvotes.contains(user): + file.upvotes.remove(user) + file.downvotes.add(user) + file.save() + return Response( + {"msg": f"Downvoted {file.filename}", "file_points": file.points}, + status=status.HTTP_200_OK, + ) + except Exception as e: + print(f"Error: {str(e)}") + return Response( + {"error": "Please enter valid file id"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + class RegisterFile(APIView): authentication_classes = [TokenAuthentication] permission_classes = [IsAuthenticated] @@ -115,6 +169,7 @@ class RegisterFile(APIView): data = request.data try: with transaction.atomic(): + user.last_poll = timezone.now() required_fields = set( ["filename", "topic", "semester", "professor", "course"] ) @@ -166,7 +221,9 @@ class RegisterFile(APIView): "course": file.course.name if file.course else None, "professor": file.professor.name if file.professor else None, } + user.points += 1 file.save() + user.save() return Response( response_data, status=status.HTTP_201_CREATED, @@ -178,9 +235,19 @@ 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] + def get(self, request, format=None): try: # Extract filters from the query parameters @@ -226,12 +293,25 @@ class FileFilterView(APIView): # Filter files based on active peers in the past hour active_peer_ids = PeerUser.objects.filter( - last_poll__gte=datetime.now() - timedelta(hours=1) + 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: + 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 + ) + file_data["peer_users"] = UserSerializer( + sorted_peer_users, many=True + ).data + return Response(serializer.data) except Exception as e: return Response( diff --git a/backend/peer_notes/settings.py b/backend/peer_notes/settings.py index 2418f69..bef179d 100644 --- a/backend/peer_notes/settings.py +++ b/backend/peer_notes/settings.py @@ -137,4 +137,4 @@ CORS_ALLOW_ALL_ORIGINS = True # CORS_ALLOWED_ORIGINS = [ # "http://localhost:5173" # ] -CORS_ALLOW_CREDENTIALS = True \ No newline at end of file +CORS_ALLOW_CREDENTIALS = True