diff --git a/.gitignore b/.gitignore
index b6e4761..3ee90ed 100644
--- a/.gitignore
+++ b/.gitignore
@@ -127,3 +127,6 @@ dmypy.json
# Pyre type checker
.pyre/
+
+# Django
+migrations/
diff --git a/app/.idea/app.iml b/app/.idea/app.iml
new file mode 100644
index 0000000..3ca8872
--- /dev/null
+++ b/app/.idea/app.iml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/.idea/inspectionProfiles/Project_Default.xml b/app/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..0c498ec
--- /dev/null
+++ b/app/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/.idea/inspectionProfiles/profiles_settings.xml b/app/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..105ce2d
--- /dev/null
+++ b/app/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/.idea/misc.xml b/app/.idea/misc.xml
new file mode 100644
index 0000000..d1e22ec
--- /dev/null
+++ b/app/.idea/misc.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/.idea/modules.xml b/app/.idea/modules.xml
new file mode 100644
index 0000000..8c4259d
--- /dev/null
+++ b/app/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/.idea/vcs.xml b/app/.idea/vcs.xml
new file mode 100644
index 0000000..6c0b863
--- /dev/null
+++ b/app/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/.idea/workspace.xml b/app/.idea/workspace.xml
new file mode 100644
index 0000000..4192d2d
--- /dev/null
+++ b/app/.idea/workspace.xml
@@ -0,0 +1,121 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1601426764677
+
+
+ 1601426764677
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ file://$PROJECT_DIR$/user/serializers.py
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/app/backends.py b/app/app/backends.py
new file mode 100644
index 0000000..f66c90b
--- /dev/null
+++ b/app/app/backends.py
@@ -0,0 +1,26 @@
+from django.contrib.auth import get_user_model
+from django.contrib.auth.backends import ModelBackend
+
+UserModel = get_user_model()
+
+
+class EmailOrUsernameLogin(ModelBackend):
+ def authenticate(self, request, username=None, password=None, **kwargs):
+ if '@' in username:
+ args = {'email': username}
+ else:
+ args = {'username': username}
+
+ if username is None or password is None:
+ return
+
+ try:
+ user = UserModel.objects.get(**args)
+ except UserModel.DoesNotExist:
+ # Run the default password hasher once to reduce the timing
+ # difference between an existing and a nonexistent user (#20760).
+ UserModel().set_password(password)
+ else:
+ if user.check_password(password) and \
+ self.user_can_authenticate(user):
+ return user
diff --git a/app/app/settings.py b/app/app/settings.py
index 4adef7d..1429e6f 100644
--- a/app/app/settings.py
+++ b/app/app/settings.py
@@ -9,13 +9,12 @@
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.1/ref/settings/
"""
-
+import os
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
-
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
@@ -27,7 +26,6 @@
ALLOWED_HOSTS = []
-
# Application definition
INSTALLED_APPS = [
@@ -37,6 +35,10 @@
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
+ 'rest_framework',
+ 'rest_framework.authtoken',
+ 'core',
+ 'user',
]
MIDDLEWARE = [
@@ -69,18 +71,19 @@
WSGI_APPLICATION = 'app.wsgi.application'
-
# Database
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
DATABASES = {
'default': {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': BASE_DIR / 'db.sqlite3',
+ 'ENGINE': 'django.db.backends.postgresql',
+ 'HOST': os.environ.get('DB_HOST'),
+ 'NAME': os.environ.get('DB_NAME'),
+ 'USER': os.environ.get('DB_USER'),
+ 'PASSWORD': os.environ.get('DB_PASS'),
}
}
-
# Password validation
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
@@ -99,7 +102,6 @@
},
]
-
# Internationalization
# https://docs.djangoproject.com/en/3.1/topics/i18n/
@@ -113,8 +115,13 @@
USE_TZ = True
-
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.1/howto/static-files/
STATIC_URL = '/static/'
+
+AUTH_USER_MODEL = 'core.User'
+
+AUTHENTICATION_BACKENDS = [
+ 'app.backends.EmailOrUsernameLogin',
+]
diff --git a/app/app/urls.py b/app/app/urls.py
index 1192fe4..f65fd5e 100644
--- a/app/app/urls.py
+++ b/app/app/urls.py
@@ -14,8 +14,9 @@
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
-from django.urls import path
+from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
+ path('api/user/', include('user.urls')),
]
diff --git a/app/core/__init__.py b/app/core/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/core/admin.py b/app/core/admin.py
new file mode 100644
index 0000000..eac1b52
--- /dev/null
+++ b/app/core/admin.py
@@ -0,0 +1,58 @@
+from django.contrib import admin
+from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
+from django.utils.translation import gettext as _
+
+from core import models
+
+
+class UserModel(BaseUserAdmin):
+ """User admin class."""
+ ordering = ['username']
+ list_display = ['email', 'username',
+ 'first_name', 'last_name', 'gender', 'stage', 'bio']
+ fieldsets = (
+ (
+ _('Personal Information'),
+ {
+ 'fields': ('first_name', 'last_name', 'stage', 'bio')
+ }
+ ),
+ (
+ _('Contact Information'),
+ {
+ 'fields': ('username', 'email')
+ },
+ ),
+ (
+ _('Permissions'),
+ {
+ 'fields': ('is_active', 'is_staff', 'is_superuser')
+ }
+ ),
+ (
+ _('Important Information'),
+ {
+ 'fields': ('id', 'last_login')
+ }
+ )
+ )
+
+ add_fieldsets = (
+ (
+ _('Person Information'),
+ {
+ 'classes': ('wide',),
+ 'fields': ('first_name', 'last_name', 'stage', 'bio')
+ }
+ ),
+ (
+ _('Account Information'),
+ {
+ 'classes': ('wide',),
+ 'fields': ('username', 'email', 'password1', 'password2')
+ }
+ )
+ )
+
+
+admin.site.register(models.User)
diff --git a/app/core/apps.py b/app/core/apps.py
new file mode 100644
index 0000000..26f78a8
--- /dev/null
+++ b/app/core/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class CoreConfig(AppConfig):
+ name = 'core'
diff --git a/app/core/management/__init__.py b/app/core/management/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/core/management/commands/__init__.py b/app/core/management/commands/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/core/management/commands/createuser.py b/app/core/management/commands/createuser.py
new file mode 100644
index 0000000..d3b8c6e
--- /dev/null
+++ b/app/core/management/commands/createuser.py
@@ -0,0 +1,72 @@
+import getpass
+
+from django.contrib.auth import get_user_model
+from django.core.management.base import BaseCommand
+
+UserModel = get_user_model()
+
+
+class Command(BaseCommand):
+
+ def add_arguments(self, parser):
+ parser.add_argument(
+ '--staff',
+ action='store_true',
+ help='Create a new staff user.',
+ )
+
+ def handle(self, *args, **options):
+
+ first_name = input('First Name: ')
+ if not first_name:
+ raise ValueError('No First name')
+
+ username = input('Username: ')
+ if not username:
+ raise ValueError('No Username')
+
+ email = input('Email: ')
+ if not email:
+ raise ValueError('No Email Address')
+
+ password = None
+ password_correct = False
+
+ while not password_correct:
+ password = getpass.getpass()
+ password2 = getpass.getpass('Password (confirm): ')
+ if password == password2:
+ password_correct = True
+ else:
+ self.stdout.write(
+ self.style.ERROR('The passwords do not match, '
+ 'please re-enter them!'),
+ )
+ try:
+ if options['staff']:
+ UserModel.objects.create_staff_user(
+ first_name=first_name,
+ username=username,
+ email=email,
+ password=password,
+ )
+ self.stdout.write(
+ self.style.SUCCESS(f'New Staff User '
+ f'Successful: {username} ({email})'),
+ )
+ else:
+ UserModel.objects.create_user(
+ first_name=first_name,
+ username=username,
+ email=email,
+ password=password,
+ )
+ self.stdout.write(
+ self.style.SUCCESS(f'New User Successful '
+ f'Created: {username} ({email})'),
+ )
+
+ except ValueError:
+ self.stdout.write(
+ self.style.ERROR('Failed to create user!'),
+ )
diff --git a/app/core/management/commands/wait_for_db.py b/app/core/management/commands/wait_for_db.py
new file mode 100644
index 0000000..7b7b18d
--- /dev/null
+++ b/app/core/management/commands/wait_for_db.py
@@ -0,0 +1,21 @@
+import time
+
+from django.db import connections
+from django.db.utils import OperationalError
+from django.core.management.base import BaseCommand
+
+
+class Command(BaseCommand):
+
+ def handle(self, *args, **options):
+ self.stdout.write('\nWait for database...')
+ db_conn = None
+ while not db_conn:
+ try:
+ db_conn = connections['default']
+ except OperationalError:
+ self.stdout.write(
+ self.style.ERROR('Database unavailable! waiting please...')
+ )
+ time.sleep(1)
+ self.stdout.write(self.style.SUCCESS('Database available now!'))
diff --git a/app/core/models.py b/app/core/models.py
new file mode 100644
index 0000000..4cf170e
--- /dev/null
+++ b/app/core/models.py
@@ -0,0 +1,86 @@
+from django.db import models
+from django.contrib.auth.models import BaseUserManager, AbstractBaseUser, \
+ PermissionsMixin
+
+from core import utils
+
+
+class UserManager(BaseUserManager):
+ """Manage user model"""
+
+ def create_user(self, email, username, password, **extra_fields):
+ """Creating and returning a new user."""
+ if not email:
+ raise ValueError('No email address.')
+ if not username or utils.not_valid_username(username):
+ raise ValueError('No valid username.')
+
+ email = self.normalize_email(email)
+ user = self.model(
+ email=email,
+ username=username,
+ **extra_fields,
+ )
+ user.set_password(password)
+ user.save(using=self._db)
+
+ return user
+
+ def create_superuser(self, email, username, password, **extra_fields):
+ """Creating and returning superuser."""
+ user = self.create_user(email, username, password, **extra_fields)
+ user.is_staff = True
+ user.is_superuser = True
+ user.is_active = True
+
+ user.save(using=self._db)
+ return user
+
+ def create_staff_user(self, email, username, password, **extra_fields):
+ """Creating and returning staff user."""
+ user = self.create_user(email, username, password, **extra_fields)
+ user.is_staff = True
+ user.is_active = True
+ user.save(using=self._db)
+
+ return user
+
+
+class User(AbstractBaseUser, PermissionsMixin):
+ """User model class."""
+ STAGES = (
+ (1, 'First'),
+ (2, 'Second'),
+ (3, 'Third'),
+ (4, 'Forth'),
+ (5, 'Fifth'),
+ (6, 'Sixth'),
+ )
+ GENDERS = [
+ (0, 'Male'),
+ (1, 'Female'),
+ ]
+
+ # Personal Information
+ first_name = models.CharField(max_length=255)
+ last_name = models.CharField(max_length=255, null=True)
+ gender = models.IntegerField(choices=GENDERS, null=True)
+ stage = models.IntegerField(choices=STAGES, null=True)
+ bio = models.TextField(null=True)
+
+ # Contact Information
+ email = models.EmailField(max_length=100, unique=True)
+ username = models.CharField(max_length=100, unique=True)
+
+ # Important Information
+ is_active = models.BooleanField(default=True)
+ is_staff = models.BooleanField(default=False)
+ is_superuser = models.BooleanField(default=False)
+
+ objects = UserManager()
+
+ USERNAME_FIELD = 'username'
+ REQUIRED_FIELDS = ['email', 'first_name', ]
+
+ def __str__(self):
+ return f"{self.username} ({self.email})"
diff --git a/app/core/tests/__init__.py b/app/core/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/core/tests/test_admin.py b/app/core/tests/test_admin.py
new file mode 100644
index 0000000..cf3a920
--- /dev/null
+++ b/app/core/tests/test_admin.py
@@ -0,0 +1,45 @@
+from django.test import TestCase, Client
+from django.contrib.auth import get_user_model
+from django.urls import reverse
+
+
+class TestAdminSite(TestCase):
+ """Test admin site"""
+
+ def setUp(self):
+ self.client = Client()
+ self.admin_user = get_user_model().objects.create_superuser(
+ email='admin@test.com',
+ password='test1234',
+ first_name='Test One',
+ username='testone',
+ )
+ self.client.force_login(self.admin_user)
+ self.user = get_user_model().objects.create_user(
+ email='user@test.com',
+ password='test1234',
+ first_name='Test',
+ username='testusername',
+ )
+
+ def test_users_list(self):
+ """Test all users lists in user page."""
+ url = reverse('admin:core_user_changelist')
+ res = self.client.get(url)
+
+ self.assertContains(res, self.user.username)
+ self.assertContains(res, self.user.email)
+
+ def test_user_change_page(self):
+ """Test user edit page works."""
+ url = reverse('admin:core_user_change', args=[self.user.id])
+ res = self.client.get(url)
+
+ self.assertEqual(res.status_code, 200)
+
+ def test_create_user_page(self):
+ """Test create user page works."""
+ url = reverse('admin:core_user_add')
+ res = self.client.get(url)
+
+ self.assertEqual(res.status_code, 200)
diff --git a/app/core/tests/test_commands.py b/app/core/tests/test_commands.py
new file mode 100644
index 0000000..bd3cb56
--- /dev/null
+++ b/app/core/tests/test_commands.py
@@ -0,0 +1,23 @@
+from unittest import mock
+
+from django.core.management import call_command
+from django.db.utils import OperationalError
+from django.test import TestCase
+
+
+class WaitForDBTest(TestCase):
+
+ def test_waite_for_db_ready(self):
+ """Test waiting for db when it's available."""
+ with mock.patch('django.db.utils.ConnectionHandler.__getitem__') as gi:
+ gi.return_value = True
+ call_command('wait_for_db')
+ self.assertEqual(gi.call_count, 1)
+
+ @mock.patch('time.sleep', return_value=True)
+ def test_wait_for_db(self, ts):
+ """Test wait for db."""
+ with mock.patch('django.db.utils.ConnectionHandler.__getitem__') as gi:
+ gi.side_effect = [OperationalError] * 5 + [True, ]
+ call_command('wait_for_db')
+ self.assertEqual(gi.call_count, 6)
diff --git a/app/core/tests/test_models.py b/app/core/tests/test_models.py
new file mode 100644
index 0000000..eaee53c
--- /dev/null
+++ b/app/core/tests/test_models.py
@@ -0,0 +1,90 @@
+from django.test import TestCase
+from django.contrib.auth import get_user_model
+
+data = {
+ 'email': 'user@test.com',
+ 'inv_email': 'email',
+ 'username': 'test',
+ 'inv_username': 'test@user',
+ 'first_name': 'Ali',
+ 'password': 'password1234',
+}
+
+
+class TestUserModel(TestCase):
+ """Test user model"""
+
+ def test_create_user(self):
+ """Test creating and returning a new user."""
+ data = {
+ 'email': 'user@test.com',
+ 'username': 'test_user',
+ 'first_name': 'Ali',
+ 'password': 'password1234',
+ }
+
+ user = get_user_model().objects.create_user(
+ email=data['email'],
+ username=data['username'],
+ password=data['password'],
+ first_name=data['first_name'],
+ )
+ self.assertEqual(user.email, data['email'])
+ self.assertEqual(user.first_name, data['first_name'])
+ self.assertEqual(user.username, data['username'])
+ self.assertTrue(user.check_password(data['password']))
+
+ def test_create_user_with_invalid_username_data(self):
+ """Test create user with invalid username."""
+ with self.assertRaises(ValueError):
+ get_user_model().objects.create_user(
+ email=data['email'],
+ username=data['inv_username'],
+ password=data['password'],
+ first_name=data['first_name'],
+ )
+
+ def test_create_user_with_no_username(self):
+ """Test creating user with no username."""
+ with self.assertRaises(ValueError):
+ get_user_model().objects.create_user(
+ email=data['email'],
+ username=None,
+ password=data['password'],
+ first_name=data['first_name'],
+ )
+
+ def test_create_user_with_no_email(self):
+ """Test creating user with no email."""
+ with self.assertRaises(ValueError):
+ get_user_model().objects.create_user(
+ email=None,
+ username=data['username'],
+ password=data['password'],
+ first_name=data['first_name'],
+ )
+
+ def test_creating_user_normalize(self):
+ """Test Creating a normalize email address for a new user."""
+ user = get_user_model().objects.create_user(
+ email=data['email'],
+ password=data['password'],
+ username=data['username'],
+ first_name=data['first_name']
+ )
+
+ self.assertEqual(user.email, data['email'].lower())
+ self.assertTrue(user.check_password(data['password']))
+
+ def test_creating_superuser(self):
+ """Test creating a new superuser."""
+
+ user = get_user_model().objects.create_superuser(
+ email=data['email'],
+ password=data['password'],
+ username=data['username'],
+ first_name=data['first_name'],
+ )
+
+ self.assertTrue(user.is_superuser)
+ self.assertTrue(user.is_staff)
diff --git a/app/core/utils.py b/app/core/utils.py
new file mode 100644
index 0000000..9269cf2
--- /dev/null
+++ b/app/core/utils.py
@@ -0,0 +1,7 @@
+def not_valid_username(username):
+ """Chack if username is valid."""
+ if not username:
+ return True
+ if '@' in username:
+ return True
+ return False
diff --git a/app/user/__init__.py b/app/user/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/user/apps.py b/app/user/apps.py
new file mode 100644
index 0000000..35048d4
--- /dev/null
+++ b/app/user/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class UserConfig(AppConfig):
+ name = 'user'
diff --git a/app/user/permissions.py b/app/user/permissions.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/user/serializers.py b/app/user/serializers.py
new file mode 100644
index 0000000..9e2489b
--- /dev/null
+++ b/app/user/serializers.py
@@ -0,0 +1,63 @@
+from django.contrib.auth import get_user_model, authenticate
+from django.utils.translation import ugettext_lazy as _
+from rest_framework import serializers
+
+
+class UserSerializer(serializers.ModelSerializer):
+ """User serializer class,"""
+
+ class Meta:
+ model = get_user_model()
+ fields = ('id', 'username', 'email', 'password',
+ 'first_name', 'last_name', 'gender',
+ 'stage', 'bio', 'is_active', 'is_staff',
+ 'is_superuser', 'last_login')
+ extra_kwargs = {
+ 'password': {
+ 'write_only': True,
+ 'min_length': 6
+ },
+ 'last_login': {
+ 'read_only': True,
+ }
+ }
+
+ def create(self, validated_data):
+ return get_user_model().objects.create_user(**validated_data)
+
+ def update(self, instance, validated_data):
+ password = validated_data.pop('password', None)
+ user = super().update(instance, validated_data)
+
+ if password:
+ user.set_password(password)
+ user.save()
+
+ return user
+
+
+class AuthTokenSerializer(serializers.Serializer):
+ """Auth token serializer."""
+ username = serializers.CharField()
+ password = serializers.CharField(
+ style={'input_type': 'password'},
+ trim_whitespace=False,
+ )
+
+ def validate(self, attrs):
+ """Validate and authenticate of user."""
+ username = attrs.get('username')
+ password = attrs.get('password')
+
+ user = authenticate(
+ request=self.context.get('request'),
+ username=username,
+ password=password
+ )
+
+ if not user:
+ msg = _('Unable to authenticate with provided credentials.')
+ raise serializers.ValidationError(msg, code='authentication')
+
+ attrs['user'] = user
+ return attrs
diff --git a/app/user/tests/__init__.py b/app/user/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/user/tests/test_user_api.py b/app/user/tests/test_user_api.py
new file mode 100644
index 0000000..3e5522d
--- /dev/null
+++ b/app/user/tests/test_user_api.py
@@ -0,0 +1,208 @@
+from django.urls import reverse
+from django.test import TestCase
+from django.contrib.auth import get_user_model
+
+from rest_framework.test import APIClient
+from rest_framework import status
+
+CREAT_USER_URL = reverse('user:create')
+LOGIN_URL = reverse('user:login')
+ME_URL = reverse('user:me')
+
+
+def create_user(email='user@test.com', username='testuser',
+ first_name='test', password='1234abcd'):
+ return get_user_model().objects. \
+ create_user(email=email, username=username,
+ first_name=first_name, password=password)
+
+
+class TestPublicUserApi(TestCase):
+ """Test public user api."""
+
+ def setUp(self):
+ self.client = APIClient()
+
+ def test_create_new_user(self):
+ """Test creating and returning new user."""
+ payload = {
+ 'first_name': 'Ali',
+ 'username': 'alawi',
+ 'email': 'ali@email.com',
+ 'password': 'testpass',
+ }
+ res = self.client.post(CREAT_USER_URL, payload)
+
+ self.assertEqual(res.status_code, status.HTTP_201_CREATED)
+ self.assertEqual(res.data['first_name'], payload['first_name'])
+ self.assertEqual(res.data['username'], payload['username'])
+ self.assertEqual(res.data['email'], payload['email'])
+ user = get_user_model().objects.filter(username=payload['username'])[0]
+ self.assertTrue(user.check_password(payload['password']))
+
+ def test_create_invalid_user(self):
+ """Test creating an invalid new user, should creatation not working."""
+ payload = {
+ 'first_name': 'Ali',
+ 'email': 'ali@email.com',
+ 'password': 'testpass',
+ }
+
+ res = self.client.post(CREAT_USER_URL, payload)
+ self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_create_user_with_too_short_password(self):
+ """Test create a new user with to short password."""
+ payload = {
+ 'first_name': 'Ali',
+ 'username': 'alawi',
+ 'email': 'ali@email.com',
+ 'password': '12',
+ }
+ res = self.client.post(CREAT_USER_URL, payload)
+ self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
+
+ user_exists = get_user_model().objects.filter(
+ email=payload['email']
+ ).exists()
+ self.assertFalse(user_exists)
+
+ def test_user_exist(self):
+ """Test creating user that already exists fails."""
+ payload = {
+ 'first_name': 'Ali',
+ 'username': 'alawi',
+ 'email': 'ali@email.com',
+ 'password': 'abcd1234',
+ }
+ create_user(**payload)
+
+ res = self.client.post(CREAT_USER_URL, payload)
+ self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_create_token_for_user_with_email(self):
+ """Test creating a token for user when login."""
+ payload = {
+ # we use @username keyword for username or email.
+ 'username': 'ali@email.com',
+ 'password': 'abcd1234',
+ }
+ create_user(email=payload['username'], password=payload['password'])
+
+ res = self.client.post(LOGIN_URL, payload)
+ self.assertEqual(res.status_code, status.HTTP_200_OK)
+ self.assertIn('token', res.data)
+
+ def test_create_token_for_user_with_username(self):
+ """Test creating a token for user when login."""
+ payload = {
+ # we use username keyword for username or email
+ 'username': 'alawi',
+ 'password': 'abcd1234',
+ }
+ create_user(username=payload['username'], password=payload['password'])
+
+ res = self.client.post(LOGIN_URL, payload)
+ self.assertEqual(res.status_code, status.HTTP_200_OK)
+ self.assertIn('token', res.data)
+
+ def test_create_token_invalid_credentials(self):
+ "Test if that token is not created if invlide credentials are given."
+ create_user(email='user@test.com', password='12345678')
+ payload = {
+ 'first_name': 'test',
+ 'email': 'user@test.com',
+ 'password': '1234abcd',
+ }
+ res = self.client.post(LOGIN_URL, payload)
+
+ self.assertNotIn('token', res.data)
+ self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_create_token_no_user(self):
+ """Test that no token created when no user exists."""
+ payload = {
+ 'first_name': 'test',
+ 'email': 'user@test.com',
+ 'password': '1234abcd',
+ }
+ res = self.client.post(LOGIN_URL, payload)
+
+ self.assertNotIn('token', res.data)
+ self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_create_token_missing_field(self):
+ """Test that email and password are required."""
+ payload = {
+ 'email': 'user@test.com',
+ }
+ res = self.client.post(LOGIN_URL, payload)
+
+ self.assertNotIn('token', res.data)
+ self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_retrieve_all_user_data_when_login(self):
+ """Test retrieve all user data when login."""
+ payload = {
+ 'first_name': 'test',
+ 'password': '1234abcd',
+ 'email': 'user@test.com',
+ 'username': 'usertest',
+ }
+
+ create_user(**payload)
+
+ res = self.client.post(LOGIN_URL, payload)
+ self.assertEqual(res.status_code, status.HTTP_200_OK)
+ self.assertEqual(res.data['username'], payload['username'])
+ self.assertEqual(res.data['email'], payload['email'])
+ self.assertEqual(res.data['first_name'], payload['first_name'])
+
+ def test_retrieve_user_unauthorized(self):
+ """Test that authentication is required for users."""
+ res = self.client.get(ME_URL)
+
+ self.assertEqual(res.status_code, status.HTTP_401_UNAUTHORIZED)
+
+
+class TestPrivateUserApi(TestCase):
+ """Test private user api."""
+
+ def setUp(self):
+ self.client = APIClient()
+ self.user = create_user()
+
+ self.client.force_authenticate(user=self.user)
+
+ def test_retrieve_profile_success(self):
+ """Test retrieve profile for logged in used."""
+ payload = {
+ 'email': self.user.email,
+ 'username': self.user.username,
+ 'first_name': self.user.first_name,
+ }
+ res = self.client.get(ME_URL)
+
+ self.assertEqual(res.status_code, status.HTTP_200_OK)
+ self.assertDictContainsSubset(payload, res.data)
+
+ def test_post_me_not_allowed(self):
+ """Test that POST is not allowed on the me url."""
+ res = self.client.post(ME_URL)
+ self.assertEqual(res.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
+
+ def test_update_user_profile(self):
+ """Test updating user profile."""
+ payload = {
+ 'first_name': 'AB Test',
+ 'last_name': 'TOTO',
+ 'password': 'newpasstest'
+ }
+
+ res = self.client.patch(ME_URL, payload)
+ self.user.refresh_from_db()
+
+ self.assertEqual(res.status_code, status.HTTP_200_OK)
+ self.assertEqual(self.user.first_name, payload['first_name'])
+ self.assertEqual(self.user.last_name, payload['last_name'])
+ self.assertTrue(self.user.check_password(payload['password']))
diff --git a/app/user/urls.py b/app/user/urls.py
new file mode 100644
index 0000000..37a8460
--- /dev/null
+++ b/app/user/urls.py
@@ -0,0 +1,11 @@
+from django.urls import path
+
+from .views import CreateUserApi, LoginView, ManageUserView
+
+app_name = 'user'
+
+urlpatterns = [
+ path('create/', CreateUserApi.as_view(), name='create'),
+ path('login/', LoginView.as_view(), name='login'),
+ path('me/', ManageUserView.as_view(), name='me')
+]
diff --git a/app/user/views.py b/app/user/views.py
new file mode 100644
index 0000000..659f5a5
--- /dev/null
+++ b/app/user/views.py
@@ -0,0 +1,51 @@
+from rest_framework import generics, authentication, permissions
+from rest_framework.authtoken import views
+from rest_framework.settings import api_settings
+from rest_framework.authtoken.models import Token
+from rest_framework.response import Response
+
+from user import serializers
+
+
+class CreateUserApi(generics.CreateAPIView):
+ """Create and return a new user."""
+ serializer_class = serializers.UserSerializer
+
+
+class LoginView(views.ObtainAuthToken):
+ """Create a new token for user."""
+ serializer_class = serializers.AuthTokenSerializer
+ renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
+
+ def post(self, request, *args, **kwargs):
+ serializer = self.serializer_class(data=request.data,
+ context={'request': request})
+ serializer.is_valid(raise_exception=True)
+ user = serializer.validated_data['user']
+ token, created = Token.objects.get_or_create(user=user)
+ return Response({
+ 'token': token.key,
+ 'id': user.id,
+ 'username': user.username,
+ 'email': user.email,
+ 'first_name': user.first_name,
+ 'last_name': user.last_name,
+ 'is_active': user.is_active,
+ 'is_staff': user.is_staff,
+ 'is_superuser': user.is_superuser,
+ 'is_authenticated': user.is_authenticated,
+ 'gender': user.get_gender_display(),
+ 'stage': user.get_stage_display(),
+ 'last_login': user.last_login,
+ 'bio': user.bio
+ })
+
+
+class ManageUserView(generics.RetrieveUpdateAPIView):
+ """Manage authenticated user."""
+ serializer_class = serializers.UserSerializer
+ authentication_classes = (authentication.TokenAuthentication,)
+ permission_classes = (permissions.IsAuthenticated,)
+
+ def get_object(self):
+ return self.request.user
diff --git a/docker-compose.yml b/docker-compose.yml
index 2f945b8..46d8d10 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -19,9 +19,11 @@ services:
- DB_PASS=mypassword
depends_on:
- db
+
db:
image: postgres:12.4-alpine
environment:
- POSTGRES_DB=app
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=mypassword
+