Skip to content

Commit

Permalink
feat(be): implement forum upvote and downvote endpoints and tests (#641)
Browse files Browse the repository at this point in the history
* feat(be): implement forum upvote modelviewset, serializer, model
>
>
Co-authored-by: MucahitErdoganUnlu [email protected]

* feat(be): implement forum downvote modelviewset, serializer, model, and unit tests
>
>
Co-authored-by: MucahitErdoganUnlu [email protected]
  • Loading branch information
meminciftci authored Nov 16, 2024
1 parent 8a507b8 commit 88def9b
Show file tree
Hide file tree
Showing 7 changed files with 384 additions and 0 deletions.
28 changes: 28 additions & 0 deletions backend/core/migrations/0002_forumupvote.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 5.1.2 on 2024-11-16 13:43

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('core', '0001_initial'),
]

operations = [
migrations.CreateModel(
name='ForumUpvote',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('forum_question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.forumquestion')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-created_at'],
'unique_together': {('user', 'forum_question')},
},
),
]
28 changes: 28 additions & 0 deletions backend/core/migrations/0003_forumdownvote.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 5.1.2 on 2024-11-16 14:13

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('core', '0002_forumupvote'),
]

operations = [
migrations.CreateModel(
name='ForumDownvote',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('forum_question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.forumquestion')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-created_at'],
'unique_together': {('user', 'forum_question')},
},
),
]
36 changes: 36 additions & 0 deletions backend/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from django.core.validators import MinValueValidator, MaxValueValidator
from django.core.exceptions import ValidationError # Import ValidationError


class CustomUser(AbstractUser):
email = models.EmailField()
full_name = models.CharField(max_length=100)
Expand Down Expand Up @@ -118,3 +119,38 @@ class Meta:

def __str__(self):
return f"{self.user} bookmarked {self.forum_question}"

class ForumUpvote(models.Model):
user = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
forum_question = models.ForeignKey(ForumQuestion, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)

class Meta:
unique_together = ("user", "forum_question")
ordering = ["-created_at"]

def save(self, *args, **kwargs):
if ForumDownvote.objects.filter(user=self.user, forum_question=self.forum_question).exists():
raise ValidationError("A user cannot upvote and downvote the same forum question at the same time.")
super().save(*args, **kwargs)

def __str__(self):
return f"{self.user} upvoted {self.forum_question}"


class ForumDownvote(models.Model):
user = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
forum_question = models.ForeignKey(ForumQuestion, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)

class Meta:
unique_together = ("user", "forum_question")
ordering = ["-created_at"]

def save(self, *args, **kwargs):
if ForumUpvote.objects.filter(user=self.user, forum_question=self.forum_question).exists():
raise ValidationError("A user cannot upvote and downvote the same forum question at the same time.")
super().save(*args, **kwargs)

def __str__(self):
return f"{self.user} downvoted {self.forum_question}"
40 changes: 40 additions & 0 deletions backend/core/serializers/forum_vote_serializer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from django.contrib.auth import get_user_model
from faker import Faker
from rest_framework import serializers

from ..models import (ForumUpvote, ForumDownvote)

User = get_user_model()
queryset = User.objects.all()


class ForumUpvoteSerializer(serializers.ModelSerializer):
class Meta:
model = ForumUpvote
fields = ("id", "user", "forum_question", "created_at")
read_only_fields = ("id", "user", "created_at")

def validate(self, attrs):
user = self.context["request"].user
forum_question = attrs["forum_question"]

if ForumUpvote.objects.filter(user=user, forum_question=forum_question).exists():
raise serializers.ValidationError("You have already upvoted this forum question.")

return attrs


class ForumDownvoteSerializer(serializers.ModelSerializer):
class Meta:
model = ForumDownvote
fields = ("id", "user", "forum_question", "created_at")
read_only_fields = ("id", "user", "created_at")

def validate(self, attrs):
user = self.context["request"].user
forum_question = attrs["forum_question"]

if ForumDownvote.objects.filter(user=user, forum_question=forum_question).exists():
raise serializers.ValidationError("You have already downvoted this forum question.")

