Skip to content

Commit

Permalink
#2 created the polls api
Browse files Browse the repository at this point in the history
  • Loading branch information
timofeysie committed Sep 18, 2024
1 parent 6283fc9 commit 3c81b0a
Show file tree
Hide file tree
Showing 14 changed files with 762 additions and 2 deletions.
309 changes: 309 additions & 0 deletions docs/work/2024.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,314 @@
# 2024

## September

### Polls

After a while, I need to refresh a few things.

First merge master into dev to update the dev branch after a flurry of work to solve some issues.

Next, run the app locally.

```txt
System check identified no issues (0 silenced).
You have 34 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): account, admin, auth, authtoken, comments, contenttypes, followers, likes, posts, profiles, sessions, sites, socialaccount.
Run 'python manage.py migrate' to apply them.
September 18, 2024 - 17:15:39
```

Ran python manage.py migrate and started the server and it works as expected.

#### Step 1

Next, scaffold the polls app.

```sh
python manage.py startapp polls
```

#### Step 2

Add it to the installed apps array in drf_two\settings.py.

Register the Polls model in admin.py

```py
from django.contrib import admin

from .models import Polls

admin.site.register(Polls)
```

#### Step 3

Then run make migrations which you have to do after updating a model:

```shell
python manage.py makemigrations
...
from .models import Polls
ImportError: cannot import name 'Polls' from 'polls.models' (C:\Users\timof\repos\timo\drf-two\polls\models.py)
```

Actually, our guide has no admin.py changes, so roll those back and:

#### Step 4

Migrate the changes.

```py
$ python manage.py makemigrations
Migrations for 'polls':
polls\migrations\0001_initial.py
- Create model Question
- Create model Answer
- Create model Vote
```

And python manage.py migrate:

```py
$ python manage.py migrate
Operations to perform:
Apply all migrations: account, admin, auth, authtoken, comments, contenttypes, followers, likes, polls, posts, profiles, sessions, sites, socialaccount
Running migrations:
Applying polls.0001_initial... OK
```

Run the server:

```shell
python manage.py runserver
```

Goto the admin url: http://127.0.0.1:8000/admin

Create a file with the dependencies:

```shell
pip freeze > requirements.txt
```

#### Step 5

Create the serializers.py file.

```py
from rest_framework import serializers
from .models import Question, Answer, Vote


class VoteSerializer(serializers.ModelSerializer):
"""
Serializer for the Vote model. It handles serialization for creating and
listing votes. Ensures that a user cannot vote more than once per question
by implementing custom validation.
"""
class Meta:
model = Vote
fields = ['id', 'answer', 'voter', 'created_at']
read_only_fields = ('voter',)

def validate(self, data):
"""
Validate that the user has not already voted for the same question.
Raises a ValidationError if the user has already voted.
"""
question = data['answer'].question
voter = self.context['request'].user
if Vote.objects.filter(answer__question=question,
voter=voter).exists():
raise serializers.ValidationError(
"You have already voted on this question.")
return data

def save(self, **kwargs):
kwargs['voter'] = self.context['request'].user
return super().save(**kwargs)


class AnswerSerializer(serializers.ModelSerializer):
"""
Serializer for the Answer model. Includes a custom method to count votes,
which is included in the serialization output.
"""
text = serializers.CharField(required=True, allow_blank=False, error_messages={"blank": "This field may not be left blank."})
votes_count = serializers.SerializerMethodField()

def get_votes_count(self, obj):
return obj.votes.distinct().count()

class Meta:
model = Answer
fields = ['id', 'text', 'created_at', 'votes_count']


class QuestionSerializer(serializers.ModelSerializer):
"""
Serializer for the Question model. Handles serialization for creating
and listing questions. Includes nested AnswerSerializers to represent
answers associated with the question, and custom methods to handle
the creation of answers within the same request as a question.
"""
owner = serializers.PrimaryKeyRelatedField(read_only=True)
owner_username = serializers.ReadOnlyField(source='owner.username')
votes_count = serializers.IntegerField(read_only=True)
answers = AnswerSerializer(many=True, required=True)

def validate_answers(self, value):
if len(value) < 2:
raise serializers.ValidationError("At least two answers are required.")
return value

class Meta:
model = Question
fields = [
'id', 'owner', 'owner_username', 'text',
'created_at', 'answers', 'votes_count'
]

def create(self, validated_data):
answers_data = validated_data.pop('answers', [])
question = Question.objects.create(**validated_data)
for answer_data in answers_data:
Answer.objects.create(question=question, **answer_data)
return question
```

#### Step 6

Add: Urls in both polls and main urls and add views in polls.

In polls/urls.py:

