From f1b20d05fb27319b386afb28a08d327fcd231861 Mon Sep 17 00:00:00 2001 From: GRACE Date: Tue, 23 Jul 2024 12:10:52 +0100 Subject: [PATCH] feat[Worked on the whole project ] --- app/app/settings.py | 60 ++++- app/app/urls.py | 3 +- app/core/admin.py | 27 +- app/core/migrations/0001_initial.py | 7 +- ...002_user_date_joined_user_last_modified.py | 26 ++ .../migrations/0003_alter_user_date_joined.py | 19 ++ .../migrations/0004_category_product_order.py | 44 ++++ .../migrations/0005_alter_user_last_login.py | 18 ++ ..._rename_name_category_cat_name_and_more.py | 23 ++ .../0007_rename_cat_name_category_name.py | 18 ++ app/core/models.py | 73 ++++-- app/core/serializers.py | 67 +++++ app/core/tests/test_models.py | 42 ---- app/core/tests/tests.py | 230 ++++++++++++++++++ app/core/urls.py | 20 ++ app/core/views.py | 84 +++++++ 16 files changed, 695 insertions(+), 66 deletions(-) create mode 100644 app/core/migrations/0002_user_date_joined_user_last_modified.py create mode 100644 app/core/migrations/0003_alter_user_date_joined.py create mode 100644 app/core/migrations/0004_category_product_order.py create mode 100644 app/core/migrations/0005_alter_user_last_login.py create mode 100644 app/core/migrations/0006_rename_name_category_cat_name_and_more.py create mode 100644 app/core/migrations/0007_rename_cat_name_category_name.py create mode 100644 app/core/serializers.py delete mode 100644 app/core/tests/test_models.py create mode 100644 app/core/tests/tests.py create mode 100644 app/core/urls.py create mode 100644 app/core/views.py diff --git a/app/app/settings.py b/app/app/settings.py index 5356348..810cd94 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -11,6 +11,7 @@ """ from pathlib import Path +from datetime import timedelta # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -37,7 +38,9 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'core' + 'rest_framework', + 'rest_framework_simplejwt.token_blacklist', + 'core', ] MIDDLEWARE = [ @@ -74,13 +77,64 @@ # Database # https://docs.djangoproject.com/en/4.0/ref/settings/#databases +# DATABASES = { +# 'default': { +# 'ENGINE': 'django.db.backends.sqlite3', +# 'NAME': BASE_DIR / 'db.sqlite3', +# } +# } + DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': 'test_db', + 'USER': 'postgres', + 'PASSWORD': 'seismic25', + 'HOST': '127.0.0.1', + 'PORT': '5432' } } +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework_simplejwt.authentication.JWTAuthentication', + ), + 'DEFAULT_PERMISSION_CLASSES': ( + 'rest_framework.permissions.IsAuthenticatedOrReadOnly', + ), + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', + 'PAGE_SIZE': 10, +} + +SIMPLE_JWT = { + 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=1), + 'ROTATE_REFRESH_TOKENS': False, + 'BLACKLIST_AFTER_ROTATION': True, + 'UPDATE_LAST_LOGIN': False, + + 'ALGORITHM': 'HS256', + 'SIGNING_KEY': SECRET_KEY, + 'VERIFYING_KEY': None, + 'AUDIENCE': None, + 'ISSUER': None, + 'JWK_URL': None, + 'LEEWAY': 0, + + 'AUTH_HEADER_TYPES': ('Bearer',), + 'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION', + 'USER_ID_FIELD': 'id', + 'USER_ID_CLAIM': 'user_id', + + 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',), + 'TOKEN_TYPE_CLAIM': 'token_type', + + 'JTI_CLAIM': 'jti', + + 'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp', + 'SLIDING_TOKEN_LIFETIME': timedelta(minutes=60), + 'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1), +} # Password validation # https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators diff --git a/app/app/urls.py b/app/app/urls.py index 7c49775..6535020 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/', include('core.urls')), ] diff --git a/app/core/admin.py b/app/core/admin.py index 479e882..5899252 100644 --- a/app/core/admin.py +++ b/app/core/admin.py @@ -1,5 +1,26 @@ -"""Manage admin page for main app.""" +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin as BaseUserAdmin +from .models import User, Product, Category, Order -# from django.contrib import admin +class UserAdmin(BaseUserAdmin): + fieldsets = ( + (None, {'fields': ('email', 'password')}), + ('Personal info', {'fields': ('first_name', 'last_name')}), + ('Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser')}), + ('Important dates', {'fields': ('last_login', 'date_joined')}), -# Register your models here. + ) + add_fieldsets = ( + (None, { + 'classes': ('wide',), + 'fields': ('email', 'username', 'first_name', 'last_name', 'password1', 'password2'), + }), + ) + list_display = ('email', 'first_name', 'last_name', 'is_staff') + search_fields = ('email', 'first_name', 'last_name') + ordering = ('email',) + +admin.site.register(User, UserAdmin) +admin.site.register(Product) +admin.site.register(Category) +admin.site.register(Order) \ No newline at end of file diff --git a/app/core/migrations/0001_initial.py b/app/core/migrations/0001_initial.py index f533e94..e3e3e0d 100644 --- a/app/core/migrations/0001_initial.py +++ b/app/core/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.0.1 on 2022-02-19 10:18 +# Generated by Django 4.0.1 on 2024-07-19 22:13 from django.db import migrations, models @@ -20,9 +20,12 @@ class Migration(migrations.Migration): ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), ('email', models.EmailField(max_length=255, unique=True)), - ('name', models.CharField(max_length=255)), + ('username', models.CharField(max_length=255, unique=True)), + ('first_name', models.CharField(max_length=255)), + ('last_name', models.CharField(max_length=255)), ('is_active', models.BooleanField(default=True)), ('is_staff', models.BooleanField(default=False)), + ('is_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')), ], diff --git a/app/core/migrations/0002_user_date_joined_user_last_modified.py b/app/core/migrations/0002_user_date_joined_user_last_modified.py new file mode 100644 index 0000000..d82420a --- /dev/null +++ b/app/core/migrations/0002_user_date_joined_user_last_modified.py @@ -0,0 +1,26 @@ +# Generated by Django 4.0.1 on 2024-07-20 09:22 + +import datetime +from django.db import migrations, models +from django.utils.timezone import utc + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='date_joined', + field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2024, 7, 20, 9, 22, 33, 982024, tzinfo=utc)), + preserve_default=False, + ), + migrations.AddField( + model_name='user', + name='last_modified', + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/app/core/migrations/0003_alter_user_date_joined.py b/app/core/migrations/0003_alter_user_date_joined.py new file mode 100644 index 0000000..b80e51e --- /dev/null +++ b/app/core/migrations/0003_alter_user_date_joined.py @@ -0,0 +1,19 @@ +# Generated by Django 4.0.1 on 2024-07-20 09:29 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0002_user_date_joined_user_last_modified'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='date_joined', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + ] diff --git a/app/core/migrations/0004_category_product_order.py b/app/core/migrations/0004_category_product_order.py new file mode 100644 index 0000000..5f370d4 --- /dev/null +++ b/app/core/migrations/0004_category_product_order.py @@ -0,0 +1,44 @@ +# Generated by Django 4.0.1 on 2024-07-20 11:04 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0003_alter_user_date_joined'), + ] + + operations = [ + migrations.CreateModel( + name='Category', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ], + ), + migrations.CreateModel( + name='Product', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('description', models.TextField()), + ('price', models.DecimalField(decimal_places=2, max_digits=10)), + ('created_at', models.DateTimeField(default=django.utils.timezone.now)), + ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='products', to='core.category')), + ], + ), + migrations.CreateModel( + name='Order', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity', models.PositiveIntegerField(default=1)), + ('date', models.DateTimeField(default=django.utils.timezone.now)), + ('products', models.ManyToManyField(to='core.Product')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='orders', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/app/core/migrations/0005_alter_user_last_login.py b/app/core/migrations/0005_alter_user_last_login.py new file mode 100644 index 0000000..a46fd14 --- /dev/null +++ b/app/core/migrations/0005_alter_user_last_login.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.1 on 2024-07-20 12:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0004_category_product_order'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='last_login', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/app/core/migrations/0006_rename_name_category_cat_name_and_more.py b/app/core/migrations/0006_rename_name_category_cat_name_and_more.py new file mode 100644 index 0000000..1b6a707 --- /dev/null +++ b/app/core/migrations/0006_rename_name_category_cat_name_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.0.1 on 2024-07-20 12:28 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0005_alter_user_last_login'), + ] + + operations = [ + migrations.RenameField( + model_name='category', + old_name='name', + new_name='cat_name', + ), + migrations.RenameField( + model_name='product', + old_name='name', + new_name='product_name', + ), + ] diff --git a/app/core/migrations/0007_rename_cat_name_category_name.py b/app/core/migrations/0007_rename_cat_name_category_name.py new file mode 100644 index 0000000..be06ee6 --- /dev/null +++ b/app/core/migrations/0007_rename_cat_name_category_name.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.1 on 2024-07-20 12:30 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0006_rename_name_category_cat_name_and_more'), + ] + + operations = [ + migrations.RenameField( + model_name='category', + old_name='cat_name', + new_name='name', + ), + ] diff --git a/app/core/models.py b/app/core/models.py index a417f39..429ac26 100644 --- a/app/core/models.py +++ b/app/core/models.py @@ -1,42 +1,85 @@ -"""Create and manage app models and methods.""" - from django.db import models -from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, \ - PermissionsMixin -# Create your models here. +from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin +from django.utils import timezone +from django.conf import settings + class UserManager(BaseUserManager): - """USER MANAGER CLASS GOING TO MANAGE OUR USER CLASS.""" + """User manager class to manage our User class.""" - def create_user(self, email, password=None, **extra_fields): - """Create_user method creates and saves new user objects.""" + def create_user(self, email, username, first_name, last_name, password=None, **extra_fields): + """Create and save a new user.""" if not email: - raise ValueError('User must have valid email address') + raise ValueError('User must have a valid email address') + if not username: + raise ValueError('User must have a valid username') + if not first_name: + raise ValueError('User must have a valid first name') + if not last_name: + raise ValueError('User must have a valid last name') - user = self.model(email=self.normalize_email(email), **extra_fields) + email = self.normalize_email(email) + user = self.model(email=email, username=username, first_name=first_name, last_name=last_name, **extra_fields) user.set_password(password) user.save(using=self._db) return user - def create_superuser(self, email, password): - """Create and saves a new super user.""" - user = self.create_user(email, password) + def create_superuser(self, email, username, first_name, last_name, password): + """Create and save a new superuser.""" + user = self.create_user(email, username, first_name, last_name, password) user.is_staff = True user.is_superuser = True + user.save(using=self._db) return user - class User(AbstractBaseUser, PermissionsMixin): """Custom user model that supports using email instead of username.""" email = models.EmailField(max_length=255, unique=True) - name = models.CharField(max_length=255) + username = models.CharField(max_length=255, unique=True) + first_name = models.CharField(max_length=255) + last_name = models.CharField(max_length=255) is_active = models.BooleanField(default=True) is_staff = models.BooleanField(default=False) + is_verified = models.BooleanField(default=False) + date_joined = models.DateTimeField(default=timezone.now) + last_modified = models.DateTimeField(auto_now=True) + last_login = models.DateTimeField(blank=True, null=True) objects = UserManager() USERNAME_FIELD = 'email' + REQUIRED_FIELDS = ['username', 'first_name', 'last_name'] + + def __str__(self): + return self.email + + +class Category(models.Model): + name = models.CharField(max_length=255) + + def __str__(self): + return self.name + +class Product(models.Model): + product_name = models.CharField(max_length=255) + description = models.TextField() + price = models.DecimalField(max_digits=10, decimal_places=2) + category = models.ForeignKey(Category, related_name='products', on_delete=models.CASCADE) + created_at = models.DateTimeField(default=timezone.now) + + def __str__(self): + return self.product_name + + +class Order(models.Model): + user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='orders', on_delete=models.CASCADE) + products = models.ManyToManyField(Product) + quantity = models.PositiveIntegerField(default=1) + date = models.DateTimeField(default=timezone.now) + + def __str__(self): + return f'Order {self.id} by {self.user.email}' diff --git a/app/core/serializers.py b/app/core/serializers.py new file mode 100644 index 0000000..bf36dc6 --- /dev/null +++ b/app/core/serializers.py @@ -0,0 +1,67 @@ +from rest_framework import serializers +from .models import User, Product, Category, Order +from django.contrib.auth import get_user_model + + +User = get_user_model() + + +class UserSerializer(serializers.ModelSerializer): + re_password = serializers.CharField(write_only=True) + + class Meta: + model = User + fields = ('id', 'username', 'email', 'password', 're_password', 'first_name', 'last_name') + extra_kwargs = {'password': {'write_only': True}} + + def validate(self, data): + if data['password'] != data['re_password']: + raise serializers.ValidationError("Passwords do not match.") + return data + + def create(self, validated_data): + validated_data.pop('re_password') + user = User.objects.create_user( + email=validated_data['email'], + username=validated_data['username'], + first_name=validated_data['first_name'], + last_name=validated_data['last_name'], + password=validated_data['password'] + ) + return user + + +class CategorySerializer(serializers.ModelSerializer): + class Meta: + model = Category + fields = ['id', 'name'] + + +class ProductSerializer(serializers.ModelSerializer): + category = serializers.PrimaryKeyRelatedField(queryset=Category.objects.all()) + + class Meta: + model = Product + fields = ['id', 'product_name', 'description', 'price', 'category', 'created_at'] + + def create(self, validated_data): + category = validated_data.pop('category') + product = Product.objects.create(category=category, **validated_data) + return product + + def update(self, instance, validated_data): + category = validated_data.pop('category') + instance.product_name = validated_data.get('product_name', instance.product_name) + instance.description = validated_data.get('description', instance.description) + instance.price = validated_data.get('price', instance.price) + instance.category = category + instance.save() + return instance + + +class OrderSerializer(serializers.ModelSerializer): + products = serializers.PrimaryKeyRelatedField(queryset=Product.objects.all(), many=True) + + class Meta: + model = Order + fields = ['id', 'user', 'products', 'quantity', 'date'] \ No newline at end of file diff --git a/app/core/tests/test_models.py b/app/core/tests/test_models.py deleted file mode 100644 index 20448eb..0000000 --- a/app/core/tests/test_models.py +++ /dev/null @@ -1,42 +0,0 @@ -"""Tests for main app are found here.""" - -from django.test import TestCase -from django.contrib.auth import get_user_model - - -class ModelTests(TestCase): - """Test model creations and management.""" - - 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 - ) - - 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.""" - email = 'test@TEST.com' - user = get_user_model().objects.create_user(email, 'testpassword123') - - self.assertEqual(user.email, email.lower()) - - def test_new_user_invalid_email(self): - """Test creating user with no email raises an error.""" - with self.assertRaises(ValueError): - get_user_model().objects.create_user(None, 'test123') - - def test_create_new_superuser(self): - """Test can create superuser.""" - user = get_user_model().objects.create_superuser( - 'test@TEST.com', - 'test123' - ) - - self.assertTrue(user.is_superuser) - self.assertTrue(user.is_staff) diff --git a/app/core/tests/tests.py b/app/core/tests/tests.py new file mode 100644 index 0000000..d93c6f5 --- /dev/null +++ b/app/core/tests/tests.py @@ -0,0 +1,230 @@ +from rest_framework.test import APITestCase +from django.urls import reverse +from rest_framework import status +from core.models import User, Product, Category, Order +from rest_framework_simplejwt.tokens import RefreshToken + + +class UserTests(APITestCase): + def setUp(self): + self.register_url = reverse('register') + self.login_url = reverse('login') + self.logout_url = reverse('logout') + self.user_data = { + 'email': 'testuser@example.com', + 'username': 'testuser', + 'password': 'password123', + 're_password': 'password123', + 'first_name': 'Test', + 'last_name': 'User' + } + + def test_register_user(self): + response = self.client.post(self.register_url, self.user_data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(User.objects.count(), 1) + + def test_login_user(self): + self.client.post(self.register_url, self.user_data, format='json') + response = self.client.post(self.login_url, { + 'email': 'testuser@example.com', + 'password': 'password123' + }, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn('access', response.data) + self.assertIn('refresh', response.data) + + def test_logout_user(self): + self.client.post(self.register_url, self.user_data, format='json') + login_response = self.client.post(self.login_url, { + 'email': 'testuser@example.com', + 'password': 'password123' + }, format='json') + refresh_token = login_response.data['refresh'] + self.client.credentials(HTTP_AUTHORIZATION='Bearer ' + login_response.data['access']) + response = self.client.post(self.logout_url, {'refresh': refresh_token}, format='json') + self.assertEqual(response.status_code, status.HTTP_205_RESET_CONTENT) + + +class CategoryTests(APITestCase): + def setUp(self): + self.category_url = reverse('category-list') + self.user = User.objects.create_user( + email='admin@example.com', + username='admin', + password='password123', + first_name='Admin', + last_name='User' + ) + self.category_data = {'name': 'Shirts'} + + def authenticate(self): + self.client.force_authenticate(user=self.user) + + def test_create_category(self): + self.authenticate() + response = self.client.post(self.category_url, self.category_data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + def test_list_categories(self): + self.authenticate() + Category.objects.create(name='Shirts') + response = self.client.get(self.category_url, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_retrieve_category(self): + self.authenticate() + category = Category.objects.create(name='Shirts') + url = reverse('category-detail', kwargs={'pk': category.pk}) + response = self.client.get(url, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_update_category(self): + self.authenticate() + category = Category.objects.create(name='Shirts') + url = reverse('category-detail', kwargs={'pk': category.pk}) + response = self.client.put(url, {'name': 'Pants'}, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_delete_category(self): + self.authenticate() + category = Category.objects.create(name='Shirts') + url = reverse('category-detail', kwargs={'pk': category.pk}) + response = self.client.delete(url) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + +class ProductTests(APITestCase): + def setUp(self): + self.category = Category.objects.create(name='Shirts') + self.product_url = reverse('product-list') + self.user = User.objects.create_user( + email='admin@example.com', + username='admin', + password='password123', + first_name='Admin', + last_name='User' + ) + self.client.force_authenticate(user=self.user) + self.product_data = { + 'product_name': 'Polo Shirt', + 'description': 'A nice polo shirt', + 'price': 29.99, + 'category': self.category.id + } + self.update_data = { + 'product_name': 'Updated Polo Shirt', + 'description': 'An updated polo shirt description', + 'price': 39.99, + 'category': self.category.id + } + + def test_create_product(self): + response = self.client.post(self.product_url, self.product_data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + def test_list_products(self): + Product.objects.create( + product_name='Polo Shirt', + description='A nice polo shirt', + price=29.99, + category=self.category + ) + response = self.client.get(self.product_url, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_retrieve_product(self): + product = Product.objects.create( + product_name='Polo Shirt', + description='A nice polo shirt', + price=29.99, + category=self.category + ) + url = reverse('product-detail', kwargs={'pk': product.pk}) + response = self.client.get(url, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_update_product(self): + product = Product.objects.create( + product_name='Polo Shirt', + description='A nice polo shirt', + price=29.99, + category=self.category + ) + url = reverse('product-detail', kwargs={'pk': product.pk}) + response = self.client.put(url, self.update_data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_delete_product(self): + product = Product.objects.create( + product_name='Polo Shirt', + description='A nice polo shirt', + price=29.99, + category=self.category + ) + url = reverse('product-detail', kwargs={'pk': product.pk}) + response = self.client.delete(url) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + def test_list_products_with_pagination(self): + for i in range(15): + Product.objects.create( + product_name=f'Product {i}', + description=f'Description for product {i}', + price=29.99 + i, + category=self.category + ) + response = self.client.get(self.product_url, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data['results']), 10) # Default page size is 10 + + def test_search_products(self): + Product.objects.create( + product_name='Searchable Product', + description='Description for searchable product', + price=29.99, + category=self.category + ) + response = self.client.get(self.product_url, {'search': 'Searchable'}, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertGreater(len(response.data['results']), 0) + + +class OrderTests(APITestCase): + def setUp(self): + self.category = Category.objects.create(name='Shirts') + self.product = Product.objects.create( + product_name='Polo Shirt', + description='A nice polo shirt', + price=29.99, + category=self.category + ) + self.user = User.objects.create_user( + email='admin@example.com', + username='admin', + password='password123', + first_name='Admin', + last_name='User' + ) + self.order_url = reverse('order-list') + self.order_history_url = reverse('order-history') + self.order_data = { + 'user': self.user.id, + 'products': [self.product.id], + 'quantity': 5 + } + self.client.force_authenticate(user=self.user) + + def test_create_order(self): + response = self.client.post(self.order_url, self.order_data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + def test_list_orders(self): + Order.objects.create(user=self.user, quantity=5) + response = self.client.get(self.order_url, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_order_history(self): + Order.objects.create(user=self.user, quantity=5) + response = self.client.get(self.order_history_url, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) \ No newline at end of file diff --git a/app/core/urls.py b/app/core/urls.py new file mode 100644 index 0000000..06f5e78 --- /dev/null +++ b/app/core/urls.py @@ -0,0 +1,20 @@ +from django.urls import path, include +from rest_framework.routers import DefaultRouter +from rest_framework_simplejwt.views import TokenRefreshView +from .views import (RegisterView, LoginView, LogoutView, + ProductViewSet, CategoryViewSet, OrderViewSet, OrderHistoryView) + +router = DefaultRouter() +router.register('products', ProductViewSet) +router.register('categories', CategoryViewSet) +router.register('orders', OrderViewSet) + + +urlpatterns = [ + path('register/', RegisterView.as_view(), name='register'), + path('login/', LoginView.as_view(), name='login'), + path('logout/', LogoutView.as_view(), name='logout'), + path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), + path('order-history/', OrderHistoryView.as_view(), name='order-history'), + path('', include(router.urls)), +] \ No newline at end of file diff --git a/app/core/views.py b/app/core/views.py new file mode 100644 index 0000000..2ac8fb7 --- /dev/null +++ b/app/core/views.py @@ -0,0 +1,84 @@ +from rest_framework import generics, status, viewsets, filters +from rest_framework.response import Response +from .models import User, Product, Category, Order +from rest_framework_simplejwt.tokens import RefreshToken +from .serializers import UserSerializer, ProductSerializer, CategorySerializer, OrderSerializer +from rest_framework.permissions import AllowAny, IsAuthenticated +from django.utils import timezone +from rest_framework.pagination import PageNumberPagination + + +class RegisterView(generics.CreateAPIView): + queryset = User.objects.all() + permission_classes = (AllowAny,) + serializer_class = UserSerializer + +class LoginView(generics.GenericAPIView): + permission_classes = (AllowAny,) + serializer_class = UserSerializer + + def post(self, request, *args, **kwargs): + try: + user = User.objects.get(email=request.data['email']) + if not user.check_password(request.data['password']): + return Response({"detail": "Invalid credentials"}, status=status.HTTP_400_BAD_REQUEST) + user.last_login = timezone.now() + user.save(update_fields=['last_login']) + refresh = RefreshToken.for_user(user) + return Response({ + 'refresh': str(refresh), + 'access': str(refresh.access_token), + }) + except User.DoesNotExist: + return Response({"detail": "Invalid credentials"}, status=status.HTTP_400_BAD_REQUEST) + +class LogoutView(generics.GenericAPIView): + permission_classes = (IsAuthenticated,) + + def post(self, request, *args, **kwargs): + try: + refresh_token = request.data.get("refresh") + if not refresh_token: + return Response({"detail": "Refresh token is required"}, status=status.HTTP_400_BAD_REQUEST) + + token = RefreshToken(refresh_token) + token.blacklist() + + return Response({"detail": "Successfully logged out."}, status=status.HTTP_205_RESET_CONTENT) + except Exception as e: + return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST) + + +class ProductPagination(PageNumberPagination): + page_size = 10 + page_size_query_param = 'page_size' + max_page_size = 100 + + +class ProductViewSet(viewsets.ModelViewSet): + queryset = Product.objects.all().order_by('created_at') + serializer_class = ProductSerializer + permission_classes = [IsAuthenticated] + pagination_class = ProductPagination + filter_backends = [filters.SearchFilter] + search_fields = ['product_name', 'description'] + +class CategoryViewSet(viewsets.ModelViewSet): + queryset = Category.objects.all().order_by('name') + serializer_class = CategorySerializer + permission_classes = [IsAuthenticated] + +class OrderViewSet(viewsets.ModelViewSet): + queryset = Order.objects.all().order_by('date') + serializer_class = OrderSerializer + permission_classes = [IsAuthenticated] + + def perform_create(self, serializer): + serializer.save(user=self.request.user) + +class OrderHistoryView(generics.ListAPIView): + serializer_class = OrderSerializer + permission_classes = [IsAuthenticated] + + def get_queryset(self): + return Order.objects.filter(user=self.request.user).order_by('date') \ No newline at end of file