diff --git a/.gitignore b/.gitignore index 4f4218d..dc33426 100644 --- a/.gitignore +++ b/.gitignore @@ -95,6 +95,11 @@ ENV/ # SQLite3 db.sqlite3 +# Migrations authors/apps/*/migrations/* .DS_Store .vscode/ + +# vscode cache files and log files +.vscode/ +logfile diff --git a/authors/apps/authentication/backends.py b/authors/apps/authentication/backends.py index 5132c2c..96c5824 100755 --- a/authors/apps/authentication/backends.py +++ b/authors/apps/authentication/backends.py @@ -1,3 +1,6 @@ +"""Configure JWT Here""" + + import datetime import logging import jwt @@ -5,6 +8,7 @@ from django.contrib.auth import get_user_model from rest_framework import exceptions + from rest_framework.authentication import TokenAuthentication from django.contrib.auth.tokens import PasswordResetTokenGenerator diff --git a/authors/apps/profiles/admin.py b/authors/apps/profiles/admin.py index 1b50530..123dd95 100644 --- a/authors/apps/profiles/admin.py +++ b/authors/apps/profiles/admin.py @@ -1,6 +1,5 @@ from django.contrib import admin - -# Register your models here. from .models import Profile +# Register your models here. admin.site.register(Profile) diff --git a/authors/apps/profiles/models.py b/authors/apps/profiles/models.py index dee037c..4e507ee 100644 --- a/authors/apps/profiles/models.py +++ b/authors/apps/profiles/models.py @@ -22,12 +22,21 @@ class Profile(models.Model): # Add fields onto `Profile` model username = models.CharField(max_length=50, blank=True) - email = models.EmailField(blank=True) - full_name = models.CharField(max_length=50, blank=True) + first_name = models.CharField(max_length=50, blank=True) + last_name = models.CharField(max_length=50, blank=True) bio = models.CharField(max_length=200, blank=True) - image = models.URLField(blank=True) + profile_photo = models.URLField(blank=True) country = models.CharField(max_length=3, blank=True) phone_number = models.CharField(max_length=15, blank=True) + twitter_handle = models.CharField(max_length=15, blank=True) + website = models.URLField(blank=True) + + def __str__(self): + """ + Schema for representation of a Profile object in Terminal + """ + return self.email + created = models.DateTimeField( auto_now_add=True, help_text="This is the time of creation of this record" @@ -38,14 +47,6 @@ class Profile(models.Model): "any time this record is updated" ) - def __str__(self): - """ - Schema for representation of a Profile object in Terminal - """ - return self.email - - - def get_followers(self): """get all users that follow a user""" followers = Following.objects.filter(followed=self.user) @@ -92,7 +93,8 @@ def init_profile(sender, instance, created, **kwargs): """ if created: Profile.objects.create( - user=instance, username=instance.username, email=instance.email) + user=instance, + username=instance.username, first_name=instance.username) @receiver(post_save, sender=User) diff --git a/authors/apps/profiles/serializers.py b/authors/apps/profiles/serializers.py index b946789..6dc52f3 100644 --- a/authors/apps/profiles/serializers.py +++ b/authors/apps/profiles/serializers.py @@ -1,5 +1,9 @@ -import logging +""" + Module serializes `Profile` model + :generates JSON from fields in `Profile` model +""" +import logging from rest_framework import serializers from authors.apps.authentication.serializers import UserSerializer @@ -13,6 +17,18 @@ class Meta: model = Profile fields = ("__all__") + def update(self, instance, prof_data): + """ + Update profile items + """ + # For every item provided in the payload, + # amend the profile accordingly + for(key, value) in prof_data.items(): + setattr(instance.profile, key, value) + instance.save() + + return instance + class ProfileSerializer2(serializers.ModelSerializer): user = serializers.SerializerMethodField() @@ -20,7 +36,7 @@ class ProfileSerializer2(serializers.ModelSerializer): class Meta: model = Profile - exclude = ('username', 'email') + exclude = ('username',) def get_user(self, obj): """ diff --git a/authors/apps/profiles/tests/__init__.py b/authors/apps/profiles/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/authors/apps/profiles/tests/test_profile.py b/authors/apps/profiles/tests/test_profile.py new file mode 100644 index 0000000..b8c693f --- /dev/null +++ b/authors/apps/profiles/tests/test_profile.py @@ -0,0 +1,157 @@ +""" + Module contains the unittests for the `profiles` app +""" +import json +from rest_framework.test import APITestCase +from django.urls import reverse + +# local imports +from authors.apps.profiles.models import Profile +from authors.apps.authentication.models import User + +# Create your tests here. + + +class TestProfileModel(APITestCase): + """ + UNITTESTS for Profile Model + """ + + def setUp(self): + """ + Set up + """ + # Generate a test client for sending API requests + # Define the endpoints for register, login + self.register_endpoint = reverse('register') + self.login_endpoint = reverse('login') + self.profile_endpoint = reverse('user_profiles') + + self.user = {"user": { + "username": "rkemmy69", + "email": "rkemmy69@mymail.com", + "password": "#Strong2-password" + } + } + + def register_user_helper(self): + """ + Helper method for registering a user and returning a user + """ + # Register a user to generate a token + register_response = self.client.post( + self.register_endpoint, self.user, format='json') + + user = User.objects.get(username=self.user['user']['username']) + user.is_active = True + user.save() + user = User.objects.get(username=self.user['user']['username']) + + # Decode response and extract user + user = json.loads( + register_response.content.decode('utf-8'))['user'] + + return user + + def test_profile_auto_created_on_user_creation(self): + """ + Test autocreation of profile for each user + """ + # profile counts before user is saved + prof_count_before = Profile.objects.count() + + # Register a user and recount profiles + self.client.post( + self.register_endpoint, self.user, format='json') + + prof_count_after = Profile.objects.count() + + # assertions + self.assertEqual(prof_count_after - prof_count_before, 1) + + def test_unlogged_in_user_cannot_view_profile(self): + """ + Test that an unlogged in user cannot view the profile + """ + # Send a GET request to view profile + view_profile_response = self.client.get( + self.profile_endpoint, format='json') + + # extract status code from response + response_message = json.loads( + view_profile_response.content.decode('utf-8'))['detail'] + + # Assertions + # assert that the response message is as below + self.assertEqual( + response_message, "Authentication credentials were not provided.") + + # Check that the reponse status code is 401 + self.assertEqual(view_profile_response.status_code, 401) + + def test_user_can_view_profile(self): + """ + Test that a logged in user can view the profile + """ + # Register a user to generate a token + # Decode response and extract token + user = self.register_user_helper() + user_token = user['token'] + + # Send a GET request to view profile with token + self.client.credentials(HTTP_AUTHORIZATION='Token ' + user_token) + view_profile_response = self.client.get( + self.profile_endpoint, format='json') + + # extract profile from response + user_profile = json.loads( + view_profile_response.content.decode('utf-8'))['profile'] + + # Assertions + # assert that the user_profile is not null + self.assertTrue(user_profile) + + # assert that the user_profile is not null + self.assertEqual(view_profile_response.status_code, 200) + + # assert that profile contains username of signed in user + self.assertEqual( + user_profile['first_name'], user['username'] + ) + + def test_user_can_update_profile(self): + """ + Test that a logged in user can view the profile + """ + # Register a user to generate a token + # Decode response and extract token + user = self.register_user_helper() + user_token = user['token'] + + # Send a PUT request to update profile with token + changes_to_profile = { + "profile": { + "country": "USB", + "twitter_handle": "@dmithamo2" + } + } + self.client.credentials(HTTP_AUTHORIZATION='Token ' + user_token) + view_profile_response = self.client.put( + self.profile_endpoint, data=changes_to_profile, format='json') + + # extract profile from response + user_profile = json.loads( + view_profile_response.content.decode('utf-8'))['profile'] + + # Assertions + # assert that the status_code is 200 OK + self.assertEqual(view_profile_response.status_code, 200) + + # assert that profile contains username of signed in user + self.assertEqual( + user_profile['country'], changes_to_profile['profile']['country']) + + # assert that twitter_handle changes as expected + self.assertEqual( + user_profile['twitter_handle'], + changes_to_profile['profile']['twitter_handle']) diff --git a/authors/apps/profiles/urls.py b/authors/apps/profiles/urls.py index 133f292..b2c03a1 100644 --- a/authors/apps/profiles/urls.py +++ b/authors/apps/profiles/urls.py @@ -1,3 +1,6 @@ +""" + Define the urls where the views for `profiles` are accessible +""" from django.urls import path from . import views from authors.apps.profiles.views import GetUserProfile, \ @@ -10,4 +13,5 @@ path('/follow', FollowUser.as_view(), name="follow"), path('followers/', ListAllFollowers.as_view(), name="followers"), path('followed/', ListAllFollowed.as_view(), name="followed"), + path('users/profiles', views.ProfileView.as_view(), name='user_profiles'), ] diff --git a/authors/apps/profiles/views.py b/authors/apps/profiles/views.py index 8fbc5d9..ab6be53 100644 --- a/authors/apps/profiles/views.py +++ b/authors/apps/profiles/views.py @@ -1,25 +1,31 @@ -from django.shortcuts import get_object_or_404 +""" +Views for profiles app +""" + import logging +from django.shortcuts import get_object_or_404 +from django.contrib.auth import get_user_model -# Create your views here. from rest_framework import generics -from django.contrib.auth import get_user_model from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView from rest_framework import exceptions, status, reverse -from authors.apps.profiles.models import Profile, Following +# Create your views here. + +# local imports from authors.apps.profiles.serializers import ( - ProfileSerializer, - FollowingSerializer, - FollowedSerializer, - FollowersSerializer, - ProfileSerializer2) + ProfileSerializer, FollowingSerializer, FollowedSerializer, + FollowersSerializer, ProfileSerializer2) + +from authors.apps.profiles.models import Profile, Following logger = logging.getLogger(__name__) +# Create your views here. + class ProfilesList(generics.ListAPIView): permission_classes = (IsAuthenticated,) queryset = Profile.objects.all() # Gets all Profiles @@ -35,7 +41,59 @@ def get(self, request, username): return Response(data) +class ProfileView(APIView): + """ + Class contains all the views possible for the `profiles` app + """ + permission_classes = (IsAuthenticated,) + + def get(self, request): + """ + Return the profiles of the logged in User + """ + # logged in user + user = request.user + user_profile = Profile.objects.get(user=user) + serializer = ProfileSerializer(user_profile) + + # render the response as defined in the API Spec + formatted_user_profile = { + "profile": serializer.data + } + + return Response(formatted_user_profile, status=status.HTTP_200_OK) + + def put(self, request): + """ + Update items on the `Profile` for logged in user + """ + # Extract profile data to be updated from request + profile_data = request.data.get('profile') + # logged in user + user = request.user + + # call serializer with extracted data and + # logged in user (who owns the profile) + serializer = ProfileSerializer(user, data=profile_data, partial=True) + serializer.is_valid(raise_exception=True) + serializer.save() + + # fetch the updated profile, and return it + user_profile = Profile.objects.get(user=user) + serializer = ProfileSerializer(user_profile) + + # render the response as defined in the API Spec + formatted_user_profile = { + "profile": serializer.data + } + + return Response(formatted_user_profile, status=status.HTTP_200_OK) + + class GetUserProfile(APIView): + """ + Defines the view for getting a User's profile + """ permission_classes = (IsAuthenticated,) serializer_class = ProfileSerializer2 @@ -66,6 +124,9 @@ def get(self, request, *args, **kwargs): class FollowUser(APIView): + """ + Defines the follower relationship + """ permission_classes = (IsAuthenticated,) serializer_class = FollowingSerializer @@ -165,6 +226,9 @@ def get(self, request): class ListAllFollowed(APIView): + """ + Lists all users who follow a user + """ permission_classes = (IsAuthenticated,) serializer_class = FollowedSerializer diff --git a/authors/urls.py b/authors/urls.py index 1f3be3e..c87ea76 100755 --- a/authors/urls.py +++ b/authors/urls.py @@ -30,4 +30,8 @@ name='profiles'), path('api/profile/', include('authors.apps.profiles.urls'), name='profile'), + path('admin/', admin.site.urls, name='admin'), + path('api/', include( + 'authors.apps.authentication.urls'), name='authentication'), + path('api/', include('authors.apps.profiles.urls'), name='profiles'), ] diff --git a/pytest.ini b/pytest.ini index 1b5de99..8c35e3a 100644 --- a/pytest.ini +++ b/pytest.ini @@ -4,3 +4,4 @@ DJANGO_SETTINGS_MODULE = authors.settings python_files = test_*.py *migrations/* # add pytest CLI commands to specify civerage and reuse of test db. This wil speed up tests addopts = --nomigrations --reuse-db --cov=authors/apps/ -p no:warnings +