Skip to content

Commit

Permalink
feat(profile):Add, read and update user profile (#17)
Browse files Browse the repository at this point in the history
- create a profile application
- Add a user profile
- Edit user profile
- View your user profile
- View other profiles
- Add tests to increase coverage

[Delivers #165273475]
  • Loading branch information
engjames authored and archibishop committed May 3, 2019
1 parent d91f09d commit 8e82eae
Show file tree
Hide file tree
Showing 22 changed files with 426 additions and 51 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -99,5 +99,6 @@ db.sqlite3

#the coverage file
.coveragerc
.DS_Store

htmlcov/*
6 changes: 1 addition & 5 deletions authors/apps/articles/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# Generated by Django 2.1 on 2019-04-25 19:53
# Generated by Django 2.1 on 2019-05-02 06:27

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


Expand All @@ -11,7 +9,6 @@ class Migration(migrations.Migration):
initial = True

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
Expand All @@ -27,7 +24,6 @@ class Migration(migrations.Migration):
('updated_at', models.DateTimeField(default=django.utils.timezone.now)),
('favorited', models.BooleanField(default=False)),
('favorite_count', models.IntegerField(default=0)),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, to_field='username')),
],
),
]
23 changes: 23 additions & 0 deletions authors/apps/articles/migrations/0002_article_author.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 2.1 on 2019-05-02 06:27

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


class Migration(migrations.Migration):

initial = True

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

operations = [
migrations.AddField(
model_name='article',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, to_field='username'),
),
]
Binary file modified authors/apps/authentication/.DS_Store
Binary file not shown.
16 changes: 15 additions & 1 deletion authors/apps/authentication/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Generated by Django 2.1 on 2019-04-23 14:54
# Generated by Django 2.1 on 2019-05-02 06:27

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


class Migration(migrations.Migration):
Expand All @@ -25,11 +28,22 @@ class Migration(migrations.Migration):
('is_staff', models.BooleanField(default=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('email_verified', models.BooleanField(default=False)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='ResetPasswordToken',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('token', models.CharField(max_length=50, unique=True)),
('email', models.EmailField(max_length=254, null=True)),
('created_at', models.DateTimeField(default=django.utils.timezone.now)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]
18 changes: 0 additions & 18 deletions authors/apps/authentication/migrations/0002_user_email_verified.py

This file was deleted.

26 changes: 0 additions & 26 deletions authors/apps/authentication/migrations/0003_resetpasswordtoken.py

This file was deleted.

2 changes: 1 addition & 1 deletion authors/apps/authentication/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ def encode_auth_token(email):
try:
payload = {
'exp': datetime.datetime.utcnow() + datetime.timedelta(
seconds=900),
seconds=1800),
'iat': datetime.datetime.utcnow(),
'sub': email
}
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'
29 changes: 29 additions & 0 deletions authors/apps/profiles/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Generated by Django 2.1 on 2019-05-02 12:53

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


class Migration(migrations.Migration):

initial = True

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name='UserProfile',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.URLField(blank=True)),
('bio', models.TextField(blank=True, max_length=200)),
('firstname', models.CharField(blank=True, max_length=25)),
('lastname', models.CharField(blank=True, max_length=25)),
('updated_at', models.DateTimeField(auto_now=True)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, to_field='username')),
],
),
]
Empty file.
30 changes: 30 additions & 0 deletions authors/apps/profiles/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from django.db import models
from authors.apps.authentication.models import User
from django.dispatch import receiver
from django.db.models.signals import post_save


class UserProfile(models.Model):
"""A model that contains different fields."""

user = models.OneToOneField(User, to_field="username", on_delete=models.CASCADE)
image = models.URLField(blank=True)
bio = models.TextField(blank=True, max_length=200)
firstname = models.CharField(blank=True, max_length=25)
lastname = models.CharField(blank=True, max_length=25)
updated_at = models.DateTimeField(auto_now=True)

def __str__(self):
"""Method to return the string representation of an object."""
return self.user.username


@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:
UserProfile.objects.create(user=instance)

@receiver(post_save, sender=User)
def save_profile(sender, instance, **kwargs):
instance.userprofile.save()

23 changes: 23 additions & 0 deletions authors/apps/profiles/renderers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
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 profile can't be found
# 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.
response = json.dumps({'profile': data})

errors = data.get('errors', None)

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

# Finally, we can render our data under the "profile" namespace.
return response
52 changes: 52 additions & 0 deletions authors/apps/profiles/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from rest_framework import serializers
from authors.apps.profiles.models import UserProfile
from .models import User
import re


class FetchUserProfileSerializer(serializers.ModelSerializer):
class Meta:
model = UserProfile
fields = '__all__'


class UpdateProfileSerializer(serializers.ModelSerializer):
username = serializers.ReadOnlyField(source='user.username')
class Meta:
model = UserProfile
fields = (
'firstname',
'lastname',
'username',
'image',
'bio'
)

def validate_username(self, username):
username1 = User.objects.filter(username=username)
if username1.exists():
raise serializers.ValidationError("Username already exists")
if len(username) <= 4:
raise serializers.ValidationError(
"username should be longer than 4 characters")
if re.search(r'[\s]', username):
raise serializers.ValidationError(
"username should not contain spaces")
if not re.search(r'[a-zA-Z]', username):
raise serializers.ValidationError(
"username should contain characters")
return username

def validate_firstname(self,firstname):
if len(firstname) <= 3:
raise serializers.ValidationError(
"Firstname should be longer than 3 characters")
return firstname

def validate_lastname(self,lastname):
if len(lastname) <= 4:
raise serializers.ValidationError(
"Lastname should be longer than 4 characters")
return lastname


Empty file.
88 changes: 88 additions & 0 deletions authors/apps/profiles/tests/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
from django.contrib.auth import get_user_model
from django.urls import reverse
from rest_framework.test import APITestCase, APIClient
from authors.apps.profiles.models import UserProfile

User = get_user_model()


class BaseTestCase(APITestCase):
"""
Testcases base for the user profile views.
"""

def setUp(self):
"""Initialize test client."""
self.client = APIClient()
self.user = User.objects.create_user(
username='test1', email='test1@example.com', password='12345678'
)
setattr(self.user, 'email_verified', True)
self.user.save()
self.data = {
'user':
{
'email': 'test1@example.com', 'password': '12345678'
}
}
self.login = reverse('user_login')
self.login_response = self.client.post(
self.login, self.data, format="json")
user_token = self.login_response.data['token']
self.auth_header = 'Bearer {}'.format(user_token)

self.profile = {
"profile": {
"firstname": "isaac",
"lastname": "kimbugwe",
"username": "isaac3",
"image": "http://127.0.0.1:8000/api/users/profile/isaac.png",
"bio": "I work at Andela"
}
}
self.profile2 = {
"profile": {
"firstname": "isaac1",
"lastname": "kimbugwe1",
"username": "isaac1",
"image": "http://127.0.0.1:8000/api/users/profile/isaac1.png",
"bio": "I work at Andela uganda"
}
}
self.profile4 = {
"profile": {
"firstname": "isaac1",
"lastname": "kim",
"username": "isaac12",
"image": "http://127.0.0.1:8000/api/users/profile/isaac12.png",
"bio": "I work at Andela uganda..."
}
}
self.profile3 = {
"profile": {
"firstname": "is",
"lastname": "kimbugwe12",
"username": "isaac12",
"image": "http://127.0.0.1:8000/api/users/profile/isaac12.png",
"bio": "I work at Andela uganda..."
}
}
self.profile5 = {
"profile": {
"firstname": "isaac23",
"lastname": "kimbugwe12",
"username": "is",
"image": "http://127.0.0.1:8000/api/users/profile/isaac12.png",
"bio": "I work at Andela uganda..."
}
}
self.user2 = User.objects.create_user(
username='test2', email='test2@example.com', password='12345678'
)
setattr(self.user2, 'email_verified', True)
self.user2.save()
self.data2 = {
'user': {
'email': 'test2@example.com', 'password': '12345678'
}
}
Loading

0 comments on commit 8e82eae

Please sign in to comment.