First commit of the V2.
New base, new info.
This commit is contained in:
0
music/__init__.py
Normal file
0
music/__init__.py
Normal file
5
music/admin.py
Normal file
5
music/admin.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.contrib import admin
|
||||
from .models import Songs
|
||||
|
||||
admin.site.register(Songs)
|
||||
|
5
music/apps.py
Normal file
5
music/apps.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class MusicConfig(AppConfig):
|
||||
name = 'music'
|
18
music/decorators.py
Normal file
18
music/decorators.py
Normal file
@ -0,0 +1,18 @@
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import status
|
||||
|
||||
|
||||
def validate_request_data(fn):
|
||||
def decorated(*args, **kwargs):
|
||||
# args[0] == GenericView Object
|
||||
title = args[0].request.data.get("title", "")
|
||||
artist = args[0].request.data.get("artist", "")
|
||||
if not title and not artist:
|
||||
return Response(
|
||||
data={
|
||||
"message": "Both title and artist are required to add a song"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return fn(*args, **kwargs)
|
||||
return decorated
|
22
music/migrations/0001_initial.py
Normal file
22
music/migrations/0001_initial.py
Normal file
@ -0,0 +1,22 @@
|
||||
# Generated by Django 2.0.3 on 2018-03-21 13:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Songs',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=255)),
|
||||
('artist', models.CharField(max_length=255)),
|
||||
],
|
||||
),
|
||||
]
|
0
music/migrations/__init__.py
Normal file
0
music/migrations/__init__.py
Normal file
11
music/models.py
Normal file
11
music/models.py
Normal file
@ -0,0 +1,11 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Songs(models.Model):
|
||||
# song title
|
||||
title = models.CharField(max_length=255, null=False)
|
||||
# name of artist or group/band
|
||||
artist = models.CharField(max_length=255, null=False)
|
||||
|
||||
def __str__(self):
|
||||
return "{} - {}".format(self.title, self.artist)
|
32
music/serializers.py
Normal file
32
music/serializers.py
Normal file
@ -0,0 +1,32 @@
|
||||
from rest_framework import serializers
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from .models import Songs
|
||||
|
||||
|
||||
class SongsSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Songs
|
||||
fields = ("title", "artist")
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
instance.title = validated_data.get("title", instance.title)
|
||||
instance.artist = validated_data.get("artist", instance.artist)
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
def create(self, validated_data):
|
||||
return Songs.objects.create(**validated_data)
|
||||
|
||||
|
||||
class TokenSerializer(serializers.Serializer):
|
||||
"""
|
||||
This serializer serializes the token data
|
||||
"""
|
||||
token = serializers.CharField(max_length=255)
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ("username", "email")
|
329
music/tests.py
Normal file
329
music/tests.py
Normal file
@ -0,0 +1,329 @@
|
||||
import json
|
||||
from django.urls import reverse
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from rest_framework.test import APITestCase, APIClient
|
||||
from rest_framework.views import status
|
||||
from .models import Songs
|
||||
from .serializers import SongsSerializer
|
||||
|
||||
# tests for models
|
||||
|
||||
|
||||
class SongsModelTest(APITestCase):
|
||||
def setUp(self):
|
||||
self.a_song = Songs.objects.create(
|
||||
title="Ugandan anthem",
|
||||
artist="George William Kakoma"
|
||||
)
|
||||
|
||||
def test_song(self):
|
||||
""""
|
||||
This test ensures that the song created in the setup
|
||||
exists
|
||||
"""
|
||||
self.assertEqual(self.a_song.title, "Ugandan anthem")
|
||||
self.assertEqual(self.a_song.artist, "George William Kakoma")
|
||||
self.assertEqual(str(self.a_song), "Ugandan anthem - George William Kakoma")
|
||||
|
||||
# tests for views
|
||||
|
||||
|
||||
class BaseViewTest(APITestCase):
|
||||
client = APIClient()
|
||||
|
||||
@staticmethod
|
||||
def create_song(title="", artist=""):
|
||||
"""
|
||||
Create a song in the db
|
||||
:param title:
|
||||
:param artist:
|
||||
:return:
|
||||
"""
|
||||
if title != "" and artist != "":
|
||||
Songs.objects.create(title=title, artist=artist)
|
||||
|
||||
def make_a_request(self, kind="post", **kwargs):
|
||||
"""
|
||||
Make a post request to create a song
|
||||
:param kind: HTTP VERB
|
||||
:return:
|
||||
"""
|
||||
if kind == "post":
|
||||
return self.client.post(
|
||||
reverse(
|
||||
"songs-list-create",
|
||||
kwargs={
|
||||
"version": kwargs["version"]
|
||||
}
|
||||
),
|
||||
data=json.dumps(kwargs["data"]),
|
||||
content_type='application/json'
|
||||
)
|
||||
elif kind == "put":
|
||||
return self.client.put(
|
||||
reverse(
|
||||
"songs-detail",
|
||||
kwargs={
|
||||
"version": kwargs["version"],
|
||||
"pk": kwargs["id"]
|
||||
}
|
||||
),
|
||||
data=json.dumps(kwargs["data"]),
|
||||
content_type='application/json'
|
||||
)
|
||||
else:
|
||||
return None
|
||||
|
||||
def fetch_a_song(self, pk=0):
|
||||
return self.client.get(
|
||||
reverse(
|
||||
"songs-detail",
|
||||
kwargs={
|
||||
"version": "v1",
|
||||
"pk": pk
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
def delete_a_song(self, pk=0):
|
||||
return self.client.delete(
|
||||
reverse(
|
||||
"songs-detail",
|
||||
kwargs={
|
||||
"version": "v1",
|
||||
"pk": pk
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
def login_a_user(self, username="", password=""):
|
||||
url = reverse(
|
||||
"auth-login",
|
||||
kwargs={
|
||||
"version": "v1"
|
||||
}
|
||||
)
|
||||
return self.client.post(
|
||||
url,
|
||||
data=json.dumps({
|
||||
"username": username,
|
||||
"password": password
|
||||
}),
|
||||
content_type="application/json"
|
||||
)
|
||||
|
||||
def login_client(self, username="", password=""):
|
||||
# get a token from DRF
|
||||
response = self.client.post(
|
||||
reverse("create-token"),
|
||||
data=json.dumps(
|
||||
{
|
||||
'username': username,
|
||||
'password': password
|
||||
}
|
||||
),
|
||||
content_type='application/json'
|
||||
)
|
||||
self.token = response.data['token']
|
||||
# set the token in the header
|
||||
self.client.credentials(
|
||||
HTTP_AUTHORIZATION='Bearer ' + self.token
|
||||
)
|
||||
self.client.login(username=username, password=password)
|
||||
return self.token
|
||||
|
||||
def register_a_user(self, username="", password="", email=""):
|
||||
return self.client.post(
|
||||
reverse(
|
||||
"auth-register",
|
||||
kwargs={
|
||||
"version": "v1"
|
||||
}
|
||||
),
|
||||
data=json.dumps(
|
||||
{
|
||||
"username": username,
|
||||
"password": password,
|
||||
"email": email
|
||||
}
|
||||
),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
# create a admin user
|
||||
self.user = User.objects.create_superuser(
|
||||
username="test_user",
|
||||
email="test@mail.com",
|
||||
password="testing",
|
||||
first_name="test",
|
||||
last_name="user",
|
||||
)
|
||||
# add test data
|
||||
self.create_song("like glue", "sean paul")
|
||||
self.create_song("simple song", "konshens")
|
||||
self.create_song("love is wicked", "brick and lace")
|
||||
self.create_song("jam rock", "damien marley")
|
||||
self.valid_data = {
|
||||
"title": "test song",
|
||||
"artist": "test artist"
|
||||
}
|
||||
self.invalid_data = {
|
||||
"title": "",
|
||||
"artist": ""
|
||||
}
|
||||
self.valid_song_id = 1
|
||||
self.invalid_song_id = 100
|
||||
|
||||
|
||||
class GetAllSongsTest(BaseViewTest):
|
||||
|
||||
def test_get_all_songs(self):
|
||||
"""
|
||||
This test ensures that all songs added in the setUp method
|
||||
exist when we make a GET request to the songs/ endpoint
|
||||
"""
|
||||
self.login_client('test_user', 'testing')
|
||||
# hit the API endpoint
|
||||
response = self.client.get(
|
||||
reverse("songs-list-create", kwargs={"version": "v1"})
|
||||
)
|
||||
# fetch the data from db
|
||||
expected = Songs.objects.all()
|
||||
serialized = SongsSerializer(expected, many=True)
|
||||
self.assertEqual(response.data, serialized.data)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
|
||||
class GetASingleSongsTest(BaseViewTest):
|
||||
|
||||
def test_get_a_song(self):
|
||||
"""
|
||||
This test ensures that a single song of a given id is
|
||||
returned
|
||||
"""
|
||||
self.login_client('test_user', 'testing')
|
||||
# hit the API endpoint
|
||||
response = self.fetch_a_song(self.valid_song_id)
|
||||
# fetch the data from db
|
||||
expected = Songs.objects.get(pk=self.valid_song_id)
|
||||
serialized = SongsSerializer(expected)
|
||||
self.assertEqual(response.data, serialized.data)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
# test with a song that does not exist
|
||||
response = self.fetch_a_song(self.invalid_song_id)
|
||||
self.assertEqual(
|
||||
response.data["message"],
|
||||
"Song with id: 100 does not exist"
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
|
||||
class AddSongsTest(BaseViewTest):
|
||||
|
||||
def test_create_a_song(self):
|
||||
"""
|
||||
This test ensures that a single song can be added
|
||||
"""
|
||||
self.login_client('test_user', 'testing')
|
||||
# hit the API endpoint
|
||||
response = self.make_a_request(
|
||||
kind="post",
|
||||
version="v1",
|
||||
data=self.valid_data
|
||||
)
|
||||
self.assertEqual(response.data, self.valid_data)
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
# test with invalid data
|
||||
response = self.make_a_request(
|
||||
kind="post",
|
||||
version="v1",
|
||||
data=self.invalid_data
|
||||
)
|
||||
self.assertEqual(
|
||||
response.data["message"],
|
||||
"Both title and artist are required to add a song"
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class UpdateSongsTest(BaseViewTest):
|
||||
|
||||
def test_update_a_song(self):
|
||||
"""
|
||||
This test ensures that a single song can be updated. In this
|
||||
test we update the second song in the db with valid data and
|
||||
the third song with invalid data and make assertions
|
||||
"""
|
||||
self.login_client('test_user', 'testing')
|
||||
# hit the API endpoint
|
||||
response = self.make_a_request(
|
||||
kind="put",
|
||||
version="v1",
|
||||
id=2,
|
||||
data=self.valid_data
|
||||
)
|
||||
self.assertEqual(response.data, self.valid_data)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
# test with invalid data
|
||||
response = self.make_a_request(
|
||||
kind="put",
|
||||
version="v1",
|
||||
id=3,
|
||||
data=self.invalid_data
|
||||
)
|
||||
self.assertEqual(
|
||||
response.data["message"],
|
||||
"Both title and artist are required to add a song"
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class DeleteSongsTest(BaseViewTest):
|
||||
|
||||
def test_delete_a_song(self):
|
||||
"""
|
||||
This test ensures that when a song of given id can be deleted
|
||||
"""
|
||||
self.login_client('test_user', 'testing')
|
||||
# hit the API endpoint
|
||||
response = self.delete_a_song(1)
|
||||
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
||||
# test with invalid data
|
||||
response = self.delete_a_song(100)
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
|
||||
class AuthLoginUserTest(BaseViewTest):
|
||||
"""
|
||||
Tests for the auth/login/ endpoint
|
||||
"""
|
||||
|
||||
def test_login_user_with_valid_credentials(self):
|
||||
# test login with valid credentials
|
||||
response = self.login_a_user("test_user", "testing")
|
||||
# assert token key exists
|
||||
self.assertIn("token", response.data)
|
||||
# assert status code is 200 OK
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
# test login with invalid credentials
|
||||
response = self.login_a_user("anonymous", "pass")
|
||||
# assert status code is 401 UNAUTHORIZED
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
|
||||
class AuthRegisterUserTest(BaseViewTest):
|
||||
"""
|
||||
Tests for auth/register/ endpoint
|
||||
"""
|
||||
def test_register_a_user(self):
|
||||
response = self.register_a_user("new_user", "new_pass", "new_user@mail.com")
|
||||
# assert status code is 201 CREATED
|
||||
self.assertEqual(response.data["username"], "new_user")
|
||||
self.assertEqual(response.data["email"], "new_user@mail.com")
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
# test with invalid data
|
||||
response = self.register_a_user()
|
||||
# assert status code
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
10
music/urls.py
Normal file
10
music/urls.py
Normal file
@ -0,0 +1,10 @@
|
||||
from django.urls import path
|
||||
from .views import ListCreateSongsView, SongsDetailView, LoginView, RegisterUsers
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('songs/', ListCreateSongsView.as_view(), name="songs-list-create"),
|
||||
path('songs/<int:pk>/', SongsDetailView.as_view(), name="songs-detail"),
|
||||
path('auth/login/', LoginView.as_view(), name="auth-login"),
|
||||
path('auth/register/', RegisterUsers.as_view(), name="auth-register")
|
||||
]
|
149
music/views.py
Normal file
149
music/views.py
Normal file
@ -0,0 +1,149 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth import authenticate, login
|
||||
|
||||
from rest_framework import generics
|
||||
from rest_framework import permissions
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import status
|
||||
from rest_framework_jwt.settings import api_settings
|
||||
|
||||
from .decorators import validate_request_data
|
||||
from .models import Songs
|
||||
from .serializers import SongsSerializer, TokenSerializer, UserSerializer
|
||||
|
||||
import logging
|
||||
import json
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Get the JWT settings
|
||||
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
|
||||
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
|
||||
|
||||
|
||||
class ListCreateSongsView(generics.ListCreateAPIView):
|
||||
"""
|
||||
GET songs/
|
||||
POST songs/
|
||||
"""
|
||||
queryset = Songs.objects.all()
|
||||
serializer_class = SongsSerializer
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
|
||||
@validate_request_data
|
||||
def post(self, request, *args, **kwargs):
|
||||
a_song = Songs.objects.create(
|
||||
title=request.data["title"],
|
||||
artist=request.data["artist"]
|
||||
)
|
||||
return Response(
|
||||
data=SongsSerializer(a_song).data,
|
||||
status=status.HTTP_201_CREATED
|
||||
)
|
||||
|
||||
|
||||
class SongsDetailView(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
GET songs/:id/
|
||||
PUT songs/:id/
|
||||
DELETE songs/:id/
|
||||
"""
|
||||
queryset = Songs.objects.all()
|
||||
serializer_class = SongsSerializer
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
try:
|
||||
a_song = self.queryset.get(pk=kwargs["pk"])
|
||||
return Response(SongsSerializer(a_song).data)
|
||||
except Songs.DoesNotExist:
|
||||
return Response(
|
||||
data={
|
||||
"message": "Song with id: {} does not exist".format(kwargs["pk"])
|
||||
},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
@validate_request_data
|
||||
def put(self, request, *args, **kwargs):
|
||||
try:
|
||||
a_song = self.queryset.get(pk=kwargs["pk"])
|
||||
serializer = SongsSerializer()
|
||||
updated_song = serializer.update(a_song, request.data)
|
||||
return Response(SongsSerializer(updated_song).data)
|
||||
except Songs.DoesNotExist:
|
||||
return Response(
|
||||
data={
|
||||
"message": "Song with id: {} does not exist".format(kwargs["pk"])
|
||||
},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
try:
|
||||
a_song = self.queryset.get(pk=kwargs["pk"])
|
||||
a_song.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
except Songs.DoesNotExist:
|
||||
return Response(
|
||||
data={
|
||||
"message": "Song with id: {} does not exist".format(kwargs["pk"])
|
||||
},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
|
||||
|
||||
class LoginView(generics.CreateAPIView):
|
||||
"""
|
||||
POST auth/login/
|
||||
"""
|
||||
|
||||
# This permission class will over ride the global permission
|
||||
# class setting
|
||||
permission_classes = (permissions.AllowAny,)
|
||||
|
||||
queryset = User.objects.all()
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
logger.error('post LoginView')
|
||||
username = request.data.get("username", "")
|
||||
password = request.data.get("password", "")
|
||||
user = authenticate(request, username=username, password=password)
|
||||
if user is not None:
|
||||
# login saves the user’s ID in the session,
|
||||
# using Django’s session framework.
|
||||
login(request, user)
|
||||
serializer = TokenSerializer(data={
|
||||
# using drf jwt utility functions to generate a token
|
||||
"token": jwt_encode_handler(
|
||||
jwt_payload_handler(user)
|
||||
)})
|
||||
serializer.is_valid()
|
||||
return Response(serializer.data)
|
||||
return Response(status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
|
||||
class RegisterUsers(generics.CreateAPIView):
|
||||
"""
|
||||
POST auth/register/
|
||||
"""
|
||||
permission_classes = (permissions.AllowAny,)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
logger.error('post registerUsers')
|
||||
username = request.data.get("username", "")
|
||||
password = request.data.get("password", "")
|
||||
email = request.data.get("email", "")
|
||||
if not username and not password and not email:
|
||||
return Response(
|
||||
data={
|
||||
"message": "username, password and email is required to register a user"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
new_user = User.objects.create_user(
|
||||
username=username, password=password, email=email
|
||||
)
|
||||
return Response(
|
||||
data=UserSerializer(new_user).data,
|
||||
status=status.HTTP_201_CREATED
|
||||
)
|
Reference in New Issue
Block a user