Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
3845588
CHORE[Add postgres support]
Olanrewajuemmanuel Jul 20, 2024
ef2dcd4
CHORE[Add DRF]
Olanrewajuemmanuel Jul 20, 2024
f3d72f7
FIX[Update requirements.txt]
Olanrewajuemmanuel Jul 20, 2024
346fa69
FEAT[Add login functionality]
Olanrewajuemmanuel Jul 20, 2024
6d56805
FEAT[Add register functionality]
Olanrewajuemmanuel Jul 20, 2024
56aad64
FEAT[Add product and order apps and models]
Olanrewajuemmanuel Jul 20, 2024
43a7770
FEAT[Add CRUD operations for Product]
Olanrewajuemmanuel Jul 21, 2024
a80acb6
FIX[Update Bearer token for authentication]
Olanrewajuemmanuel Jul 21, 2024
6095095
FEAT[Add order implementation]
Olanrewajuemmanuel Jul 21, 2024
1f48a68
FEAT[Add pagination]
Olanrewajuemmanuel Jul 21, 2024
232b3ff
FIX[Fix pagination error on server output]
Olanrewajuemmanuel Jul 21, 2024
8bb2d3d
FEAT[Add search and filter functionality for products view]
Olanrewajuemmanuel Jul 21, 2024
123f533
TEST[Add unit tests for User views and model]
Olanrewajuemmanuel Jul 21, 2024
4877aae
FIX[Fix error message for permissions]
Olanrewajuemmanuel Jul 21, 2024
28d4614
FEAT[Add option for users to be staff on sign up]
Olanrewajuemmanuel Jul 21, 2024
2635c93
TEST[Add unit tests for Product views and model]
Olanrewajuemmanuel Jul 21, 2024
378ad5b
TEST[Add unit tests for Order views and models]
Olanrewajuemmanuel Jul 22, 2024
8fb2ff0
Merge branch 'tests/unit-tests' into olanrewaju/update
Olanrewajuemmanuel Jul 22, 2024
30f8843
FEAT[Add endpoint for Product categories]
Olanrewajuemmanuel Jul 22, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 51 additions & 17 deletions app/app/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,26 @@
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.0/ref/settings/
"""

import os
from pathlib import Path
from datetime import timedelta
from dotenv import load_dotenv

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent

load_dotenv()

# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-h-h69cr05lmc*w4vtkf+5qltg8#&#xf8fe(v9j9oxs-*-^#vjd'
SECRET_KEY = os.getenv('SECRET_KEY')

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []
DEBUG = os.getenv("IS_LIVE") != 'True'

ALLOWED_HOSTS = ['*']

# Application definition

Expand All @@ -37,7 +39,13 @@
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'core'
'rest_framework',
'rest_framework_simplejwt',
'rest_framework_simplejwt.token_blacklist',
'django_filters',
'core',
'products',
'orders',
]

MIDDLEWARE = [
Expand Down Expand Up @@ -70,36 +78,63 @@

WSGI_APPLICATION = 'app.wsgi.application'


# Database
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.getenv("DB_NAME"),
'USER': os.getenv("DB_USER"),
'PASSWORD': os.getenv("DB_PASSWORD"),
'HOST': os.getenv('DB_HOST'),
'PORT': '',
}
}


# Password validation
# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
'NAME':
'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
'NAME':
'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
'NAME':
'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
'NAME':
'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]

REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly',
],
'DEFAULT_AUTHENTICATION_CLASSES':
('rest_framework_simplejwt.authentication.JWTAuthentication', ),
'DEFAULT_PAGINATION_CLASS':
'core.pagination.StandardResultsSetPagination',
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend',
'rest_framework.filters.SearchFilter'
],
}

SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=10),
'REFRESH_TOKEN_LIFETIME': timedelta(days=30),
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True,
'AUTH_HEADER_TYPES': ('Bearer', ),
'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken', ),
}

# Internationalization
# https://docs.djangoproject.com/en/4.0/topics/i18n/
Expand All @@ -112,7 +147,6 @@

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.0/howto/static-files/

Expand All @@ -123,4 +157,4 @@

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

AUTH_USER_MODEL = 'core.User'
AUTH_USER_MODEL = 'core.User'
16 changes: 15 additions & 1 deletion app/app/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,22 @@
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
from rest_framework.routers import DefaultRouter
from core.views import UserViewSet
from products.views import ProductViewSet, ProductCategoryViewSet
from orders.views import OrderViewSet

router = DefaultRouter()

router.register('users', UserViewSet, basename='users')
router.register('products', ProductViewSet, basename='products')
router.register('orders', OrderViewSet, basename='orders')
router.register('categories',
ProductCategoryViewSet,
basename='product_categories')

urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include(router.urls)),
]
6 changes: 4 additions & 2 deletions app/core/admin.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Manage admin page for main app."""