return attrs
219 changes: 219 additions & 0 deletions backend/core/tests/test_forum_upvote_downvote.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
from rest_framework.test import APITestCase, APIClient
from rest_framework import status
from django.urls import reverse
from rest_framework_simplejwt.tokens import RefreshToken
from faker import Faker
from core.models import ForumUpvote, ForumDownvote, ForumQuestion
from django.contrib.auth import get_user_model

User = get_user_model()
fake = Faker()


class ForumUpvoteAPITest(APITestCase):
def setUp(self):
# Create a test user
self.user = User.objects.create_user(
username=fake.user_name(),
password="testpassword",
email=fake.email(),
full_name=fake.name()
)

# Authenticate the test client
self.client = APIClient()
refresh = RefreshToken.for_user(self.user)
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {str(refresh.access_token)}')

# Create a ForumQuestion
self.forum_question = ForumQuestion.objects.create(
title="Test Forum Question",
question="This is a test question for votes.",
author=self.user
)

# Vote data
self.data = {"forum_question": self.forum_question.id}

def test_create_forum_upvote(self):
"""Test creating a forum vote"""
# Send POST request to create a new vote
response = self.client.post(reverse('forum-upvote-list'), self.data, format='json')

# Assertions
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertTrue(ForumUpvote.objects.filter(user=self.user, forum_question=self.forum_question).exists())
self.assertIn("id", response.data)
self.assertIn("user", response.data)
self.assertIn("forum_question", response.data)

def test_delete_forum_upvote(self):
"""Test deleting a forum vote"""
# Create a vote to delete
self.client.post(reverse('forum-upvote-list'), self.data, format='json')
forum_vote = ForumUpvote.objects.get(user=self.user, forum_question=self.forum_question)

# Send DELETE request to remove the vote
response = self.client.delete(reverse('forum-upvote-detail', args=[forum_vote.id]))

# Assertions
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertFalse(ForumUpvote.objects.filter(id=forum_vote.id).exists())

def test_cannot_upvote_same_forum_question_twice(self):
"""Test that the same forum question cannot be voted twice"""
# Create the first vote
self.client.post(reverse('forum-upvote-list'), self.data, format='json')

# Attempt to create a duplicate vote
response = self.client.post(reverse('forum-upvote-list'), self.data, format='json')

# Assertions
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(
ForumUpvote.objects.filter(user=self.user, forum_question=self.forum_question).count(),
1 # Ensure only one vote exists
)

def test_cannot_delete_other_users_upvote(self):
"""Test that a user cannot delete another user's vote"""
# Create another user and their vote
other_user = User.objects.create_user(
username="otheruser",
password="otherpassword",
email="[email protected]",
full_name="Other User"
)
other_vote = ForumUpvote.objects.create(user=other_user, forum_question=self.forum_question)

# Attempt to delete the other user's vote
response = self.client.delete(reverse('forum-upvote-detail', args=[other_vote.id]))

# Assertions
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertTrue(ForumUpvote.objects.filter(id=other_vote.id).exists())

def test_get_list_upvote_pagination(self):
"""Test that the upvotes are paginated"""
# Create 15 upvotes
# for _ in range(15):
ForumUpvote.objects.create(user=self.user, forum_question=self.forum_question)

# Send GET request to retrieve the upvotes
response = self.client.get(reverse('forum-upvote-list'))

# Assertions
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn("count", response.data)
self.assertIn("next", response.data)
self.assertIn("previous", response.data)
self.assertIn("results", response.data)
self.assertIn("id", response.data['results'][0])
self.assertIn("user", response.data['results'][0])
self.assertIn("forum_question", response.data['results'][0])
self.assertIn("created_at", response.data['results'][0])


class ForumDownvoteAPITest(APITestCase):
def setUp(self):
# Create a test user
self.user = User.objects.create_user(
username=fake.user_name(),
password="testpassword",
email=fake.email(),
full_name=fake.name()
)

# Authenticate the test client
self.client = APIClient()
refresh = RefreshToken.for_user(self.user)
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {str(refresh.access_token)}')

