Skip to content

Commit

Permalink
feat(profiles):users create their profiles
Browse files Browse the repository at this point in the history
-write tests on profiles CRUD
-create profiles model
-implement creation of profiles after user creation
-implement retrieving of single and all profiles
-implement updating of single profiles instances
-add profiles urls to url.py file
-verify if tests running on CRUD profiles
-include swagger api configuartions
-remove typo in base settings file

[Finishes #164046250]
  • Loading branch information
BrianSerem committed Mar 11, 2019
1 parent 2d86944 commit a50604a
Show file tree
Hide file tree
Showing 15 changed files with 395 additions and 15 deletions.
27 changes: 22 additions & 5 deletions authors/apps/authentication/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from rest_framework import serializers
from rest_framework.validators import UniqueValidator
from .models import User
from authors.apps.profiles.serializers import GetProfileSerializer


class RegistrationSerializer(serializers.ModelSerializer):
Expand Down Expand Up @@ -115,6 +116,10 @@ def validate(self, data):
'This user has been deactivated.'
)

# Django provides a flag on our `User` model called `is_active`. The
# purpose of this flag to tell us whether the user has been banned
# or otherwise deactivated. This will almost never be the case, but
# it is worth checking for. Raise an exception in this case.
# The `validate` method should return a dictionary of validated data.
# This is the data that is passed to the `create` and `update` methods
# that we will see later on.
Expand All @@ -137,10 +142,11 @@ class UserSerializer(serializers.ModelSerializer):
min_length=8,
write_only=True
)
profile = GetProfileSerializer()

class Meta:
model = User
fields = ('email', 'username', 'password', 'token',)
fields = ('email', 'username', 'password', 'token', 'profile',)

# The `read_only_fields` option is an alternative for explicitly
# specifying the field with `read_only=True` like we did for password
Expand All @@ -160,21 +166,32 @@ def update(self, instance, validated_data):
# salting passwords, which is important for security. What that means
# here is that we need to remove the password field from the
# `validated_data` dictionary before iterating over it.

password = validated_data.pop('password', None)
profile_data = validated_data.pop('profile', {})

for (key, value) in validated_data.items():
# For the keys remaining in `validated_data`, we will set them on
# For the keys remaining in `validated_data`
# we will set them on
# the current `User` instance one at a time.
setattr(instance, key, value)

if password is not None:
# `.set_password()` is the method mentioned above. It handles all
# `.set_password()` is the method mentioned
# above. It handles all
# of the security stuff that we shouldn't be concerned with.
instance.set_password(password)

# Finally, after everything has been updated, we must explicitly save
# the model. It's worth pointing out that `.set_password()` does not
# Finally, after everything has been updated,
# we must explicitly save
# the model. It's worth pointing out that
# `.set_password()` does not
# save the model.
instance.save()

for (key, value) in profile_data.items():
setattr(instance.profile, key, value)
# Save profile
instance.profile.save()

return instance
28 changes: 26 additions & 2 deletions authors/apps/authentication/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,36 @@ def retrieve(self, request, *args, **kwargs):
return Response(serializer.data, status=status.HTTP_200_OK)

def update(self, request, *args, **kwargs):
serializer_data = request.data.get('user', {})
serializer_data = request.data
user_data = {
'username': serializer_data.get('username', request.user.username),
'email': serializer_data.get('email', request.user.email),
'profile': {
'first_name': serializer_data.get(
'first_name', request.user.profile.last_name),
'last_name': serializer_data.get(
'last_name', request.user.profile.last_name),
'birth_date': serializer_data.get(
'birth_date', request.user.profile.birth_date),
'bio': serializer_data.get('bio', request.user.profile.bio),
'image': serializer_data.get(
'image', request.user.profile.image),
'city': serializer_data.get(
'city', request.user.profile.city),
'country': serializer_data.get(
'country', request.user.profile.country),
'phone': serializer_data.get(
'phone', request.user.profile.phone),
'website': serializer_data.get(
'website', request.user.profile.website),

}
}

# Here is that serialize, validate, save pattern we talked about
# before.
serializer = self.serializer_class(
request.user, data=serializer_data, partial=True
request.user, data=user_data, partial=True
)
serializer.is_valid(raise_exception=True)
serializer.save()
Expand Down
8 changes: 8 additions & 0 deletions authors/apps/core/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _


class TimeStampModel(models.Model):
created_at = models.DateTimeField(_('created at'), auto_now_add=True)
updated_at = models.DateTimeField(
_('updated at'), auto_now_add=False, auto_now=True)
11 changes: 11 additions & 0 deletions authors/apps/profiles/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from rest_framework.exceptions import APIException


class ProfileDoesNotExist(APIException):
status_code = 400
default_detail = 'The requested profile does not exist.'


class UserIsNotAuthenticated(APIException):
status_code = 403
default_detail = 'You should be logged in to proceed.'
52 changes: 52 additions & 0 deletions authors/apps/profiles/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from django.conf import settings
from django.db import models
from django.db.models.signals import post_save
from authors.apps.core.models import TimeStampModel
from cloudinary.models import CloudinaryField
from cloudinary import CloudinaryImage


class Profile(TimeStampModel):
user = models.OneToOneField(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
first_name = models.CharField(
'first name', max_length=30, blank=True, null=True)
last_name = models.CharField(
'last name', max_length=30, blank=True, null=True)
birth_date = models.DateField('last name', null=True, blank=True)
bio = models.TextField('bio', default='', null=True, blank=True)
city = models.CharField('city', blank=True, null=True,
max_length=100, default='')
country = models.CharField('country', blank=True,
null=True, max_length=100, default='')
phone = models.IntegerField('phone', blank=True, null=True, default=0)
website = models.URLField('website', blank=True, null=True, default='')
image = CloudinaryField(
'image', default="image/upload/v1552193974/gyzbaptikqalgthxfdnh.png") # noqa

def __str__(self):
return self.user.username

@property
def get_username(self):
return self.user.username

def get_cloudinary_url(self):
image_url = CloudinaryImage(str(self.image)).build_url(
width=100, height=150, crop='fill')
return image_url


"""
Signal receiver for 'post_save' signal sent by User model upon saving
"""


def create_profile(sender, **kwargs):
if kwargs.get('created'):
user_profile = Profile(user=kwargs.get('instance'))
user_profile.save()


# connect the signal to the handler function
post_save.connect(create_profile, sender=settings.AUTH_USER_MODEL)
15 changes: 15 additions & 0 deletions authors/apps/profiles/renderers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import json

from rest_framework.renderers import JSONRenderer


class ProfileJSONRenderer(JSONRenderer):
charset = 'utf-8'

def render(self, data, media_type=None, renderer_context=None):

# Finally, we can render our data under the "profile" namespace.
return json.dumps({
'profile': data

})
21 changes: 21 additions & 0 deletions authors/apps/profiles/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from rest_framework import serializers

from .models import Profile


class GetProfileSerializer(serializers.ModelSerializer):
"""
serializers for user profile upon user registration.
"""

username = serializers.ReadOnlyField(source='get_username')
image_url = serializers.ReadOnlyField(source='get_cloudinary_url')

class Meta:
model = Profile

fields = (
'username', 'first_name', 'last_name', 'bio', 'image', 'image_url',
'website', 'city', 'phone', 'country')

read_only_fields = ("created_at", "updated_at")
Empty file.
23 changes: 23 additions & 0 deletions authors/apps/profiles/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from django.test import TestCase

from ..models import Profile
from authors.apps.authentication.models import User


class TestProfile(TestCase):
""" class to test the profile models"""

def setUp(self):
""" Setup some code that is used by the unittests"""
self.email = 'serem@gmail.com'
self.username = 'testing'
self.password = 'jcbsdhcvshucj!!'

# create a user that will be logged in
self.user = User.objects.create_user(
self.username, self.email, self.password)
self.profile = self.user.profile

def test_string_representation(self):
""" test for the value returned by __str__ """
self.assertEqual(str(self.profile), self.username)
142 changes: 142 additions & 0 deletions authors/apps/profiles/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
from django.test import TestCase, Client

import json

from authors.apps.authentication.models import User
from authors.apps.profiles.models import Profile
from authors.apps.profiles.renderers import ProfileJSONRenderer


class TestProfileViews(TestCase):
def setUp(self):
""" Funtion to setup some code that will be needed for unittests """
self.email = 'boomboom@gmail.com'
self.username = 'testing12'
self.password = 'testuserpass'

# create a user that will be logged in
self.user = User.objects.create_user(
self.username, self.email, self.password)

# verify a user's account and save
self.user.is_verified = True
self.user.save()

self.data = {
'user': {
'username': self.username,
'email': self.email,
'password': self.password,
}
}
self.updated_data = {

'username': "newname",
'email': "another@email.com",
'password': "otherpaswword",
'bio': 'i like classical music'

}
self.errornious_updated_data = {
"website": "notavalidurlforawebsiteman"
}
self.image_link = {
"image":"cats.jpg"
}

self.test_client = Client()

token = self.login_a_user()
headers = {'HTTP_AUTHORIZATION': 'Bearer ' + token}

def tearDown(self):
pass

def login_a_user(self):
"""
Reusable function to login a user
"""

response = self.test_client.post(
"/api/users/login/", data=json.dumps(self.data), content_type='application/json')
token = response.json()['user']['token']
return token

def register_user(self, user_details_dict):
""" Register anew user to the system
Args:
user_details_dict: a dictionary with username, email, password of the user
Returns: an issued post request to the user registration endpoint
"""
return self.test_client.post(
"/api/users/", data=json.dumps(user_details_dict), content_type='application/json')

@property
def token(self):
return self.login_a_user()

def test_retrieve_profile(self):
""" test for the retrive profile endpoint """
token = self.login_a_user()
headers = {'HTTP_AUTHORIZATION': 'Bearer ' + token}
response = self.test_client.get(
"/api/profiles/{}/".format(self.username), **headers, content_type='application/json')

self.assertEqual(response.status_code, 200)

def test_update_existing_profie(self):
""" test for updating exisiting user profile"""
token = self.login_a_user()
headers = {'HTTP_AUTHORIZATION': 'Bearer ' + token}
self.register_user(self.data)
response = self.test_client.put(
"/api/user/", **headers, content_type='application/json', data=json.dumps(self.updated_data))

self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['user']['email'], "another@email.com")
self.assertEqual(response.json()['user']['username'], "newname")
self.assertEqual(
response.json()['user']['profile']['bio'], 'i like classical music')