# from django.contrib import admin
from django.contrib import admin
from .models import User

# Register your models here.
# Register your models here
admin.site.register(User)
13 changes: 13 additions & 0 deletions app/core/pagination.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from rest_framework.pagination import PageNumberPagination


class LargeResultsSetPagination(PageNumberPagination):
page_size = 1000
page_size_query_param = 'page_size'
max_page_size = 10000


class StandardResultsSetPagination(PageNumberPagination):
page_size = 100
page_size_query_param = 'page_size'
max_page_size = 1000
27 changes: 27 additions & 0 deletions app/core/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from rest_framework import permissions


class OwnerPermission(permissions.BasePermission):
""" Allow owner only to update records. """

message = 'You do not have permissions to update this record'

def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return request.user == obj.user or request.user.is_staff


class IsAdminOrReadOnly(permissions.BasePermission):
"""
Custom permission to only allow admins to create and modify objects.
"""

message = 'Staff level or higher is required to update records'

def has_permission(self, request, view):
if request.method in permissions.SAFE_METHODS:
return True

# Only allow write actions for admin users
return request.user and request.user.is_staff
17 changes: 17 additions & 0 deletions app/core/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from rest_framework import serializers
from .models import User
from django.contrib.auth.hashers import make_password


class UserSerializer(serializers.ModelSerializer):
password = serializers.CharField(max_length=255, write_only=True)
is_staff = serializers.BooleanField(required=False)

class Meta:
model = User
fields = ("email", "password", "name", "is_staff")

def create(self, validated_data):
validated_data['password'] = make_password(
validated_data.get('password'))
return super(UserSerializer, self).create(validated_data)
12 changes: 4 additions & 8 deletions app/core/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,14 @@ def test_create_user_with_email_successful(self):
"""Test creating a new user with an email is successful."""
email = 'test@test.com'
password = 'testpassword123'
user = get_user_model().objects.create_user(
email=email,
password=password
)
user = get_user_model().objects.create_user(email=email,
password=password)

self.assertEqual(user.email, email)
self.assertTrue(user.check_password(password))

def test_new_user_email_normalized(self):
"""Test the email for a new uer is normalized."""
"""Test the email for a new user is normalized."""
email = 'test@TEST.com'
user = get_user_model().objects.create_user(email, 'testpassword123')

Expand All @@ -34,9 +32,7 @@ def test_new_user_invalid_email(self):
def test_create_new_superuser(self):
"""Test can create superuser."""
user = get_user_model().objects.create_superuser(
'test@TEST.com',
'test123'
)
'test@TEST.com', 'test123')

self.assertTrue(user.is_superuser)
self.assertTrue(user.is_staff)
26 changes: 26 additions & 0 deletions app/core/tests/test_setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from django.urls import reverse
from django.contrib.auth import get_user_model
from rest_framework.test import APITestCase
from faker import Faker


class UserTestsSetUp(APITestCase):

def setUp(self):
""" Handles set up of test variables and env for User. """
self.faker = Faker()
self.user_data = {
'email': self.faker.email(),
'password': self.faker.password(),
'name': self.faker.name_female(),
}
self.user = get_user_model().objects.create_user(
email=self.user_data['email'], password=self.user_data['password'])

self.register_url = reverse('users-register')
self.login_url = reverse('users-login')

return super().setUp()

def tearDown(self):
return super().tearDown()
66 changes: 66 additions & 0 deletions app/core/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from rest_framework import status
from .test_setup import UserTestsSetUp


class UserTestViews(UserTestsSetUp):

def test_user_cannot_register_without_data(self):
"""
Ensure users cannot register without providing data.
"""
response = self.client.post(self.register_url)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

def test_user_cannot_register_with_duplicate_data(self):
"""
Ensure users cannot register an existing email.
"""
response = self.client.post(self.register_url,
self.user_data,
format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

def test_user_can_register_with_correct_data(self):
"""
Ensure users can register with correct data.
"""
response = self.client.post(self.register_url, {
'email': self.faker.email(),
'password': self.faker.password(),
'name': self.faker.name_male(),
},
format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)

def test_user_can_login_successfully_and_get_tokens(self):
"""
Ensure users can login and receive an access token and refresh token in response.
"""
response = self.client.post(self.login_url,
self.user_data,
format='json')
self.assertIn('access', response.data)
self.assertIn('refresh', response.data)

def test_user_cannot_login_with_incorrect_data(self):
"""
Ensure users cannot login with incorrect credentials.
"""
response = self.client.post(self.login_url, {
'email': 'incorrect@mail.com',
'password': 'somepassword'
},
format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

def test_inactive_user_cannot_login(self):
"""
Ensure inactive accounts cannot login.
"""
self.user.is_active = False
self.user.save()
response = self.client.post(self.login_url,
self.user_data,
format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data.get('message'), 'Inactive account')
Loading