diff --git a/.gitignore b/.gitignore index b6e4761..a54de49 100644 --- a/.gitignore +++ b/.gitignore @@ -109,6 +109,8 @@ venv/ ENV/ env.bak/ venv.bak/ +/myenv +myenv/ # Spyder project settings .spyderproject diff --git a/app/app/settings.py b/app/app/settings.py index 5356348..01784ae 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -11,7 +11,9 @@ """ from pathlib import Path - +from datetime import timedelta +from decouple import config +import os # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -20,13 +22,14 @@ # 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#(v9j9oxs-*-^#vjd' +SECRET_KEY = os.getenv('SECRET_KEY','') # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True +DEBUG = os.getenv('DEBUG',True) -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = [ ] +FRONTEND_URL ='http://localhost:8000' # Application definition @@ -37,7 +40,12 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'core' + #...third party apps + 'rest_framework', + 'rest_framework_simplejwt', + # local apps + 'core', + 'inventory', ] MIDDLEWARE = [ @@ -75,9 +83,18 @@ # https://docs.djangoproject.com/en/4.0/ref/settings/#databases DATABASES = { + # 'default': { + # 'ENGINE': 'django.db.backends.sqlite3', + # 'NAME': BASE_DIR / 'db.sqlite3', + # } + 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': 'inventory_app', + 'USER': os.getenv('POSTGRESUSER',''), + 'PASSWORD': os.getenv('POSTGRESPASS',''), + 'HOST': os.getenv('POSTGRESHOST',''), + 'PORT': os.getenv('POSTGRESPORT','5432'), } } @@ -123,4 +140,46 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' -AUTH_USER_MODEL = 'core.User' \ No newline at end of file +AUTH_USER_MODEL = 'core.User' + + +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework_simplejwt.authentication.JWTAuthentication', + ], + + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.IsAuthenticated', + ], +} + +AUTHENTICATION_BACKENDS = ['django.contrib.auth.backends.ModelBackend','inventory.custom_auth.EmailNameAuthBackend'] + + +SIMPLE_JWT = { + "ACCESS_TOKEN_LIFETIME": timedelta(minutes=60), + "REFRESH_TOKEN_LIFETIME": timedelta(days=1), + "ROTATE_REFRESH_TOKENS": True, + "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", + "USER_AUTHENTICATION_RULE": "rest_framework_simplejwt.authentication.default_user_authentication_rule", + "AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",), + "TOKEN_TYPE_CLAIM": "token_type", + "TOKEN_USER_CLASS": "rest_framework_simplejwt.models.TokenUser", + "JTI_CLAIM": "jti", + "SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp", + "SLIDING_TOKEN_LIFETIME": timedelta(minutes=60), + "SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1), +} + diff --git a/app/app/urls.py b/app/app/urls.py index 7c49775..ad7fe07 100644 --- a/app/app/urls.py +++ b/app/app/urls.py @@ -14,8 +14,11 @@ 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 inventory.views import endpoints urlpatterns = [ path('admin/', admin.site.urls), + path('',endpoints), + path('api/v1/',include('inventory.urls')), ] diff --git a/app/core/admin.py b/app/core/admin.py index 479e882..b280654 100644 --- a/app/core/admin.py +++ b/app/core/admin.py @@ -1,5 +1,6 @@ """Manage admin page for main app.""" -# from django.contrib import admin - +from django.contrib import admin +from .models import User # Register your models here. +admin.site.register(User) diff --git a/app/core/migrations/0001_initial.py b/app/core/migrations/0001_initial.py index f533e94..e7207c4 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-21 08:49 from django.db import migrations, models diff --git a/app/core/models.py b/app/core/models.py index a417f39..3bcbca4 100644 --- a/app/core/models.py +++ b/app/core/models.py @@ -25,7 +25,7 @@ def create_superuser(self, email, password): user = self.create_user(email, password) user.is_staff = True user.is_superuser = True - + user.save() return user diff --git a/app/inventory/__init__.py b/app/inventory/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/inventory/admin.py b/app/inventory/admin.py new file mode 100644 index 0000000..1f2bd6f --- /dev/null +++ b/app/inventory/admin.py @@ -0,0 +1,9 @@ +from django.contrib import admin +from .models import Category, Product,Order,OrderItem + +# Register your models here. + +admin.site.register(Category) +admin.site.register(Product) +admin.site.register(Order) +admin.site.register(OrderItem) \ No newline at end of file diff --git a/app/inventory/apps.py b/app/inventory/apps.py new file mode 100644 index 0000000..3d0d059 --- /dev/null +++ b/app/inventory/apps.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig + + +class InventoryConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'inventory' + + diff --git a/app/inventory/custom_auth.py b/app/inventory/custom_auth.py new file mode 100644 index 0000000..512fd81 --- /dev/null +++ b/app/inventory/custom_auth.py @@ -0,0 +1,25 @@ +from django.contrib.auth.backends import ModelBackend + +from django.contrib.auth import get_user_model +User = get_user_model()#get the user model used for the entire project + + +''' +This class is responsible for user login +permiting them to use their names or email to login +''' +class EmailNameAuthBackend(ModelBackend): + def authenticate(self,requst,email =None, password = None): + try: + user = User.objects.get(email=email) + success = user.check_password(password) + if success: + return user + except User.DoesNotExist: + try: + user = User.objects.get(name=email) + success = user.check_password(password) + if success: + return user + except User.DoesNotExist: + return None diff --git a/app/inventory/migrations/0001_initial.py b/app/inventory/migrations/0001_initial.py new file mode 100644 index 0000000..5eba093 --- /dev/null +++ b/app/inventory/migrations/0001_initial.py @@ -0,0 +1,83 @@ +# Generated by Django 4.0.1 on 2024-07-19 11:17 + +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='Category', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('slug', models.SlugField(allow_unicode=True, editable=False, max_length=100)), + ('name', models.CharField(max_length=100)), + ('date_created', models.DateTimeField(auto_now_add=True)), + ], + options={ + 'verbose_name_plural': 'categories', + 'db_table': 'inventory_category', + }, + ), + migrations.CreateModel( + name='Order', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('slug', models.SlugField(allow_unicode=True, editable=False, max_length=100)), + ('date_created', models.DateTimeField(auto_now_add=True)), + ], + options={ + 'verbose_name_plural': 'orders', + 'db_table': 'inventory_order', + }, + ), + migrations.CreateModel( + name='Product', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('slug', models.SlugField(allow_unicode=True, editable=False, max_length=100)), + ('name', models.CharField(max_length=100)), + ('description', models.TextField()), + ('price', models.DecimalField(decimal_places=2, max_digits=10)), + ('date_created', models.DateTimeField(auto_now_add=True)), + ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='products', to='inventory.category')), + ], + options={ + 'verbose_name_plural': 'products', + 'db_table': 'inventory_product', + }, + ), + migrations.CreateModel( + name='OrderItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('slug', models.SlugField(allow_unicode=True, editable=False, max_length=100)), + ('quantity', models.PositiveIntegerField()), + ('date_created', models.DateTimeField(auto_now_add=True)), + ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='order_items', to='inventory.order')), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='order_items', to='inventory.product')), + ], + options={ + 'verbose_name_plural': 'order_items', + 'db_table': 'inventory_order_item', + }, + ), + migrations.AddField( + model_name='order', + name='products', + field=models.ManyToManyField(through='inventory.OrderItem', to='inventory.Product'), + ), + migrations.AddField( + model_name='order', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='orders', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/app/inventory/migrations/0002_alter_orderitem_quantity_alter_product_category_and_more.py b/app/inventory/migrations/0002_alter_orderitem_quantity_alter_product_category_and_more.py new file mode 100644 index 0000000..eca71ab --- /dev/null +++ b/app/inventory/migrations/0002_alter_orderitem_quantity_alter_product_category_and_more.py @@ -0,0 +1,39 @@ +# Generated by Django 4.0.1 on 2024-07-21 08:33 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='orderitem', + name='quantity', + field=models.PositiveIntegerField(default=1), + ), + migrations.AlterField( + model_name='product', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='products', to='inventory.category'), + ), + migrations.AlterField( + model_name='product', + name='description', + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name='product', + name='name', + field=models.CharField(blank=True, max_length=100, null=True), + ), + migrations.AlterField( + model_name='product', + name='price', + field=models.DecimalField(decimal_places=2, default=0.0, max_digits=10), + ), + ] diff --git a/app/inventory/migrations/__init__.py b/app/inventory/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/inventory/models.py b/app/inventory/models.py new file mode 100644 index 0000000..575d38d --- /dev/null +++ b/app/inventory/models.py @@ -0,0 +1,99 @@ +from django.db import models +from django.template.defaultfilters import slugify + +from django.contrib.auth import get_user_model + +# Create your models here. +User = get_user_model() +def unique_slugify(instance, slug): + model = instance.__class__ + unique_slug = slug + record_count = 0 + while model.objects.filter(slug=unique_slug).exists(): + unique_slug = slug + "-1" + while model.objects.filter(slug=unique_slug).exists(): + record_count = record_count + 1 + unique_slug = slug + "-" + str(record_count) + return unique_slug + +class Category(models.Model): + slug = models.SlugField(max_length=100, + null=False, blank=False, + editable=False, + allow_unicode=True) + name = models.CharField(max_length=100) + date_created = models.DateTimeField(auto_now_add =True) + + class Meta: + db_table ='inventory_category' + verbose_name_plural ='categories' + def __str__(self): + return self.name + def save(self, *args, **kwargs): + if not self.slug: + slug_text = slugify(f'category-{self.name}') + self.slug = unique_slugify(self, slug_text) + super().save(*args, **kwargs) + +class Product(models.Model): + slug = models.SlugField(max_length=100, + null=False, blank=False, + editable=False, + allow_unicode=True) + name = models.CharField(max_length=100, blank=True, null=True) + description = models.TextField(blank=True, null=True) + price = models.DecimalField(max_digits=10, decimal_places=2,default=0.00) + category = models.ForeignKey(Category, related_name='products', on_delete=models.CASCADE,blank=True, null=True) + date_created = models.DateTimeField(auto_now_add =True) + + class Meta: + db_table ='inventory_product' + verbose_name_plural ='products' + def __str__(self): + return self.name + def save(self, *args, **kwargs): + if not self.slug: + slug_text = slugify(f'product-{self.name}') + self.slug = unique_slugify(self, slug_text) + super().save(*args, **kwargs) + +class Order(models.Model): + slug = models.SlugField(max_length=100, + null=False, blank=False, + editable=False, + allow_unicode=True) + user = models.ForeignKey(User, related_name='orders', on_delete=models.CASCADE) + products = models.ManyToManyField(Product, through='OrderItem') + date_created = models.DateTimeField(auto_now_add =True) + + class Meta: + db_table ='inventory_order' + verbose_name_plural ='orders' + def __str__(self): + return f'order-for-{self.user.email}' + def save(self, *args, **kwargs): + if not self.slug: + slug_text = slugify(f'Order-from-{self.user.email}') + self.slug = unique_slugify(self, slug_text) + super().save(*args, **kwargs) + +class OrderItem(models.Model): + slug = models.SlugField(max_length=100, + null=False, blank=False, + editable=False, + allow_unicode=True) + order = models.ForeignKey(Order, related_name='order_items', on_delete=models.CASCADE) + product = models.ForeignKey(Product, related_name='order_items', on_delete=models.CASCADE) + quantity = models.PositiveIntegerField(default=1) + date_created = models.DateTimeField(auto_now_add =True) + + class Meta: + db_table ='inventory_order_item' + verbose_name_plural ='order_items' + def __str__(self): + return f'{self.quantity} of {self.product.name}' + def save(self, *args, **kwargs): + if not self.slug: + slug_text = slugify(f'Order-item-for{self.order.user.email}-{self.quantity}') + self.slug = unique_slugify(self, slug_text) + super().save(*args, **kwargs) \ No newline at end of file diff --git a/app/inventory/serializers.py b/app/inventory/serializers.py new file mode 100644 index 0000000..dffef85 --- /dev/null +++ b/app/inventory/serializers.py @@ -0,0 +1,56 @@ +from rest_framework import serializers +from django.contrib.auth import get_user_model +from .models import Category, Product,Order,OrderItem + +User = get_user_model() + + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ['email', 'name', 'password'] + extra_kwargs = {'password': {'write_only': True}} + def create(self, validated_data): + password = validated_data.pop('password', None) + instance = self.Meta.model(**validated_data) + if password is not None: + instance.set_password(password) + instance.save() + return instance + + +class CategorySerializer(serializers.ModelSerializer): + class Meta: + model = Category + fields = ['slug','name'] + +class ProductSerializer(serializers.ModelSerializer): + + class Meta: + model = Product + fields = ['slug', 'name', 'description', 'price', 'category','date_created'] + + +class OrderItemSerializer(serializers.ModelSerializer): + product = ProductSerializer(many =False) + class Meta: + model = OrderItem + fields = ['slug','product', 'quantity','date_created'] + + + +class OrderSerializer(serializers.ModelSerializer): + #order_items = OrderItemSerializer(many=True) + class Meta: + model = Order + fields = '__all__' + + + + +class OrderHistorySerializer(serializers.ModelSerializer): + order_items = OrderItemSerializer(many=True) + + class Meta: + model = Order + fields = ['slug', 'date_created', 'order_items'] \ No newline at end of file diff --git a/app/inventory/templates/registration/account-activation.html b/app/inventory/templates/registration/account-activation.html new file mode 100644 index 0000000..87843e4 --- /dev/null +++ b/app/inventory/templates/registration/account-activation.html @@ -0,0 +1,39 @@ + + + + + + + +
+

