Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Django rest framework - Assesment #6

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion articles/admin.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.contrib import admin

from articles.models import Article
from articles.models import Article, Tag

admin.site.register(Article)
admin.site.register(Tag)
3 changes: 3 additions & 0 deletions articles/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@
class ArticlesConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "articles"

def ready(self):
import articles.signals
49 changes: 49 additions & 0 deletions articles/migrations/0002_auto_20210510_0638.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Generated by Django 3.2.2 on 2021-05-10 06:38

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


class Migration(migrations.Migration):

dependencies = [
("articles", "0001_initial"),
]

operations = [
migrations.CreateModel(
name="Tag",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("slug", models.SlugField()),
("name", models.CharField(max_length=25)),
(
"parent",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="articles.tag",
),
),
],
),
migrations.AddField(
model_name="article",
name="tag",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="articles.tag",
),
),
]
10 changes: 10 additions & 0 deletions articles/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,19 @@ class Article(models.Model):
title = models.CharField(max_length=32)
slug = models.CharField(max_length=32, unique=True)
content = models.TextField()
tag = models.ForeignKey("Tag", on_delete=models.CASCADE, blank=True, null=True)

class Meta:
ordering = ["id"]

def __str__(self):
return self.title


class Tag(models.Model):
parent = models.ForeignKey("Tag", on_delete=models.CASCADE, blank=True, null=True)
slug = models.SlugField()
name = models.CharField(max_length=25)

def __str__(self):
return f"name:<{self.name}>-slug<{self.slug}>"
15 changes: 14 additions & 1 deletion articles/serializers.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
from rest_framework import serializers

from articles.models import Article
from articles.models import Article, Tag


class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = "__all__"
depth = 2

def perform_delete_on_tag(self):
tag = self.instance.tag
if tag:
self.instance.tag = None
self.instance.save()


class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
fields = "__all__"
26 changes: 26 additions & 0 deletions articles/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from rest_framework.exceptions import NotAcceptable

from django.db.models.signals import pre_delete, pre_save
from django.dispatch import receiver

from articles.models import Article, Tag


@receiver(pre_delete, sender=Tag)
def check_tag_before_del(sender, instance, using, **kwargs):
tag_present = Article.objects.filter(tag_id=instance.id)
if tag_present:
raise NotAcceptable(detail="This tag is used by article", code=400)


@receiver(pre_save, sender=Tag)
def check_tag_before_del(sender, instance, using, update_fields, **kwargs):
tag_present = Article.objects.filter(tag_id=instance.id)
if tag_present:
updated_slug = instance.slug
previous_slug = tag_present[0].slug
if updated_slug != previous_slug:
raise NotAcceptable(
detail="This tag is used by article and 'slug' cannot be updated",
code=400,
)
1 change: 1 addition & 0 deletions articles/tests/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
class ArticleFactory(factory.django.DjangoModelFactory):
title = factory.Sequence(lambda n: f"Article {n}")
slug = factory.Sequence(lambda n: f"article-{n}")
content = factory.Sequence(lambda n: f"content-{n}")

class Meta:
model = Article
43 changes: 43 additions & 0 deletions articles/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,49 @@ def test_create(self):
article = Article.objects.get(id=response.data["id"]) # newly-created
self.assertEqual(article.slug, "test-article")

def test_not_found_title_filter(self):
article1, article2 = ArticleFactory.create_batch(2)
response = self.client.get(f'{reverse("article_list")}?title=xyz')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 0)

def test_not_found_content_filter(self):
article1, article2 = ArticleFactory.create_batch(2)
response = self.client.get(f'{reverse("article_list")}?content=xyz')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 0)

def test_title_filter(self):
article1, article2 = ArticleFactory.create_batch(2)
response = self.client.get(f'{reverse("article_list")}?title={article1.title}')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 1)
self.assertEqual(response.data[0].get("title"), article1.title)

