Skip to content

Commit

Permalink
feat(tagging-articles): tag articles
Browse files Browse the repository at this point in the history
Users should be able to tag their articles

- Modify articles.models.py
- Add tag relations serializer
- Modify articles.serializers.py
- Modify articles.urls.py, correct endpoint /api/tags
- Modify articles.views.py

[Finishes #162163178]
  • Loading branch information
sekayasin committed Dec 19, 2018
1 parent 003e6ee commit f31b335
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 41 deletions.
20 changes: 17 additions & 3 deletions authors/apps/articles/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@

class Article(TimestampedModel):
"""
Create and return an `Article` with a title, description and body.
Create and return an `Article` with a title,
description, body and tags associated with the article.
"""
slug = models.SlugField(
db_index=True,
Expand All @@ -23,7 +24,7 @@ class Article(TimestampedModel):
on_delete=models.CASCADE,
related_name='articles')
score = models.FloatField(default=0)
images = ArrayField(models.URLField(max_length=255),default=list)
images = ArrayField(models.URLField(max_length=255), default=list)
likes = models.IntegerField(
db_index=True,
default=False
Expand All @@ -32,6 +33,9 @@ class Article(TimestampedModel):
db_index=True,
default=False
)
tags = models.ManyToManyField(
'articles.Tag', related_name='articles'
)

def __str__(self):
return self.title
Expand All @@ -45,7 +49,7 @@ class Rating(models.Model):
article_id = models.ForeignKey(Article, on_delete=models.CASCADE)
score = models.IntegerField()


class Impressions(TimestampedModel):
"""
Create an `Impression` with a slug, user details, likes and dislikes.
Expand All @@ -70,3 +74,13 @@ class Impressions(TimestampedModel):
default=None
)


class Tag(TimestampedModel):
"""
Tag Model to allow authors add tags to their articles they create
"""
tag = models.CharField(max_length=255)
slug = models.SlugField(db_index=True, unique=True)

def __str__(self):
return self.tag
23 changes: 23 additions & 0 deletions authors/apps/articles/relations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from rest_framework import serializers

from .models import Tag


class TagRelatedField(serializers.RelatedField):
"""
A Special Serializer field for Tags.
If a user tags an article with an non existing tag, that new tag will also be created on article creation
"""

def get_queryset(self):
return Tag.objects.all()

def to_internal_value(self, data):
tag, created = Tag.objects.get_or_create(tag=data, slug=data.lower())
return tag

def to_representation(self, value):
"""
Return a tag property
"""
return value.tag
27 changes: 21 additions & 6 deletions authors/apps/articles/serializers.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from rest_framework import serializers
from authors.apps.profiles.serializers import GetUserProfileSerializer
from .models import Article, Rating
from authors.apps.profiles.serializers import (
GetUserProfileSerializer)

from .models import (
Article, Impressions)
Article, Rating, Impressions, Tag
)
from .relations import TagRelatedField


class ArticleSerializer(serializers.ModelSerializer):
Expand All @@ -17,18 +17,25 @@ class ArticleSerializer(serializers.ModelSerializer):
createdAt = serializers.SerializerMethodField(method_name='get_created_at')
updatedAt = serializers.SerializerMethodField(method_name='get_updated_at')
score = serializers.FloatField(required=False)
tagList = TagRelatedField(many=True, required=False, source='tags')

class Meta:
model = Article
fields = (
'author', 'body', 'createdAt', 'description',
'slug', 'title', 'updatedAt', 'score', 'images',
'likes', 'dislikes'
'likes', 'dislikes', 'tagList'
)

def create(self, validated_data):
author = self.context.get('author', None)
return Article.objects.create(author=author, **validated_data)
tags = validated_data.pop('tags', [])
article = Article.objects.create(author=author, **validated_data)

for tag in tags:
article.tags.add(tag)

return article

def get_created_at(self, instance):
return instance.created_at.isoformat()
Expand All @@ -42,6 +49,7 @@ class Meta:
model = Rating
fields = ('user', 'article_id', 'score')


class ImpressionSerializer(serializers.ModelSerializer):
"""
Serializes impressions requests and adds to an Article.
Expand Down Expand Up @@ -70,3 +78,10 @@ def get_updated_at(self, instance):
return instance.updated_at.isoformat()


class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
fields = ('tag',)

def to_representation(self, obj):
return obj.tag
20 changes: 20 additions & 0 deletions authors/apps/articles/tests/test_tag_article.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from rest_framework.test import APIClient
from .base_test import BaseTest
from rest_framework import status


class TestTagArticle(BaseTest):
def test_create_article_with_tags(self):
reponse = self.client.post(
'/api/articles/',
data={
"article": {
"title": "How to fight dragons 8",
"description": "Ever wonder jckvlahvhow?",
"body": "You have kenglto believe",
"tagList": ["dragons", "fight"]
}
},
format='json')
self.assertEqual(reponse.status_code, status.HTTP_201_CREATED)

6 changes: 3 additions & 3 deletions authors/apps/articles/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
from rest_framework.routers import DefaultRouter
from .views import (
ArticleViewSet, ArticleRetrieve,
LikeArticle, DislikeArticle, RatingsView)

LikeArticle, DislikeArticle, RatingsView, TagListView)

