Skip to content

Commit

Permalink
feat(Reporting): Enable reporting of an article
Browse files Browse the repository at this point in the history
 - write unite tests
 - enable users to post reports
 - enable superuser to get all reports
 - enable superuser to get reports on a single article

[Finishes #159952021]
  • Loading branch information
Colline Waitire authored and Innocent Asiimwe committed Sep 24, 2018
1 parent 162a127 commit 405c214
Show file tree
Hide file tree
Showing 9 changed files with 278 additions and 28 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 Article

`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
49 changes: 36 additions & 13 deletions authors/apps/articles/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ class Article(models.Model):

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 @@ -41,9 +42,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)

favorites_count = models.IntegerField(default=0)

Expand Down Expand Up @@ -85,9 +88,12 @@ 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:
Expand All @@ -96,13 +102,16 @@ class Meta:

class Comments(models.Model):

article = models.ForeignKey(Article, related_name='comments', on_delete=models.CASCADE, blank=True, null=True)
article = models.ForeignKey(
Article, related_name='comments', on_delete=models.CASCADE, blank=True, null=True)

author = models.ForeignKey(User, on_delete=models.CASCADE)

body = models.TextField(null=False, blank=False, error_messages={"required": "You cannot submit without a comment."})
body = models.TextField(null=False, blank=False, error_messages={
"required": "You cannot submit without a comment."})

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)

def __str__(self):
"""
Expand All @@ -117,14 +126,17 @@ class Meta:

class Replies(models.Model):

comment = models.ForeignKey(Comments, related_name='replies', on_delete=models.CASCADE, blank=True, null=True)
comment = models.ForeignKey(
Comments, related_name='replies', on_delete=models.CASCADE, blank=True, null=True)

author = models.ForeignKey(User, related_name='replies', on_delete=models.CASCADE, blank=True , null=True)
author = models.ForeignKey(
User, related_name='replies', on_delete=models.CASCADE, blank=True, null=True)

content = models.TextField(null=False, blank=False,
error_messages={"required": "You cannot submit without a reply."})
error_messages={"required": "You cannot submit without a reply."})

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)

def __str__(self):
"""
Expand All @@ -136,3 +148,14 @@ class Meta:
get_latest_by = 'created_at'
ordering = ['-created_at']


class ArticleReport(models.Model):
"""
Model for creating reports made on articles
"""
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)
34 changes: 33 additions & 1 deletion authors/apps/articles/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,22 @@
from rest_framework import serializers
from rest_framework.pagination import PageNumberPagination
from authors.apps.articles.exceptions import NotFoundException
from authors.apps.articles.models import Article, Tag, Rating, Comments, Replies

from authors.apps.articles.models import (Article,
Tag, Rating, ArticleReport, Comments, Replies)
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):
"""
Implements a custom relational field by overriding RelatedFied.
returns a list of tag names.
"""

def to_representation(self, value):

return value.tag_name
Expand Down Expand Up @@ -122,6 +126,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 @@ -249,3 +263,21 @@ 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 @@ -306,3 +306,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"
Expand Down
4 changes: 0 additions & 4 deletions authors/apps/articles/tests/test_replies.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,6 @@ def test_update_reply(self):
self.post_comment), content_type='application/json')
self.assertEqual(201, response.status_code)
self.assertIsInstance(response.json(), dict)
print('response --- > ', response)

id = response.json().get('id')
response = self.client.post(
Expand Down Expand Up @@ -173,7 +172,6 @@ def test_reply_update_with_no_content(self):
self.post_comment), content_type='application/json')
self.assertEqual(201, response.status_code)
self.assertIsInstance(response.json(), dict)
print('response --- > ', response)

id = response.json().get('id')

Expand Down Expand Up @@ -221,8 +219,6 @@ def test_delete_reply(self):
self.post_comment), content_type='application/json')
self.assertEqual(201, response.status_code)
self.assertIsInstance(response.json(), dict)
print('response --- > ', response)

id = response.json().get('id')
response = self.client.post(
"/api/articles/comment/{}/replies/".format(id), data=json.dumps(
Expand Down
15 changes: 10 additions & 5 deletions authors/apps/articles/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,27 @@
"""
from django.urls import path
from rest_framework.routers import DefaultRouter
from authors.apps.articles.views import ArticleViewSet, FavoriteArticlesAPIView, TagViewSet, RatingsView
from authors.apps.articles.views import CommentsView,RepliesView
from authors.apps.articles.views import ArticleViewSet, TagViewSet, RatingsView

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

urlpatterns = [

path('<article_slug>/favorite/', FavoriteArticlesAPIView.as_view(), name="favorite"),
path('<article_slug>/unfavorite/', FavoriteArticlesAPIView.as_view(), name="unfavorite"),
path('<article_slug>/favorite/',
FavoriteArticlesAPIView.as_view(), name="favorite"),
path('<article_slug>/unfavorite/',
FavoriteArticlesAPIView.as_view(), name="unfavorite"),
path("<slug>/rate/", RatingsView.as_view()),
path('<slug>/comment/', CommentsView.as_view()),
path('comment/<Id>/', CommentsView.as_view()),
path('comment/', CommentsView.as_view()),
path('comment/<commentID>/replies/', RepliesView.as_view()),
path('comment/replies/<Id>/', RepliesView.as_view()),

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

router = DefaultRouter()
Expand Down
Loading

0 comments on commit 405c214

Please sign in to comment.