```py
from django.urls import path
from . import views

urlpatterns = [
# Questions
path('questions/', views.QuestionList.as_view(), name='question-list'),
path('questions/<int:pk>/', views.QuestionDetail.as_view(), name='question-detail'),

# Answers
path('answers/', views.AnswerList.as_view(), name='answer-list'),
path('answers/<int:pk>/', views.AnswerDetail.as_view(), name='answer-detail'),

# Votes
path('votes/', views.VoteList.as_view(), name='vote-list'),
path('votes/<int:pk>/', views.VoteDetail.as_view(), name='vote-detail'),
]
```

In drf_two\urls.py:

```py
path('', include('polls.urls')),
```

Update the polls\views.py:

```py
from django.db.models import Count
from rest_framework import generics, permissions, filters
from rest_framework.exceptions import ValidationError
from .models import Question, Answer, Vote
from .serializers import QuestionSerializer, AnswerSerializer, VoteSerializer
from drf_two.permissions import IsOwnerOrReadOnly


class QuestionList(generics.ListCreateAPIView):
"""
Provides a list of all questions and allows authenticated users to create
new questions. Questions are listed with a count of votes for their
answers, ordered by creation date in descending order. The view also
supports search functionality on question text and owner username.
"""
queryset = Question.objects.annotate(votes_count=Count(
'answers__votes', distinct=True)).order_by('-created_at')
serializer_class = QuestionSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
filter_backends = [filters.SearchFilter]
search_fields = [
'owner__username',
'text',
]

def perform_create(self, serializer):
serializer.save(owner=self.request.user)


class QuestionDetail(generics.RetrieveUpdateDestroyAPIView):
"""
Provides detailed view for a specific question, including capabilities
to update or delete. Only the owner of the question has the permissions
to update or delete it.
"""
queryset = Question.objects.annotate(votes_count=Count(
'answers__votes', distinct=True)).order_by('-created_at')
serializer_class = QuestionSerializer
permission_classes = [IsOwnerOrReadOnly]


class AnswerList(generics.ListCreateAPIView):
"""
Lists all answers for questions, annotated with a count of votes
for each answer, ordered by their creation date in descending order.
"""
queryset = Answer.objects.annotate(votes_count=Count(
'votes', distinct=True)).order_by('-created_at')
serializer_class = AnswerSerializer


class AnswerDetail(generics.RetrieveUpdateDestroyAPIView):
"""
Provides a detailed view for a specific answer, allowing retrieval,
update, and deletion operations. Updates and deletions are restricted
to the owner of the answer.
"""
queryset = Answer.objects.annotate(votes_count=Count(
'votes', distinct=True))
serializer_class = AnswerSerializer


class VoteList(generics.ListCreateAPIView):
"""
Provides a list of all votes and allows authenticated users
to create a vote on an answer. Prevents a user from voting
more than once on the same question.
"""
queryset = Vote.objects.all()
serializer_class = VoteSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]

def perform_create(self, serializer):
user = self.request.user
question = serializer.validated_data['answer'].question

if Vote.objects.filter(answer__question=question, voter=user).exists():
raise ValidationError(
{"error": "You have already voted on this question"})

serializer.save(voter=user, question=question)

def get_serializer_context(self):
context = super().get_serializer_context()
context['request'] = self.request
return context


class VoteDetail(generics.RetrieveUpdateDestroyAPIView):
"""
Allows detailed operations on a specific vote.
"""
queryset = Vote.objects.all()
serializer_class = VoteSerializer
```

## March

It looks like we should not be committing the db.sqlite3 file so adding that to the gitignore file.

```sh
Expand Down
1 change: 1 addition & 0 deletions drf_two/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
'comments',
'likes',
'followers',
'polls',
]
SITE_ID = 1
MIDDLEWARE = [
Expand Down
1 change: 1 addition & 0 deletions drf_two/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@
path('', include('comments.urls')),
path('', include('likes.urls')),
path('', include('followers.urls')),
path('', include('polls.urls')),
]
Empty file added polls/__init__.py
Empty file.
3 changes: 3 additions & 0 deletions polls/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.contrib import admin

# Register your models here.
6 changes: 6 additions & 0 deletions polls/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class PollsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'polls'
55 changes: 55 additions & 0 deletions polls/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Generated by Django 3.2 on 2024-09-18 08:36

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


class Migration(migrations.Migration):

initial = True

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name='Question',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('text', models.TextField()),
('created_at', models.DateTimeField(auto_now_add=True)),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='questions', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='Answer',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('text', models.TextField()),
('created_at', models.DateTimeField(auto_now_add=True)),
('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='polls.question')),
],
options={
'ordering': ['created_at'],
},
),
migrations.CreateModel(
name='Vote',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('answer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='votes', to='polls.answer')),
('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='polls.question')),
('voter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-created_at'],
'unique_together': {('question', 'voter')},
},
),
]
Empty file added polls/migrations/__init__.py
Empty file.
Loading

0 comments on commit 3c81b0a

Please sign in to comment.