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

Task 1,2,3 and partially 4 #1

Open
wants to merge 1 commit 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
28 changes: 28 additions & 0 deletions articles/migrations/0002_auto_20210508_1335.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 3.2.2 on 2021-05-08 13:35

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=32)),
('parent', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='articles.tag')),
],
),
migrations.AddField(
model_name='article',
name='tag',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='articles.tag'),
),
]
16 changes: 16 additions & 0 deletions articles/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver


class Article(models.Model):
Expand All @@ -7,9 +9,23 @@ 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.PROTECT, null=True)

class Meta:
ordering = ["id"]

def __str__(self):
return self.title


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

def __str__(self):
return self.name

def __repr__(self):
return self.name

21 changes: 20 additions & 1 deletion articles/serializers.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,28 @@
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__"


class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
fields = "__all__"


class UpdateArticleTagSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = ('Tag',)


class ListArticleWithTagsSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
fields = "__all__"
depth = 1
76 changes: 42 additions & 34 deletions articles/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,53 @@

from django.urls import reverse

from articles.models import Article
from articles.models import Article, Tag
from articles.tests.factories import ArticleFactory


class ArticleListCreateAPIViewTest(APITestCase):
def test_list(self):
article1, article2 = ArticleFactory.create_batch(2)
response = self.client.get(reverse("article_list"))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
[article["id"] for article in response.data], [article1.id, article2.id]
class ArticleDetailAPIViewTest(APITestCase):

def setUp(self):
self.article_1 = Article.objects.create(
id=1,
title="Sample title",
slug="mu-slug",
content="Sample content",
created_at="2020-03-14"
)
self.tag_1 = Tag.objects.create(
slug="my-slug",
name="slug-name",
)
self.tag_2 = Tag.objects.create(
slug="my-slug",
name="slug-name",
parent=self.tag_1
)
self.tag_3 = Tag.objects.create(
slug="my-slug",
name="slug-name",
parent=self.tag_2

def test_create(self):
self.assertEqual(Article.objects.count(), 0)
data = {
"title": "Test Article",
"slug": "test-article",
"content": "Lorem ipsum",
}
response = self.client.post(reverse("article_list"), data=data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
article = Article.objects.get(id=response.data["id"]) # newly-created
self.assertEqual(article.slug, "test-article")
)
self.article_2 = Article.objects.create(
id=2,
title="Sample title_2",
slug="mu-slug_2",
content="Sample content",
)
self.article_3 = Article.objects.create(
id=3,
title="Sample title_3",
slug="mu-slug_3",
content="Sample content",
)

def test_get_article(self):
response = self.client.get(reverse("article-list"))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.client.credentials()

class ArticleDetailAPIViewTest(APITestCase):
def test_update(self):
article = ArticleFactory(slug="my-slug")
data = {"slug": "updated-slug"}
url = reverse("article_detail", args=(article.id,))
response = self.client.patch(url, data=data)
# retrieve a single article
response = self.client.get(reverse("article-detail", kwargs={'pk': "1"}))
self.assertEqual(response.status_code, status.HTTP_200_OK)
updated_article = Article.objects.get(id=article.id)
self.assertEqual(updated_article.slug, "updated-slug")

def test_delete(self):
article = ArticleFactory()
url = reverse("article_detail", args=(article.id,))
response = self.client.delete(url)
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertEqual(Article.objects.count(), 0)
18 changes: 18 additions & 0 deletions articles/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from django.conf.urls import include
from django.urls import path

from rest_framework import routers
from rest_framework.routers import DefaultRouter
import articles.views as av

router = DefaultRouter(trailing_slash=False)
app_router = routers.DefaultRouter()
app_router.register('article', av.ArticleViewSet, 'article')
app_router.register('tag', av.TagViewSet, 'tag')


urlpatterns = [

path('', include(app_router.urls)),

]
87 changes: 79 additions & 8 deletions articles/views.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,85 @@
from rest_framework import generics
from django.db.models import Q
from django.shortcuts import get_object_or_404
from rest_framework import generics, status, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response

from articles.models import Article
from articles.serializers import ArticleSerializer
from articles.models import Article, Tag
import articles.serializers as ats


class ArticleListCreateAPIView(generics.ListCreateAPIView):
class ArticleViewSet(viewsets.ViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
serializer_class = ats.ArticleSerializer
ordering_fields = ['title', 'created_at']

def list(self, request, *args, **kwargs):
queryset = Article.objects.all()
title = self.request.query_params.get('title')
content = self.request.query_params.get('content')
if title or content is not None:
try:
queryset = queryset.filter(Q(title__icontains=title) | Q(content__icontains=content))
serializer = ats.ArticleSerializer(queryset, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
except Article.DoesNotExist:
return Response(self.serializer_class.errors, status=status.HTTP_404_NOT_FOUND)
else:
serializer = ats.ArticleSerializer(self.queryset, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)

def retrieve(self, request, pk=None):
try:
queryset = Article.objects.all()
article = get_object_or_404(queryset, pk=pk)
serializer = self.serializer_class(article)
return Response(serializer.data, status=status.HTTP_200_OK)
except Article.DoesNotExist:
return Response(self.serializer_class.errors, status=status.HTTP_404_NOT_FOUND)


class TagViewSet(viewsets.ViewSet):

def create(self, request, *args, **kwargs):
try:
serializer = ats.TagSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
return Response({"message": str(e)}, status=status.HTTP_400_BAD_REQUEST)

def list(self, request):
queryset = Tag.objects.all()
serializer = ats.TagSerializer(queryset, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)

def update(self, request, pk=None):
article = Article.objects.get(id=request.data.get('article'))
tag = Tag.objects.get(id=request.data.get('tag'))
if tag and article is not None:
serializer = ats.UpdateArticleTagSerializer(request.data)
if serializer.is_valid():
serializer.save(tag=tag)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
if article is not None: # specify an article you want to remove its tag
serializer = ats.UpdateArticleTagSerializer(request.data)
if serializer.is_valid():
serializer.save(tag=None)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

@action(detail=False, )
def get_article_by_tag(self, request):
try:
article = Article.objects.get(pk=request.data.get('tag_id'))
if article is not None:
serializer = ats.ListArticleWithTagsSerializer(article, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
except Article.DoesNotExist:
return Response("Article does not exist", status=status.HTTP_404_NOT_FOUND)



class ArticleDetailAPIView(generics.RetrieveUpdateDestroyAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
15 changes: 8 additions & 7 deletions project/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,22 @@
"""

from pathlib import Path
from decouple import config

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "django-insecure-a52$mel_^u^u*ft8u2c#v3usd@whtf3@bbf05fu%ia1r1&xtm("
SECRET_KEY = config("SECRET_KEY")

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = [
Expand All @@ -37,6 +36,7 @@
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
'rest_framework',
"articles",
]

Expand Down Expand Up @@ -69,7 +69,11 @@
]

WSGI_APPLICATION = "project.wsgi.application"

REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.AllowAny',
),
}

# Database
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
Expand All @@ -81,7 +85,6 @@
}
}


# Password validation
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators

Expand All @@ -100,7 +103,6 @@
},
]


# Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/

Expand All @@ -114,7 +116,6 @@

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/

Expand Down
11 changes: 6 additions & 5 deletions project/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@
"""
from django.contrib import admin
from django.urls import path
from django.conf.urls import include

from articles.views import ArticleDetailAPIView, ArticleListCreateAPIView
import articles.views as av

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/", include('articles.urls')),



]