def test_content_filter(self):
article1, article2 = ArticleFactory.create_batch(2)
response = self.client.get(
f'{reverse("article_list")}?content={article1.content}'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 1)
self.assertEqual(response.data[0].get("content"), article1.content)

def test_not_found_content_and_title_filter(self):
article1, article2 = ArticleFactory.create_batch(2)
response = self.client.get(f'{reverse("article_list")}?content=xyz&title=xyz')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 0)

def test_content_and_title_filter(self):
article1, article2 = ArticleFactory.create_batch(2)
response = self.client.get(
f'{reverse("article_list")}?content={article1.content}&title={article1.title}'
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 1)
self.assertEqual(response.data[0].get("content"), article1.content)


class ArticleDetailAPIViewTest(APITestCase):
def test_update(self):
Expand Down
69 changes: 66 additions & 3 deletions articles/views.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,77 @@
from rest_framework import generics
from rest_framework import generics, status
from rest_framework.response import Response

from articles.models import Article
from articles.serializers import ArticleSerializer
from django.db.models import Q

from articles.models import Article, Tag
from articles.serializers import ArticleSerializer, TagSerializer


class ArticleListCreateAPIView(generics.ListCreateAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
ordering_fields = ["title", "created_at"]

def get_queryset(self):
"""
added params filter for 'title' and 'content'
"""
title = self.request.query_params.get("title")
content = self.request.query_params.get("content")

if title is not None and content is None:
queryset = self.queryset.filter(title__exact=title)
return queryset
elif content is not None and title is None:
queryset = self.queryset.filter(content__exact=content)
return queryset
elif content and title:
queryset = self.queryset.filter(
Q(content__exact=content) & Q(title__exact=title)
)
return queryset

return self.queryset.all()


class ArticleDetailAPIView(generics.RetrieveUpdateDestroyAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer

def destroy(self, request, pk):
# Note the use of `get_queryset()` instead of `self.queryset`
remove_tag = request.query_params.get("removeTag")
instance = self.get_object()

try:
if remove_tag and self.validate_remove_tag(remove_tag=remove_tag):
self.serializer_class(instance=instance).perform_delete_on_tag()
return Response(status=status.HTTP_204_NO_CONTENT)
except ValueError as error:
return Response(
data=dict(error=str(error)), status=status.HTTP_400_BAD_REQUEST
)
self.perform_destroy(instance)
return Response(status=status.HTTP_204_NO_CONTENT)

def validate_remove_tag(self, remove_tag):
if remove_tag.lower() != "true":
raise ValueError("specify 'removeTag' to be true")
return True


class TagListCreateAPIView(generics.ListCreateAPIView):
queryset = Tag.objects.all()
serializer_class = TagSerializer


class ArticleTagRetrieveAPIView(generics.RetrieveAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
lookup_url_kwarg = "tag"

def get(self, request, *args, **kwargs):
key = self.kwargs[self.lookup_url_kwarg]
article = Article.objects.filter(tag_id=key)
serializer = self.serializer_class(article, many=True)
return Response(serializer.data)
13 changes: 12 additions & 1 deletion project/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,23 @@
from django.contrib import admin
from django.urls import path

from articles.views import ArticleDetailAPIView, ArticleListCreateAPIView
from articles.views import (
ArticleDetailAPIView,
ArticleListCreateAPIView,
ArticleTagRetrieveAPIView,
TagListCreateAPIView,
)

urlpatterns = [
path("admin/", admin.site.urls),
path("api/articles/", ArticleListCreateAPIView.as_view(), name="article_list"),
path(
"api/articles/<int:pk>/", ArticleDetailAPIView.as_view(), name="article_detail"
),
path("api/tag/", TagListCreateAPIView.as_view(), name="article_list_create"),
path(
"api/articles/tag/<int:tag>/",
ArticleTagRetrieveAPIView.as_view(),
name="article_tag_retrieve",
),
]
Loading