+ User Account +

+

Account Verification

+
+
+{% autoescape off %} +

Hello {{ user.username }}

+

Thank you for your interest in joining us! as last step, please +Click Here
+To confirm your email address with us.

+If you are unable to click the above link, please copy and paste the full URL below into your browser: +{{domain}}/verify-email/?uid={{uid}}&confirmation_token={{token}} +{% endautoescape %} + +
+
+
+
+Thank you +
+
+

+ Read more + +

+ +
+ + diff --git a/app/inventory/tests.py b/app/inventory/tests.py new file mode 100644 index 0000000..286eaf2 --- /dev/null +++ b/app/inventory/tests.py @@ -0,0 +1,97 @@ +from rest_framework.test import APITestCase, APIClient +from rest_framework import status +from django.urls import reverse +from django.contrib.auth import get_user_model +from .models import Product, Category, Order, OrderItem + +User = get_user_model() + +class CustomTokenObtainPairViewTest(APITestCase): + + def setUp(self): + self.user = User.objects.create_user(email='firstuser@gmail.com', password='Testing123', name='First User') + self.user.is_email_verified = True + self.user.save() + self.url = reverse('token') + + def test_token_obtain_pair(self): + response = self.client.post(self.url, {'email': 'firstuser@gmail.com', 'password': 'Testing123'}) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn('access', response.data) + self.assertIn('refresh', response.data) + +class UserRegistrationViewTest(APITestCase): + + def setUp(self): + self.url = reverse('user_register') + + def test_user_registration(self): + response = self.client.post(self.url, {'email': 'newuser@gmail.com', 'password': 'Testing123', 'name': 'New User'}) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn('data', response.data) + +class ProductViewTest(APITestCase): + + def setUp(self): + self.category = Category.objects.create(name='Category1', + slug='test-category') + self.product = Product.objects.create(name='Product1', slug='test-product', + description='Test Description', + category=self.category) + self.url = reverse('product_list_create') + self.lst_url = reverse('product_list') + + def test_get_products(self): + response = self.client.get(self.lst_url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + self.assertEqual(response.data[0]['name'], 'Product1') + + def test_post_product(self): + data = { + 'name': 'New Product', + 'slug': 'new-product', + 'description': 'New Description', + 'price':200, + 'category': self.category.slug + } + response = self.client.post(self.url, data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(response.data['name'], 'New Product') + +class CategoryViewTest(APITestCase): + + def setUp(self): + self.category = Category.objects.create(name='Test Category', slug='test-category') + self.url = reverse('category_list_create') + self.lst_url =reverse('category_list') + + def test_get_categories(self): + response = self.client.get(self.lst_url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + self.assertEqual(response.data[0]['name'], 'Test Category') + + def test_post_category(self): + data = {'name': 'New Category', 'slug': 'new-category'} + response = self.client.post(self.url, data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(response.data['name'], 'New Category') + +class OrderViewTest(APITestCase): + + def setUp(self): + self.user = User.objects.create_user(email='firstuser@gmail.com', password='Testing123', name='First User') + self.client.force_authenticate(user=self.user) + self.category = Category.objects.create(name='Test Category', slug='test-category') + self.product = Product.objects.create(name='Test Product', slug='test-product', + description='Test Description', + category=self.category) + self.url = reverse('order_list_create') + + def test_get_orders(self): + order = Order.objects.create(user=self.user) + OrderItem.objects.create(order=order, product=self.product, quantity=1) + response = self.client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data[0]['order_items'][0]['product']['name'], 'Test Product') diff --git a/app/inventory/urls.py b/app/inventory/urls.py new file mode 100644 index 0000000..5c9014c --- /dev/null +++ b/app/inventory/urls.py @@ -0,0 +1,26 @@ +from django.urls import path +from rest_framework_simplejwt.views import TokenRefreshView +from .views import CustomTokenObtain,UserRegistrationView,ProductView,CategoryView,OrderView + +urlpatterns = [ + # JWT views + path("token/", CustomTokenObtain.as_view(), name="token"), + path("token/refresh/", TokenRefreshView.as_view(), name="refresh-token"), + + # other views + path("register/", UserRegistrationView.as_view(),name='user_register'), + #products urls + path('inventory/products/',ProductView.as_view(),name='product_list'),# list all products + path('inventory/product/create/',ProductView.as_view(),name ='product_list_create'),#Create a new product + path('inventory/product//',ProductView.as_view()),#detail view of a single product + path('inventory/product//update/',ProductView.as_view()),# update a product + path('inventory/product//delete/',ProductView.as_view()),#delete a product + #category urls + path('inventory/categories/',CategoryView.as_view(),name='category_list'),#list all categories + path('inventory/category//',CategoryView.as_view()),#list category item + path('inventory/category/create/',CategoryView.as_view(),name='category_list_create'),#create a category item + #Order view + path('inventory/order/create/',OrderView.as_view(), name ='order_list_create'),# create order + path('inventory/order-history/',OrderView.as_view()),# all user order history + path('inventory/order//history/',OrderView.as_view()),# single user order history +] \ No newline at end of file diff --git a/app/inventory/views.py b/app/inventory/views.py new file mode 100644 index 0000000..5ce309b --- /dev/null +++ b/app/inventory/views.py @@ -0,0 +1,229 @@ +from django.shortcuts import render +from rest_framework_simplejwt.serializers import TokenObtainPairSerializer +from rest_framework_simplejwt.views import TokenObtainPairView +from rest_framework import generics, status +from rest_framework_simplejwt.tokens import RefreshToken +from .custom_auth import EmailNameAuthBackend +from rest_framework.views import APIView +from rest_framework.permissions import IsAuthenticated +from rest_framework.decorators import permission_classes +from django.db import transaction +from rest_framework.response import Response +from rest_framework.permissions import AllowAny +from django.db.models import Q +from rest_framework.pagination import PageNumberPagination +from rest_framework.decorators import api_view +from django.contrib.auth import authenticate +from django.contrib.auth import get_user_model + + +#local imports +from .serializers import UserSerializer,ProductSerializer,\ + OrderItemSerializer,OrderSerializer,CategorySerializer,OrderHistorySerializer +from .models import Category, Product,Order,OrderItem +# Create your views here. + + +User = get_user_model() +class CustomTokenObtainPairSerializer(TokenObtainPairSerializer): + @classmethod + def get_token(cls, user):# serialize a JWT token combining user name and email + token = super().get_token(user) + token["name"] = user.name + token["email"] = user.email + + return token + +class CustomTokenObtain(TokenObtainPairView): + serializer_class = CustomTokenObtainPairSerializer + def post(self, request, *args, **kwargs): + data = request.data #get the request data and retrieve email/name and password + name = data['email']or None + password = data['password'] or None + user = authenticate(request, email=name , password=password)# try to authenticate with the info + if user is not None:# if user is authenticated and not none + if user.is_active: + refresh = RefreshToken.for_user(user) #get a refresh token + token = { + 'refresh': str(refresh), + 'access': str(refresh.access_token), + } + return Response(token, status=status.HTTP_200_OK)# respond with the access and refresh token + return Response({'error': 'Account disabled!'}, status=status.HTTP_401_UNAUTHORIZED) + return Response({'error': 'Invalid login Details'}, status=status.HTTP_401_UNAUTHORIZED) + + +class UserRegistrationView(generics.GenericAPIView): + serializer_class = UserSerializer + permission_classes =[] + def post(self, request,*args,**kwargs): + data = request.data.copy() + serializer = self.serializer_class(data=data) + if serializer.is_valid(): + serializer = serializer.save() + + data = {'message':f'Account created succesffuly!','data':UserSerializer(serializer).data} + return Response(data,status=status.HTTP_200_OK) + return Response(serializer.errors,status=status.HTTP_400_BAD_REQUEST) + + + +class ProductView(APIView):#One View for all CRUD on Product model + authentication_classes=[] + permission_classes =[AllowAny] + serializer_class = ProductSerializer #serializer class to be used all round the ProducView via self + model_class = Product #model class to be used all round the ProducView via self + def get(self, request, slug=None,*args,**kwargs): + search_query = request.query_params.get('search', None) + queryset = Product.objects.all() + if search_query: + queryset = queryset.filter( + Q(name__icontains=search_query) | + Q(description__icontains=search_query) | + Q(category__name__icontains=search_query) + ) + paginator = PageNumberPagination() + paginator.page_size = 1 + paginated_queryset = paginator.paginate_queryset(queryset, request) + serializer = self.serializer_class(paginated_queryset, many=True) + return paginator.get_paginated_response(serializer.data) + if slug: + try: + product = self.model_class.objects.get(slug=slug) + serializer = self.serializer_class(product,many =False) + return Response(serializer.data) + except self.model_class.DoesNotExist: + return Response({'error':'Object not found!'}, status=status.HTTP_400_BAD_REQUEST) + else: + products = self.model_class.objects.all() + serializer = self.serializer_class(products, many=True) + for item in serializer.data: + item['category'] = CategorySerializer(Category.objects.get(id = item['category'])).data + return Response(serializer.data) + + def post(self, request,*args,**kwargs): + data = request.data.copy() + category_identifier =request.data.get('category','') + category = Category.objects.filter(# get ctegory by Id or slug + Q(slug__icontains=category_identifier) |Q(id__icontains=category_identifier)) + data['category'] = category[0].id + serializer = self.serializer_class(data=data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def put(self, request, slug): + try: + product = self.model_class.objects.get(slug=slug) + except self.model_class.DoesNotExist:# object not found + return Response({'error':'Object not found!'}, status=status.HTTP_400_BAD_REQUEST) + serializer = self.serializer_class(product, data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data,status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def delete(self, request, slug): + try: + product = self.model_class.objects.get(slug=slug) + except self.model_class.DoesNotExist: + return Response({'error':'Object not found!'}, status=status.HTTP_400_BAD_REQUEST) + else: + product.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + + + +class CategoryView(APIView): + authentication_classes=[] + permission_classes =[] + serializer_class = CategorySerializer + model_class = Category + def get(self, request, slug=None,*args,**kwargs): + if slug: + try: + category = self.model_class.objects.get(slug=slug) + serializer = self.serializer_class(category,many =False) + return Response(serializer.data,status=status.HTTP_200_OK) + except self.model_class.DoesNotExist: + return Response({'error':'Object not found!'}, status=status.HTTP_400_BAD_REQUEST) + categories = self.model_class.objects.all() + serializer = self.serializer_class(categories, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + def post(self, request,*args,**kwargs): + data = request.data.copy() + serializer = self.serializer_class(data=data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +class OrderView(APIView): + permission_classes =[IsAuthenticated] + serializer_class = OrderSerializer + model_class = Order + def get(self, request, slug=None,*args,**kwargs): + if slug: + try: + order = self.model_class.objects.get(slug=slug,user = request.user) + serializer = OrderHistorySerializer(order,many =False) + return Response(serializer.data) + except self.model_class.DoesNotExist: + return Response({'error':'Object not found!'}, status=status.HTTP_400_BAD_REQUEST) + orders = Order.objects.filter(user=request.user) + serializer = OrderHistorySerializer(orders, many=True) + return Response(serializer.data) + + @transaction.atomic #use atomic to rolback if need be + def post(self, request, *args, **kwargs): + data = request.data.copy() + data['user'] = request.user.id + + serializer = self.serializer_class(data=data) + if serializer.is_valid(): + order = serializer.save() + products_data = data.get('products', []) + + for item_data in products_data: + try: + product = Product.objects.get(slug=item_data['slug']) + quantity = item_data.get('quantity', 1) + OrderItem.objects.create(order=order, product=product, quantity=quantity) + except Product.DoesNotExist: + transaction.set_rollback(True)# rollback actions on db + return Response({'error': f"Product with slug {item_data['slug']} not found"}, status=status.HTTP_400_BAD_REQUEST) + + return Response(serializer.data, status=status.HTTP_201_CREATED) + + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['GET']) +@permission_classes([AllowAny]) +def endpoints(request):# endpoint to keep all out endpoint at the root url for easy access and development + data = [ + + "api/v1/register/", + "api/v1/token/", + "api/v1/token/refresh/", + #products urls + 'api/v1/inventory/products/', + 'api/v1/inventory/product/create/', + 'api/v1/inventory/product//', + 'api/v1/inventory/product//update/', + 'api/v1/inventory/product//delete/', + #category urls + 'api/v1/inventory/categories/', + 'api/v1/inventory/category//', + 'api/v1/inventory/category/create/', + #Order urls + 'api/v1/inventory/order/create/', + 'api/v1/inventory/order-history/', + 'api/v1/inventory/order//history/', +] + + + return Response(data) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 843358c..2d0bc5e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,11 @@ -Django>=3.0,<4.0.2 +asgiref==3.8.1 +Django==4.0.1 djangorestframework==3.13.1 - -# flake8>=3.6.0,<3.7.0 \ No newline at end of file +djangorestframework-simplejwt==5.3.1 +PyJWT==2.8.0 +python-decouple==3.8 +pytz==2024.1 +six==1.16.0 +sqlparse==0.5.1 +typing_extensions==4.12.2 +tzdata==2024.1