Skip to content

Commit

Permalink
Merge pull request #13 from andela/ft-user-create-profile-165305265
Browse files Browse the repository at this point in the history
#165305265 Users should be able to create their profiles
  • Loading branch information
rnjane committed Apr 30, 2019
2 parents 12c12f0 + c3294c0 commit 0329c11
Show file tree
Hide file tree
Showing 26 changed files with 593 additions and 19 deletions.
5 changes: 4 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,7 @@ omit =
config/*
test/*
manage.py

authors/apps/profiles/utils.py
authors/wsgi.py
authors/apps/profiles/permissions.py
authors/apps/profiles/apps.py
3 changes: 3 additions & 0 deletions .env_example
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ DB_HOST=''
DOMAIN='localhost:8000'
EMAIL_HOST_USER='<your email here>'
EMAIL_HOST_PASSWORD='<Your password here>'
CLOUDINARY_CLOUD_NAME='cloudinary-name'
CLOUDINARY_API_KEY='cloudinary-api-key'
CLOUDINARY_API_SECRET='cloudinary-api-secret'
6 changes: 6 additions & 0 deletions authors/apps/authentication/messages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
errors = {
"profile_missing": {"errors": "profile with this username does not exist",
"status": 404},
"bad_image": {"errors": "Ensure that the file is an image",
"status": 400}
}
1 change: 0 additions & 1 deletion authors/apps/authentication/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ def create_superuser(self, username, email, password):
user = self.create_user(username, email, password)
user.is_superuser = True
user.is_staff = True
user.is_active = True
user.save()

return user
Expand Down
3 changes: 2 additions & 1 deletion authors/apps/authentication/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@

from .renderers import UserJSONRenderer
from .serializers import (
LoginSerializer, RegistrationSerializer, UserSerializer, UidAndTokenSerializer
LoginSerializer, RegistrationSerializer,
UserSerializer, UidAndTokenSerializer
)
from authors.apps.authentication.models import User
from authors.apps.core import exceptions
Expand Down
3 changes: 3 additions & 0 deletions authors/apps/profiles/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.contrib import admin

# Register your models here.
5 changes: 5 additions & 0 deletions authors/apps/profiles/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.apps import AppConfig


class ProfilesConfig(AppConfig):
name = 'profiles'
26 changes: 26 additions & 0 deletions authors/apps/profiles/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 2.1 on 2019-04-25 10:04

from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = [
]

operations = [
migrations.CreateModel(
name='UserProfile',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('username', models.CharField(max_length=30)),
('bio', models.TextField(blank=True, max_length=250)),
('image', models.URLField(null=True)),
('following', models.BooleanField(default=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
),
]
40 changes: 40 additions & 0 deletions authors/apps/profiles/migrations/0002_auto_20190426_0237.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Generated by Django 2.1 on 2019-04-26 02:37

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('profiles', '0001_initial'),
]

operations = [
migrations.RemoveField(
model_name='userprofile',
name='following',
),
migrations.RemoveField(
model_name='userprofile',
name='username',
),
migrations.AddField(
model_name='userprofile',
name='first_name',
field=models.CharField(blank=True, max_length=50),
),
migrations.AddField(
model_name='userprofile',
name='last_name',
field=models.CharField(blank=True, max_length=50),
),
migrations.AddField(
model_name='userprofile',
name='user',
field=models.OneToOneField(default=0, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
preserve_default=False,
),
]
Empty file.
28 changes: 28 additions & 0 deletions authors/apps/profiles/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from django.db import models
from ..authentication.models import User
from django.db.models.signals import post_save


class UserProfile(models.Model):
"""
The User Profile Model
"""

user = models.OneToOneField(User, on_delete=models.CASCADE)
first_name = models.CharField(max_length=50, blank=True)
last_name = models.CharField(max_length=50, blank=True)
bio = models.TextField(max_length=250, blank=True)
image = models.URLField(null=True)
created_at = models.DateTimeField(auto_now_add=True, editable=False)
updated_at = models.DateTimeField(auto_now=True)

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


def create_profile(sender, **kwargs):
if kwargs['created']:
user_profile = UserProfile.objects.create(user=kwargs["instance"])


post_save.connect(create_profile, sender=User)
18 changes: 18 additions & 0 deletions authors/apps/profiles/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from rest_framework import permissions


class IsGetOrIsAuthenticated(permissions.BasePermission):

def has_permission(self, request, view):

if request.method == 'GET':
return True

return request.user and request.user.is_authenticated

def has_object_permission(self, request, view, obj):

if request.method in permissions.SAFE_METHODS:
return True

return obj.user == request.user
24 changes: 24 additions & 0 deletions authors/apps/profiles/renderers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import json

from rest_framework.renderers import JSONRenderer


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

def render(self, data, media_type=None, renderer_context=None):
# If the view throws an error (such as the user can't be authenticated
# or something similar), `data` will contain an `errors` key. We want
# the default JSONRenderer to handle rendering errors, so we need to
# check for this case.
errors = data.get('errors', None)

if errors is not None:
# As mentioned about, we will let the default JSONRenderer handle
# rendering errors.
return super(ProfileJSONRenderer, self).render(data)

# Finally, we can render our data under the "user" namespace.
return json.dumps({
'profile': data
})
36 changes: 36 additions & 0 deletions authors/apps/profiles/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from .models import UserProfile
from rest_framework import serializers


class ProfileSerializer(serializers.ModelSerializer):
"""
Handles serialization and deserialization
of User Profile objects.
"""

username = serializers.CharField(source="user.username", read_only=True)
email = serializers.CharField(source="user.email", read_only=True)
created_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S")
updated_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S")

bio = serializers.CharField(
max_length=250,
min_length=8
)

class Meta:
model = UserProfile
fields = ('first_name', "last_name", "username",
"email", "bio", "image",
"created_at", "updated_at")

def update(self, instance, validated_data):
"""Performs an update on a User Profile."""

for (key, value) in validated_data.items():

setattr(instance, key, value)

instance.save()

return instance
3 changes: 3 additions & 0 deletions authors/apps/profiles/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.test import TestCase

# Create your tests here.
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 (
ProfileRetreiveUpdateAPIView
)

urlpatterns = [
path("profiles/<username>",
ProfileRetreiveUpdateAPIView.as_view()),
]
56 changes: 56 additions & 0 deletions authors/apps/profiles/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import datetime
import cloudinary.uploader
from django.http import JsonResponse
from rest_framework.exceptions import ParseError

from ..authentication.messages import errors


def ImageUploader(image):
"""
Upload image to cloudinary
:param image: FILE from post request
:return: uploaded image data if image was uploaded successfully else None
"""

if not str(image.name).endswith(('.png', '.jpg', '.jpeg')):
raise ParseError(errors["bad_image"])

try:
image_data = cloudinary.uploader.upload(
image,
public_id=str(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
.replace("-", "")
.replace(":", "")
.replace(" ", "")),
crop='limit',
width=2000,
height=2000,
eager=[
{'width': 200, 'height': 200,
'crop': 'thumb', 'gravity': 'face',
'radius': 20, 'effect': 'sepia'},
{'width': 100, 'height': 150,
'crop': 'fit', 'format': 'png'}
]
)
return image_data

except Exception as e:
raise ParseError({"error": e.__dict__})


def validate_image_upload(request):
"""
Check if image is provided and uploaded
:param request: put request
:return: uploaded image data if successful else error raised
"""

if request.FILES.get("image", False):

image = ImageUploader(request.FILES["image"])

request.data["image"] = image.get(
"secure_url", request.FILES["image"])
79 changes: 79 additions & 0 deletions authors/apps/profiles/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import json
from django.http.response import Http404
from rest_framework import status
from rest_framework.generics import RetrieveUpdateAPIView
from rest_framework.exceptions import (ParseError, NotFound)
from rest_framework.response import Response
from rest_framework.views import APIView

from .models import UserProfile
from .permissions import IsGetOrIsAuthenticated
from ..authentication.messages import errors
from .renderers import ProfileJSONRenderer
from .serializers import ProfileSerializer
from .utils import validate_image_upload


class ProfileRetreiveUpdateAPIView(RetrieveUpdateAPIView):
"""
get:
Get user profile.
put:
Update user profile.
"""

permission_classes = (IsGetOrIsAuthenticated,)
renderer_classes = (ProfileJSONRenderer,)
serializer_class = ProfileSerializer

def retrieve(self, request, username):
"""
get:
Get user profile.
"""

try:
user = UserProfile.objects.select_related('user').get(
user__username=username)

except UserProfile.DoesNotExist:

raise NotFound(errors["profile_missing"])

serializer = self.serializer_class(user)

return Response(serializer.data, status=status.HTTP_200_OK)

def update(self, request, username):
"""
put:
Update user profile.
:param username: username associated with
user account
:return: updated user profile details if successful
"""

serializer_data, image = request.data, ''

try:
user = UserProfile.objects.select_related('user').get(
user__username=username)

except UserProfile.DoesNotExist:

raise NotFound(errors["profile_missing"])

self.check_object_permissions(request, user)

validate_image_upload(request)

serializer = self.serializer_class(
user, data=serializer_data, partial=True)

serializer.is_valid(raise_exception=True)

serializer.save()

return Response(serializer.data, status=status.HTTP_200_OK)
Loading

0 comments on commit 0329c11

Please sign in to comment.