urlpatterns = [
path('articles/', ArticleViewSet.as_view()),
path('articles/<slug>', ArticleRetrieve.as_view()),
path('articles/<slug>/ratings/', RatingsView.as_view()),
path('articles/like/<slug>', LikeArticle.as_view()),
path('articles/dislike/<slug>', DislikeArticle.as_view())
path('articles/dislike/<slug>', DislikeArticle.as_view()),
path('tags', TagListView.as_view()),
]
59 changes: 30 additions & 29 deletions authors/apps/articles/views.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,24 @@
from rest_framework import mixins, status, viewsets
from rest_framework.permissions import IsAuthenticatedOrReadOnly, IsAuthenticated
from rest_framework.response import Response
from rest_framework.exceptions import NotFound
from rest_framework.generics import (
RetrieveUpdateDestroyAPIView,RetrieveUpdateAPIView,
ListCreateAPIView)
from .models import Article
from authors.apps.profiles.models import UserProfile
from rest_framework.generics import (
RetrieveUpdateDestroyAPIView,RetrieveUpdateAPIView,
ListCreateAPIView)
from rest_framework.views import APIView
from .models import Article, Rating

from .renderers import ArticleJSONRenderer
from .serializers import ArticleSerializer, RatingSerializer
from .pagination import PageNumbering
from django.db.models import Avg
from authors.apps.profiles.models import UserProfile
from rest_framework.permissions import (
IsAuthenticatedOrReadOnly, IsAuthenticated)
from rest_framework.response import Response
from rest_framework.exceptions import NotFound
from rest_framework.generics import (
RetrieveUpdateDestroyAPIView,
ListCreateAPIView)
RetrieveUpdateDestroyAPIView, RetrieveUpdateAPIView,
ListCreateAPIView, ListAPIView)

from .models import (
Article, Impressions)
Article, Impressions, Rating, Tag)

from authors.apps.profiles.models import UserProfile
from .renderers import ArticleJSONRenderer
from .serializers import (
ArticleSerializer, ImpressionSerializer,
RatingSerializer)
RatingSerializer, TagSerializer)
from .pagination import PageNumbering
from django.db.models import Avg
from authors.apps.profiles.models import UserProfile
from ..authentication.models import User
from django.db.models import Count

Expand Down Expand Up @@ -90,7 +78,7 @@ def update(self, request, slug):
def destroy(self, request, slug):
try:
serializer_instance = self.queryset.get(slug=slug)
except Article.DoesNotExist:
except Article.DoesNotExist:
raise NotFound('An article with this slug does not exist.')

self.perform_destroy(serializer_instance)
Expand Down Expand Up @@ -126,7 +114,7 @@ def post(self, request, slug):
self.store_rating(rating)
self.update_article_rating(article.id)
return Response({'message': 'Rating successfully updated.'}, status=201)

def store_rating(self, rating):
try:
article_rating = Rating.objects.filter(
Expand All @@ -139,7 +127,7 @@ def store_rating(self, rating):
serializer = RatingSerializer(data=rating)
serializer.is_valid(raise_exception=True)
serializer.save()

def update_article_rating(self, article_id):
article_ratings = Rating.objects.all().filter(article_id=article_id)
average = article_ratings.aggregate(Avg('score'))
Expand Down Expand Up @@ -180,8 +168,8 @@ def post(self, request, slug):
def updateimpression(self, impression):
try:
item = Impressions.objects.filter(
user = impression['user'],
slug = impression['slug']
user=impression['user'],
slug=impression['slug']
)[0]
if item.likes == True:
item.likes = False
Expand Down Expand Up @@ -230,8 +218,8 @@ def post(self, request, slug):
def updateimpression(self, impression):
try:
item = Impressions.objects.filter(
user = impression['user'],
slug = impression['slug']
user=impression['user'],
slug=impression['slug']
)[0]
if item.dislikes == True:
item.dislikes = False
Expand All @@ -246,3 +234,16 @@ def updateimpression(self, impression):
serializer.is_valid(raise_exception=True)
serializer.save()


class TagListView(ListAPIView):
queryset = Tag.objects.all()
permission_classes = (IsAuthenticated,)
serializer_class = TagSerializer

def taglist(self, request):
serializer_data = self.get_queryset()
serializer = self.serializer_class(serializer_data, many=True)

return Response({
'tags': serializer_data
}, status=status.HTTP_200_OK)

0 comments on commit f31b335

Please sign in to comment.