Skip to content

Commit

Permalink
Merge 5ae0db1 into 78174d3
Browse files Browse the repository at this point in the history
  • Loading branch information
collinewait committed Sep 21, 2018
2 parents 78174d3 + 5ae0db1 commit a3a72a6
Show file tree
Hide file tree
Showing 7 changed files with 266 additions and 14 deletions.
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,18 @@ The preferred JSON object to be returned by the API should be structured as foll
]
}
```

### List of reports

```source-json
{
"reports": [
"user": "jake",
"article": "how-to-train-your-dragon",
"report_message": "let me report this issue"
]
}
```
### Errors and Status Codes
If a request fails any validations, expect errors in the following format:

Expand Down Expand Up @@ -510,6 +522,33 @@ Example request body:

Authentication and super user required. returns a tag.

### Report Articlerticle

`POST /api/articles/reports/<slug>/`

Example request body:

```source-json
{
"report_message": "let me report this issue"
}
```
Required fields: `report_message`.

Authentication required, returns a report.

### Get all Reports of all Articles

`GET /api/articles/reports/`

Authentication and super user required, returns multiple reports.

### Get all Reports on a single Article

`GET /api/articles/reports/<slug>/`

Authentication and super user required, returns multiple reports.

##### Steps to install the project locally.

1. Install PostgresQL on the machine.
Expand Down
32 changes: 25 additions & 7 deletions authors/apps/articles/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class to declare an article
from authors.apps.articles.utils import generate_slug
from authors.apps.authentication.models import User


class Tag(models.Model):
"""
Tag for the article(s). Every tag has unique tag_name.
Expand All @@ -20,13 +21,16 @@ def __str__(self):
return self.tag_name

# noinspection SpellCheckingInspection


class Article(models.Model):
"""
A model for an article
"""
slug = models.SlugField(max_length=100, unique=True)

author = models.ForeignKey(User, on_delete=models.CASCADE, related_name="articles", null=True)
author = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="articles", null=True)

title = models.CharField(max_length=255, null=False, blank=False,
error_messages={"required": "Write a short title for your article."})
Expand All @@ -37,9 +41,11 @@ class Article(models.Model):
body = models.TextField(null=False, blank=False,
error_messages={"required": "You cannot submit an article without body."})

created_at = models.DateTimeField(auto_created=True, auto_now=False, default=timezone.now)
created_at = models.DateTimeField(
auto_created=True, auto_now=False, default=timezone.now)

updated_at = models.DateTimeField(auto_created=True, auto_now=False, default=timezone.now)
updated_at = models.DateTimeField(
auto_created=True, auto_now=False, default=timezone.now)

favorited = models.BooleanField(default=False)

Expand All @@ -62,7 +68,7 @@ def save(self, *args, **kwargs):
:param kwargs:
"""
self.slug = generate_slug(Article, self)

super(Article, self).save(*args, **kwargs)

@property
Expand All @@ -83,10 +89,22 @@ class Rating(models.Model):
"""
Model for creating article ratings or votes
"""
article = models.ForeignKey(Article, related_name="scores", on_delete=models.CASCADE)
rated_by = models.ForeignKey(User, on_delete=models.CASCADE, related_name="scores", null=True)
rated_at = models.DateTimeField(auto_created=True, default=timezone.now, auto_now=False)
article = models.ForeignKey(
Article, related_name="scores", on_delete=models.CASCADE)
rated_by = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="scores", null=True)
rated_at = models.DateTimeField(
auto_created=True, default=timezone.now, auto_now=False)
score = models.DecimalField(max_digits=4, decimal_places=2)

class Meta:
ordering = ('-score',)


class ArticleReport(models.Model):
article = models.ForeignKey(
Article, blank=False, null=False, on_delete=models.CASCADE)
user = models.ForeignKey(
User, blank=False, null=False, on_delete=models.CASCADE)
report_message = models.TextField(blank=True, null=True)
reported_at = models.DateTimeField(auto_now_add=True)
32 changes: 31 additions & 1 deletion authors/apps/articles/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
from rest_framework.pagination import PageNumberPagination

from authors.apps.articles.exceptions import NotFoundException
from authors.apps.articles.models import Article, Tag, Rating
from authors.apps.articles.models import Article, Tag, Rating, ArticleReport
from authors.apps.articles.utils import get_date
from authors.apps.authentication.models import User
from authors.apps.profiles.models import UserProfile
from authors.apps.profiles.serializers import UserProfileSerializer
from rest_framework.exceptions import NotFound


class TagRelatedField(serializers.RelatedField):
Expand Down Expand Up @@ -108,6 +109,16 @@ def validate_for_update(data: dict, user, slug):
})
return article, data

@staticmethod
def get_article_object(slug):
"""This method returns an instance of Article"""
article = None
try:
article = Article.objects.get(slug=slug)
except Article.DoesNotExist:
raise NotFound("An article with this slug does not exist")
return article

def to_representation(self, instance):
"""
formats serializer display response
Expand Down Expand Up @@ -231,3 +242,22 @@ class behaviours
"""
model = Rating
fields = ("score", "rated_by", "rated_at", "article")


class ArticleReportSerializer(serializers.ModelSerializer):
"""
Handles serialization and deserialization of ArticleReportSerializer objects.
"""

def create(self, validated_data):
return ArticleReport.objects.create(**validated_data)

def to_representation(self, instance):
response = super().to_representation(instance)
response["user"] = instance.user.username
response["article"] = instance.article.slug
return response

class Meta:
model = ArticleReport
fields = ['user', 'article', 'report_message']
107 changes: 107 additions & 0 deletions authors/apps/articles/tests/test_articles.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,3 +317,110 @@ def test_tag_update_on_article(self):
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertIn("django_restful", response.json()["article"]["tagList"])
self.assertNotIn("django", response.json()["article"]["tagList"])

def test_reporting_an_article(self):
"""Test that a user is able to report an article"""
self.client.post(
"/api/articles/",
self.post_article_with_tags,
format="json")
resp = self.client.get("/api/articles/")
slug = resp.json()["article"]["results"]["slug"]
response = self.client.post(
"/api/articles/reports/{}/".format(slug),
self.post_report,
format="json"
)

self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual("for real I have nothing to report.",
response.json()["report_message"])

def test_reporting_on_non_existing_article(self):
"""
Test that an error is raised when a user tries to
report an article that does not exist.
"""
response = self.client.post(
"/api/articles/reports/wrong_slug/",
self.post_report,
format="json"
)

self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertEqual("An article with this slug does not exist",
response.json()["detail"])

def test_report_message_validation(self):
"""
Test that an error is raised when the request is
missing: {"report_message": "the report message here"} or
has a blank report message.
"""
self.client.post(
"/api/articles/",
self.post_article_with_tags,
format="json")
resp = self.client.get("/api/articles/")
slug = resp.json()["article"]["results"]["slug"]
response = self.client.post(
"/api/articles/reports/{}/".format(slug),
" ",
format="json"
)

self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual("A report message is required",
response.json()["detail"])

def test_all_reports_returned(self):
"""Test that all reports on all articles are returned."""
self.user.is_superuser = True
self.user.save()
self.client.post(
"/api/articles/",
self.post_article_with_tags,
format="json")
resp = self.client.get("/api/articles/")
slug = resp.json()["article"]["results"]["slug"]
self.client.post(
"/api/articles/reports/{}/".format(slug),
self.post_report,
format="json"
)

response = self.client.get("/api/articles/reports/")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual("for real I have nothing to report.",
response.json()["reports"][0]["report_message"])

def test_reports_of_a_single_article_returned(self):
"""Test that all reports of a single article are returned."""
self.user.is_superuser = True
self.user.save()
self.client.post(
"/api/articles/",
self.post_article_with_tags,
format="json")
resp = self.client.get("/api/articles/")
slug = resp.json()["article"]["results"]["slug"]
self.client.post(
"/api/articles/reports/{}/".format(slug),
self.post_report,
format="json"
)

response = self.client.get("/api/articles/reports/{}/".format(slug))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual("for real I have nothing to report.",
response.json()["reports"][0]["report_message"])

def test_non_superuser_denied_report_viewing(self):
"""
Test that an error is returned when a non super user tries to
view reports made on articles
"""
response = self.client.get("/api/articles/reports/")
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual("permission denied, you do not have access rights.",
response.json()["detail"])
4 changes: 4 additions & 0 deletions authors/apps/articles/tests/test_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ class TestData:
}
}

post_report = {
"report_message": "for real I have nothing to report."
}

user_name = "iroq"
user_email = "iroq@sims.andela"
password = "teamiroq1"
10 changes: 7 additions & 3 deletions authors/apps/articles/urls.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
"""
Defines urls used in article package
"""
from django.urls import path
from django.urls import path, re_path
from rest_framework.routers import DefaultRouter
from django.views.decorators.csrf import csrf_exempt

from authors.apps.articles.views import ArticleViewSet, TagViewSet, RatingsView
from authors.apps.articles.views import (
ArticleViewSet, TagViewSet, RatingsView, ArticleReportView)

urlpatterns = [
path("<slug>/rate/", RatingsView.as_view())
path("<slug>/rate/", RatingsView.as_view()),
path("reports/", ArticleReportView.as_view()),
path("reports/<slug>/", ArticleReportView.as_view()),
]

router = DefaultRouter()
Expand Down
56 changes: 53 additions & 3 deletions authors/apps/articles/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@

from authors.apps.articles.exceptions import (
NotFoundException, InvalidQueryParameterException)
from authors.apps.articles.models import Article, Tag
from authors.apps.articles.models import Article, Tag, ArticleReport
from authors.apps.articles.renderer import ArticleJSONRenderer, TagJSONRenderer
from authors.apps.articles.serializers import (RatingSerializer,
ArticleSerializer, PaginatedArticleSerializer, TagSerializer)
from authors.apps.articles.serializers import (RatingSerializer, ArticleReportSerializer,
ArticleSerializer, PaginatedArticleSerializer, TagSerializer)
from authors.apps.articles.permissions import IsSuperuser


# noinspection PyUnusedLocal,PyMethodMayBeStatic


Expand Down Expand Up @@ -192,3 +193,52 @@ def destroy(self, request, *args, **kwargs):
self.perform_destroy(instance)
return Response(
{"message": "tag deleted successfuly"}, status=status.HTTP_204_NO_CONTENT)


class ArticleReportView(APIView):
"""
Handles creating, reading, updating and deleting reports
made on an article
"""
permission_classes = (IsAuthenticated, )
serializer_class = ArticleReportSerializer

def post(self, request, slug):
"""This method handles post requests when reporting an article."""
message = None
if "report_message" in request.data and request.data["report_message"].strip():
article = ArticleSerializer.get_article_object(slug)
user = request.user.id
message = request.data["report_message"]
data = {"user": user, "article": article.id,
"report_message": message}
serializer = self.serializer_class(data=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)
return Response({"detail": "A report message is required"},
status=status.HTTP_400_BAD_REQUEST)

def get(self, request, slug=None):
"""This method returns reports made on an article"""
article_reports = None
serializer = None
if request.user.is_superuser:
if slug:

article = ArticleSerializer.get_article_object(slug)
article_reports = ArticleReport.objects.filter(
article=article.id)
serializer = self.serializer_class(article_reports, many=True)
return Response({"reports": serializer.data}, status=status.HTTP_200_OK)

article_reports = ArticleReport.objects.all()
serializer = self.serializer_class(article_reports, many=True)
return Response({"reports": serializer.data}, status=status.HTTP_200_OK)
return Response({"detail": "permission denied, you do not have access rights."},
status=status.HTTP_403_FORBIDDEN)

0 comments on commit a3a72a6

Please sign in to comment.