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 @@ + + + + + +
+ +Account Verification
+Thank you for your interest in joining us! as last step, please
+Click Here
+To confirm your email address with us.