def test_list_all_profiles(self):
""" test for checking if app returns all available profiles """
token = self.login_a_user()
headers = {'HTTP_AUTHORIZATION': 'Bearer ' + token}
response = self.client.get(
"/api/profiles/", **headers, content_type='application/json')
self.assertEqual(response.status_code, 200)

def test_get_non_existing_profile(self):
""" test for checking if app catches non-existing profile error """
token = self.login_a_user()
headers = {'HTTP_AUTHORIZATION': 'Bearer ' + token}
response = self.client.get(
"/api/profiles/serdgddddadscw/", **headers, content_type='application/json')
self.assertEqual(response.status_code, 400)
self.assertEqual(
response.json()['profile']['detail'], "The requested profile does not exist.")

def test_if_renderer_catches_errors(self):
""" test is an error is caught by profile json renderer """
token = self.login_a_user()
headers = {'HTTP_AUTHORIZATION': 'Bearer ' + token}
self.register_user(self.data)
response = self.test_client.put(
"/api/user/", **headers, content_type='application/json', data=json.dumps(self.errornious_updated_data))

self.assertEqual(response.json()['errors']['profile']['website'], [
"Enter a valid URL."])

def test_if_image_uploads_successfully(self):
""" test if an profile image uploads successfully """
token = self.login_a_user()
headers = {'HTTP_AUTHORIZATION': 'Bearer ' + token}
self.register_user(self.data)
response = self.test_client.put(
"/api/user/", **headers, content_type='application/json', data=json.dumps(self.image_link))

self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['user']['profile']['image_url'],
"https://res.cloudinary.com/dbsri2qtr/image/upload/c_fill,h_150,w_100/cats")
10 changes: 10 additions & 0 deletions authors/apps/profiles/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from django.urls import path

from .views import ProfileRetrieveAPIView, ProfilesListAPIView

app_name = 'profiles'

urlpatterns = [
path('profiles/<username>/', ProfileRetrieveAPIView.as_view()),
path('profiles/', ProfilesListAPIView.as_view()),
]
Loading

0 comments on commit a50604a

Please sign in to comment.