# Create a ForumQuestion
self.forum_question = ForumQuestion.objects.create(
title="Test Forum Question",
question="This is a test question for votes.",
author=self.user
)

# Vote data
self.data = {"forum_question": self.forum_question.id}

def test_create_forum_downvote(self):
"""Test creating a forum downvote"""
# Send POST request to create a new downvote
response = self.client.post(reverse('forum-downvote-list'), self.data, format='json')

# Assertions
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertTrue(ForumDownvote.objects.filter(user=self.user, forum_question=self.forum_question).exists())
self.assertIn("id", response.data)
self.assertIn("user", response.data)
self.assertIn("forum_question", response.data)

def test_delete_forum_downvote(self):
"""Test deleting a forum downvote"""
# Create a downvote to delete
self.client.post(reverse('forum-downvote-list'), self.data, format='json')
forum_vote = ForumDownvote.objects.get(user=self.user, forum_question=self.forum_question)

# Send DELETE request to remove the downvote
response = self.client.delete(reverse('forum-downvote-detail', args=[forum_vote.id]))

# Assertions
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertFalse(ForumDownvote.objects.filter(id=forum_vote.id).exists())

def test_cannot_downvote_same_forum_question_twice(self):
"""Test that the same forum question cannot be downvoted twice"""
# Create the first downvote
self.client.post(reverse('forum-downvote-list'), self.data, format='json')

# Attempt to create a duplicate downvote
response = self.client.post(reverse('forum-downvote-list'), self.data, format='json')

# Assertions
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(
ForumDownvote.objects.filter(user=self.user, forum_question=self.forum_question).count(),
1 # Ensure only one downvote exists
)

def test_cannot_delete_other_users_downvote(self):
"""Test that a user cannot delete another user's vote"""
# Create another user and their vote
other_user = User.objects.create_user(
username="otheruser",
password="otherpassword",
email="[email protected]",
full_name="Other User"
)
other_vote = ForumDownvote.objects.create(user=other_user, forum_question=self.forum_question)

# Attempt to delete the other user's vote
response = self.client.delete(reverse('forum-downvote-detail', args=[other_vote.id]))

# Assertions
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertTrue(ForumDownvote.objects.filter(id=other_vote.id).exists())

def test_get_list_downvote_pagination(self):
"""Test that the upvotes are paginated"""
# Create 15 upvotes
# for _ in range(15):
ForumDownvote.objects.create(user=self.user, forum_question=self.forum_question)

# Send GET request to retrieve the upvotes
response = self.client.get(reverse('forum-downvote-list'))

# Assertions
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn("count", response.data)
self.assertIn("next", response.data)
self.assertIn("previous", response.data)
self.assertIn("results", response.data)
self.assertIn("id", response.data['results'][0])
self.assertIn("user", response.data['results'][0])
self.assertIn("forum_question", response.data['results'][0])
self.assertIn("created_at", response.data['results'][0])

4 changes: 4 additions & 0 deletions backend/core/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from .views.rate_quiz_views import RateQuizViewSet
from .views.take_quiz_views import TakeQuizViewSet
from .views.forum_bookmark_views import ForumBookmarkViewSet
from .views.forum_vote_views import ForumUpvoteViewSet
from .views.forum_vote_views import ForumDownvoteViewSet
from rest_framework.routers import DefaultRouter
from .views import views
from .views.tagging_views import TaggingView
Expand Down Expand Up @@ -33,6 +35,8 @@
router.register(r'rate-quiz', RateQuizViewSet, basename='rate-quiz')
router.register(r'take-quiz', TakeQuizViewSet, basename='take-quiz')
router.register(r'forum-bookmarks', ForumBookmarkViewSet, basename='forumbookmark')
router.register(r'forum-upvote', ForumUpvoteViewSet, basename='forum-upvote')
router.register(r'forum-downvote', ForumDownvoteViewSet, basename='forum-downvote')

urlpatterns = [
path('admin/', admin.site.urls),
Expand Down
Loading

0 comments on commit 88def9b

Please sign in to comment.